[
  {
    "path": ".codex/skills/screen2navkey/SKILL.md",
    "content": "---\nname: screen2navkey\ndescription: convert Voyager Screen to navigation 3\n---\n\n## 任务背景\n目前项目中使用了 Voyager(cafe.adriel.voyager:voyager-navigator) 作为导航框架，现在我希望将 Voyager 替换成 navigation3(androidx.navigation3:navigation3-runtime).\n\n## 任务内容\n目前 nav3 我已经集成并且完成了部分代码的重构，现在你需要帮我做一件事情，将一些 Screen 替换成 一个 Composable 函数 + NavKey。\n\n你的目的是把继承自 cafe.adriel.voyager.core.screen.Screen 或者 com.zhangke.fread.common.page.BaseScreen 的类改成 androix.navigaton3 的一个 NavKey + 对应的 Composable 函数。\n\n比如现在有这样的一个 Screen：\n```kotlin\nclass ProfileScreen : BaseScreen() {\n\n    @Composable\n    override fun Content() {\n        super.Content()\n        val viewModel = getViewModel<ProfileHomeViewModel>()\n        Box(\n            modifier = Modifier\n                .fillMaxSize()\n                .background(MaterialTheme.colors.background),\n        ) {\n            Text(text = \"Profile Screen\")\n        }\n    }\n}\n\n```\n那么你需要改成如下方式，并且新增一个 NavKey:\n```kotlin\n\nobject ProfileScreenKey: NavKey\n\n@Composable\nfun ProfileScreen(viewModel: ProfileHomeViewModel){\n    Box(\n        modifier = Modifier\n            .fillMaxSize()\n            .background(MaterialTheme.colors.background),\n    ) {\n        Text(text = \"Profile Screen\")\n    }\n}\n```\n但如果这个页面有参数，那么 key 也应该带一个参数：\n```kotlin\ndata class DetailScreenKey(val itemId: String) : NavKey\n\n@Composable\nfun DetailScreen(viewModel: DetailViewModel){\n    Box(\n        modifier = Modifier\n            .fillMaxSize()\n            .background(MaterialTheme.colors.background),\n    ) {\n        Text(text = \"Detail Screen for item: $itemId\")\n    }\n}\n```\n\n然后你需要把这个新增的 NavKey 注册到当前模块的 NavEntryProvider 中，比如：\n```kotlin\nclass ProfileNavEntryProvider : NavEntryProvider {\n\n    override fun EntryProviderScope<NavKey>.build() {\n        entry<ProfileScreenKey> {\n            ProfileScreen(koinViewModel())\n        }\n        entry<CreatePlanScreenNavKey> { key ->\n            // with parameters\n            CreatePlanScreen(koinViewModel { parametersOf(key.lexicon) })\n        }\n    }\n\n    override fun PolymorphicModuleBuilder<NavKey>.polymorph() {\n        subclass(ProfileScreenKey::class)\n        subclass(CreatePlanScreenNavKey::class)\n    }\n}\n```\n\n## 工作流程\n你需要 Follow 以下工作流程：\n1. 首先找到给定模块中所有符合如下条件的 Screen：\n    a. 继承自 cafe.adriel.voyager.core.screen.Screen 或者 com.zhangke.fread.common.page.BaseScreen\n    b. 不包含任何嵌套 **Navigator**\n2. 将这些符合条件的 Screen 列出并输出到控制台\n3. 逐个重构这些 Screen\n4. 对于每个 Screen，首先创建该 Screen 的 NavKey，比如给 ProfileScreen 创建一个 ProfileScreenNavKey.\n5. 将 ProfileScreen 改为 @Composable 函数。\n6. 对于使用了 navigationResult 的地方请保持不动，不要试图修改相关的代码，即使有编译报错也不用管，保留原样。\n7. 将 ProfileScreenNavKey 以及这个 @Composable 函数 注册到该模块的 NavEntryProvider 中。\n8. 找到这个 Screen 的相关引用，并将跳转处改为这个 Screen 的 NavKey\n9. 结束这个 Screen 重构并进入下一个 Screen。\n10. 直到所有重构完所有满足条件的 Screen。\n\n## 绝对禁止\n一下内容为绝对禁止修改的规则：\n1. 对于已经修改完成的类请不要再改\n2. 你只应该修改 Screen 和 navigation3 相关的代码，其他的代码不要改，即使你觉得有问题也不要改\n3. 不要做任何超出我要求的事情\n4. 遇到不属于上述情况的页面请直接忽略，不要自己想办法解决\n5. 不要求改任何嵌套的 Navigator 页面，遇到嵌套的情况直接跳过\n6. 不要修改任何已经使用 navigation3 的页面\n7. 不要通过代码引用的方式找某个页面的引用并且试图修改其引用点\n8. 不要修改任何超出要求的代码\n\n"
  },
  {
    "path": ".codex/skills/voyager2nav3/SKILL.md",
    "content": "---\nname: voyager2nav3\ndescription: convert Voyager Screen to navigation 3\n---\n\n目前项目中使用了 Voyager(cafe.adriel.voyager:voyager-navigator) 作为导航框架，现在我希望将 Voyager 替换成 navigation3(androidx.navigation3:navigation3-runtime).\nnav3我已经集成并且完成了部分代码，现在你需要帮我做一件事情，将所有的 Screen 替换成 一个 Composable 函数。\n\n抽象类不要改， 只需要把继承自 cafe.adriel.voyager.core.screen.Screen 或者 com.zhangke.fread.common.page.BaseScreen 的类改成 Composable 函数即可。\n\n比如现在有这样的一个 Screen：\n```kotlin\nclass ProfileScreen : BaseScreen() {\n\n    @Composable\n    override fun Content() {\n        super.Content()\n        val viewModel = getViewModel<ProfileHomeViewModel>()\n        Box(\n            modifier = Modifier\n                .fillMaxSize()\n                .background(MaterialTheme.colors.background),\n        ) {\n            Text(text = \"Profile Screen\")\n        }\n    }\n}\n\n```\n那么你需要改成如下方式，并且新增一个 NavKey:\n```kotlin\n\nobject ProfileScreenKey: NavKey\n\n@Composable\nfun ProfileScreen(viewModel: ProfileHomeViewModel){\n    Box(\n        modifier = Modifier\n            .fillMaxSize()\n            .background(MaterialTheme.colors.background),\n    ) {\n        Text(text = \"Profile Screen\")\n    }\n}\n```\n但如果这个页面有参数，那么 key 也应该带一个参数：\n```kotlin\ndata class DetailScreenKey(val itemId: String) : NavKey\n\n@Composable\nfun DetailScreen(viewModel: DetailViewModel){\n    Box(\n        modifier = Modifier\n            .fillMaxSize()\n            .background(MaterialTheme.colors.background),\n    ) {\n        Text(text = \"Detail Screen for item: $itemId\")\n    }\n}\n```\n\n然后你需要把这个新增的 NavKey 注册到当前模块的 NavEntryProvider 中，比如：\n```kotlin\nclass ProfileNavEntryProvider : NavEntryProvider {\n\n    override fun EntryProviderScope<NavKey>.build() {\n        entry<ProfileScreenKey> {\n            ProfileScreen(koinViewModel())\n        }\n        entry<CreatePlanScreenNavKey> { key ->\n            // with parameters\n            CreatePlanScreen(koinViewModel { parametersOf(key.lexicon) })\n        }\n    }\n\n    override fun PolymorphicModuleBuilder<NavKey>.polymorph() {\n        subclass(ProfileScreenKey::class)\n        subclass(CreatePlanScreenNavKey::class)\n    }\n}\n```\n然后跳转该页面的地方也需要同步修改,首先替换 LocalNavigator.currentOrThrow 为 LocalNavBackStack.currentOrThrow,然后通过 NavKey 跳转页面。\n\n你需要按照模块来完成工作。\n\n对于已经修改完成的类请不要再改。\n\n你只应该修改 Screen 和 navigation3 相关的代码，其他的代码不要改，即使你觉得有问题也不要改。\n\n不要修改任何 Tab 以及其直接引用的页面。\n\n遇到不属于上述情况的页面请直接忽略，不要自己想办法解决。\n\n不要求改任何嵌套的 Navigator 页面，遇到嵌套的情况直接跳过。\n\n不要做任何超出我要求的事情。\n\n"
  },
  {
    "path": ".editorconfig",
    "content": "[*.kt]\nmax_line_length = 100\n\n[*.java]\nmax_line_length = 100\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]\npatreon: # Replace with a single Patreon username e.g., user1\nopen_collective: # Replace with a single Open Collective username e.g., user1\nko_fi: zhangke\ntidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel\npolar: # Replace with a single Polar username e.g., user1\nbuy_me_a_coffee: # Replace with a single Buy Me a Coffee username e.g., user1\nthanks_dev: # Replace with a single thanks.dev username e.g., u/gh/user1\ncommunity_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry\nliberapay: # Replace with a single Liberapay username e.g., user1\nissuehunt: # Replace with a single IssueHunt username e.g., user1\notechie: # Replace with a single Otechie username e.g., user1\ncustom: ['https://afdian.com/a/_0cdc1']\n"
  },
  {
    "path": ".github/ci-gradle.properties",
    "content": "org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=2g -Dfile.encoding=UTF-8\norg.gradle.daemon=false\norg.gradle.parallel=true\norg.gradle.workers.max=2\n\n# kotlin\nkotlin.compiler.execution.strategy=in-process\nkotlin.native.ignoreDisabledTargets=true\n\n# other\nwarningsAsErrors=false"
  },
  {
    "path": ".github/workflows/build_apk.yml",
    "content": "on:\n  push:\n    tags:\n      - '**'\n\nconcurrency:\n  group: ${{ github.event.pull_request.number }}-build-ci\n  cancel-in-progress: true\n\npermissions:\n  contents: write\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout repository and submodules\n        uses: actions/checkout@v4\n        with:\n          submodules: recursive\n\n      - name: Validate Gradle Wrapper\n        uses: gradle/wrapper-validation-action@v3\n\n      - name: Copy CI gradle.properties\n        run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties\n\n      - name: Set up JDK\n        uses: actions/setup-java@v3\n        with:\n          distribution: 'zulu'\n          java-version: 17\n\n      - name: Decode keystore\n        run: printf \"%s\" \"${{ secrets.FREADKEYSTORE }}\" | base64 --decode > fread-keystore.jks\n\n      - name: Create keystore.properties\n        run: |\n          cat > keystore.properties <<EOF\n          storePassword=${{ secrets.STOREPASSWORD }}\n          keyPassword=${{ secrets.KEYPASSWORD }}\n          keyAlias=${{ secrets.KEYALIAS }}\n          storeFile=../fread-keystore.jks\n          EOF\n\n      - name: Build with Gradle\n        run: bash ./gradlew assembleRelease\n\n      - name: Upload APK\n        uses: actions/upload-artifact@v4\n        with:\n          name: app\n          path: app/build/outputs/apk/release/*.apk\n\n      - name: Build Debug APK\n        run: bash ./gradlew assembleDebug\n\n      - name: Upload Debug APK\n        uses: actions/upload-artifact@v4\n        with:\n          name: app-debug\n          path: app/build/outputs/apk/debug/*.apk\n\n      - name: Create draft GitHub Release\n        uses: softprops/action-gh-release@v2\n        with:\n          draft: true\n          tag_name: ${{ github.ref_name }}\n          name: Fread ${{ github.ref_name }}\n          files: |\n            app/build/outputs/apk/release/*.apk\n            app/build/outputs/apk/debug/*.apk\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/check.yml",
    "content": "name: Check CI\n\non:\n  pull_request:\n    paths-ignore:\n      - 'documents/**'\n      - '**.md'\n\nconcurrency:\n  group: ${{ github.event.pull_request.number }}-check-ci\n  cancel-in-progress: true\n\njobs:\n  assemble:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout repository and submodules\n        uses: actions/checkout@v4\n        with:\n          submodules: recursive\n\n      - name: Validate Gradle Wrapper\n        uses: gradle/wrapper-validation-action@v3\n\n      - name: Copy CI gradle.properties\n        run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties\n\n      - name: Set up JDK\n        uses: actions/setup-java@v3\n        with:\n          distribution: 'zulu'\n          java-version: 17\n\n      - name: Decode keystore\n        run: printf \"%s\" \"${{ secrets.FREADKEYSTORE }}\" | base64 --decode > fread-keystore.jks\n\n      - name: Create keystore.properties\n        run: |\n          cat > keystore.properties <<EOF\n          storePassword=${{ secrets.STOREPASSWORD }}\n          keyPassword=${{ secrets.KEYPASSWORD }}\n          keyAlias=${{ secrets.KEYALIAS }}\n          storeFile=../fread-keystore.jks\n          EOF\n\n      - name: Assemble\n        run: ./gradlew assembleRelease --stacktrace\n"
  },
  {
    "path": ".gitignore",
    "content": "*.iml\n.gradle\n.idea/\n/local.properties\n.DS_Store\n/build\n/captures\n.externalNativeBuild\n.cxx\nlocal.properties\n# Built application files\n*.apk\n*.aar\n*.ap_\n*.aab\n.kotlin\n\n# Files for the ART/Dalvik VM\n*.dex\n\n# Java class files\n*.class\n\n# Generated files\nbin/\ngen/\nout/\n#  Uncomment the following line in case you need and you don't have the release build type files in your app\n# release/\n\n# Gradle files\n.gradle/\nbuild/\n\n# Local configuration file (sdk path, etc)\nlocal.properties\n\n# Proguard folder generated by Eclipse\nproguard/\n\n# Log Files\n*.log\n\n# Android Studio Navigation editor temp files\n.navigation/\n\n# Android Studio captures folder\ncaptures/\n\n# IntelliJ\n*.iml\n\n# Keystore files\n# Uncomment the following lines if you do not want to check your keystore files in.\n#*.jks\n#*.keystore\n\n# External native build folder generated in Android Studio 2.2 and later\n.externalNativeBuild\n.cxx/\n\n# Google Services (e.g. APIs or Firebase)\n# google-services.json\n\n# Freeline\nfreeline.py\nfreeline/\nfreeline_project_description.json\n\n# fastlane\nfastlane/report.xml\nfastlane/Preview.html\nfastlane/screenshots\nfastlane/test_output\nfastlane/readme.md\n\n# Version control\nvcs.xml\n\n# lint\nlint/intermediates/\nlint/generated/\nlint/outputs/\nlint/tmp/\n# lint/reports/\n\n/plugins/Fread-Firebase/\n/fread-keystore.jks\n/keystore.properties\n\n"
  },
  {
    "path": "Agents.md",
    "content": "## Compact Instructions\n\nWhen compressing, preserve in priority order:\n\n1. Architecture decisions (NEVER summarize)\n2. Modified files and their key changes\n3. Current verification status (pass/fail)\n4. Open TODOs and rollback notes\n5. Tool outputs (can delete, keep pass/fail only)\n\n"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [2026] [Zhangke]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "README.md",
    "content": "<div align=\"center\">\n  <img src=\"/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp\">\n  <h1>Fread</h1>\n</div>\n\nFread is a decentralized microblogging client that seamlessly integrates Mastodon, Bluesky, and RSS — all in one place. \n\n<p align=\"center\">\n  <a href=\"https://play.google.com/store/apps/details?id=com.zhangke.fread\">\n    <img src=\"google-play-download.png\" height=\"60\" style=\"vertical-align: middle;\"/>\n  </a>\n  <a href=\"https://f-droid.org/packages/com.zhangke.fread/\">\n    <img src=\"https://fdroid.gitlab.io/artwork/badge/get-it-on.png\" height=\"60\" style=\"vertical-align: middle;\"/>\n  </a>\n  <a href=\"https://github.com/0xZhangKe/Fread/releases/latest\">\n    <img src=\"ic_download_apk.png\" height=\"60\" style=\"vertical-align: middle;\"/>\n  </a>\n</p>\n\n\n - `fread-xxx-google-play-signed.apk`: Signed by Google Play and includes push notifications. If you are used to updating through Google Play, you can choose this version.\n - `fread-xxx-fdroid.apk`: The version available on F-Droid does not include push notifications, and its signature is inconsistent with Google Play. If you are used to using F-Droid, please use this version.\n - `fread-xxx-debug.apk`: Test version, contains some logs, the functionality is the same as the Google Play version, but the signature is the same as F-Droid.\n\nBecause Fread accidentally used Google Play managed signatures, there are currently two different signed versions of Fread distributed. **These two versions cannot be upgraded to each other**. Please choose with caution.\n\n## Screenshots\n![screenshot](/screenshot/screenshot.jpg)\n\n## Build\nThe signing key is required, or you can simply delete the signing configuration in app/build.gradle to complete the compilation.\n\n### Build disable firebase\nManually disable Firebase join compilation\n```\n./gradlew assembleRelease -PdisableFirebase=true\n```\n## Blogs\n- [Why Open Source](https://medium.com/@kezhang404/after-two-years-of-development-the-fread-project-is-now-open-source-8adcf690bfac)\n- [Support Bluesky](https://medium.com/@kezhang404/fread-now-supports-bluesky-a-unified-gateway-to-the-decentralized-web-17f518ba877c)\n- [Fread Introduce](https://medium.com/@kezhang404/fread-the-next-generation-mastodon-client-30bc50e279fd)\n- [The Philosophy of Modular Division in a Large Android Project\n](https://medium.com/@kezhang404/the-philosophy-of-modular-division-in-a-large-android-project-e588a5dcdb78)\n- [Fread Features and Midterm Review ](https://medium.com/p/fread-features-and-midterm-review-c961e1a8930f)\n\n[**Deepwiki**](https://deepwiki.com/0xZhangKe/Fread)\n\n## Star History\n\n[![Star History Chart](https://api.star-history.com/svg?repos=0xZhangKe/Fread&type=Date)](https://www.star-history.com/#0xZhangKe/Fread&Date)\n\n## Discussion Group\n- [Telegram](https://t.me/+-SlbKcNbJSphNWI1)\n\n## Official Account\n- [Mastodon](https://mastodon.social/@fread)\n\n## Donate\n[Ko-Fi](https://ko-fi.com/zhangke) or [Afdian](https://afdian.com/a/_0cdc1)\n\n### Sponsors\nThanks to the following users for their support:\n<p align=\"center\">\n  <a>\n    <img src=\"https://pic1.afdiancdn.com/user/user_upload_osl/8b2c4a6a82705a10fee99c38cf59a202_w132_h132_s3.jpeg\" width=\"64px\" style=\"border-radius:50%;\" alt=\"user1\"/>\n  </a>\n  &nbsp;&nbsp;&nbsp;\n  <a href=\"https://github.com/user3\">\n    <img src=\"https://ko-fi.com/img/anon2.png?v=1\" width=\"64px\" style=\"border-radius:50%;\" alt=\"user3\"/>\n  </a>\n</p>\n"
  },
  {
    "path": "app/.gitignore",
    "content": "/build\n/debug\n/release\n/google-services.json\n"
  },
  {
    "path": "app/build.gradle.kts",
    "content": "import java.io.FileInputStream\nimport java.util.Properties\n\nplugins {\n    id(\"fread.android.application\")\n    id(\"fread.compose.multiplatform\")\n    id(\"com.google.devtools.ksp\")\n}\n\nval keystorePropertiesFile: File = rootProject.file(\"keystore.properties\")\nval keystoreProperties = Properties()\nkeystoreProperties.load(FileInputStream(keystorePropertiesFile))\n\nandroid {\n    namespace = \"com.zhangke.fread\"\n\n    buildFeatures {\n        buildConfig = true\n    }\n\n    dependenciesInfo {\n        includeInApk = false\n        includeInBundle = false\n    }\n\n    signingConfigs {\n        getByName(\"debug\") {\n            storeFile = file(keystoreProperties.getProperty(\"storeFile\"))\n            keyPassword = keystoreProperties.getProperty(\"keyPassword\")\n            storePassword = keystoreProperties.getProperty(\"storePassword\")\n            keyAlias = keystoreProperties.getProperty(\"keyAlias\")\n        }\n        create(\"release\") {\n            storeFile = file(keystoreProperties.getProperty(\"storeFile\"))\n            keyPassword = keystoreProperties.getProperty(\"keyPassword\")\n            storePassword = keystoreProperties.getProperty(\"storePassword\")\n            keyAlias = keystoreProperties.getProperty(\"keyAlias\")\n        }\n    }\n\n    defaultConfig {\n        applicationId = \"com.zhangke.fread\"\n        versionCode = 108010\n        versionName = \"1.8.1\"\n\n        testInstrumentationRunner = \"androidx.test.runner.AndroidJUnitRunner\"\n    }\n\n    buildTypes {\n        release {\n            isMinifyEnabled = true\n            proguardFiles(\"proguard-rules.pro\")\n        }\n        debug {\n            isMinifyEnabled = false\n            proguardFiles(\"proguard-rules.pro\")\n        }\n        getByName(\"release\") {\n            signingConfig = signingConfigs.getByName(\"release\")\n        }\n        getByName(\"debug\") {\n            signingConfig = signingConfigs.getByName(\"debug\")\n        }\n    }\n\n    bundle {\n        language {\n            enableSplit = false\n        }\n    }\n}\n\ndependencies {\n    implementation(project(path = \":framework\"))\n    implementation(project(path = \":bizframework:status-provider\"))\n    implementation(project(path = \":commonbiz:common\"))\n    implementation(project(path = \":commonbiz:analytics\"))\n    implementation(project(path = \":commonbiz:status-ui\"))\n    implementation(project(path = \":commonbiz:sharedscreen\"))\n    implementation(project(path = \":feature:feeds\"))\n    implementation(project(path = \":feature:explore\"))\n    implementation(project(path = \":feature:profile\"))\n    implementation(project(path = \":feature:notifications\"))\n    implementation(project(path = \":plugins:activitypub-app\"))\n    implementation(project(path = \":plugins:rss\"))\n    implementation(project(path = \":plugins:bluesky\"))\n    if (gradle.extra[\"enableFirebaseModule\"] == true) {\n        implementation(project(path = \":plugins:fread-firebase\"))\n    }\n    implementation(project(path = \":app-hosting\"))\n\n    implementation(compose.material3)\n    implementation(compose.components.resources)\n\n    implementation(libs.bundles.androidx.activity)\n    implementation(libs.imageLoader)\n\n    implementation(libs.krouter.runtime)\n\n    implementation(libs.bundles.androidx.media3)\n    implementation(libs.androidx.appcompat)\n    implementation(libs.bundles.androidx.nav3)\n//    ksp(libs.krouter.reducing.compiler)\n\n    implementation(libs.koin.core)\n    implementation(libs.koin.android)\n}\n\nlistOf(\"assembleRelease\", \"assembleDebug\").forEach { taskName ->\n    tasks.whenTaskAdded {\n        if (name == taskName) {\n            doLast {\n                val buildType = if (taskName.contains(\"Release\")) \"release\" else \"debug\"\n                val apkDir = layout.buildDirectory.dir(\"outputs/apk/$buildType\").get().asFile\n                val apkFile = apkDir.listFiles()?.firstOrNull { it.name.endsWith(\".apk\") }\n                if (apkFile != null) {\n                    val versionCode = android.defaultConfig.versionCode\n                    val enableFirebaseModule = gradle.extra[\"enableFirebaseModule\"] == true\n                    val renamedApkName = if (enableFirebaseModule) {\n                        \"fread-$versionCode-$buildType.apk\"\n                    } else {\n                        \"fread-$versionCode-fdroid.apk\"\n                    }\n                    val newFile = File(apkDir, renamedApkName)\n                    apkFile.renameTo(newFile)\n                    println(\">>> APK renamed to ${newFile.absolutePath}\")\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguardFiles setting in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n\n# Uncomment this to preserve the line number information for\n# debugging stack traces.\n#-keepattributes SourceFile,LineNumberTable\n\n-dontobfuscate\n-keepattributes *\n-dontwarn androidx.test.platform.app.InstrumentationRegistry\n\n-keepnames class * implements java.io.Serializable\n-keepclassmembers class * implements java.io.Serializable {\n    static final long serialVersionUID;\n    private static final java.io.ObjectStreamField[] serialPersistentFields;\n    !static !transient <fields>;\n    private void writeObject(java.io.ObjectOutputStream);\n    private void readObject(java.io.ObjectInputStream);\n    java.lang.Object writeReplace();\n    java.lang.Object readResolve();\n}\n\n-keep class * implements android.os.Parcelable {\n  public static final android.os.Parcelable$Creator *;\n}\n\n-keep enum androidx.compose.material3.SheetValue { *; }\n\n-keepclassmembers class **.R$* {\n    public static <fields>;\n}\n\n-keep class * extends androidx.room.RoomDatabase\n-dontwarn androidx.room.paging.**\n-dontwarn androidx.lifecycle.LiveData\n-keep @androidx.room.Entity class * {\n    *;\n}\n\n# Ktor\n-dontwarn org.slf4j.impl.StaticLoggerBinder\n-dontwarn io.ktor.client.network.sockets.TimeoutExceptionsCommonKt\n-dontwarn io.ktor.client.plugins.HttpTimeout$HttpTimeoutCapabilityConfiguration\n-dontwarn io.ktor.client.plugins.HttpTimeout$Plugin\n-dontwarn io.ktor.client.plugins.HttpTimeout\n-dontwarn io.ktor.util.InternalAPI\n-dontwarn io.ktor.utils.io.ByteReadChannelJVMKt\n-dontwarn io.ktor.utils.io.CoroutinesKt\n-dontwarn io.ktor.utils.io.ReadSessionKt\n-dontwarn io.ktor.utils.io.core.Buffer$Companion\n-dontwarn io.ktor.utils.io.core.Buffer\n-dontwarn io.ktor.utils.io.core.ByteBuffersKt\n-dontwarn io.ktor.utils.io.core.BytePacketBuilder\n-dontwarn io.ktor.utils.io.core.ByteReadPacket$Companion\n-dontwarn io.ktor.utils.io.core.ByteReadPacket\n-dontwarn io.ktor.utils.io.core.CloseableJVMKt\n-dontwarn io.ktor.utils.io.core.Input\n-dontwarn io.ktor.utils.io.core.InputArraysKt\n-dontwarn io.ktor.utils.io.core.InputPrimitivesKt\n-dontwarn io.ktor.utils.io.core.Output\n-dontwarn io.ktor.utils.io.core.OutputPrimitivesKt\n-dontwarn io.ktor.utils.io.core.PreviewKt\n-dontwarn io.ktor.client.network.sockets.SocketTimeoutException\n\n-dontwarn androidx.window.extensions.area.ExtensionWindowAreaPresentation\n-dontwarn androidx.window.extensions.core.util.function.Consumer\n-dontwarn androidx.window.extensions.core.util.function.Function\n-dontwarn androidx.window.extensions.core.util.function.Predicate\n\n# Parcelable\n-keep class * implements android.os.Parcelable {\n  public static final android.os.Parcelable$Creator *;\n}\n\n-keep class com.zhangke.activitypub.entities.** { *; }\n\n-keepnames class <1>$$serializer { # -keepnames suffices; class is kept when serializer() is kept.\n    static <1>$$serializer INSTANCE;\n}\n"
  },
  {
    "path": "app/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\">\n\n    <uses-permission android:name=\"android.permission.INTERNET\" />\n    <uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\" />\n    <uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\" />\n\n    <application\n        android:name=\".FreadAndroidApplication\"\n        android:allowBackup=\"true\"\n        android:exported=\"true\"\n        android:icon=\"@mipmap/ic_launcher\"\n        android:label=\"@string/app_name\"\n        android:roundIcon=\"@mipmap/ic_launcher_round\"\n        android:supportsRtl=\"true\"\n        android:theme=\"@style/Fread\"\n        tools:replace=\"android:allowBackup\">\n\n        <activity\n            android:name=\".screen.FreadActivity\"\n            android:exported=\"true\"\n            android:launchMode=\"singleTask\"\n            android:windowSoftInputMode=\"adjustResize\">\n            <intent-filter>\n                <action android:name=\"android.intent.action.MAIN\" />\n\n                <category android:name=\"android.intent.category.LAUNCHER\" />\n            </intent-filter>\n\n            <intent-filter>\n                <action android:name=\"android.intent.action.VIEW\" />\n\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <category android:name=\"android.intent.category.BROWSABLE\" />\n\n                <data\n                    android:scheme=\"https\"\n                    android:host=\"mastodon.social\"\n                    android:path=\"/\" />\n            </intent-filter>\n\n            <intent-filter>\n                <action android:name=\"android.intent.action.VIEW\" />\n\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <category android:name=\"android.intent.category.BROWSABLE\" />\n\n                <data\n                    android:scheme=\"https\"\n                    android:host=\"mastodon.social\"\n                    android:pathPrefix=\"/@\" />\n            </intent-filter>\n\n            <intent-filter>\n                <action android:name=\"android.intent.action.VIEW\" />\n\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <category android:name=\"android.intent.category.BROWSABLE\" />\n\n                <data\n                    android:scheme=\"https\"\n                    android:host=\"mastodon.online\"\n                    android:path=\"/\" />\n            </intent-filter>\n\n            <intent-filter>\n                <action android:name=\"android.intent.action.VIEW\" />\n\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <category android:name=\"android.intent.category.BROWSABLE\" />\n\n                <data\n                    android:scheme=\"https\"\n                    android:host=\"mastodon.online\"\n                    android:pathPrefix=\"/@\" />\n            </intent-filter>\n\n            <intent-filter>\n                <action android:name=\"android.intent.action.VIEW\" />\n\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <category android:name=\"android.intent.category.BROWSABLE\" />\n\n                <data\n                    android:scheme=\"https\"\n                    android:host=\"androiddev.social\"\n                    android:path=\"/\" />\n            </intent-filter>\n\n            <intent-filter>\n                <action android:name=\"android.intent.action.VIEW\" />\n\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <category android:name=\"android.intent.category.BROWSABLE\" />\n\n                <data\n                    android:scheme=\"https\"\n                    android:host=\"androiddev.social\"\n                    android:pathPrefix=\"/@\" />\n            </intent-filter>\n\n            <intent-filter>\n                <action android:name=\"android.intent.action.VIEW\" />\n\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <category android:name=\"android.intent.category.BROWSABLE\" />\n\n                <data\n                    android:scheme=\"https\"\n                    android:host=\"m.cmx.im\"\n                    android:path=\"/\" />\n            </intent-filter>\n\n            <intent-filter>\n                <action android:name=\"android.intent.action.VIEW\" />\n\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <category android:name=\"android.intent.category.BROWSABLE\" />\n\n                <data\n                    android:scheme=\"https\"\n                    android:host=\"m.cmx.im\"\n                    android:pathPrefix=\"/@\" />\n            </intent-filter>\n\n            <intent-filter>\n                <action android:name=\"android.intent.action.VIEW\" />\n\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <category android:name=\"android.intent.category.BROWSABLE\" />\n\n                <data\n                    android:scheme=\"https\"\n                    android:host=\"alive.bar\"\n                    android:path=\"/\" />\n            </intent-filter>\n\n            <intent-filter>\n                <action android:name=\"android.intent.action.VIEW\" />\n\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <category android:name=\"android.intent.category.BROWSABLE\" />\n\n                <data\n                    android:scheme=\"https\"\n                    android:host=\"alive.bar\"\n                    android:pathPrefix=\"/@\" />\n            </intent-filter>\n\n            <intent-filter>\n                <action android:name=\"android.intent.action.VIEW\" />\n\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <category android:name=\"android.intent.category.BROWSABLE\" />\n\n                <data\n                    android:scheme=\"https\"\n                    android:host=\"wxw.moe\"\n                    android:path=\"/\" />\n            </intent-filter>\n\n            <intent-filter>\n                <action android:name=\"android.intent.action.VIEW\" />\n\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <category android:name=\"android.intent.category.BROWSABLE\" />\n\n                <data\n                    android:scheme=\"https\"\n                    android:host=\"wxw.moe\"\n                    android:pathPrefix=\"/@\" />\n            </intent-filter>\n\n            <intent-filter>\n                <action android:name=\"android.intent.action.VIEW\" />\n\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <category android:name=\"android.intent.category.BROWSABLE\" />\n\n                <data\n                    android:scheme=\"https\"\n                    android:host=\"o3o.ca\"\n                    android:path=\"/\" />\n            </intent-filter>\n\n            <intent-filter>\n                <action android:name=\"android.intent.action.VIEW\" />\n\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <category android:name=\"android.intent.category.BROWSABLE\" />\n\n                <data\n                    android:scheme=\"https\"\n                    android:host=\"o3o.ca\"\n                    android:pathPrefix=\"/@\" />\n            </intent-filter>\n\n            <intent-filter>\n                <action android:name=\"android.intent.action.VIEW\" />\n\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <category android:name=\"android.intent.category.BROWSABLE\" />\n\n                <data\n                    android:scheme=\"https\"\n                    android:host=\"pawoo.net\"\n                    android:path=\"/\" />\n            </intent-filter>\n\n            <intent-filter>\n                <action android:name=\"android.intent.action.VIEW\" />\n\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <category android:name=\"android.intent.category.BROWSABLE\" />\n\n                <data\n                    android:scheme=\"https\"\n                    android:host=\"pawoo.net\"\n                    android:pathPrefix=\"/@\" />\n            </intent-filter>\n\n            <intent-filter>\n                <action android:name=\"android.intent.action.VIEW\" />\n\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <category android:name=\"android.intent.category.BROWSABLE\" />\n\n                <data\n                    android:scheme=\"https\"\n                    android:host=\"mstdn.jp\"\n                    android:path=\"/\" />\n            </intent-filter>\n\n            <intent-filter>\n                <action android:name=\"android.intent.action.VIEW\" />\n\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <category android:name=\"android.intent.category.BROWSABLE\" />\n\n                <data\n                    android:scheme=\"https\"\n                    android:host=\"mstdn.jp\"\n                    android:pathPrefix=\"/@\" />\n            </intent-filter>\n\n            <intent-filter>\n                <action android:name=\"android.intent.action.VIEW\" />\n\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <category android:name=\"android.intent.category.BROWSABLE\" />\n\n                <data\n                    android:scheme=\"https\"\n                    android:host=\"infosec.exchange\"\n                    android:path=\"/\" />\n            </intent-filter>\n\n            <intent-filter>\n                <action android:name=\"android.intent.action.VIEW\" />\n\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <category android:name=\"android.intent.category.BROWSABLE\" />\n\n                <data\n                    android:scheme=\"https\"\n                    android:host=\"infosec.exchange\"\n                    android:pathPrefix=\"/@\" />\n            </intent-filter>\n\n            <intent-filter>\n                <action android:name=\"android.intent.action.VIEW\" />\n\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <category android:name=\"android.intent.category.BROWSABLE\" />\n\n                <data\n                    android:scheme=\"https\"\n                    android:host=\"mstdn.social\"\n                    android:path=\"/\" />\n            </intent-filter>\n\n            <intent-filter>\n                <action android:name=\"android.intent.action.VIEW\" />\n\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <category android:name=\"android.intent.category.BROWSABLE\" />\n\n                <data\n                    android:scheme=\"https\"\n                    android:host=\"mstdn.social\"\n                    android:pathPrefix=\"/@\" />\n            </intent-filter>\n\n            <intent-filter>\n                <action android:name=\"android.intent.action.VIEW\" />\n\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <category android:name=\"android.intent.category.BROWSABLE\" />\n\n                <data\n                    android:scheme=\"https\"\n                    android:host=\"planet.moe\"\n                    android:path=\"/\" />\n            </intent-filter>\n\n            <intent-filter>\n                <action android:name=\"android.intent.action.VIEW\" />\n\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <category android:name=\"android.intent.category.BROWSABLE\" />\n\n                <data\n                    android:scheme=\"https\"\n                    android:host=\"planet.moe\"\n                    android:pathPrefix=\"/@\" />\n            </intent-filter>\n\n            <intent-filter>\n                <action android:name=\"android.intent.action.VIEW\" />\n\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <category android:name=\"android.intent.category.BROWSABLE\" />\n\n                <data\n                    android:scheme=\"https\"\n                    android:host=\"mas.to\"\n                    android:path=\"/\" />\n            </intent-filter>\n\n            <intent-filter>\n                <action android:name=\"android.intent.action.VIEW\" />\n\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <category android:name=\"android.intent.category.BROWSABLE\" />\n\n                <data\n                    android:scheme=\"https\"\n                    android:host=\"mas.to\"\n                    android:pathPrefix=\"/@\" />\n            </intent-filter>\n\n            <intent-filter>\n                <action android:name=\"android.intent.action.VIEW\" />\n\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <category android:name=\"android.intent.category.BROWSABLE\" />\n\n                <data\n                    android:scheme=\"https\"\n                    android:host=\"fosstodon.org\"\n                    android:path=\"/\" />\n            </intent-filter>\n\n            <intent-filter>\n                <action android:name=\"android.intent.action.VIEW\" />\n\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <category android:name=\"android.intent.category.BROWSABLE\" />\n\n                <data\n                    android:scheme=\"https\"\n                    android:host=\"fosstodon.org\"\n                    android:pathPrefix=\"/@\" />\n            </intent-filter>\n\n            <intent-filter>\n                <action android:name=\"android.intent.action.VIEW\" />\n\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <category android:name=\"android.intent.category.BROWSABLE\" />\n\n                <data\n                    android:scheme=\"https\"\n                    android:host=\"hachyderm.io\"\n                    android:path=\"/\" />\n            </intent-filter>\n\n            <intent-filter>\n                <action android:name=\"android.intent.action.VIEW\" />\n\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <category android:name=\"android.intent.category.BROWSABLE\" />\n\n                <data\n                    android:scheme=\"https\"\n                    android:host=\"hachyderm.io\"\n                    android:pathPrefix=\"/@\" />\n            </intent-filter>\n\n            <intent-filter>\n                <action android:name=\"android.intent.action.VIEW\" />\n\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <category android:name=\"android.intent.category.BROWSABLE\" />\n\n                <data\n                    android:scheme=\"https\"\n                    android:host=\"piaille.fr\"\n                    android:path=\"/\" />\n            </intent-filter>\n\n            <intent-filter>\n                <action android:name=\"android.intent.action.VIEW\" />\n\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <category android:name=\"android.intent.category.BROWSABLE\" />\n\n                <data\n                    android:scheme=\"https\"\n                    android:host=\"piaille.fr\"\n                    android:pathPrefix=\"/@\" />\n            </intent-filter>\n\n            <intent-filter>\n                <action android:name=\"android.intent.action.VIEW\" />\n\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <category android:name=\"android.intent.category.BROWSABLE\" />\n\n                <data\n                    android:scheme=\"https\"\n                    android:host=\"mastodon.world\"\n                    android:path=\"/\" />\n            </intent-filter>\n\n            <intent-filter>\n                <action android:name=\"android.intent.action.VIEW\" />\n\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <category android:name=\"android.intent.category.BROWSABLE\" />\n\n                <data\n                    android:scheme=\"https\"\n                    android:host=\"mastodon.world\"\n                    android:pathPrefix=\"/@\" />\n            </intent-filter>\n\n            <intent-filter>\n                <action android:name=\"android.intent.action.VIEW\" />\n\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <category android:name=\"android.intent.category.BROWSABLE\" />\n\n                <data\n                    android:scheme=\"https\"\n                    android:host=\"social.vivaldi.net\"\n                    android:path=\"/\" />\n            </intent-filter>\n\n            <intent-filter>\n                <action android:name=\"android.intent.action.VIEW\" />\n\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <category android:name=\"android.intent.category.BROWSABLE\" />\n\n                <data\n                    android:scheme=\"https\"\n                    android:host=\"social.vivaldi.net\"\n                    android:pathPrefix=\"/@\" />\n            </intent-filter>\n\n            <intent-filter>\n                <action android:name=\"android.intent.action.VIEW\" />\n\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <category android:name=\"android.intent.category.BROWSABLE\" />\n\n                <data\n                    android:scheme=\"https\"\n                    android:host=\"aethy.com\"\n                    android:path=\"/\" />\n            </intent-filter>\n\n            <intent-filter>\n                <action android:name=\"android.intent.action.VIEW\" />\n\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <category android:name=\"android.intent.category.BROWSABLE\" />\n\n                <data\n                    android:scheme=\"https\"\n                    android:host=\"aethy.com\"\n                    android:pathPrefix=\"/@\" />\n            </intent-filter>\n\n            <intent-filter>\n                <action android:name=\"android.intent.action.VIEW\" />\n\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <category android:name=\"android.intent.category.BROWSABLE\" />\n\n                <data\n                    android:scheme=\"https\"\n                    android:host=\"bsky.app\"\n                    android:path=\"/\" />\n            </intent-filter>\n\n            <intent-filter>\n                <action android:name=\"android.intent.action.VIEW\" />\n\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <category android:name=\"android.intent.category.BROWSABLE\" />\n\n                <data\n                    android:scheme=\"https\"\n                    android:host=\"bsky.app\"\n                    android:pathPrefix=\"/profile\" />\n            </intent-filter>\n            <intent-filter>\n                <action android:name=\"dev.zwander.mastodonredirect.intent.action.OPEN_FEDI_LINK\" />\n\n                <category android:name=\"android.intent.category.DEFAULT\" />\n            </intent-filter>\n\n            <intent-filter>\n                <action android:name=\"android.intent.action.SEND\" />\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <data android:mimeType=\"text/plain\" />\n            </intent-filter>\n        </activity>\n    </application>\n</manifest>\n"
  },
  {
    "path": "app/src/main/java/com/zhangke/fread/FreadAndroidApplication.kt",
    "content": "package com.zhangke.fread\n\n/**\n * Created by ZhangKe on 2022/11/27.\n */\nclass FreadAndroidApplication : HostingApplication()\n"
  },
  {
    "path": "app/src/main/java/com/zhangke/fread/screen/FreadActivity.kt",
    "content": "package com.zhangke.fread.screen\n\nimport android.Manifest\nimport android.content.Intent\nimport android.content.pm.PackageManager\nimport android.os.Build\nimport android.os.Bundle\nimport android.util.Log\nimport androidx.activity.ComponentActivity\nimport androidx.activity.compose.setContent\nimport androidx.activity.enableEdgeToEdge\nimport androidx.activity.result.contract.ActivityResultContracts\nimport androidx.compose.material3.ColorScheme\nimport androidx.compose.material3.dynamicDarkColorScheme\nimport androidx.compose.material3.dynamicLightColorScheme\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.core.content.ContextCompat\nimport androidx.lifecycle.lifecycleScope\nimport com.zhangke.framework.activity.TopActivityManager\nimport com.zhangke.framework.architect.theme.FreadTheme\nimport com.zhangke.fread.common.config.FreadConfigManager\nimport com.zhangke.fread.common.daynight.DayNightHelper\nimport com.zhangke.fread.common.deeplink.ExternalInputHandler\nimport com.zhangke.fread.common.theme.ThemeType\nimport com.zhangke.fread.common.utils.ActivityResultCallback\nimport com.zhangke.fread.common.utils.CallbackableActivity\nimport com.zhangke.fread.status.StatusProvider\nimport kotlinx.coroutines.launch\nimport org.koin.android.ext.android.inject\n\nclass FreadActivity : ComponentActivity(), CallbackableActivity {\n\n    private val requestPermissionLauncher = registerForActivityResult(\n        ActivityResultContracts.RequestPermission(),\n    ) {\n        if (it) {\n            subscribeNotification()\n        }\n    }\n\n    private val callbacks = mutableMapOf<Int, ActivityResultCallback>()\n\n    private val dayNightHelper by inject<DayNightHelper>()\n    private val freadConfigManager by inject<FreadConfigManager>()\n    private val statusProvider by inject<StatusProvider>()\n\n    override fun onNewIntent(intent: Intent) {\n        super.onNewIntent(intent)\n        handleIntent(intent)\n    }\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        dayNightHelper.setDefaultMode()\n        enableEdgeToEdge()\n        super.onCreate(savedInstanceState)\n\n        initNotification()\n\n        intent?.let(::handleIntent)\n\n        setContent {\n            val themeType by freadConfigManager.themeTypeFlow.collectAsState()\n            val dayNightMode by dayNightHelper.dayNightModeFlow.collectAsState()\n            val amoledMode by dayNightHelper.amoledModeFlow.collectAsState()\n            val darkTheme = dayNightMode.isNight\n            FreadTheme(\n                darkTheme = darkTheme,\n                amoledMode = amoledMode,\n                dynamicColors = getDynamicColorScheme(darkTheme, themeType),\n            ) {\n                FreadApp()\n            }\n        }\n    }\n\n    private fun getDynamicColorScheme(\n        dark: Boolean,\n        themeType: ThemeType,\n    ): ColorScheme? {\n        if (themeType != ThemeType.SYSTEM_DYNAMIC) return null\n        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {\n            if (dark) {\n                dynamicDarkColorScheme(this)\n            } else {\n                dynamicLightColorScheme(this)\n            }\n        } else {\n            null\n        }\n    }\n\n    private fun initNotification() {\n        if (checkNotificationPermission()) {\n            subscribeNotification()\n        }\n    }\n\n    private fun subscribeNotification() {\n        lifecycleScope.launch {\n            statusProvider.accountManager.subscribeNotification()\n        }\n    }\n\n    private fun checkNotificationPermission(): Boolean {\n        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return true\n        val selfPermissionState =\n            ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)\n        if (selfPermissionState == PackageManager.PERMISSION_GRANTED) return true\n//        if (shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS)) return false\n        requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)\n        return false\n    }\n\n    private fun handleIntent(intent: Intent) {\n        Log.d(\"Z_TEST\", \"intent: ${intent.dataString}\")\n        lifecycleScope.launch {\n            if (intent.action == Intent.ACTION_SEND) {\n                if (intent.type?.startsWith(\"text/\") == true) {\n                    intent.getStringExtra(Intent.EXTRA_TEXT)\n                        ?.takeIf { it.isNotEmpty() }\n                        ?.let { ExternalInputHandler.handle(it) }\n                }\n            } else {\n                intent.data?.toString()\n                    ?.takeIf { it.isNotEmpty() }\n                    ?.let { ExternalInputHandler.handle(it) }\n            }\n            setIntent(Intent())\n        }\n    }\n\n    override fun registerCallback(requestCode: Int, callback: ActivityResultCallback) {\n        callbacks[requestCode] = callback\n    }\n\n    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {\n        super.onActivityResult(requestCode, resultCode, data)\n        TopActivityManager.updateTopActivity(this)\n        callbacks[requestCode]?.invoke(resultCode, data)\n    }\n}\n\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_launcher_foreground.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"108dp\"\n    android:height=\"108dp\"\n    android:viewportWidth=\"482\"\n    android:viewportHeight=\"326\">\n  <group android:scaleX=\"0.56\"\n      android:scaleY=\"0.37875518\"\n      android:translateX=\"106.04\"\n      android:translateY=\"101.2629\">\n    <path\n        android:pathData=\"M143.38,29.17C132.31,25.02 119.93,22.99 106.32,23.46C40.93,25.74 8.19,83.78 10.64,153.88C13.09,223.98 49.94,279.58 115.18,277.31C142.02,276.37 177.74,256.04 193.42,239.27\"\n        android:fillColor=\"#03E77A\"\n        android:fillType=\"evenOdd\"/>\n    <path\n        android:pathData=\"M143.38,29.17C132.31,25.02 119.93,22.99 106.32,23.46C40.93,25.74 8.19,83.78 10.64,153.88C13.09,223.98 49.88,277.31 115.18,277.31C142.12,277.31 162.05,268.85 177.73,252.08\"\n        android:fillColor=\"#31ACFA\"\n        android:fillType=\"evenOdd\"/>\n    <path\n        android:pathData=\"M4.64,154.09C3.39,118.14 11.12,84.75 28.08,59.86C45.17,34.78 71.44,18.67 106.11,17.46C120.46,16.96 133.63,19.11 145.49,23.56C148.59,24.72 150.16,28.18 149,31.28C147.84,34.38 144.38,35.96 141.28,34.79C130.99,30.93 119.41,29.01 106.53,29.46C75.81,30.53 53.01,44.58 38,66.62C22.85,88.85 15.44,119.52 16.63,153.67C17.83,187.8 27.37,217.3 44.03,238.13C60.56,258.8 84.36,271.31 115.18,271.31C140.65,271.31 158.93,263.4 173.35,247.98C175.61,245.56 179.41,245.43 181.83,247.7C184.25,249.96 184.38,253.76 182.11,256.18C165.17,274.3 143.58,283.31 115.18,283.31C80.71,283.31 53.46,269.14 34.65,245.63C15.98,222.27 5.9,190.06 4.64,154.09Z\"\n        android:fillColor=\"#0978D8\"/>\n    <path\n        android:pathData=\"M140.48,31.33C161.56,18.79 192.82,6.4 222.27,6.4C243.26,6.4 273.52,9.95 292.97,25.51C294.89,27.05 301.64,21.95 306.56,20.85C332.32,15.03 376.04,31.33 379.52,112.7C381.04,148.09 375.04,175.07 368.98,189.68C365.66,197.71 352.54,215.98 338.11,214.09C323.68,212.21 320.26,189.09 321.13,195.68C322.58,206.78 325.74,216.97 332.32,224.21C347.57,240.99 367.64,242.45 381.11,230.96C386.57,226.92 390.12,220.51 391.76,211.74C387.46,191.78 390.31,179.28 400.32,174.22C405.5,171.61 416.37,173.46 422.39,179.58C427.7,184.96 424.15,194.96 431.46,200.41C439.93,206.73 460.6,196.96 468.3,203.72C490.18,222.94 469.19,232.72 461.15,235.44C452.82,238.27 444.69,241.28 439.09,248.7C433.49,256.11 425.21,269.23 413.25,279.95C386.65,303.8 377.89,313.16 328.98,315.08C280.07,317 217.09,297.38 179.97,263.61C162.54,247.77 157.37,247.35 157.37,247.35C157.37,247.35 140.34,31.47 140.48,31.33Z\"\n        android:fillColor=\"#31ACFA\"\n        android:fillType=\"evenOdd\"/>\n    <path\n        android:pathData=\"M297.31,26.03C300.43,23.97 304.01,21.46 306.56,20.87C332.32,14.92 376.04,31.6 379.52,114.94C381.04,151.19 375.04,178.81 368.98,193.78C365.66,202 357.62,218.78 335.11,218.78\"\n        android:fillColor=\"#31ACFA\"\n        android:fillType=\"evenOdd\"/>\n    <path\n        android:pathData=\"M305.21,15.02C319.95,11.62 339.41,14.77 355.52,30.07C371.61,45.34 383.73,72.07 385.52,114.69C387.06,151.65 380.97,180.14 374.55,196.03C372.8,200.34 369.6,207.51 363.48,213.61C357.2,219.88 348.07,224.78 335.11,224.78C331.8,224.78 329.11,222.1 329.11,218.78C329.11,215.47 331.8,212.78 335.11,212.78C344.67,212.78 350.81,209.3 355.01,205.12C359.37,200.77 361.84,195.44 363.42,191.54C369.11,177.48 375.01,150.73 373.53,115.19C371.83,74.48 360.35,51.19 347.26,38.77C334.21,26.38 318.92,24.17 307.91,26.72C307.62,26.79 306.77,27.13 305.23,28.05C303.78,28.92 302.29,29.93 300.61,31.04C297.85,32.87 294.13,32.1 292.3,29.34C290.48,26.57 291.24,22.85 294,21.02C295.45,20.07 297.32,18.8 299.07,17.75C300.73,16.75 302.95,15.55 305.21,15.02Z\"\n        android:fillColor=\"#0978D8\"/>\n    <path\n        android:pathData=\"M316.8,151.57C316.8,128.11 319.23,95.45 306.3,55.74C300.35,37.44 287.15,26.58 271.54,20.19C255.78,13.74 237.87,12 223.57,12C209.61,12 194.23,14.75 180.31,18.94C166.34,23.15 154.31,28.67 146.91,33.92C144.21,35.84 140.46,35.2 138.54,32.5C136.63,29.8 137.26,26.05 139.96,24.13C148.79,17.86 162.16,11.87 176.85,7.45C191.61,3.01 208.18,0 223.57,0C238.62,0 258.29,1.8 276.09,9.08C294.04,16.43 310.39,29.52 317.71,52.03C331.36,93.95 328.8,128.83 328.8,151.57C328.8,163.6 328.04,176.96 329.1,189.87C330.16,202.8 332.98,213.6 339.07,220.55C345.93,228.37 352.45,232.12 358.08,233.04C363.48,233.92 368.97,232.35 374.4,227.55L374.71,227.29C378.58,224.32 381.79,221.04 384.82,214.45C385.75,212.43 385.7,210.42 384.96,207.81C384.57,206.44 384.05,205.08 383.42,203.49C382.85,202.02 382.13,200.2 381.65,198.39C379.2,189.11 381.49,181.05 386.6,175.39C391.52,169.93 398.73,167.07 405.7,167.06C415.01,167.06 425.92,172.64 431.16,184.38C432.28,186.9 433.07,188.96 433.67,190.54C434.32,192.24 434.66,193.09 434.99,193.71C435.24,194.17 435.36,194.27 435.47,194.35C435.67,194.51 436.18,194.84 437.51,195.32C439.27,195.96 441.16,195.94 444.46,195.42C447.51,194.94 452.03,193.96 457.12,193.96C463.51,193.96 470.51,197.31 475.33,202.69C480.36,208.31 483.31,216.47 480.86,226C478.23,236.23 470.5,240.6 463.76,242.97C460.5,244.12 457,244.95 454.38,245.66C451.48,246.45 449.6,247.09 448.38,247.82C447.17,248.54 445.29,250.52 443.41,252.99C442.55,254.11 441.83,255.15 441.32,255.91C441.07,256.29 440.89,256.57 440.78,256.75C440.78,256.75 440.77,256.76 440.77,256.77C440.64,257.01 440.49,257.25 440.32,257.48C431.9,269.1 420.42,282.77 402.76,296.65C375,320.49 340.96,327.35 302.43,324.65C243.76,320.55 204.05,297.09 167.22,262.36C164.81,260.09 164.7,256.29 166.97,253.88C169.24,251.47 173.04,251.36 175.45,253.63C210.83,286.99 248.04,308.82 303.26,312.68C339.92,315.25 370.46,308.63 395.05,287.45L395.26,287.28C411.75,274.34 422.47,261.63 430.41,250.7C430.47,250.61 430.52,250.52 430.58,250.43C430.77,250.13 431.02,249.72 431.33,249.26C431.95,248.32 432.83,247.06 433.88,245.7C435.78,243.21 438.85,239.54 442.2,237.53C445.02,235.84 448.4,234.85 451.24,234.08C454.36,233.23 457.08,232.6 459.78,231.65C464.95,229.83 468.07,227.54 469.24,223.01C470.59,217.77 469.04,213.66 466.39,210.7C463.53,207.5 459.66,205.96 457.12,205.96C453.26,205.96 450.22,206.66 446.32,207.28C442.65,207.85 438.19,208.33 433.42,206.6C431.49,205.9 429.67,205.05 428.08,203.81C426.4,202.5 425.28,200.99 424.42,199.39C423.65,197.95 423.03,196.3 422.46,194.83C421.86,193.25 421.18,191.46 420.2,189.26C417,182.09 410.53,179.06 405.7,179.06C401.84,179.07 397.97,180.69 395.51,183.43C393.22,185.96 391.8,189.81 393.26,195.33C393.52,196.34 393.96,197.49 394.6,199.12C395.19,200.63 395.93,202.52 396.51,204.53C397.68,208.66 398.29,213.88 395.73,219.46C391.81,227.98 387.3,232.73 382.19,236.68C374.55,243.36 365.6,246.42 356.14,244.88C346.85,243.36 338.02,237.54 330.05,228.46C321.31,218.49 318.25,204.38 317.14,190.85C316.03,177.3 316.8,162.65 316.8,151.57ZM441,256.28C441.01,256.23 441.03,256.19 441.05,256.14C441.03,256.19 441.02,256.23 441,256.28ZM441.13,255.93C441.14,255.89 441.16,255.83 441.18,255.76C441.17,255.82 441.15,255.87 441.13,255.93ZM441.35,255.12C441.36,255.06 441.37,255 441.38,254.93C441.37,255 441.36,255.06 441.35,255.12ZM441.4,254.79C441.42,254.69 441.43,254.59 441.44,254.48C441.44,254.48 441.44,254.48 441.44,254.48C441.43,254.58 441.42,254.69 441.4,254.79ZM441.46,254.21C441.46,254.16 441.46,254.1 441.46,254.05C441.46,254.1 441.46,254.15 441.46,254.21ZM441.46,253.94C441.46,253.86 441.46,253.78 441.46,253.69C441.46,253.77 441.46,253.86 441.46,253.94ZM441.44,253.49C441.44,253.41 441.43,253.33 441.42,253.25C441.43,253.33 441.44,253.41 441.44,253.49ZM441.41,253.18C441.4,253.08 441.38,252.98 441.36,252.87C441.38,252.98 441.4,253.08 441.41,253.18ZM441.34,252.76C441.32,252.66 441.3,252.56 441.27,252.45C441.3,252.56 441.32,252.66 441.34,252.76ZM441.24,252.34C441.21,252.25 441.19,252.16 441.16,252.07C441.19,252.16 441.21,252.25 441.24,252.34ZM441.1,251.92C441.07,251.84 441.04,251.75 441.01,251.67C441.04,251.75 441.07,251.84 441.1,251.92ZM440.96,251.56C440.95,251.53 440.93,251.49 440.92,251.46C440.91,251.45 440.91,251.44 440.9,251.43C440.92,251.47 440.94,251.52 440.96,251.56ZM438.74,248.94C438.74,248.93 438.73,248.93 438.72,248.92C438.71,248.92 438.7,248.91 438.7,248.91C438.71,248.92 438.73,248.93 438.74,248.94Z\"\n        android:fillColor=\"#0978D8\"/>\n    <path\n        android:pathData=\"M232.54,79.64C240.23,81.7 247.79,78.39 249.44,72.24C251.08,66.1 246.19,59.45 238.5,57.39C230.82,55.33 223.25,58.64 221.61,64.79C219.96,70.93 224.86,77.58 232.54,79.64Z\"\n        android:fillColor=\"#0978D8\"/>\n    <path\n        android:pathData=\"M311.94,76.63C305.99,78.33 300,75.63 298.55,70.58C297.1,65.54 301.45,60.07 307.39,58.36C307.51,58.26 308.57,62.3 309.77,66.45C311.2,71.36 312.4,76.5 311.94,76.63Z\"\n        android:fillColor=\"#0978D8\"\n        android:fillType=\"evenOdd\"/>\n    <path\n        android:pathData=\"M205.69,203.5C210.95,221.45 227.57,230.45 240.61,232.9C257.05,235.99 251.64,250.36 231.35,253.74C213.63,256.7 194.41,249.66 180.64,234.91C178.65,232.78 185.63,230.61 192.24,221.88C198.86,213.15 205.34,202.29 205.69,203.5Z\"\n        android:fillColor=\"#ffffff\"\n        android:fillType=\"evenOdd\"/>\n    <path\n        android:pathData=\"M152.21,234C173.19,234 190.21,214.75 190.21,191C190.21,167.25 173.19,148 152.21,148C131.22,148 114.21,167.25 114.21,191C114.21,214.75 131.22,234 152.21,234Z\"\n        android:fillColor=\"#31ACFA\"/>\n    <path\n        android:pathData=\"M198.27,195.84C197.93,192.54 200.32,189.59 203.61,189.24C206.91,188.9 209.86,191.29 210.21,194.58C212.86,219.85 194.53,242.49 169.27,245.14C165.97,245.49 163.02,243.1 162.67,239.8C162.33,236.51 164.72,233.55 168.01,233.21C186.69,231.24 200.24,214.51 198.27,195.84Z\"\n        android:fillColor=\"#0978D8\"/>\n    <path\n        android:pathData=\"M360,236.94C363.26,237.51 365.44,240.62 364.86,243.88L364.86,243.88L361.39,263.57C360.81,266.83 357.7,269.01 354.44,268.44C351.18,267.86 349,264.75 349.57,261.49L349.57,261.49L353.05,241.8C353.62,238.54 356.73,236.36 360,236.94Z\"\n        android:fillColor=\"#0978D8\"\n        android:fillType=\"evenOdd\"/>\n    <path\n        android:pathData=\"M331.65,208.34C332.73,211.46 331.07,214.88 327.94,215.95L327.94,215.95L305.98,223.51C302.85,224.59 299.43,222.92 298.35,219.79C297.27,216.66 298.94,213.25 302.07,212.17L302.07,212.17L324.03,204.62C327.16,203.54 330.57,205.21 331.65,208.34Z\"\n        android:fillColor=\"#0978D8\"\n        android:fillType=\"evenOdd\"/>\n    <path\n        android:pathData=\"M227.88,168.38C244.66,166.92 256.86,149.67 255.13,129.86C253.39,110.06 238.38,95.19 221.6,96.66C204.82,98.13 192.63,115.37 194.36,135.18C196.09,154.99 211.1,169.85 227.88,168.38Z\"\n        android:fillColor=\"#ffffff\"/>\n    <group>\n      <clip-path\n          android:pathData=\"M227.88,168.38C244.66,166.92 256.86,149.67 255.13,129.86C253.39,110.06 238.38,95.19 221.6,96.66C204.82,98.13 192.63,115.37 194.36,135.18C196.09,154.99 211.1,169.85 227.88,168.38Z\"/>\n      <path\n          android:pathData=\"M241.74,166.17C258.52,164.7 270.76,147.9 269.07,128.64C267.39,109.39 252.42,94.97 235.64,96.43C218.86,97.9 206.62,114.7 208.3,133.96C209.99,153.22 224.96,167.64 241.74,166.17Z\"\n          android:fillColor=\"#2A2779\"/>\n    </group>\n    <path\n        android:pathData=\"M222.04,88C238.27,88 248.35,101.47 252.65,114.29C254.45,119.67 255.68,129.78 255.07,136.97C254.79,140.27 251.88,142.72 248.58,142.43C245.28,142.15 242.83,139.25 243.11,135.94C243.6,130.26 242.51,121.81 241.27,118.1C237.6,107.16 230.53,100 222.04,100C215.15,100 209.34,102.58 204.92,107.53C200.41,112.59 197.02,120.5 196.01,131.55C195.71,134.85 192.79,137.28 189.49,136.98C186.19,136.67 183.76,133.75 184.06,130.45C185.24,117.6 189.3,107.01 195.97,99.54C202.73,91.97 211.83,88 222.04,88Z\"\n        android:fillColor=\"#2A2779\"/>\n    <path\n        android:pathData=\"M309.61,100.15C311.37,97.74 313.98,94.99 314.89,94.05C314.83,94.09 316.95,107.74 317.32,124.64C317.68,141.53 316.8,160.41 316.7,160.35C312.05,159.55 308.1,157.07 305.72,153.9C295.5,140.32 299.79,113.61 309.61,100.15Z\"\n        android:fillColor=\"#2A2779\"\n        android:fillType=\"evenOdd\"/>\n  </group>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/shape_alert_dialog_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"rectangle\">\n\n    <corners android:radius=\"16.dp\" />\n</shape>"
  },
  {
    "path": "app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@color/ic_launcher_background\"/>\n    <foreground android:drawable=\"@drawable/ic_launcher_foreground\"/>\n    <monochrome android:drawable=\"@drawable/ic_launcher_foreground\"/>\n</adaptive-icon>"
  },
  {
    "path": "app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@color/ic_launcher_background\"/>\n    <foreground android:drawable=\"@drawable/ic_launcher_foreground\"/>\n    <monochrome android:drawable=\"@drawable/ic_launcher_foreground\"/>\n</adaptive-icon>"
  },
  {
    "path": "app/src/main/res/values/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"purple_200\">#FFBB86FC</color>\n    <color name=\"purple_500\">#FF6200EE</color>\n    <color name=\"purple_700\">#FF3700B3</color>\n    <color name=\"teal_200\">#FF03DAC5</color>\n    <color name=\"teal_700\">#FF018786</color>\n    <color name=\"black\">#FF000000</color>\n    <color name=\"white\">#FFFFFFFF</color>\n</resources>"
  },
  {
    "path": "app/src/main/res/values/ic_launcher_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"ic_launcher_background\">#B2E1FF</color>\n</resources>"
  },
  {
    "path": "app/src/main/res/values/strings.xml",
    "content": "<resources>\n    <string name=\"app_name\">Fread</string>\n</resources>"
  },
  {
    "path": "app/src/main/res/values/themes.xml",
    "content": "<resources>\n    <style name=\"Fread\" parent=\"Theme.AppCompat.DayNight.NoActionBar\" />\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-zh/strings.xml",
    "content": "<resources>\n    <string name=\"app_name\">Fread</string>\n</resources>"
  },
  {
    "path": "app-hosting/build.gradle.kts",
    "content": "plugins {\n    id(\"fread.project.feature.kmp\")\n    id(\"com.google.devtools.ksp\")\n    id(\"kotlin-parcelize\")\n}\n\nandroid {\n    namespace = \"com.zhangke.fread.hosting\"\n}\n\nkotlin {\n    listOf(\n        iosX64(),\n        iosArm64(),\n        iosSimulatorArm64()\n    ).forEach { iosTarget ->\n        iosTarget.binaries.framework {\n            baseName = \"FreadKit\"\n            isStatic = true\n        }\n    }\n    sourceSets {\n        commonMain {\n            dependencies {\n                implementation(project(path = \":framework\"))\n                implementation(project(path = \":bizframework:status-provider\"))\n                implementation(project(path = \":commonbiz:common\"))\n                implementation(project(path = \":commonbiz:analytics\"))\n                implementation(project(path = \":commonbiz:status-ui\"))\n                implementation(project(path = \":commonbiz:sharedscreen\"))\n                implementation(project(path = \":feature:feeds\"))\n                implementation(project(path = \":feature:explore\"))\n                implementation(project(path = \":feature:profile\"))\n                implementation(project(path = \":feature:notifications\"))\n                implementation(project(path = \":plugins:activitypub-app\"))\n                implementation(project(path = \":plugins:rss\"))\n                implementation(project(path = \":plugins:bluesky\"))\n                if (gradle.extra[\"enableFirebaseModule\"] == true) {\n                    implementation(project(path = \":plugins:fread-firebase\"))\n                }\n\n                implementation(compose.components.resources)\n\n                implementation(libs.jetbrains.lifecycle.viewmodel)\n                implementation(libs.krouter.runtime)\n                implementation(libs.imageLoader)\n                implementation(libs.kotlinx.serialization.core)\n                implementation(libs.kotlinx.serialization.json)\n                implementation(libs.multiplatformsettings.coroutines)\n                implementation(libs.rssparser)\n            }\n        }\n        commonTest {\n            dependencies {\n                implementation(kotlin(\"test\"))\n            }\n        }\n        androidMain {\n            dependencies {\n                implementation(compose.preview)\n\n                implementation(libs.androidx.core.ktx)\n                implementation(libs.bundles.androidx.activity)\n                implementation(libs.bundles.androidx.datastore)\n            }\n        }\n    }\n    configureCommonMainKsp()\n}\n\ndependencies {\n    kspAll(libs.krouter.reducing.compiler)\n}\n\ncompose {\n    resources {\n        publicResClass = false\n        packageOfResClass = \"com.zhangke.fread.hosting\"\n        generateResClass = always\n    }\n}\n"
  },
  {
    "path": "app-hosting/src/androidMain/kotlin/com/zhangke/fread/HostingApplication.kt",
    "content": "package com.zhangke.fread\n\nimport android.app.Application\nimport com.seiko.imageloader.ImageLoader\nimport com.seiko.imageloader.ImageLoaderFactory\nimport com.zhangke.framework.activity.TopActivityManager\nimport com.zhangke.framework.utils.initApplication\nimport com.zhangke.framework.utils.initDebuggable\nimport com.zhangke.framework.utils.isDebugMode\nimport com.zhangke.fread.di.FreadApplication\nimport org.koin.core.component.KoinComponent\n\nabstract class HostingApplication : Application(),\n    ImageLoaderFactory,\n    KoinComponent {\n\n    override fun onCreate() {\n        super.onCreate()\n        initDebuggable(isDebugMode())\n        initApplication(this)\n        TopActivityManager.init(this)\n\n        FreadApplication.initialize()\n    }\n\n    override fun newImageLoader(): ImageLoader {\n        return getKoin().get()\n    }\n}"
  },
  {
    "path": "app-hosting/src/androidMain/kotlin/com/zhangke/fread/composable/LoadingPage.android.kt",
    "content": "package com.zhangke.fread.composable\n\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.tooling.preview.Preview\n\n@Preview\n@Composable\nprivate fun PreviewLoadingPage() {\n    LoadingPage(modifier = Modifier.fillMaxSize())\n}\n"
  },
  {
    "path": "app-hosting/src/androidMain/kotlin/com/zhangke/fread/di/HostingModule.android.kt",
    "content": "package com.zhangke.fread.di\n\nimport com.seiko.imageloader.ImageLoader\nimport com.seiko.imageloader.cache.memory.maxSizePercent\nimport com.seiko.imageloader.component.setupDefaultComponents\nimport com.seiko.imageloader.intercept.bitmapMemoryCacheConfig\nimport com.seiko.imageloader.intercept.imageMemoryCacheConfig\nimport com.seiko.imageloader.intercept.painterMemoryCacheConfig\nimport com.seiko.imageloader.option.androidContext\nimport com.zhangke.fread.common.utils.StorageHelper\nimport com.zhangke.fread.utils.ActivityHelper\nimport org.koin.android.ext.koin.androidContext\nimport org.koin.core.module.Module\nimport org.koin.core.module.dsl.singleOf\n\nactual fun Module.createPlatformModule() {\n    singleOf(::ActivityHelper)\n    single<ImageLoader> {\n        val context = androidContext()\n        val storageHelper = get<StorageHelper>()\n        ImageLoader {\n            options {\n                androidContext(context)\n            }\n            components {\n                setupDefaultComponents()\n            }\n            interceptor {\n                // cache 25% memory bitmap\n                bitmapMemoryCacheConfig {\n                    maxSizePercent(context, 0.25)\n                }\n                // cache 50 image\n                imageMemoryCacheConfig {\n                    maxSize(50)\n                }\n                // cache 50 painter\n                painterMemoryCacheConfig {\n                    maxSize(50)\n                }\n                diskCacheConfig {\n                    directory(storageHelper.cacheDir.resolve(\"image_cache\"))\n                    maxSizeBytes(512L * 1024 * 1024) // 512MB\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app-hosting/src/androidMain/kotlin/com/zhangke/fread/di/PlatformedFreadApplication.android.kt",
    "content": "package com.zhangke.fread.di\n\nimport com.zhangke.framework.utils.appContext\nimport org.koin.android.ext.koin.androidContext\nimport org.koin.android.ext.koin.androidLogger\nimport org.koin.core.KoinApplication\n\nactual class PlatformedFreadApplication {\n\n    actual fun KoinApplication.initKoin() {\n        androidLogger()\n        androidContext(appContext)\n    }\n}\n"
  },
  {
    "path": "app-hosting/src/androidMain/kotlin/com/zhangke/fread/screen/DeviceCornerRadius.android.kt",
    "content": "package com.zhangke.fread.screen\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.platform.LocalView\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport androidx.core.view.ViewCompat\nimport androidx.core.view.RoundedCornerCompat\n\n@Composable\nactual fun rememberDeviceCornerRadius(): Dp {\n    val view = LocalView.current\n    val density = LocalDensity.current\n    val insets = ViewCompat.getRootWindowInsets(view)\n    val radiusPx =\n        listOf(\n            insets?.getRoundedCorner(RoundedCornerCompat.POSITION_TOP_LEFT),\n            insets?.getRoundedCorner(RoundedCornerCompat.POSITION_TOP_RIGHT),\n            insets?.getRoundedCorner(RoundedCornerCompat.POSITION_BOTTOM_RIGHT),\n            insets?.getRoundedCorner(RoundedCornerCompat.POSITION_BOTTOM_LEFT),\n        ).maxOfOrNull { it?.radius ?: 0 } ?: 0\n    return if (radiusPx > 0) {\n        with(density) { radiusPx.toDp() }\n    } else {\n        16.dp\n    }\n}\n"
  },
  {
    "path": "app-hosting/src/androidMain/kotlin/com/zhangke/fread/utils/ActivityHelper.android.kt",
    "content": "package com.zhangke.fread.utils\n\nimport android.content.Context\nimport android.content.Intent\nimport com.zhangke.framework.utils.startActivityCompat\n\nactual class ActivityHelper(private val context: Context) {\n    actual fun goHome() {\n        val intent = Intent(Intent.ACTION_MAIN).apply {\n            flags = Intent.FLAG_ACTIVITY_NEW_TASK\n            addCategory(Intent.CATEGORY_HOME)\n        }\n        context.startActivityCompat(intent)\n    }\n}\n"
  },
  {
    "path": "app-hosting/src/commonMain/kotlin/com/zhangke/fread/CommonNavEntryProvider.kt",
    "content": "package com.zhangke.fread\n\nimport androidx.navigation3.runtime.EntryProviderScope\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.nav.NavEntryProvider\nimport com.zhangke.fread.screen.FreadHomeScreenContent\nimport com.zhangke.fread.screen.FreadHomeScreenNavKey\nimport kotlinx.serialization.modules.PolymorphicModuleBuilder\nimport kotlinx.serialization.modules.subclass\nimport org.koin.compose.viewmodel.koinViewModel\n\nclass CommonNavEntryProvider : NavEntryProvider {\n\n    override fun EntryProviderScope<NavKey>.build() {\n        entry<FreadHomeScreenNavKey> {\n            FreadHomeScreenContent(koinViewModel())\n        }\n    }\n\n    override fun PolymorphicModuleBuilder<NavKey>.polymorph() {\n        subclass(FreadHomeScreenNavKey::class)\n    }\n}\n"
  },
  {
    "path": "app-hosting/src/commonMain/kotlin/com/zhangke/fread/auth/AuthenticationPage.kt",
    "content": "package com.zhangke.fread.auth\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material3.Button\nimport androidx.compose.material3.Card\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport com.zhangke.fread.localization.LocalizedString\nimport org.jetbrains.compose.resources.stringResource\n\n@Composable\nfun AuthenticationPage(\n    isOauthFailed: Boolean,\n    onCancelClick: () -> Unit,\n    onLoginClick: () -> Unit,\n) {\n    val interactionSource = remember { MutableInteractionSource() }\n\n    Box(\n        modifier = Modifier\n            .fillMaxSize()\n            .background(Color(0xff424242))\n            .clickable(\n                interactionSource = interactionSource,\n                indication = null,\n            ) {\n                onCancelClick()\n            }\n    ) {\n\n        Card(\n            modifier = Modifier\n                .align(Alignment.Center)\n                .padding(start = 50.dp, end = 50.dp)\n                .clickable(\n                    interactionSource = interactionSource,\n                    indication = null,\n                ) {}\n        ) {\n\n            Column {\n\n                val loginMessageResId = if (isOauthFailed) {\n                    LocalizedString.authentication_page_failed_title\n                } else {\n                    LocalizedString.authentication_page_normal_title\n                }\n\n                Text(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .padding(start = 50.dp, top = 20.dp, end = 50.dp),\n                    text = stringResource(loginMessageResId),\n                    fontSize = 16.sp,\n                    fontWeight = FontWeight.SemiBold\n                )\n\n                Row(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .padding(start = 15.dp, top = 15.dp, end = 15.dp, bottom = 15.dp),\n                    horizontalArrangement = Arrangement.End,\n                    verticalAlignment = Alignment.CenterVertically\n                ) {\n\n                    Button(\n                        onClick = onCancelClick\n                    ) {\n                        Text(text = stringResource(LocalizedString.cancel))\n                    }\n                    Button(\n                        modifier = Modifier.padding(start = 15.dp),\n                        onClick = onLoginClick\n                    ) {\n                        Text(text = stringResource(LocalizedString.login))\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app-hosting/src/commonMain/kotlin/com/zhangke/fread/composable/LoadingPage.kt",
    "content": "package com.zhangke.fread.composable\n\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.material3.CircularProgressIndicator\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\n\n@Composable\nfun LoadingPage(modifier: Modifier = Modifier) {\n    Box(modifier = modifier.fillMaxSize(), Alignment.Center) {\n        CircularProgressIndicator()\n    }\n}\n"
  },
  {
    "path": "app-hosting/src/commonMain/kotlin/com/zhangke/fread/di/FreadApplication.kt",
    "content": "package com.zhangke.fread.di\n\nimport com.zhangke.framework.module.ModuleStartup\nimport com.zhangke.fread.activitypub.app.activityPubModule\nimport com.zhangke.fread.bluesky.blueskyModule\nimport com.zhangke.fread.common.commonModule\nimport com.zhangke.fread.commonbiz.shared.sharedScreenModule\nimport com.zhangke.fread.explore.di.exploreModule\nimport com.zhangke.fread.feature.message.di.notificationsModule\nimport com.zhangke.fread.feeds.di.feedsModule\nimport com.zhangke.fread.profile.di.profileModule\nimport com.zhangke.fread.rss.rssModule\nimport com.zhangke.fread.status.statusProviderModel\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport org.koin.core.KoinApplication\nimport org.koin.core.context.startKoin\n\nobject FreadApplication {\n\n    fun initialize() {\n        val koin = startKoin {\n            PlatformedFreadApplication().apply {\n                initKoin()\n            }\n            modules(\n                hostingModule,\n                commonModule,\n                profileModule,\n                activityPubModule,\n                statusProviderModel,\n                blueskyModule,\n                rssModule,\n                profileModule,\n                exploreModule,\n                notificationsModule,\n                feedsModule,\n                sharedScreenModule,\n            )\n        }\n        CoroutineScope(Dispatchers.Main).launch {\n            koin.koin.getAll<ModuleStartup>().forEach { it.onAppCreate() }\n        }\n    }\n}\n\nexpect class PlatformedFreadApplication() {\n\n    fun KoinApplication.initKoin()\n}\n"
  },
  {
    "path": "app-hosting/src/commonMain/kotlin/com/zhangke/fread/di/HostingModule.kt",
    "content": "package com.zhangke.fread.di\n\nimport com.zhangke.framework.nav.NavEntryProvider\nimport com.zhangke.fread.CommonNavEntryProvider\nimport com.zhangke.fread.screen.main.MainViewModel\nimport com.zhangke.fread.screen.main.drawer.MainDrawerViewModel\nimport org.koin.core.module.Module\nimport org.koin.core.module.dsl.factoryOf\nimport org.koin.core.module.dsl.viewModelOf\nimport org.koin.dsl.bind\nimport org.koin.dsl.module\n\nval hostingModule = module {\n    createPlatformModule()\n\n    factoryOf(::CommonNavEntryProvider) bind NavEntryProvider::class\n\n    viewModelOf(::MainViewModel)\n    viewModelOf(::MainDrawerViewModel)\n}\n\nexpect fun Module.createPlatformModule()\n"
  },
  {
    "path": "app-hosting/src/commonMain/kotlin/com/zhangke/fread/screen/DeviceCornerRadius.kt",
    "content": "package com.zhangke.fread.screen\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.unit.Dp\n\n@Composable\nexpect fun rememberDeviceCornerRadius(): Dp\n"
  },
  {
    "path": "app-hosting/src/commonMain/kotlin/com/zhangke/fread/screen/FreadApp.kt",
    "content": "package com.zhangke.fread.screen\n\nimport androidx.compose.animation.ExperimentalSharedTransitionApi\nimport androidx.compose.animation.SharedTransitionLayout\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.material.ExperimentalMaterialApi\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.ui.Modifier\nimport androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator\nimport androidx.navigation3.runtime.NavKey\nimport androidx.navigation3.runtime.entryProvider\nimport androidx.navigation3.runtime.rememberDecoratedNavEntries\nimport androidx.navigation3.runtime.rememberNavBackStack\nimport androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator\nimport androidx.navigation3.scene.DialogSceneStrategy\nimport androidx.navigation3.scene.SceneInfo\nimport androidx.navigation3.scene.SinglePaneSceneStrategy\nimport androidx.navigation3.scene.rememberSceneState\nimport androidx.navigation3.ui.NavDisplay\nimport androidx.navigationevent.compose.NavigationBackHandler\nimport androidx.navigationevent.compose.rememberNavigationEventState\nimport androidx.savedstate.serialization.SavedStateConfiguration\nimport com.seiko.imageloader.ImageLoader\nimport com.seiko.imageloader.LocalImageLoader\nimport com.zhangke.framework.blur.LocalEnableBlurAppBarStyle\nimport com.zhangke.framework.nav.LocalNavBackStack\nimport com.zhangke.framework.nav.LocalSharedTransitionScope\nimport com.zhangke.framework.nav.NavEntryProvider\nimport com.zhangke.framework.nav.popIfNotRoot\nimport com.zhangke.fread.common.action.LocalComposableActions\nimport com.zhangke.fread.common.browser.BrowserLauncher\nimport com.zhangke.fread.common.browser.LocalActivityBrowserLauncher\nimport com.zhangke.fread.common.bubble.BubbleManager\nimport com.zhangke.fread.common.bubble.LocalBubbleManager\nimport com.zhangke.fread.common.config.FreadConfigManager\nimport com.zhangke.fread.common.config.LocalConfigManager\nimport com.zhangke.fread.common.config.LocalFreadConfigManager\nimport com.zhangke.fread.common.config.LocalLocalConfigManager\nimport com.zhangke.fread.common.daynight.DayNightHelper\nimport com.zhangke.fread.common.daynight.LocalActivityDayNightHelper\nimport com.zhangke.fread.common.handler.LocalTextHandler\nimport com.zhangke.fread.common.handler.TextHandler\nimport com.zhangke.fread.common.language.ActivityLanguageHelper\nimport com.zhangke.fread.common.language.LocalActivityLanguageHelper\nimport com.zhangke.fread.common.review.FreadReviewManager\nimport com.zhangke.fread.common.review.LocalFreadReviewManager\nimport com.zhangke.fread.common.utils.GlobalScreenNavigation\nimport com.zhangke.fread.common.utils.LocalMediaFileHelper\nimport com.zhangke.fread.common.utils.LocalPlatformUriHelper\nimport com.zhangke.fread.common.utils.LocalToastHelper\nimport com.zhangke.fread.common.utils.MediaFileHelper\nimport com.zhangke.fread.common.utils.PlatformUriHelper\nimport com.zhangke.fread.common.utils.ToastHelper\nimport com.zhangke.fread.commonbiz.shared.LocalModuleScreenVisitor\nimport com.zhangke.fread.commonbiz.shared.ModuleScreenVisitor\nimport com.zhangke.fread.status.ui.style.LocalStatusUiConfig\nimport com.zhangke.fread.status.ui.style.StatusUiConfig\nimport com.zhangke.fread.utils.ActivityHelper\nimport com.zhangke.fread.utils.LocalActivityHelper\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.flow.debounce\nimport kotlinx.coroutines.launch\nimport kotlinx.serialization.modules.SerializersModule\nimport kotlinx.serialization.modules.polymorphic\nimport org.koin.compose.getKoin\n\n@OptIn(\n    ExperimentalMaterialApi::class,\n    ExperimentalSharedTransitionApi::class\n)\n@Composable\nfun FreadApp() {\n    val koin = getKoin()\n    val freadConfigManager: FreadConfigManager = koin.get()\n    val statusConfig by freadConfigManager.statusConfigFlow.collectAsState()\n    val browserLauncher = koin.get<BrowserLauncher>()\n    val imageLoader: ImageLoader = koin.get()\n    val freadReviewManager: FreadReviewManager = koin.get()\n\n    val localConfigManager: LocalConfigManager = koin.get()\n    val platformUriHelper: PlatformUriHelper = koin.get()\n    val mediaFileHelper: MediaFileHelper = koin.get()\n    val toastHelper: ToastHelper = koin.get()\n    val activityDayNightHelper: DayNightHelper = koin.get()\n    val activityLanguageHelper: ActivityLanguageHelper = koin.get()\n    val textHandler: TextHandler = koin.get()\n    val activityHelper: ActivityHelper = koin.get()\n    val moduleScreenVisitor: ModuleScreenVisitor = koin.get()\n    val bubbleManager: BubbleManager = koin.get()\n    val enableBlurAppBarStyle by freadConfigManager.enableBlurAppBarStyleFlow.collectAsState()\n    CompositionLocalProvider(\n        LocalStatusUiConfig provides StatusUiConfig.create(config = statusConfig),\n        LocalImageLoader provides imageLoader,\n        LocalLocalConfigManager provides localConfigManager,\n        LocalFreadConfigManager provides freadConfigManager,\n        LocalPlatformUriHelper provides platformUriHelper,\n        LocalMediaFileHelper provides mediaFileHelper,\n        LocalFreadReviewManager provides freadReviewManager,\n        LocalActivityLanguageHelper provides activityLanguageHelper,\n        LocalActivityDayNightHelper provides activityDayNightHelper,\n        LocalTextHandler provides textHandler,\n        LocalToastHelper provides toastHelper,\n        LocalActivityHelper provides activityHelper,\n        LocalModuleScreenVisitor provides moduleScreenVisitor,\n        LocalBubbleManager provides bubbleManager,\n        LocalActivityBrowserLauncher provides browserLauncher,\n        LocalEnableBlurAppBarStyle provides enableBlurAppBarStyle,\n    ) {\n        val navEntryProviders = remember(koin) { koin.getAll<NavEntryProvider>() }\n        val backStack = rememberNavBackStack(\n            configuration = SavedStateConfiguration {\n                serializersModule = SerializersModule {\n                    polymorphic(NavKey::class) {\n                        for (provider in navEntryProviders) {\n                            with(provider) {\n                                polymorph()\n                            }\n                        }\n                    }\n                }\n            },\n            FreadHomeScreenNavKey,\n        )\n        SharedTransitionLayout {\n            CompositionLocalProvider(\n                LocalSharedTransitionScope provides this,\n                LocalNavBackStack provides backStack,\n            ) {\n                val onBack: () -> Unit = { backStack.popIfNotRoot() }\n                val entries = rememberDecoratedNavEntries(\n                    backStack = backStack,\n                    entryDecorators = listOf(\n                        rememberSaveableStateHolderNavEntryDecorator(),\n                        rememberViewModelStoreNavEntryDecorator(),\n                        rememberPredictiveBackEntryDecorator(),\n                    ),\n                    entryProvider = entryProvider {\n                        for (provider in navEntryProviders) {\n                            with(provider) { build() }\n                        }\n                    },\n                )\n                val sceneState = rememberSceneState(\n                    entries = entries,\n                    sceneStrategy = remember {\n                        DialogSceneStrategy<NavKey>() then SinglePaneSceneStrategy()\n                    },\n                    onBack = onBack,\n                )\n                val currentInfo = SceneInfo(sceneState.currentScene)\n                val previousSceneInfos = sceneState.previousScenes.map { SceneInfo(it) }\n                val navigationEventState = rememberNavigationEventState(\n                    currentInfo = currentInfo,\n                    backInfo = previousSceneInfos,\n                )\n                NavigationBackHandler(\n                    state = navigationEventState,\n                    isBackEnabled = sceneState.currentScene.previousEntries.isNotEmpty(),\n                    onBackCompleted = {\n                        repeat(entries.size - sceneState.currentScene.previousEntries.size) {\n                            onBack()\n                        }\n                    },\n                )\n                val predictiveBackState = rememberPredictiveBackState(navigationEventState)\n                LaunchedEffect(Unit) {\n                    GlobalScreenNavigation.openScreenFlow\n                        .debounce(300)\n                        .collect { key -> backStack.add(key) }\n                }\n                CompositionLocalProvider(\n                    LocalPredictiveBackState provides predictiveBackState,\n                ) {\n                    NavDisplay(\n                        sceneState = sceneState,\n                        navigationEventState = navigationEventState,\n                        transitionSpec = freadTransitionSpec(),\n                        popTransitionSpec = freadPopTransitionSpec(),\n                        predictivePopTransitionSpec = freadPredictivePopTransitionSpec(),\n                    )\n                }\n                val browserLauncher = LocalActivityBrowserLauncher.current\n                RegisterNotificationAction(browserLauncher)\n                val bubbles by bubbleManager.bubbleListFlow.collectAsState()\n                if (bubbles.isNotEmpty()) {\n                    Column(modifier = Modifier.fillMaxWidth()) {\n                        for (bubble in bubbles) {\n                            with(bubble) { Content() }\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun RegisterNotificationAction(\n    browserLauncher: BrowserLauncher,\n) {\n    val composableActions = LocalComposableActions.current\n    val coroutineScope = rememberCoroutineScope()\n    LaunchedEffect(composableActions) {\n        composableActions.actionFlow.collect { action ->\n            if (handleHttpUrl(action, browserLauncher, coroutineScope)) {\n                composableActions.resetReplayCache()\n            }\n        }\n    }\n}\n\nprivate fun handleHttpUrl(\n    action: String,\n    browserLauncher: BrowserLauncher,\n    coroutineScope: CoroutineScope\n): Boolean {\n    if (!action.lowercase().startsWith(\"http\")) return false\n    coroutineScope.launch {\n        browserLauncher.launchWebTabInApp(url = action, isFromExternal = true)\n    }\n    return true\n}\n"
  },
  {
    "path": "app-hosting/src/commonMain/kotlin/com/zhangke/fread/screen/FreadScreen.kt",
    "content": "package com.zhangke.fread.screen\n\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.slideInVertically\nimport androidx.compose.animation.slideOutVertically\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.RowScope\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.widthIn\nimport androidx.compose.foundation.pager.HorizontalPager\nimport androidx.compose.foundation.pager.PagerState\nimport androidx.compose.foundation.pager.rememberPagerState\nimport androidx.compose.material3.DrawerState\nimport androidx.compose.material3.DrawerValue\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.LocalContentColor\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.ModalDrawerSheet\nimport androidx.compose.material3.ModalNavigationDrawer\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableLongStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.ExperimentalComposeUiApi\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.layout.onSizeChanged\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.unit.dp\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.blur.LocalBlurController\nimport com.zhangke.framework.blur.rememberBlurController\nimport com.zhangke.framework.composable.BackHandler\nimport com.zhangke.framework.composable.ConsumeFlow\nimport com.zhangke.framework.composable.LocalContentPadding\nimport com.zhangke.framework.composable.NavigationBar\nimport com.zhangke.framework.composable.NavigationBarItem\nimport com.zhangke.framework.nav.Tab\nimport com.zhangke.framework.utils.pxToDp\nimport com.zhangke.fread.common.action.LocalComposableActions\nimport com.zhangke.fread.common.action.OpenNotificationPageAction\nimport com.zhangke.fread.common.review.LocalFreadReviewManager\nimport com.zhangke.fread.common.utils.getCurrentTimeMillis\nimport com.zhangke.fread.explore.screens.home.ExploreTab\nimport com.zhangke.fread.feature.message.screens.home.NotificationsTab\nimport com.zhangke.fread.feeds.pages.home.FeedsHomeTab\nimport com.zhangke.fread.profile.screen.home.ProfileTab\nimport com.zhangke.fread.screen.main.MainViewModel\nimport com.zhangke.fread.screen.main.drawer.MainDrawer\nimport com.zhangke.fread.status.ui.common.LocalNestedTabConnection\nimport com.zhangke.fread.status.ui.common.NestedTabConnection\nimport com.zhangke.fread.status.ui.style.LocalStatusUiConfig\nimport com.zhangke.fread.status.ui.update.AppUpdateDialog\nimport com.zhangke.fread.status.ui.utils.getScreenWidth\nimport com.zhangke.fread.utils.LocalActivityHelper\nimport kotlinx.coroutines.launch\nimport kotlinx.serialization.Serializable\n\n@Serializable\nobject FreadHomeScreenNavKey : NavKey\n\n@OptIn(ExperimentalComposeUiApi::class)\n@Composable\nfun FreadHomeScreenContent(viewModel: MainViewModel) {\n    val density = LocalDensity.current\n    val activityHelper = LocalActivityHelper.current\n    val statusUiConfig = LocalStatusUiConfig.current\n    val tabs = remember { createMainTabs() }\n    val uiState by viewModel.uiState.collectAsState()\n    val drawerState = remember { DrawerState(initialValue = DrawerValue.Closed) }\n    val coroutineScope = rememberCoroutineScope()\n    val nestedTabConnection = remember { NestedTabConnection() }\n    var inFeedsTab by rememberSaveable { mutableStateOf(false) }\n    var navigationBarHeight by remember { mutableStateOf(64.dp) }\n    ConsumeFlow(nestedTabConnection.openDrawerFlow) {\n        if (inFeedsTab) {\n            drawerState.open()\n        }\n    }\n    val blurController = rememberBlurController()\n    CompositionLocalProvider(\n        LocalNestedTabConnection provides nestedTabConnection,\n        LocalContentPadding provides PaddingValues(bottom = navigationBarHeight),\n        LocalBlurController provides blurController,\n        LocalContentColor provides MaterialTheme.colorScheme.onSurface,\n    ) {\n        ModalNavigationDrawer(\n            drawerState = drawerState,\n            gesturesEnabled = inFeedsTab,\n            drawerContent = {\n                val drawerWidth = getScreenWidth() * 0.8F\n                ModalDrawerSheet(\n                    modifier = Modifier.widthIn(max = drawerWidth),\n                    drawerContainerColor = MaterialTheme.colorScheme.surface,\n                ) {\n                    MainDrawer(\n                        onDismissRequest = {\n                            coroutineScope.launch {\n                                drawerState.close()\n                            }\n                        },\n                    )\n                }\n            },\n        ) {\n            val pagerState = rememberPagerState(pageCount = tabs::size)\n            BackHandler(true) {\n                if (drawerState.isOpen) {\n                    coroutineScope.launch {\n                        drawerState.close()\n                    }\n                } else if (inFeedsTab) {\n                    activityHelper.goHome()\n                } else {\n                    pagerState.requestScrollToPage(0)\n                }\n            }\n            LaunchedEffect(pagerState.currentPage) {\n                inFeedsTab = pagerState.currentPage == 0\n            }\n            RegisterNotificationAction(tabs, pagerState)\n            Box(modifier = Modifier.fillMaxSize()) {\n                HorizontalPager(\n                    state = pagerState,\n                    userScrollEnabled = false,\n                ) { page ->\n                    Box(modifier = Modifier.fillMaxSize()) {\n                        tabs[page].Content()\n                    }\n                }\n                val inImmersiveMode by nestedTabConnection.inImmersiveFlow.collectAsState()\n                AnimatedVisibility(\n                    modifier = Modifier\n                        .align(Alignment.BottomCenter)\n                        .fillMaxWidth(),\n                    visible = !inFeedsTab || !inImmersiveMode || !statusUiConfig.immersiveNavBar,\n                    enter = slideInVertically(\n                        initialOffsetY = { it },\n                    ),\n                    exit = slideOutVertically(\n                        targetOffsetY = { it },\n                    ),\n                ) {\n                    NavigationBar(\n                        modifier = Modifier.onSizeChanged {\n                            navigationBarHeight = it.height.pxToDp(density)\n                        },\n                    ) {\n                        tabs.forEachIndexed { index, tab ->\n                            TabNavigationItem(\n                                tab = tab,\n                                index = index,\n                                pagerState = pagerState,\n                                detectDoubleTap = inFeedsTab,\n                                onDoubleTap = {\n                                    coroutineScope.launch {\n                                        nestedTabConnection.scrollToTop()\n                                    }\n                                },\n                            )\n                        }\n                    }\n                }\n            }\n        }\n    }\n    if (uiState.newAppReleaseInfo != null) {\n        AppUpdateDialog(\n            appReleaseInfo = uiState.newAppReleaseInfo!!,\n            onCancel = viewModel::onCancelClick,\n            onUpdateClick = viewModel::onUpdateClick,\n        )\n    }\n}\n\nprivate fun createMainTabs(): List<Tab> {\n    return listOf(\n        FeedsHomeTab(),\n        ExploreTab(),\n        NotificationsTab(),\n        ProfileTab(),\n    )\n}\n\n@Composable\nprivate fun RowScope.TabNavigationItem(\n    tab: Tab,\n    index: Int,\n    pagerState: PagerState,\n    detectDoubleTap: Boolean,\n    onDoubleTap: () -> Unit,\n) {\n    val selected = pagerState.currentPage == index\n    val latestClickTime = remember { mutableLongStateOf(0L) }\n    val freadReviewManager = LocalFreadReviewManager.current\n    NavigationBarItem(\n        selected = selected,\n        onClick = {\n            if (selected) {\n                val currentTime = getCurrentTimeMillis()\n                if (detectDoubleTap && currentTime - latestClickTime.value < 500) {\n                    onDoubleTap()\n                }\n                latestClickTime.value = currentTime\n                return@NavigationBarItem\n            } else {\n                pagerState.requestScrollToPage(index)\n                latestClickTime.value = 0L\n                freadReviewManager.trigger()\n            }\n        },\n        alwaysShowLabel = false,\n        icon = {\n            Icon(\n                painter = tab.options!!.icon!!,\n                contentDescription = tab.options!!.title,\n            )\n        },\n    )\n}\n\n@Composable\nprivate fun RegisterNotificationAction(\n    tabs: List<Tab>,\n    pagerState: PagerState,\n) {\n    val composableActions = LocalComposableActions.current\n    LaunchedEffect(tabs, composableActions) {\n        composableActions.actionFlow.collect { action ->\n            if (handleNotificationAction(action, tabs, pagerState)) {\n                composableActions.resetReplayCache()\n            }\n        }\n    }\n}\n\nprivate fun handleNotificationAction(\n    action: String,\n    tabs: List<Tab>,\n    pagerState: PagerState,\n): Boolean {\n    if (!action.startsWith(OpenNotificationPageAction.URI)) return false\n    val notificationTabIndex = tabs.indexOfFirst { it is NotificationsTab }\n    if (pagerState.currentPage != notificationTabIndex) {\n        pagerState.requestScrollToPage(notificationTabIndex)\n    }\n    return true\n}\n"
  },
  {
    "path": "app-hosting/src/commonMain/kotlin/com/zhangke/fread/screen/NavDisplayTransitions.kt",
    "content": "package com.zhangke.fread.screen\n\nimport androidx.compose.animation.AnimatedContentTransitionScope\nimport androidx.compose.animation.ContentTransform\nimport androidx.compose.animation.EnterTransition\nimport androidx.compose.animation.ExitTransition\nimport androidx.compose.animation.core.tween\nimport androidx.compose.animation.fadeIn\nimport androidx.compose.animation.fadeOut\nimport androidx.compose.animation.scaleIn\nimport androidx.navigation3.scene.Scene\nimport androidx.navigationevent.NavigationEvent.SwipeEdge\n\nprivate const val DEFAULT_TRANSITION_DURATION_MILLISECOND = 700\nprivate const val PREDICTIVE_POP_END_DURATION_MILLISECOND = 1\n\nfun <T : Any> freadTransitionSpec(): AnimatedContentTransitionScope<Scene<T>>.() -> ContentTransform =\n    {\n        ContentTransform(\n            targetContentEnter = defaultFadeIn() + scaleIn(initialScale = 0.6F),\n            initialContentExit = defaultFadeOut(),\n        )\n    }\n\nfun <T : Any> freadPopTransitionSpec(): AnimatedContentTransitionScope<Scene<T>>.() -> ContentTransform =\n    {\n        ContentTransform(\n            targetContentEnter = defaultFadeIn(),\n            initialContentExit = defaultFadeOut(),\n        )\n    }\n\nfun <T : Any> freadPredictivePopTransitionSpec(): AnimatedContentTransitionScope<Scene<T>>.(@SwipeEdge Int) -> ContentTransform =\n    {\n        ContentTransform(\n            targetContentEnter = EnterTransition.None,\n            initialContentExit = ExitTransition.None,\n        )\n    }\n\nprivate fun defaultFadeIn(): EnterTransition = fadeIn(\n    animationSpec = tween(durationMillis = DEFAULT_TRANSITION_DURATION_MILLISECOND)\n)\n\nprivate fun defaultFadeOut(): ExitTransition = fadeOut(\n    animationSpec = tween(durationMillis = DEFAULT_TRANSITION_DURATION_MILLISECOND)\n)\n"
  },
  {
    "path": "app-hosting/src/commonMain/kotlin/com/zhangke/fread/screen/PredictiveBackEntryDecorator.kt",
    "content": "package com.zhangke.fread.screen\n\nimport androidx.compose.animation.EnterExitState\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.Immutable\nimport androidx.compose.runtime.SideEffect\nimport androidx.compose.runtime.compositionLocalOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.draw.drawWithContent\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.TransformOrigin\nimport androidx.compose.ui.graphics.graphicsLayer\nimport androidx.compose.ui.unit.dp\nimport androidx.navigation3.runtime.NavEntry\nimport androidx.navigation3.runtime.NavEntryDecorator\nimport androidx.navigation3.ui.LocalNavAnimatedContentScope\nimport androidx.navigationevent.NavigationEvent\nimport androidx.navigationevent.NavigationEventTransitionState\nimport androidx.navigationevent.compose.NavigationEventState\nimport com.zhangke.framework.architect.theme.dialogScrim\nimport com.zhangke.framework.nav.FREAD_DIALOG_METADATA_KEY\n\n@Immutable\ndata class PredictiveBackState(\n    val inProgress: Boolean = false,\n    val progress: Float = 0f,\n    @param:NavigationEvent.SwipeEdge val swipeEdge: Int = NavigationEvent.EDGE_NONE,\n)\n\nval LocalPredictiveBackState = compositionLocalOf { PredictiveBackState() }\n\nprivate const val PREDICTIVE_BACK_TARGET_SCALE = 0.8f\nprivate const val PREDICTIVE_BACK_PIVOT_EDGE_FRACTION = 0.85f\n\n@Immutable\nprivate data class RetainedExitPredictiveBackState(\n    val progress: Float,\n    @param:NavigationEvent.SwipeEdge val swipeEdge: Int,\n)\n\n@Composable\nfun <T : Any> rememberPredictiveBackEntryDecorator(): NavEntryDecorator<T> =\n    remember { NavEntryDecorator { entry -> PredictiveBackDecoratedEntry(entry) } }\n\n@Composable\nfun rememberPredictiveBackState(\n    navigationEventState: NavigationEventState<*>,\n): PredictiveBackState {\n    return when (val transitionState = navigationEventState.transitionState) {\n        is NavigationEventTransitionState.InProgress -> {\n            if (transitionState.direction == NavigationEventTransitionState.TRANSITIONING_BACK) {\n                PredictiveBackState(\n                    inProgress = true,\n                    progress = transitionState.latestEvent.progress.coerceIn(0f, 1f),\n                    swipeEdge = transitionState.latestEvent.swipeEdge,\n                )\n            } else {\n                PredictiveBackState()\n            }\n        }\n\n        else -> PredictiveBackState()\n    }\n}\n\n@Composable\nprivate fun <T : Any> PredictiveBackDecoratedEntry(entry: NavEntry<T>) {\n    val isDialogEntry = entry.metadata[FREAD_DIALOG_METADATA_KEY] as? Boolean\n    if (isDialogEntry == true) {\n        entry.Content()\n        return\n    }\n    val predictiveBackState = LocalPredictiveBackState.current\n    val transition = LocalNavAnimatedContentScope.current.transition\n    val isExiting = transition.targetState == EnterExitState.PostExit\n    val isEntering =\n        transition.targetState == EnterExitState.Visible && transition.currentState == EnterExitState.PreEnter\n    val deviceCornerRadius = rememberDeviceCornerRadius()\n    var retainedExitPredictiveBackState by remember {\n        mutableStateOf<RetainedExitPredictiveBackState?>(null)\n    }\n    val activeExitPredictiveBackState =\n        if (predictiveBackState.inProgress && isExiting) {\n            RetainedExitPredictiveBackState(\n                progress = predictiveBackState.progress,\n                swipeEdge = predictiveBackState.swipeEdge,\n            )\n        } else {\n            null\n        }\n    SideEffect {\n        when {\n            activeExitPredictiveBackState != null -> {\n                retainedExitPredictiveBackState = activeExitPredictiveBackState\n            }\n\n            !isExiting -> {\n                retainedExitPredictiveBackState = null\n            }\n        }\n    }\n    // Keep the last predictive-back frame for the exiting entry until it is removed.\n    val effectiveExitPredictiveBackState =\n        activeExitPredictiveBackState ?: retainedExitPredictiveBackState?.takeIf { isExiting }\n    val scale =\n        if (effectiveExitPredictiveBackState != null) {\n            1f - (1f - PREDICTIVE_BACK_TARGET_SCALE) * effectiveExitPredictiveBackState.progress\n        } else {\n            1f\n        }\n    val transformOrigin =\n        when (effectiveExitPredictiveBackState?.swipeEdge ?: predictiveBackState.swipeEdge) {\n            NavigationEvent.EDGE_LEFT ->\n                TransformOrigin(PREDICTIVE_BACK_PIVOT_EDGE_FRACTION, 0.5f)\n\n            NavigationEvent.EDGE_RIGHT ->\n                TransformOrigin(1f - PREDICTIVE_BACK_PIVOT_EDGE_FRACTION, 0.5f)\n\n            else -> TransformOrigin.Center\n        }\n    val clipRadius = if (effectiveExitPredictiveBackState != null) {\n        deviceCornerRadius\n    } else {\n        0.dp\n    }\n    val scrimVisible = predictiveBackState.inProgress && isEntering\n    val scrimColor = if (scrimVisible) {\n        MaterialTheme.colorScheme.dialogScrim\n    } else {\n        Color.Transparent\n    }\n    val modifier = Modifier.then(\n        if (scale != 1f) {\n            Modifier.graphicsLayer {\n                scaleX = scale\n                scaleY = scale\n                this.transformOrigin = transformOrigin\n            }\n        } else {\n            Modifier\n        }\n    ).then(\n        if (clipRadius > 0.dp) {\n            Modifier.clip(RoundedCornerShape(clipRadius))\n        } else {\n            Modifier\n        }\n    ).then(\n        if (scrimColor.alpha > 0f) {\n            Modifier.drawWithContent {\n                drawContent()\n                drawRect(scrimColor)\n            }\n        } else {\n            Modifier\n        }\n    )\n    Box(modifier = modifier) {\n        entry.Content()\n    }\n}\n"
  },
  {
    "path": "app-hosting/src/commonMain/kotlin/com/zhangke/fread/screen/main/MainPageUiState.kt",
    "content": "package com.zhangke.fread.screen.main\n\nimport com.zhangke.fread.common.update.AppReleaseInfo\n\ndata class MainPageUiState(\n    val newAppReleaseInfo: AppReleaseInfo?,\n)\n"
  },
  {
    "path": "app-hosting/src/commonMain/kotlin/com/zhangke/fread/screen/main/MainViewModel.kt",
    "content": "package com.zhangke.fread.screen.main\n\nimport androidx.lifecycle.ViewModel\nimport com.zhangke.framework.ktx.launchInViewModel\nimport com.zhangke.fread.common.update.AppReleaseInfo\nimport com.zhangke.fread.common.update.AppUpdateManager\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.update\n\nclass MainViewModel (\n    private val updateManager: AppUpdateManager,\n) : ViewModel() {\n\n    private val _uiState = MutableStateFlow(MainPageUiState(null))\n    val uiState = _uiState.asStateFlow()\n\n    init {\n        launchInViewModel {\n            if (updateManager.enableAutoCheckUpdate) {\n                delay(5000)\n                updateManager.checkForUpdate()\n                    .onSuccess { (needUpdate, info) ->\n                        if (needUpdate) {\n                            _uiState.update { it.copy(newAppReleaseInfo = info) }\n                        }\n                    }\n            }\n        }\n    }\n\n    fun onCancelClick(releaseInfo: AppReleaseInfo) {\n        _uiState.update { it.copy(newAppReleaseInfo = null) }\n        launchInViewModel {\n            updateManager.ignoreVersion(releaseInfo)\n        }\n    }\n\n    fun onUpdateClick(releaseInfo: AppReleaseInfo) {\n        _uiState.update { it.copy(newAppReleaseInfo = null) }\n        launchInViewModel {\n            updateManager.updateApp(releaseInfo)\n        }\n    }\n}"
  },
  {
    "path": "app-hosting/src/commonMain/kotlin/com/zhangke/fread/screen/main/drawer/MainDrawer.kt",
    "content": "package com.zhangke.fread.screen.main.drawer\n\nimport androidx.compose.animation.core.animateDpAsState\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Add\nimport androidx.compose.material.icons.filled.Settings\nimport androidx.compose.material.icons.outlined.Coffee\nimport androidx.compose.material.icons.outlined.Settings\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.HorizontalDivider\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.key\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.alpha\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport com.zhangke.framework.composable.currentOrThrow\nimport com.zhangke.framework.composable.ConsumeOpenScreenFlow\nimport com.zhangke.framework.composable.SimpleIconButton\nimport com.zhangke.framework.nav.LocalNavBackStack\nimport com.zhangke.fread.commonbiz.shared.LocalModuleScreenVisitor\nimport com.zhangke.fread.common.composable.EmptyContent\nimport com.zhangke.fread.feeds.pages.manager.add.type.SelectContentTypeScreenNavKey\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.profile.screen.setting.SettingScreenNavKey\nimport com.zhangke.fread.status.model.FreadContent\nimport com.zhangke.fread.status.ui.common.LocalNestedTabConnection\nimport com.zhangke.fread.statusui.ic_drag_indicator\nimport kotlinx.coroutines.launch\nimport org.burnoutcrew.reorderable.ReorderableItem\nimport org.burnoutcrew.reorderable.detectReorderAfterLongPress\nimport org.burnoutcrew.reorderable.rememberReorderableLazyListState\nimport org.burnoutcrew.reorderable.reorderable\nimport org.jetbrains.compose.resources.painterResource\nimport org.jetbrains.compose.resources.stringResource\nimport org.koin.compose.viewmodel.koinViewModel\n\n@Composable\nfun MainDrawer(onDismissRequest: () -> Unit) {\n    val backStack = LocalNavBackStack.currentOrThrow\n    val screenVisitor = LocalModuleScreenVisitor.current\n    val viewModel = koinViewModel<MainDrawerViewModel>()\n    val uiState by viewModel.uiState.collectAsState()\n    val mainTabConnection = LocalNestedTabConnection.current\n    val coroutineScope = rememberCoroutineScope()\n    MainDrawerContent(\n        uiState = uiState,\n        onContentConfigClick = {\n            onDismissRequest()\n            coroutineScope.launch {\n                mainTabConnection.scrollToContentTab(it)\n            }\n        },\n        onAddContentClick = {\n            onDismissRequest()\n            backStack.add(SelectContentTypeScreenNavKey)\n        },\n        onMove = viewModel::onContentConfigMove,\n        onEditClick = {\n            onDismissRequest()\n            viewModel.onContentConfigEditClick(it)\n        },\n        onSettingClick = {\n            onDismissRequest()\n            backStack.add(SettingScreenNavKey)\n        },\n        onDonateClick = {\n            backStack.add(screenVisitor.profileScreenVisitor.getDonateScreen())\n        },\n    )\n    ConsumeOpenScreenFlow(viewModel.openScreenFlow)\n}\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nprivate fun MainDrawerContent(\n    uiState: MainDrawerUiState,\n    onContentConfigClick: (FreadContent) -> Unit,\n    onAddContentClick: () -> Unit,\n    onMove: (from: Int, to: Int) -> Unit,\n    onEditClick: (FreadContent) -> Unit,\n    onSettingClick: () -> Unit,\n    onDonateClick: () -> Unit,\n) {\n    val contentConfigList = uiState.contentConfigList\n    Surface(modifier = Modifier.fillMaxSize()) {\n        if (contentConfigList.isEmpty()) {\n            EmptyContent(Modifier.fillMaxSize(), onClick = onAddContentClick)\n        } else {\n            Column(\n                modifier = Modifier.fillMaxSize()\n            ) {\n                TopAppBar(\n                    title = {\n                        Text(text = stringResource(LocalizedString.main_drawer_title))\n                    },\n                    actions = {\n                        SimpleIconButton(\n                            onClick = onAddContentClick,\n                            imageVector = Icons.Default.Add,\n                            contentDescription = \"Add Content\",\n                        )\n                    },\n                )\n                HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp))\n                Spacer(modifier = Modifier.height(16.dp))\n                var configListInUi by remember(contentConfigList) {\n                    mutableStateOf(contentConfigList)\n                }\n                key(contentConfigList) {\n                    val state = rememberReorderableLazyListState(\n                        onMove = { from, to ->\n                            if (contentConfigList.isEmpty()) return@rememberReorderableLazyListState\n                            configListInUi = configListInUi.toMutableList().apply {\n                                add(to.index, removeAt(from.index))\n                            }\n                        },\n                        onDragEnd = { startIndex, endIndex ->\n                            onMove(startIndex, endIndex)\n                        }\n                    )\n                    LazyColumn(\n                        state = state.listState,\n                        modifier = Modifier\n                            .fillMaxWidth()\n                            .weight(1F)\n                            .reorderable(state)\n                            .detectReorderAfterLongPress(state)\n                    ) {\n                        itemsIndexed(\n                            items = configListInUi,\n                            key = { _, item -> item.hashCode() }\n                        ) { _, contentConfig ->\n                            ReorderableItem(state, contentConfig.hashCode()) { dragging ->\n                                val elevation by animateDpAsState(\n                                    targetValue = if (dragging) 16.dp else 0.dp,\n                                    label = \"MainDrawerItemElevation\",\n                                )\n                                Surface(\n                                    modifier = Modifier\n                                        .fillMaxWidth(),\n                                    shadowElevation = elevation,\n                                ) {\n                                    ContentConfigItem(\n                                        modifier = Modifier\n                                            .fillMaxWidth(),\n                                        mainDrawerContent = contentConfig,\n                                        onClick = { onContentConfigClick(contentConfig.content) },\n                                        onEditClick = onEditClick,\n                                    )\n                                }\n                            }\n                        }\n                    }\n                }\n                HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp))\n                Row(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .height(58.dp)\n                        .clickable {\n                            onSettingClick()\n                        },\n                    verticalAlignment = Alignment.CenterVertically,\n                ) {\n                    Spacer(modifier = Modifier.width(16.dp))\n                    Icon(\n                        imageVector = Icons.Outlined.Settings,\n                        contentDescription = \"Settings\",\n                    )\n                    Spacer(modifier = Modifier.width(16.dp))\n                    Text(text = stringResource(LocalizedString.main_drawer_settings))\n                }\n                Row(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .height(48.dp)\n                        .clickable { onDonateClick() },\n                    verticalAlignment = Alignment.CenterVertically,\n                ) {\n                    Spacer(modifier = Modifier.width(16.dp))\n                    Icon(\n                        imageVector = Icons.Outlined.Coffee,\n                        contentDescription = \"Donate\",\n                    )\n                    Spacer(modifier = Modifier.width(16.dp))\n                    Text(text = stringResource(LocalizedString.main_drawer_donate))\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun ContentConfigItem(\n    modifier: Modifier,\n    mainDrawerContent: MainDrawerContent,\n    onClick: () -> Unit,\n    onEditClick: (FreadContent) -> Unit,\n) {\n    Row(\n        modifier = modifier\n            .clickable { onClick() }\n            .padding(horizontal = 16.dp),\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        Column(\n            modifier = Modifier\n                .align(Alignment.CenterVertically)\n                .weight(1F)\n                .padding(top = 16.dp, bottom = 6.dp, end = 4.dp),\n        ) {\n            Text(\n                modifier = Modifier,\n                text = mainDrawerContent.content.name,\n                style = MaterialTheme.typography.titleMedium,\n                maxLines = 1,\n                overflow = TextOverflow.Ellipsis,\n            )\n            Spacer(modifier = Modifier.height(4.dp))\n            mainDrawerContent.content.Subtitle(mainDrawerContent.account)\n        }\n        SimpleIconButton(\n            modifier = Modifier\n                .align(Alignment.CenterVertically)\n                .size(24.dp)\n                .alpha(0.7F)\n                .padding(2.dp),\n            onClick = { onEditClick(mainDrawerContent.content) },\n            imageVector = Icons.Default.Settings,\n            contentDescription = \"Edit Content Config\",\n        )\n        Spacer(modifier = Modifier.width(16.dp))\n        Icon(\n            modifier = Modifier\n                .align(Alignment.CenterVertically)\n                .size(24.dp)\n                .alpha(0.7F)\n                .padding(2.dp),\n            painter = painterResource(com.zhangke.fread.statusui.Res.drawable.ic_drag_indicator),\n            contentDescription = \"Drag for reorder Content Config\",\n        )\n    }\n}\n"
  },
  {
    "path": "app-hosting/src/commonMain/kotlin/com/zhangke/fread/screen/main/drawer/MainDrawerUiState.kt",
    "content": "package com.zhangke.fread.screen.main.drawer\n\nimport com.zhangke.fread.status.account.LoggedAccount\nimport com.zhangke.fread.status.model.FreadContent\n\ndata class MainDrawerUiState(\n    val contentConfigList: List<MainDrawerContent>\n)\n\ndata class MainDrawerContent(\n    val content: FreadContent,\n    val account: LoggedAccount?,\n)\n"
  },
  {
    "path": "app-hosting/src/commonMain/kotlin/com/zhangke/fread/screen/main/drawer/MainDrawerViewModel.kt",
    "content": "package com.zhangke.fread.screen.main.drawer\n\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.fread.common.content.FreadContentRepo\nimport com.zhangke.fread.feeds.pages.manager.edit.EditMixedContentScreenNavKey\nimport com.zhangke.fread.status.StatusProvider\nimport com.zhangke.fread.status.content.MixedContent\nimport com.zhangke.fread.status.model.FreadContent\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.asSharedFlow\nimport kotlinx.coroutines.flow.update\nimport kotlinx.coroutines.launch\n\nclass MainDrawerViewModel (\n    private val contentRepo: FreadContentRepo,\n    private val statusProvider: StatusProvider,\n) : ViewModel() {\n\n    private val _uiState = MutableStateFlow(MainDrawerUiState(emptyList()))\n    val uiState: StateFlow<MainDrawerUiState> get() = _uiState\n\n    private val _openScreenFlow = MutableSharedFlow<NavKey>()\n    val openScreenFlow = _openScreenFlow.asSharedFlow()\n\n    init {\n        viewModelScope.launch {\n            contentRepo.getAllContentFlow()\n                .collect { list ->\n                    val contentList = mapToMainDrawerContent(list)\n                    _uiState.update { it.copy(contentConfigList = contentList) }\n                }\n        }\n    }\n\n    private suspend fun mapToMainDrawerContent(\n        contentList: List<FreadContent>,\n    ): List<MainDrawerContent> {\n        val allAccounts = statusProvider.accountManager.getAllLoggedAccount()\n        return contentList.map { content ->\n            val account = allAccounts.find { it.uri == content.accountUri }\n            MainDrawerContent(content, account)\n        }\n    }\n\n    fun onContentConfigMove(from: Int, to: Int) {\n        viewModelScope.launch {\n            val configList = _uiState.value.contentConfigList\n            if (configList.isEmpty()) return@launch\n            contentRepo.reorderConfig(configList[from].content, configList[to].content)\n        }\n    }\n\n    fun onContentConfigEditClick(content: FreadContent) {\n        viewModelScope.launch {\n            if (content is MixedContent) {\n                _openScreenFlow.emit(EditMixedContentScreenNavKey(content.id))\n            } else {\n                statusProvider.screenProvider.getEditContentConfigScreenScreen(content)\n                    ?.let { _openScreenFlow.emit(it) }\n            }\n        }\n    }\n}"
  },
  {
    "path": "app-hosting/src/commonMain/kotlin/com/zhangke/fread/utils/ActivityHelper.kt",
    "content": "package com.zhangke.fread.utils\n\nimport androidx.compose.runtime.staticCompositionLocalOf\n\nexpect class ActivityHelper {\n    fun goHome()\n}\n\ninternal val LocalActivityHelper = staticCompositionLocalOf<ActivityHelper> {\n    error(\"No ActivityHelper provided\")\n}\n"
  },
  {
    "path": "app-hosting/src/iosMain/kotlin/com/zhangke/fread/di/HostingModule.ios.kt",
    "content": "package com.zhangke.fread.di\n\nimport com.seiko.imageloader.ImageLoader\nimport com.seiko.imageloader.component.setupDefaultComponents\nimport com.seiko.imageloader.intercept.bitmapMemoryCacheConfig\nimport com.seiko.imageloader.intercept.imageMemoryCacheConfig\nimport com.seiko.imageloader.intercept.painterMemoryCacheConfig\nimport com.zhangke.framework.module.ModuleStartup\nimport com.zhangke.fread.common.utils.StorageHelper\nimport com.zhangke.fread.startup.KRouterStartup\nimport com.zhangke.fread.utils.ActivityHelper\nimport org.koin.core.module.Module\nimport org.koin.core.module.dsl.singleOf\nimport platform.UIKit.UIApplication\n\nactual fun Module.createPlatformModule() {\n    singleOf(::ActivityHelper)\n    single<ImageLoader> {\n        ImageLoader {\n            components {\n                setupDefaultComponents()\n            }\n            interceptor {\n                // cache 32MB bitmap\n                bitmapMemoryCacheConfig {\n                    maxSize(32 * 1024 * 1024) // 32MB\n                }\n                // cache 50 image\n                imageMemoryCacheConfig {\n                    maxSize(50)\n                }\n                // cache 50 painter\n                painterMemoryCacheConfig {\n                    maxSize(50)\n                }\n                diskCacheConfig {\n                    directory(get<StorageHelper>().cacheDir.resolve(\"image_cache\"))\n                    maxSizeBytes(512L * 1024 * 1024) // 512MB\n                }\n            }\n        }\n    }\n    factory<UIApplication> { UIApplication.sharedApplication }\n    factory<ModuleStartup> { KRouterStartup() }\n}\n"
  },
  {
    "path": "app-hosting/src/iosMain/kotlin/com/zhangke/fread/di/PlatformedFreadApplication.ios.kt",
    "content": "package com.zhangke.fread.di\n\nimport org.koin.core.KoinApplication\n\nactual class PlatformedFreadApplication {\n\n    actual fun KoinApplication.initKoin() {\n\n    }\n}"
  },
  {
    "path": "app-hosting/src/iosMain/kotlin/com/zhangke/fread/screen/DeviceCornerRadius.ios.kt",
    "content": "package com.zhangke.fread.screen\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\n\n@Composable\nactual fun rememberDeviceCornerRadius(): Dp = 24.dp\n"
  },
  {
    "path": "app-hosting/src/iosMain/kotlin/com/zhangke/fread/screen/FreadViewController.kt",
    "content": "package com.zhangke.fread.screen\n\nimport androidx.compose.ui.window.ComposeUIViewController\nimport platform.UIKit.UIViewController\n\ntypealias FreadViewController = () -> UIViewController\n\n@Suppress(\"FunctionName\")\ninternal fun FreadViewController(\n    iosFreadApp: IosFreadApp,\n): UIViewController = ComposeUIViewController {\n    iosFreadApp()\n}\n"
  },
  {
    "path": "app-hosting/src/iosMain/kotlin/com/zhangke/fread/screen/IosFreadApp.kt",
    "content": "package com.zhangke.fread.screen\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport com.zhangke.fread.common.browser.BrowserLauncher\nimport com.zhangke.fread.common.browser.LocalActivityBrowserLauncher\n\ntypealias IosFreadApp = @Composable () -> Unit\n\n@Composable\ninternal fun IosFreadApp(\n    activityBrowserLauncher: BrowserLauncher,\n    freadApp: FreadApp,\n) {\n    CompositionLocalProvider(\n        LocalActivityBrowserLauncher provides activityBrowserLauncher,\n    ) {\n        freadApp()\n    }\n}\n"
  },
  {
    "path": "app-hosting/src/iosMain/kotlin/com/zhangke/fread/startup/KRouterStartup.kt",
    "content": "package com.zhangke.fread.startup\n\nimport com.zhangke.framework.module.ModuleStartup\nimport com.zhangke.krouter.KRouter\n\nclass KRouterStartup : ModuleStartup {\n    override fun onAppCreate() {\n        @Suppress(\"UNRESOLVED_REFERENCE\")\n        KRouter.addRouterModule(com.zhangke.krouter.generated.AutoReducingModule())\n    }\n}\n"
  },
  {
    "path": "app-hosting/src/iosMain/kotlin/com/zhangke/fread/utils/ActivityHelper.ios.kt",
    "content": "package com.zhangke.fread.utils\n\nimport platform.UIKit.UIViewController\n\nactual class ActivityHelper(\n    private val viewController: Lazy<UIViewController>,\n) {\n    actual fun goHome() {\n        viewController.value.dismissViewControllerAnimated(false, null)\n    }\n}\n"
  },
  {
    "path": "appprivacy.html",
    "content": "<h1 id=\"fread-privacy-policy\">Fread Privacy Policy</h1>\n<p>ZhangKe built the Fread app. This SERVICE is provided by ZhangKe is intended for use as is.</p>\n<p>This page is used to inform visitors regarding my policies with the collection, use, and disclosure of Personal Information if anyone decided to use my Service.</p>\n<p>If you choose to use my Service, then you agree to the collection and use of information in relation to this policy.\n    The Personal Information that I collect is used for providing and improving the Service.\n    I will not use or share your information with anyone except as described in this Privacy Policy.</p>\n<h2 id=\"information-collection-and-use\">Information Collection and Use</h2>\n<p>For a better experience, while using our Service, I may require you to provide us with certain personally identifiable information, including but not limited to ZhangKe. </p>\n<p>The app does use third-party services that may collect information used to identify you.</p>\n<p>Link to the privacy policy of third-party service providers used by the app</p>\n<p><a href=\"https://policies.google.com/privacy\">Google Play Services</a></p>\n<p><a href=\"https://firebase.google.com/policies/analytics\">Google Analytics for Firebase</a></p>\n<p><a href=\"https://firebase.google.com/support/privacy/\">Firebase Crashlytics</a></p>\n<p><a href=\"https://mastodon.social/privacy-policy\">Mastodon Privacy Policy</a></p>\n<h2 id=\"log-data\">Log Data</h2>\n<p>I want to inform you that whenever you use my Service, in a case of an error in the app I collect data and information (through third-party products) on your phone called Log Data. This Log Data may include information such as your device Internet Protocol (“IP”) address, device name, operating system version, the configuration of the app when utilizing my Service, the time and date of your use of the Service, and other statistics.</p>\n<h2 id=\"service-providers\">Service Providers</h2>\n<p>I may employ third-party companies and individuals due to the following reasons:</p>\n<p>To facilitate our Service;\n    To provide the Service on our behalf;\n    To perform Service-related services; or\n    To assist us in analyzing how our Service is used.\n    I want to inform users of this Service that these third parties have access to their Personal Information. The reason is to perform the tasks assigned to them on our behalf. However, they are obligated not to disclose or use the information for any other purpose.</p>\n<h2 id=\"security\">Security</h2>\n<p>I value your trust in providing us your Personal Information, thus we are striving to use commercially acceptable means of protecting it. But remember that no method of transmission over the internet, or method of electronic storage is 100% secure and reliable, and I cannot guarantee its absolute security.</p>\n<h2 id=\"links-to-other-sites\">Links to Other Sites</h2>\n<p>This Service may contain links to other sites. If you click on a third-party link, you will be directed to that site. Note that these external sites are not operated by me. Therefore, I strongly advise you to review the Privacy Policy of these websites. I have no control over and assume no responsibility for the content, privacy policies, or practices of any third-party sites or services.</p>\n<h2 id=\"children-s-privacy\">Children’s Privacy</h2>\n<p>These Services do not address anyone under the age of 13. I do not knowingly collect personally identifiable information from children under 13 years of age. In the case I discover that a child under 13 has provided me with personal information, I immediately delete this from our servers. If you are a parent or guardian and you are aware that your child has provided us with personal information, please contact me so that I will be able to do the necessary actions.</p>\n<h2 id=\"changes-to-this-privacy-policy\">Changes to This Privacy Policy</h2>\n<p>I may update our Privacy Policy from time to time. Thus, you are advised to review this page periodically for any changes. I will notify you of any changes by posting the new Privacy Policy on this page.</p>\n<p>This policy is effective as of 2027-07-18</p>\n<h2 id=\"contact-us\">Contact Us</h2>\n<p>If you have any questions or suggestions about my Privacy Policy, do not hesitate to contact me at zhangkeport@gmail.com.</p>\n"
  },
  {
    "path": "bizframework/status-provider/.gitignore",
    "content": "/build"
  },
  {
    "path": "bizframework/status-provider/build.gradle.kts",
    "content": "plugins {\n    id(\"fread.project.framework.kmp\")\n    id(\"kotlin-parcelize\")\n}\n\nandroid {\n    namespace = \"com.zhangke.fread.commonbiz.status.provider\"\n}\n\nkotlin {\n    sourceSets {\n        commonMain {\n            dependencies {\n                implementation(project(path = \":framework\"))\n\n                implementation(compose.components.resources)\n                implementation(libs.arrow.core)\n                implementation(libs.kotlinx.serialization.core)\n                implementation(libs.kotlinx.serialization.json)\n\n                implementation(libs.androidx.room)\n\n                implementation(libs.ksoup)\n                implementation(libs.ktml)\n                implementation(libs.imageLoader)\n\n                implementation(project(\":thirds:halilibo-richtext-ui\"))\n            }\n        }\n        commonTest {\n            dependencies {\n                implementation(kotlin(\"test\"))\n            }\n        }\n        androidMain {\n            dependencies {\n                implementation(libs.androidx.core.ktx)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "bizframework/status-provider/consumer-rules.pro",
    "content": ""
  },
  {
    "path": "bizframework/status-provider/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguardFiles setting in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n\n# Uncomment this to preserve the line number information for\n# debugging stack traces.\n#-keepattributes SourceFile,LineNumberTable\n\n# If you keep the line number information, uncomment this to\n# hide the original source file name.\n#-renamesourcefileattribute SourceFile"
  },
  {
    "path": "bizframework/status-provider/src/androidMain/kotlin/com/zhangke/fread/status/richtext/RichTextBuilder.android.kt",
    "content": "package com.zhangke.fread.status.richtext\n\nimport com.zhangke.fread.status.model.Mention\nimport moe.tlaster.ktml.dom.Element\nimport moe.tlaster.ktml.dom.Node\n\nprivate fun replaceMentionAndHashtag(\n    mentions: List<Mention>,\n    node: Node,\n    host: String,\n) {\n    if (node is Element) {\n        val href = node.attributes[\"href\"]\n        val mention = mentions.firstOrNull { it.url == href }\n        if (mention != null) {\n            node.attributes[\"href\"] = buildMentionUrl(mention, host)\n        }\n        node.children.forEach { replaceMentionAndHashtag(mentions, it, host) }\n    }\n}\n\nprivate fun buildMentionUrl(\n    mention: Mention,\n    host: String,\n): String {\n    return \"fread://${host}/user/${mention.id}\"\n}\n"
  },
  {
    "path": "bizframework/status-provider/src/androidMain/kotlin/com/zhangke/fread/status/utils/ImplementerFinder.kt",
    "content": "package com.zhangke.fread.status.utils\n\nimport java.util.ServiceLoader\n\ninline fun <reified T> findImplementer(): T {\n    val list = findImplementers<T>()\n    if (list.size != 1)\n        throw IllegalStateException(\"${T::class.qualifiedName} has multiple implementers\")\n    return list.first()\n}\n\ninline fun <reified T> findImplementers(): List<T> {\n    return ServiceLoader.load(T::class.java, T::class.java.classLoader)\n        .iterator()\n        .asSequence()\n        .toList()\n}\n"
  },
  {
    "path": "bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/StatusProvider.kt",
    "content": "package com.zhangke.fread.status\n\nimport com.zhangke.fread.status.account.AccountManager\nimport com.zhangke.fread.status.account.IAccountManager\nimport com.zhangke.fread.status.content.ContentManager\nimport com.zhangke.fread.status.content.IContentManager\nimport com.zhangke.fread.status.notification.INotificationResolver\nimport com.zhangke.fread.status.notification.NotificationResolver\nimport com.zhangke.fread.status.platform.IPlatformResolver\nimport com.zhangke.fread.status.platform.PlatformResolver\nimport com.zhangke.fread.status.publish.IPublishBlogManager\nimport com.zhangke.fread.status.publish.PublishBlogManager\nimport com.zhangke.fread.status.screen.IStatusScreenProvider\nimport com.zhangke.fread.status.screen.StatusScreenProvider\nimport com.zhangke.fread.status.search.ISearchEngine\nimport com.zhangke.fread.status.search.SearchEngine\nimport com.zhangke.fread.status.source.IStatusSourceResolver\nimport com.zhangke.fread.status.source.StatusSourceResolver\nimport com.zhangke.fread.status.status.IStatusResolver\nimport com.zhangke.fread.status.status.StatusResolver\n\n/**\n * Created by ZhangKe on 2022/12/9.\n */\nclass StatusProvider(private val providers: List<IStatusProvider>) {\n\n    val contentManager = ContentManager(providers.map { it.contentManager })\n\n    val screenProvider = StatusScreenProvider(providers.map { it.screenProvider })\n\n    val searchEngine = SearchEngine(providers.map { it.searchEngine })\n\n    val statusResolver = StatusResolver(providers.map { it.statusResolver })\n\n    val statusSourceResolver = StatusSourceResolver(providers.map { it.statusSourceResolver })\n\n    val accountManager = AccountManager(providers.map { it.accountManager })\n\n    val notificationResolver = NotificationResolver(providers.map { it.notificationResolver })\n\n    val publishManager = PublishBlogManager(providers.map { it.publishManager })\n}\n\ninterface IStatusProvider {\n\n    val contentManager: IContentManager\n\n    val screenProvider: IStatusScreenProvider\n\n    val searchEngine: ISearchEngine\n\n    val statusResolver: IStatusResolver\n\n    val statusSourceResolver: IStatusSourceResolver\n\n    val accountManager: IAccountManager\n\n    val notificationResolver: INotificationResolver\n\n    val publishManager: IPublishBlogManager\n}\n"
  },
  {
    "path": "bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/StatusProviderModel.kt",
    "content": "package com.zhangke.fread.status\n\nimport org.koin.dsl.module\n\nval statusProviderModel = module {\n\n    single { StatusProvider(getAll<IStatusProvider>()) }\n}\n"
  },
  {
    "path": "bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/account/AccountManager.kt",
    "content": "package com.zhangke.fread.status.account\n\nimport com.zhangke.fread.status.author.BlogAuthor\nimport com.zhangke.fread.status.model.FreadContent\nimport com.zhangke.fread.status.model.LoggedAccountDetail\nimport com.zhangke.fread.status.model.Relationships\nimport com.zhangke.fread.status.platform.BlogPlatform\nimport com.zhangke.fread.status.uri.FormalUri\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.combine\n\nclass AccountManager(\n    private val accountManagerList: List<IAccountManager>,\n) {\n\n    suspend fun getAllLoggedAccount(): List<LoggedAccount> {\n        return accountManagerList.flatMap { it.getAllLoggedAccount() }\n    }\n\n    fun getAllAccountFlow(): Flow<List<LoggedAccount>> {\n        val flowList = accountManagerList.mapNotNull {\n            it.getAllAccountFlow()\n        }\n        return combine(*flowList.toTypedArray()) {\n            it.flatMap { list -> list }\n        }\n    }\n\n    fun getAllAccountDetailFlow(): Flow<List<LoggedAccountDetail>> {\n        val flowList = accountManagerList.mapNotNull {\n            it.getAllAccountDetailFlow()\n        }\n        return combine(*flowList.toTypedArray()) {\n            it.flatMap { list -> list }\n        }\n    }\n\n    suspend fun triggerAuthBySource(\n        platform: BlogPlatform,\n        account: LoggedAccount? = null,\n    ) {\n        for (manager in accountManagerList) {\n            manager.triggerLaunchAuth(platform, account)\n        }\n    }\n\n    suspend fun refreshAllAccountInfo(): List<AccountRefreshResult> {\n        return accountManagerList.flatMap {\n            it.refreshAllAccountInfo()\n        }\n    }\n\n    suspend fun logout(account: LoggedAccount) {\n        accountManagerList.forEach {\n            if (it.logout(account)) return@forEach\n        }\n    }\n\n    suspend fun selectContentWithAccount(\n        contentList: List<FreadContent>,\n        account: LoggedAccount,\n    ): List<FreadContent> {\n        return accountManagerList.flatMap { manager ->\n            manager.selectContentWithAccount(contentList, account)\n        }\n    }\n\n    fun subscribeNotification() {\n        for (manager in accountManagerList) {\n            manager.subscribeNotification()\n        }\n    }\n\n    suspend fun getRelationships(\n        account: LoggedAccount,\n        accounts: List<BlogAuthor>,\n    ): Result<Map<FormalUri, Relationships>> {\n        val allResult = mutableMapOf<FormalUri, Relationships>()\n        for (manager in accountManagerList) {\n            manager.getRelationships(account, accounts).onSuccess { map -> allResult.putAll(map) }\n        }\n        return Result.success(allResult)\n    }\n\n    suspend fun unblockAccount(\n        account: LoggedAccount,\n        user: BlogAuthor,\n    ): Result<Unit> {\n        return accountManagerList.firstNotNullOf { it.unblockAccount(account, user) }\n    }\n\n    suspend fun cancelFollowRequest(\n        account: LoggedAccount,\n        user: BlogAuthor,\n    ): Result<Unit> {\n        return accountManagerList.firstNotNullOf { it.cancelFollowRequest(account, user) }\n    }\n}\n\ninterface IAccountManager {\n\n    suspend fun getAllLoggedAccount(): List<LoggedAccount>\n\n    fun getAllAccountFlow(): Flow<List<LoggedAccount>>?\n\n    fun getAllAccountDetailFlow(): Flow<List<LoggedAccountDetail>>? {\n        return null\n    }\n\n    suspend fun triggerLaunchAuth(platform: BlogPlatform, account: LoggedAccount?)\n\n    suspend fun refreshAllAccountInfo(): List<AccountRefreshResult>\n\n    suspend fun logout(account: LoggedAccount): Boolean\n\n    suspend fun selectContentWithAccount(\n        contentList: List<FreadContent>,\n        account: LoggedAccount,\n    ): List<FreadContent>\n\n    suspend fun getRelationships(\n        account: LoggedAccount,\n        accounts: List<BlogAuthor>,\n    ): Result<Map<FormalUri, Relationships>>\n\n    suspend fun unblockAccount(\n        account: LoggedAccount,\n        user: BlogAuthor,\n    ): Result<Unit>?\n\n    suspend fun cancelFollowRequest(\n        account: LoggedAccount,\n        user: BlogAuthor,\n    ): Result<Unit>?\n\n    fun subscribeNotification()\n}\n\nsealed interface AccountRefreshResult {\n\n    val account: LoggedAccount\n\n    data class Success(override val account: LoggedAccount) : AccountRefreshResult\n\n    data class Failure(\n        override val account: LoggedAccount,\n        val error: Throwable,\n    ) : AccountRefreshResult\n}\n"
  },
  {
    "path": "bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/account/AuthenticationFailureException.kt",
    "content": "package com.zhangke.fread.status.account\n\nclass AuthenticationFailureException(override val message: String?) : RuntimeException(message)\n\nval Throwable.isAuthenticationFailure: Boolean\n    get() = this is AuthenticationFailureException\n"
  },
  {
    "path": "bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/account/LoggedAccount.kt",
    "content": "package com.zhangke.fread.status.account\n\nimport com.zhangke.framework.utils.WebFinger\nimport com.zhangke.fread.status.model.Emoji\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.platform.BlogPlatform\nimport com.zhangke.fread.status.richtext.RichText\nimport com.zhangke.fread.status.richtext.buildRichText\nimport com.zhangke.fread.status.uri.FormalUri\n\ninterface LoggedAccount {\n    val uri: FormalUri\n    val webFinger: WebFinger\n    val platform: BlogPlatform\n    val id: String?\n    val userName: String\n    val description: String?\n    val avatar: String?\n    val emojis: List<Emoji>\n    val prettyHandle: String\n\n    val humanizedName: RichText\n        get() = buildRichText(\n            document = getFixedName(),\n            emojis = emojis,\n        )\n\n    val humanizedDescription: RichText\n        get() = buildRichText(\n            document = description.orEmpty(),\n            emojis = emojis,\n        )\n\n    val locator: PlatformLocator\n        get() = PlatformLocator(baseUrl = platform.baseUrl, accountUri = uri)\n\n    private fun getFixedName(): String {\n        if (userName.isNotEmpty()) return userName\n        val nameFromHandle = prettyHandle.removePrefix(\"@\")\n            .split('@')\n            .firstOrNull()\n        if (!nameFromHandle.isNullOrEmpty() && nameFromHandle.isNotBlank()) return nameFromHandle\n        return \"\"\n    }\n}\n"
  },
  {
    "path": "bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/author/BlogAuthor.kt",
    "content": "package com.zhangke.fread.status.author\n\nimport com.zhangke.framework.utils.PlatformSerializable\nimport com.zhangke.framework.utils.WebFinger\nimport com.zhangke.framework.utils.prettyHandle\nimport com.zhangke.fread.status.model.Emoji\nimport com.zhangke.fread.status.model.Relationships\nimport com.zhangke.fread.status.richtext.RichText\nimport com.zhangke.fread.status.richtext.buildRichText\nimport com.zhangke.fread.status.uri.FormalUri\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class BlogAuthor(\n    // 对于 Bluesky 来说，个人数据应该通过 DID 获取 PDS endpoint，而不是直接使用 baseUrl\n    val uri: FormalUri,\n    val webFinger: WebFinger,\n    val handle: String,\n    val name: String,\n    val description: String,\n    val avatar: String?,\n    val emojis: List<Emoji>,\n    val userId: String? = null,\n    val bot: Boolean = false,\n    val banner: String? = null,\n    val followersCount: Long? = null,\n    val followingCount: Long? = null,\n    val statusesCount: Long? = null,\n    val relationships: Relationships? = null,\n) : PlatformSerializable {\n\n    val humanizedName: RichText by lazy {\n        buildRichText(\n            document = getFixedName(),\n            mentions = emptyList(),\n            emojis = emojis,\n            hashTags = emptyList(),\n        )\n    }\n\n    val humanizedDescription: RichText by lazy {\n        buildRichText(\n            document = description,\n            mentions = emptyList(),\n            emojis = emojis,\n            hashTags = emptyList(),\n        )\n    }\n\n    val prettyHandle: String = handle.prettyHandle()\n\n    private fun getFixedName(): String {\n        if (name.isNotEmpty()) return name\n        val nameFromHandle = handle.removePrefix(\"@\")\n            .split('@')\n            .firstOrNull()\n        if (!nameFromHandle.isNullOrEmpty() && nameFromHandle.isNotBlank()) return nameFromHandle\n        return \"\"\n    }\n}\n\nfun BlogAuthor.updateFollowingState(following: Boolean): BlogAuthor {\n    val relationships = this.relationships ?: Relationships.default()\n    return this.copy(relationships = relationships.copy(following = following))\n}\n"
  },
  {
    "path": "bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/blog/Blog.kt",
    "content": "package com.zhangke.fread.status.blog\n\nimport com.zhangke.framework.datetime.Instant\nimport com.zhangke.framework.utils.PlatformSerializable\nimport com.zhangke.fread.status.author.BlogAuthor\nimport com.zhangke.fread.status.model.BlogFiltered\nimport com.zhangke.fread.status.model.Emoji\nimport com.zhangke.fread.status.model.Facet\nimport com.zhangke.fread.status.model.FormattingTime\nimport com.zhangke.fread.status.model.HashtagInStatus\nimport com.zhangke.fread.status.model.Mention\nimport com.zhangke.fread.status.model.StatusVisibility\nimport com.zhangke.fread.status.model.isActivityPub\nimport com.zhangke.fread.status.model.isBluesky\nimport com.zhangke.fread.status.model.isRss\nimport com.zhangke.fread.status.platform.BlogPlatform\nimport com.zhangke.fread.status.richtext.RichText\nimport com.zhangke.fread.status.richtext.RichTextType\nimport com.zhangke.fread.status.richtext.buildRichText\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class Blog(\n    val id: String,\n    val author: BlogAuthor,\n    val title: String?,\n    val description: String?,\n    val content: String,\n    val url: String,\n    // http link\n    val link: String,\n    val createAt: Instant,\n    val formattedCreateAt: String,\n    val supportEdit: Boolean,\n    val like: Like,\n    val forward: Forward,\n    val bookmark: Bookmark,\n    val reply: Reply,\n    val quote: Quote,\n    val sensitive: Boolean,\n    val spoilerText: String,\n    /**\n     * ISO 639 Part 1 two-letter language code\n     */\n    val language: String? = null,\n    // 对于 Bluesky 来说，个人数据应该通过 DID 获取 PDS endpoint，而不是直接使用 baseUrl\n    val platform: BlogPlatform,\n    val mediaList: List<BlogMedia>,\n    val emojis: List<Emoji>,\n    val mentions: List<Mention>,\n    val tags: List<HashtagInStatus>,\n    val facets: List<Facet>,\n    val pinned: Boolean = false,\n    val poll: BlogPoll?,\n    val visibility: StatusVisibility,\n    val embeds: List<BlogEmbed> = emptyList(),\n    val supportTranslate: Boolean = false,\n    val editedAt: Instant? = null,\n    val formattedEditAt: String? = null,\n    val application: PostingApplication? = null,\n    val filtered: List<BlogFiltered>? = null,\n    val isReply: Boolean = false,\n) : PlatformSerializable {\n\n    val richTextType: RichTextType\n        get() {\n            if (platform.protocol.isActivityPub) return RichTextType.HTML\n            if (platform.protocol.isBluesky) return RichTextType.PLAINTEXT\n            if (platform.protocol.isRss) return RichTextType.HTML\n            return RichTextType.UNKNOWN\n        }\n\n    val humanizedSpoilerText: RichText by lazy {\n        buildRichText(\n            document = spoilerText,\n            type = richTextType,\n            mentions = mentions,\n            emojis = emojis,\n            hashTags = tags,\n        )\n    }\n\n    val humanizedContent: RichText by lazy {\n        buildRichText(\n            document = content,\n            type = richTextType,\n            mentions = mentions,\n            emojis = emojis,\n            hashTags = tags,\n            facets = facets,\n        )\n    }\n\n    val humanizedDescription: RichText by lazy {\n        buildRichText(\n            document = description.orEmpty(),\n            type = richTextType,\n            mentions = mentions,\n            emojis = emojis,\n            hashTags = tags,\n        )\n    }\n\n    val formattingDisplayTime: FormattingTime by lazy {\n        FormattingTime(createAt)\n    }\n\n    val sensitiveByFilter: Boolean\n        get() {\n            if (filtered.isNullOrEmpty()) return false\n            return filtered.any {\n                it.action == BlogFiltered.FilterAction.WARN || it.action == BlogFiltered.FilterAction.BLUR\n            }\n        }\n\n    @Serializable\n    data class Like(\n        val support: Boolean,\n        val likedCount: Long? = null,\n        val liked: Boolean? = null,\n    )\n\n    @Serializable\n    data class Forward(\n        val support: Boolean,\n        val forwardCount: Long? = null,\n        val forward: Boolean? = null,\n    )\n\n    @Serializable\n    data class Bookmark(\n        val support: Boolean,\n        val bookmarked: Boolean? = null,\n    )\n\n    @Serializable\n    data class Reply(\n        val support: Boolean,\n        val repliesCount: Long? = null,\n    )\n\n    @Serializable\n    data class Quote(\n        val support: Boolean,\n        val enabled: Boolean = false,\n        val currentUserApproval: CurrentUserQuoteApproval? = null,\n    )\n}\n"
  },
  {
    "path": "bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/blog/BlogEmbed.kt",
    "content": "package com.zhangke.fread.status.blog\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\nsealed interface BlogEmbed {\n\n    @Serializable\n    data class Link(\n        val url: String,\n        val title: String,\n        val description: String,\n        val video: Boolean = false,\n        val authorName: String? = null,\n        val authorUrl: String? = null,\n        val providerName: String? = null,\n        val providerUrl: String? = null,\n        val html: String? = null,\n        val width: Int? = null,\n        val height: Int? = null,\n        val image: String? = null,\n        val embedUrl: String? = null,\n        val blurhash: String? = null,\n    ) : BlogEmbed {\n\n        companion object {\n\n            private val standardRange = 0.5f..2f\n            private const val DEFAULT_ASPECT_RATIO = 2F\n        }\n\n        val aspectRatio: Float\n            get() {\n                if (width == null || height == null) return DEFAULT_ASPECT_RATIO\n                val ratio = width / height.toFloat()\n                if (ratio.isNaN() || ratio.isInfinite()) {\n                    return DEFAULT_ASPECT_RATIO\n                }\n                if (ratio in standardRange) {\n                    return ratio\n                }\n                return DEFAULT_ASPECT_RATIO\n            }\n\n    }\n\n    @Serializable\n    data class Blog(\n        val blog: com.zhangke.fread.status.blog.Blog,\n    ) : BlogEmbed\n\n    @Serializable\n    data class UnavailableQuote(\n        val reason: String,\n        val blogId: String?,\n    ) : BlogEmbed\n}\n"
  },
  {
    "path": "bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/blog/BlogMedia.kt",
    "content": "package com.zhangke.fread.status.blog\n\nimport com.zhangke.framework.utils.PlatformSerializable\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class BlogMedia(\n    val id: String,\n    val url: String,\n    val type: BlogMediaType,\n    val previewUrl: String?,\n    val remoteUrl: String?,\n    val description: String?,\n    val blurhash: String?,\n    val meta: BlogMediaMeta?,\n): PlatformSerializable"
  },
  {
    "path": "bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/blog/BlogMediaMeta.kt",
    "content": "package com.zhangke.fread.status.blog\n\nimport com.zhangke.framework.utils.PlatformSerializable\nimport kotlinx.serialization.Serializable\n\n@Serializable\nsealed class BlogMediaMeta: PlatformSerializable {\n\n    @Serializable\n    data class ImageMeta(\n        val original: LayoutMeta?,\n        val small: LayoutMeta?,\n        val focus: FocusMeta?,\n    ) : BlogMediaMeta(), PlatformSerializable {\n\n        @Serializable\n        data class LayoutMeta(\n            val width: Long?,\n            val height: Long?,\n            val size: String?,\n            val aspect: Float?,\n        ): PlatformSerializable\n\n        @Serializable\n        data class FocusMeta(\n            val x: Float?,\n            val y: Float?,\n        ): PlatformSerializable\n    }\n\n    @Serializable\n    data class VideoMeta(\n        val length: String?,\n        val duration: Double?,\n        val fps: Int?,\n        val size: String?,\n        val width: Long?,\n        val height: Long?,\n        val aspect: Float?,\n        val audioEncode: String?,\n        val audioBitrate: String?,\n        val audioChannels: String?,\n        val original: LayoutMeta?,\n        val small: LayoutMeta?,\n    ) : BlogMediaMeta(), PlatformSerializable {\n\n        @Serializable\n        data class LayoutMeta(\n            val width: Long?,\n            val height: Long?,\n            val size: String?,\n            val aspect: Float?,\n            val frameRate: String?,\n            val duration: Double?,\n            val bitrate: Int?,\n        ): PlatformSerializable\n    }\n\n    @Serializable\n    data class GifvMeta(\n        val length: String?,\n        val duration: Double?,\n        val fps: Int?,\n        val size: String?,\n        val width: Int?,\n        val height: Int?,\n        val aspect: Float?,\n        val original: LayoutMeta?,\n        val small: LayoutMeta?,\n    ) : BlogMediaMeta(), PlatformSerializable {\n\n        @Serializable\n        data class LayoutMeta(\n            val width: Long?,\n            val height: Long?,\n            val size: String?,\n            val aspect: Float?,\n            val frameRate: String?,\n            val duration: Double?,\n            val bitrate: Int?,\n        ): PlatformSerializable\n    }\n\n    @Serializable\n    data class AudioMeta(\n        val length: String?,\n        val duration: Double?,\n        val audioEncode: String?,\n        val audioBitrate: String?,\n        val audioChannels: String?,\n        val original: FrameMeta?,\n    ) : BlogMediaMeta(), PlatformSerializable {\n\n        @Serializable\n        data class FrameMeta(\n            val duration: Double?,\n            val bitrate: Int?,\n        ): PlatformSerializable\n    }\n}\n\nfun BlogMediaMeta.asImageMeta(): BlogMediaMeta.ImageMeta = this as BlogMediaMeta.ImageMeta\n\nfun BlogMediaMeta.asImageMetaOrNull(): BlogMediaMeta.ImageMeta? = this as? BlogMediaMeta.ImageMeta\n"
  },
  {
    "path": "bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/blog/BlogMediaType.kt",
    "content": "package com.zhangke.fread.status.blog\n\nenum class BlogMediaType {\n\n    UNKNOWN,\n    IMAGE,\n    GIFV,\n    VIDEO,\n    AUDIO,\n}\n"
  },
  {
    "path": "bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/blog/BlogPoll.kt",
    "content": "package com.zhangke.fread.status.blog\n\nimport com.zhangke.framework.utils.PlatformSerializable\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class BlogPoll(\n    val id: String,\n    /**\n     * ISO 8601 Datetime\n     */\n    val expiresAt: String?,\n    val expired: Boolean,\n    /**\n     * Does the poll allow multiple-choice answers?\n     */\n    val multiple: Boolean,\n    /**\n     * How many votes have been received.\n     */\n    val votesCount: Int,\n    /**\n     * How many unique accounts have voted on a multiple-choice poll.\n     */\n    val votersCount: Int,\n    val options: List<Option>,\n    val voted: Boolean?,\n    val ownVotes: List<Int>,\n): PlatformSerializable {\n\n    @Serializable\n    data class Option(\n        val index: Int,\n        val title: String,\n        val votesCount: Int?,\n    ): PlatformSerializable\n}\n"
  },
  {
    "path": "bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/blog/BlogServer.kt",
    "content": "package com.zhangke.fread.status.blog\n\ndata class BlogServer(\n    val baseUrl: String,\n    val name: String,\n    val description: String,\n    val avatar: String?,\n    val protocol: String,\n)"
  },
  {
    "path": "bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/blog/BlogTranslation.kt",
    "content": "package com.zhangke.fread.status.blog\n\nimport com.zhangke.framework.utils.PlatformSerializable\nimport com.zhangke.fread.status.richtext.RichText\nimport com.zhangke.fread.status.richtext.buildRichText\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.Transient\n\n@Serializable\ndata class BlogTranslation(\n    val content: String,\n    val spoilerText: String,\n    val poll: Poll?,\n    val attachments: List<Attachment>?,\n    val detectedSourceLanguage: String,\n    val provider: String,\n) : PlatformSerializable {\n\n    @Transient\n    private var humanizedSpoilerText: RichText? = null\n\n    @Transient\n    private var humanizedContent: RichText? = null\n\n    fun getHumanizedSpoilerText(blog: Blog): RichText {\n        if (humanizedSpoilerText != null) return humanizedSpoilerText!!\n        return buildRichText(\n            document = spoilerText,\n            mentions = blog.mentions,\n            emojis = blog.emojis,\n            hashTags = blog.tags,\n        ).also { humanizedSpoilerText = it }\n    }\n\n    fun getHumanizedContent(blog: Blog): RichText {\n        if (humanizedContent != null) return humanizedContent!!\n        return buildRichText(\n            document = content,\n            mentions = blog.mentions,\n            emojis = blog.emojis,\n            hashTags = blog.tags,\n        ).also { humanizedContent = it }\n    }\n\n    @Serializable\n    data class Poll(\n        val id: String,\n        val options: List<Option>,\n    ) : PlatformSerializable {\n\n        @Serializable\n        data class Option(\n            val title: String,\n        )\n    }\n\n    @Serializable\n    data class Attachment(\n        val id: String,\n        val description: String,\n    ) : PlatformSerializable\n}\n"
  },
  {
    "path": "bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/blog/CurrentUserQuoteApproval.kt",
    "content": "package com.zhangke.fread.status.blog\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\nenum class CurrentUserQuoteApproval {\n\n    AUTOMATIC,\n    MANUAL,\n    DENIED,\n    UNKNOWN;\n\n    val quotable: Boolean get() = this == AUTOMATIC || this == MANUAL\n}\n"
  },
  {
    "path": "bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/blog/PostingApplication.kt",
    "content": "package com.zhangke.fread.status.blog\n\nimport com.zhangke.framework.utils.PlatformSerializable\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class PostingApplication(\n    val name: String,\n    val website: String?,\n) : PlatformSerializable\n"
  },
  {
    "path": "bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/content/ContentManager.kt",
    "content": "package com.zhangke.fread.status.content\n\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.composable.TextString\nimport com.zhangke.fread.status.model.ContentConfig\nimport com.zhangke.fread.status.model.FreadContent\nimport com.zhangke.fread.status.platform.BlogPlatform\n\nclass ContentManager(\n    private val providerList: List<IContentManager>\n) {\n\n    suspend fun addContent(\n        platform: BlogPlatform,\n        action: AddContentAction,\n    ) {\n        providerList.forEach { it.addContent(platform, action) }\n    }\n\n    fun restoreContent(config: ContentConfig): FreadContent? {\n        return providerList.firstNotNullOfOrNull { it.restoreContent(config) }\n    }\n}\n\ninterface IContentManager {\n\n    suspend fun addContent(platform: BlogPlatform, action: AddContentAction)\n\n    fun restoreContent(config: ContentConfig): FreadContent?\n}\n\ndata class AddContentAction(\n    val onShowSnackBarMessage: suspend (TextString) -> Unit,\n    val onFinishPage: suspend () -> Unit,\n    val onOpenNewPage: suspend (NavKey) -> Unit,\n)\n"
  },
  {
    "path": "bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/content/MixedContent.kt",
    "content": "package com.zhangke.fread.status.content\n\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.text.AnnotatedString\nimport androidx.compose.ui.text.SpanStyle\nimport androidx.compose.ui.text.buildAnnotatedString\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.sp\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.account.LoggedAccount\nimport com.zhangke.fread.status.model.FreadContent\nimport com.zhangke.fread.status.uri.FormalUri\nimport kotlinx.serialization.Serializable\nimport org.jetbrains.compose.resources.stringResource\n\n@Serializable\ndata class MixedContent(\n    override val id: String,\n    override val order: Int,\n    override val name: String,\n    val sourceUriList: List<FormalUri>,\n) : FreadContent {\n\n    override val accountUri: FormalUri? = null\n\n    override fun newOrder(newOrder: Int): FreadContent {\n        return copy(order = newOrder)\n    }\n\n    @Composable\n    override fun Subtitle(account: LoggedAccount?) {\n        Text(\n            modifier = Modifier,\n            text = buildSubtitle(),\n            style = MaterialTheme.typography.labelMedium,\n            maxLines = 1,\n            overflow = TextOverflow.Ellipsis,\n        )\n    }\n\n    @Composable\n    private fun buildSubtitle(): AnnotatedString {\n        return buildAnnotatedString {\n            val prefix = stringResource(LocalizedString.mixed_content_subtitle_1)\n            val suffix = stringResource(LocalizedString.mixed_content_subtitle_2)\n            append(prefix)\n            val sizeString = \" \" + sourceUriList.size.toString() + \" \"\n            append(sizeString)\n            addStyle(\n                style = SpanStyle(\n                    fontSize = 14.sp,\n                    fontWeight = FontWeight.Medium,\n                ),\n                start = prefix.length,\n                end = prefix.length + sizeString.length,\n            )\n            append(suffix)\n        }\n    }\n}\n"
  },
  {
    "path": "bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/model/BlogFiltered.kt",
    "content": "package com.zhangke.fread.status.model\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class BlogFiltered(\n    val id: String,\n    val title: String,\n    val action: FilterAction,\n    val keywordMatches: List<String>,\n) {\n\n    enum class FilterAction {\n        WARN,\n        HIDE,\n        BLUR,\n    }\n}\n"
  },
  {
    "path": "bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/model/ContentConfig.kt",
    "content": "package com.zhangke.fread.status.model\n\nimport com.zhangke.framework.network.FormalBaseUrl\nimport com.zhangke.fread.status.uri.FormalUri\nimport kotlinx.serialization.Serializable\n\n@Serializable\nsealed interface ContentConfig {\n\n    val id: Long\n    val order: Int\n\n    val configName: String\n        get() = when (this) {\n            is MixedContent -> name\n            is ActivityPubContent -> name\n            is BlueskyContent -> name\n        }\n\n    @Serializable\n    data class MixedContent(\n        override val id: Long,\n        override val order: Int,\n        val name: String,\n        val sourceUriList: List<FormalUri>,\n    ) : ContentConfig\n\n    @Serializable\n    data class ActivityPubContent(\n        override val id: Long,\n        override val order: Int,\n        val name: String,\n        val baseUrl: FormalBaseUrl,\n        val showingTabList: List<ContentTab>,\n        val hiddenTabList: List<ContentTab>,\n    ) : ContentConfig {\n\n        // hidden 列表中的 order 字段可能没有意义，因为并不会按照它在首页排序\n        @Serializable\n        sealed class ContentTab {\n\n            abstract val order: Int\n\n            abstract fun newOrder(order: Int): ContentTab\n\n            @Serializable\n            data class HomeTimeline(\n                override val order: Int,\n            ) : ContentTab() {\n                override fun newOrder(order: Int): ContentTab {\n                    return copy(order = order)\n                }\n            }\n\n            @Serializable\n            data class LocalTimeline(\n                override val order: Int,\n            ) : ContentTab() {\n\n                override fun newOrder(order: Int): ContentTab {\n                    return copy(order = order)\n                }\n            }\n\n            @Serializable\n            data class PublicTimeline(\n                override val order: Int,\n            ) : ContentTab() {\n\n                override fun newOrder(order: Int): ContentTab {\n                    return copy(order = order)\n                }\n            }\n\n            @Serializable\n            data class Trending(\n                override val order: Int,\n            ) : ContentTab() {\n\n                override fun newOrder(order: Int): ContentTab {\n                    return copy(order = order)\n                }\n            }\n\n            @Serializable\n            data class ListTimeline(\n                val listId: String,\n                val name: String,\n                override val order: Int,\n            ) : ContentTab() {\n\n                override fun newOrder(order: Int): ContentTab {\n                    return copy(order = order)\n                }\n            }\n        }\n    }\n\n    @Serializable\n    data class BlueskyContent(\n        override val id: Long,\n        override val order: Int,\n        val name: String,\n        val baseUrl: FormalBaseUrl,\n        val tabList: List<BlueskyTab>,\n    ) : ContentConfig {\n\n        @Serializable\n        sealed interface BlueskyTab {\n\n            val order: Int\n            val title: String\n            val hide: Boolean\n\n            @Serializable\n            data class FollowingTab(\n                override val title: String,\n                override val order: Int,\n                override val hide: Boolean,\n            ) : BlueskyTab {\n\n                companion object {\n\n                    fun default(): FollowingTab {\n                        return FollowingTab(\n                            title = \"Following\",\n                            order = 0,\n                            hide = false,\n                        )\n                    }\n                }\n            }\n\n            @Serializable\n            data class FeedsTab(\n                val feedUri: String,\n                override val title: String,\n                override val order: Int,\n                override val hide: Boolean,\n            ) : BlueskyTab\n\n            @Serializable\n            data class ListTab(\n                val listUri: String,\n                override val title: String,\n                override val order: Int,\n                override val hide: Boolean,\n            ) : BlueskyTab\n        }\n    }\n}\n\nfun List<ContentConfig.ActivityPubContent.ContentTab>.dropNotExistListTab(\n    allListId: Set<String>\n): List<ContentConfig.ActivityPubContent.ContentTab> {\n    return this.filter {\n        if (it is ContentConfig.ActivityPubContent.ContentTab.ListTimeline) {\n            it.listId in allListId\n        } else {\n            true\n        }\n    }\n}\n"
  },
  {
    "path": "bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/model/ContentType.kt",
    "content": "package com.zhangke.fread.status.model\n\nenum class ContentType {\n\n    MIXED,\n    ACTIVITY_PUB,\n    BLUESKY,\n\n}\n"
  },
  {
    "path": "bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/model/Emoji.kt",
    "content": "package com.zhangke.fread.status.model\n\nimport com.zhangke.framework.utils.PlatformSerializable\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class Emoji(\n    val shortcode: String,\n    val url: String,\n    val staticUrl: String,\n): PlatformSerializable\n"
  },
  {
    "path": "bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/model/FacetFeatureUnion.kt",
    "content": "package com.zhangke.fread.status.model\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class Facet(\n    val byteStart: Long,\n    val byteEnd: Long,\n    val features: List<FacetFeatureUnion>,\n)\n\n@Serializable\nsealed interface FacetFeatureUnion {\n\n    @Serializable\n    data class Mention(val did: String) : FacetFeatureUnion\n\n    @Serializable\n    data class Tag(val tag: String) : FacetFeatureUnion\n\n    @Serializable\n    data class Link(val uri: String) : FacetFeatureUnion\n}\n"
  },
  {
    "path": "bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/model/FormattingTime.kt",
    "content": "package com.zhangke.fread.status.model\n\nimport androidx.compose.runtime.Composable\nimport com.zhangke.framework.datetime.Instant\nimport com.zhangke.framework.utils.PlatformSerializable\nimport com.zhangke.fread.status.utils.DateTimeFormatter\nimport com.zhangke.fread.status.utils.defaultFormatConfig\nimport com.zhangke.fread.status.utils.defaultUiFormatConfig\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class FormattingTime(val time: Instant): PlatformSerializable {\n\n    private var _formattedTime: String? = null\n\n    @Composable\n    fun formattedTime(): String {\n        if (!_formattedTime.isNullOrEmpty()) return _formattedTime!!\n        _formattedTime = DateTimeFormatter.format(time.epochMillis, defaultUiFormatConfig())\n        return _formattedTime!!\n    }\n\n    suspend fun parse(): String {\n        if (!_formattedTime.isNullOrEmpty()) return _formattedTime!!\n        _formattedTime = DateTimeFormatter.format(time.epochMillis, defaultFormatConfig())\n        return _formattedTime!!\n    }\n}\n"
  },
  {
    "path": "bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/model/FreadContent.kt",
    "content": "package com.zhangke.fread.status.model\n\nimport androidx.compose.runtime.Composable\nimport com.zhangke.fread.status.account.LoggedAccount\nimport com.zhangke.fread.status.uri.FormalUri\n\ninterface FreadContent {\n\n    val id: String\n\n    val order: Int\n\n    val name: String\n\n    val accountUri: FormalUri?\n\n    fun newOrder(newOrder: Int): FreadContent\n\n    @Composable\n    fun Subtitle(account: LoggedAccount?)\n}\n"
  },
  {
    "path": "bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/model/Hashtag.kt",
    "content": "package com.zhangke.fread.status.model\n\nimport com.zhangke.framework.composable.TextString\n\ndata class Hashtag(\n    val name: String,\n    val url: String,\n    val description: TextString,\n    val history: History,\n    val following: Boolean,\n    val protocol: StatusProviderProtocol,\n) {\n\n    data class History(\n        val history: List<Float>,\n        val min: Float?,\n        val max: Float?,\n    )\n}\n"
  },
  {
    "path": "bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/model/HashtagInStatus.kt",
    "content": "package com.zhangke.fread.status.model\n\nimport com.zhangke.framework.utils.PlatformSerializable\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class HashtagInStatus(\n    val name: String,\n    val url: String,\n    val protocol: StatusProviderProtocol,\n): PlatformSerializable\n"
  },
  {
    "path": "bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/model/LoggedAccountDetail.kt",
    "content": "package com.zhangke.fread.status.model\n\nimport com.zhangke.fread.status.account.LoggedAccount\nimport com.zhangke.fread.status.author.BlogAuthor\n\ndata class LoggedAccountDetail(\n    val account: LoggedAccount,\n    val author: BlogAuthor,\n)\n"
  },
  {
    "path": "bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/model/Mention.kt",
    "content": "package com.zhangke.fread.status.model\n\nimport com.zhangke.framework.utils.PlatformSerializable\nimport com.zhangke.framework.utils.WebFinger\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class Mention(\n    val id: String,\n    val username: String,\n    val url: String,\n    val webFinger: WebFinger,\n    val protocol: StatusProviderProtocol,\n): PlatformSerializable\n"
  },
  {
    "path": "bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/model/PagedData.kt",
    "content": "package com.zhangke.fread.status.model\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class PagedData<T>(\n    val list: List<T>,\n    val cursor: String?,\n)\n"
  },
  {
    "path": "bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/model/PlatformLocator.kt",
    "content": "package com.zhangke.fread.status.model\n\nimport com.zhangke.framework.network.FormalBaseUrl\nimport com.zhangke.framework.utils.Parcelize\nimport com.zhangke.framework.utils.PlatformParcelable\nimport com.zhangke.framework.utils.PlatformSerializable\nimport com.zhangke.fread.status.uri.FormalUri\nimport kotlinx.serialization.Serializable\n\n@Parcelize\n@Serializable\ndata class PlatformLocator(\n    val baseUrl: FormalBaseUrl,\n    val accountUri: FormalUri? = null,\n) : PlatformParcelable, PlatformSerializable\n"
  },
  {
    "path": "bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/model/PostInteractionSetting.kt",
    "content": "package com.zhangke.fread.status.model\n\ndata class PostInteractionSetting(\n    val allowQuote: Boolean,\n    val replySetting: ReplySetting,\n) {\n\n    companion object {\n\n        fun default(): PostInteractionSetting {\n            return PostInteractionSetting(\n                allowQuote = true,\n                replySetting = ReplySetting.Everybody,\n            )\n        }\n    }\n}\n\nsealed interface ReplySetting {\n\n    data object Nobody : ReplySetting\n\n    data object Everybody : ReplySetting\n\n    data class Combined(val options: List<CombineOption>) : ReplySetting\n\n    val combinedMentions: Boolean\n        get() = this is Combined && options.contains(CombineOption.Mentioned)\n\n    val combinedFollowing: Boolean\n        get() = this is Combined && options.contains(CombineOption.Following)\n\n    val combinedFollowers: Boolean\n        get() = this is Combined && options.contains(CombineOption.Followers)\n\n    sealed interface CombineOption {\n\n        data object Mentioned : CombineOption\n\n        data object Following : CombineOption\n\n        data object Followers : CombineOption\n\n        data class UserInList(val listView: StatusList) : CombineOption\n    }\n}\n"
  },
  {
    "path": "bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/model/PublishBlogRules.kt",
    "content": "package com.zhangke.fread.status.model\n\ndata class PublishBlogRules(\n    val maxCharacters: Int,\n    val maxMediaCount: Int,\n    val mediaAltMaxCharacters: Int,\n    val maxPollOptions: Int,\n    val supportSpoiler: Boolean,\n    val supportPoll: Boolean,\n    val maxLanguageCount: Int,\n)\n"
  },
  {
    "path": "bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/model/QuoteApprovalPolicy.kt",
    "content": "package com.zhangke.fread.status.model\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\nenum class QuoteApprovalPolicy {\n    PUBLIC,\n    FOLLOWERS,\n    FOLLOWING,\n    NOBODY,\n}\n"
  },
  {
    "path": "bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/model/Relationships.kt",
    "content": "package com.zhangke.fread.status.model\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class Relationships(\n    val blocking: Boolean,\n    val blockedBy: Boolean,\n    val following: Boolean,\n    val followedBy: Boolean,\n    val muting: Boolean,\n    val requested: Boolean?,\n    val requestedBy: Boolean?,\n) {\n\n    companion object {\n\n        fun default(\n            blocking: Boolean = false,\n            blockedBy: Boolean = false,\n            following: Boolean = false,\n            followedBy: Boolean = false,\n            muting: Boolean = false,\n            requested: Boolean? = null,\n            requestedBy: Boolean? = null,\n        ): Relationships {\n            return Relationships(\n                blocking = blocking,\n                blockedBy = blockedBy,\n                following = following,\n                followedBy = followedBy,\n                muting = muting,\n                requested = requested,\n                requestedBy = requestedBy,\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/model/StatusActionType.kt",
    "content": "package com.zhangke.fread.status.model\n\nenum class StatusActionType {\n\n    LIKE,\n    FORWARD,\n    BOOKMARK,\n    REPLY,\n    DELETE,\n    SHARE,\n    PIN,\n    EDIT,\n    QUOTE,\n}\n"
  },
  {
    "path": "bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/model/StatusList.kt",
    "content": "package com.zhangke.fread.status.model\n\ndata class StatusList(\n    val name: String,\n    val cid: String,\n    val uri: String,\n)"
  },
  {
    "path": "bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/model/StatusProviderProtocol.kt",
    "content": "package com.zhangke.fread.status.model\n\nimport com.zhangke.framework.utils.Parcelize\nimport com.zhangke.framework.utils.PlatformParcelable\nimport com.zhangke.framework.utils.PlatformSerializable\nimport kotlinx.serialization.Serializable\n\nconst val ACTIVITY_PUB_PROTOCOL_ID = \"ActivityPub\"\nconst val RSS_PROTOCOL_ID = \"RSS\"\nconst val BLUESKY_PROTOCOL_ID = \"Bluesky\"\n\n@Serializable\n@Parcelize\ndata class StatusProviderProtocol(\n    val id: String,\n    val name: String,\n) : PlatformParcelable, PlatformSerializable\n\nfun createActivityPubProtocol(): StatusProviderProtocol {\n    return StatusProviderProtocol(\n        id = ACTIVITY_PUB_PROTOCOL_ID,\n        name = \"Mastodon\",\n    )\n}\n\nfun createBlueskyProtocol(): StatusProviderProtocol {\n    return StatusProviderProtocol(\n        id = BLUESKY_PROTOCOL_ID,\n        name = \"Bluesky\",\n    )\n}\n\nfun createRssProtocol(): StatusProviderProtocol {\n    return StatusProviderProtocol(\n        id = RSS_PROTOCOL_ID,\n        name = \"RSS\",\n    )\n}\n\nval StatusProviderProtocol.isActivityPub: Boolean\n    get() = id == ACTIVITY_PUB_PROTOCOL_ID\n\nval StatusProviderProtocol.notActivityPub: Boolean\n    get() = !isActivityPub\n\nval StatusProviderProtocol.isRss: Boolean\n    get() = id == RSS_PROTOCOL_ID\n\nval StatusProviderProtocol.notRss: Boolean\n    get() = !isRss\n\nval StatusProviderProtocol.isBluesky: Boolean\n    get() = id == BLUESKY_PROTOCOL_ID\n\nval StatusProviderProtocol.notBluesky: Boolean\n    get() = !isBluesky\n"
  },
  {
    "path": "bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/model/StatusUiState.kt",
    "content": "package com.zhangke.fread.status.model\n\nimport com.zhangke.framework.utils.PlatformSerializable\nimport com.zhangke.fread.status.author.BlogAuthor\nimport com.zhangke.fread.status.author.updateFollowingState\nimport com.zhangke.fread.status.blog.BlogTranslation\nimport com.zhangke.fread.status.status.model.Status\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class StatusUiState(\n    val status: Status,\n    val logged: Boolean,\n    val isOwner: Boolean,\n    val blogTranslationState: BlogTranslationUiState,\n    val locator: PlatformLocator,\n) : PlatformSerializable\n\n@Serializable\ndata class BlogTranslationUiState(\n    val support: Boolean,\n    val translating: Boolean = false,\n    val showingTranslation: Boolean = false,\n    val blogTranslation: BlogTranslation? = null,\n) : PlatformSerializable {\n\n    companion object {\n        val DEFAULT = BlogTranslationUiState(support = false)\n    }\n}\n\nfun StatusUiState.updateBlogAuthor(block: (BlogAuthor) -> BlogAuthor): StatusUiState {\n    val newStatus = when (status) {\n        is Status.NewBlog -> {\n            status.copy(\n                blog = status.blog.copy(\n                    author = block(status.blog.author)\n                )\n            )\n        }\n\n        is Status.Reblog -> {\n            status.copy(\n                reblog = status.reblog.copy(author = block(status.reblog.author))\n            )\n        }\n    }\n    return copy(status = newStatus)\n}\n\nfun StatusUiState.updateFollowingState(following: Boolean): StatusUiState {\n    return updateBlogAuthor {\n        it.updateFollowingState(following)\n    }\n}\n\nfun List<StatusUiState>.updateStatus(\n    status: StatusUiState,\n): List<StatusUiState> {\n    return map {\n        if (it.status.intrinsicBlog.id == status.status.intrinsicBlog.id) {\n            status\n        } else {\n            it\n        }\n    }\n}\n"
  },
  {
    "path": "bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/model/StatusVisibility.kt",
    "content": "package com.zhangke.fread.status.model\n\nimport com.zhangke.framework.utils.PlatformSerializable\nimport kotlinx.serialization.Serializable\n\n@Serializable\nenum class StatusVisibility : PlatformSerializable {\n    PUBLIC,\n    UNLISTED,\n    PRIVATE,\n    DIRECT,\n}\n"
  },
  {
    "path": "bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/notification/NotificationResolver.kt",
    "content": "package com.zhangke.fread.status.notification\n\nimport com.zhangke.fread.status.account.LoggedAccount\nimport com.zhangke.fread.status.author.BlogAuthor\n\nclass NotificationResolver(\n    private val resolverList: List<INotificationResolver>\n) {\n\n    suspend fun getNotifications(\n        account: LoggedAccount,\n        type: INotificationResolver.NotificationRequestType,\n        cursor: String?,\n    ): Result<PagedStatusNotification> = resolverList.firstNotNullOf {\n        it.getNotifications(account, type, cursor)\n    }\n\n    /**\n     * Just return updated user details for the given users.\n     */\n    suspend fun getNotificationUserDetail(\n        account: LoggedAccount,\n        users: List<BlogAuthor>,\n    ): Result<List<BlogAuthor>> {\n        return resolverList.firstNotNullOfOrNull { resolver ->\n            resolver.getNotificationUserDetail(account, users)\n        } ?: Result.success(emptyList())\n    }\n\n    suspend fun rejectFollowRequest(\n        account: LoggedAccount,\n        requestAuthor: BlogAuthor,\n    ): Result<Unit> {\n        return resolverList.firstNotNullOf {\n            it.rejectFollowRequest(account, requestAuthor)\n        }\n    }\n\n    suspend fun acceptFollowRequest(\n        account: LoggedAccount,\n        requestAuthor: BlogAuthor,\n    ): Result<Unit> {\n        return resolverList.firstNotNullOf {\n            it.acceptFollowRequest(account, requestAuthor)\n        }\n    }\n\n    suspend fun updateUnreadNotification(\n        account: LoggedAccount,\n        notificationLastReadId: String,\n    ): Result<Unit> {\n        return resolverList.firstNotNullOf {\n            it.updateUnreadNotification(account, notificationLastReadId)\n        }\n    }\n}\n\ninterface INotificationResolver {\n\n    enum class NotificationRequestType {\n        ALL,\n        MENTION,\n    }\n\n    suspend fun getNotifications(\n        account: LoggedAccount,\n        type: NotificationRequestType = NotificationRequestType.ALL,\n        cursor: String? = null,\n    ): Result<PagedStatusNotification>?\n\n    suspend fun getNotificationUserDetail(\n        account: LoggedAccount,\n        users: List<BlogAuthor>,\n    ): Result<List<BlogAuthor>>? {\n        return null\n    }\n\n    suspend fun rejectFollowRequest(\n        account: LoggedAccount,\n        requestAuthor: BlogAuthor\n    ): Result<Unit>?\n\n    suspend fun acceptFollowRequest(\n        account: LoggedAccount,\n        requestAuthor: BlogAuthor,\n    ): Result<Unit>?\n\n    suspend fun updateUnreadNotification(\n        account: LoggedAccount,\n        notificationLastReadId: String,\n    ): Result<Unit>?\n}\n"
  },
  {
    "path": "bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/notification/StatusNotification.kt",
    "content": "package com.zhangke.fread.status.notification\n\nimport com.zhangke.framework.datetime.Instant\nimport com.zhangke.fread.status.author.BlogAuthor\nimport com.zhangke.fread.status.blog.Blog\nimport com.zhangke.fread.status.model.FormattingTime\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.model.StatusUiState\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class PagedStatusNotification(\n    val notifications: List<StatusNotification>,\n    val cursor: String?,\n    val reachEnd: Boolean,\n)\n\n@Serializable\nsealed interface StatusNotification {\n\n    val id: String\n\n    val createAt: Instant\n\n    val locator: PlatformLocator\n\n    val status: StatusUiState?\n\n    val unread: Boolean\n\n    val formattingDisplayTime: FormattingTime\n\n    @Serializable\n    data class Like(\n        override val locator: PlatformLocator,\n        override val id: String,\n        val author: BlogAuthor,\n        val blog: Blog,\n        override val createAt: Instant,\n        override val unread: Boolean,\n    ) : StatusNotification {\n\n        override val formattingDisplayTime: FormattingTime by lazy {\n            FormattingTime(createAt)\n        }\n\n        override val status: StatusUiState? = null\n    }\n\n    @Serializable\n    data class Follow(\n        override val locator: PlatformLocator,\n        override val id: String,\n        val author: BlogAuthor,\n        override val createAt: Instant,\n        override val unread: Boolean,\n    ) : StatusNotification {\n\n        override val status: StatusUiState? = null\n\n        override val formattingDisplayTime: FormattingTime by lazy {\n            FormattingTime(createAt)\n        }\n    }\n\n    @Serializable\n    data class Mention(\n        override val id: String,\n        val author: BlogAuthor,\n        override val status: StatusUiState,\n        override val unread: Boolean,\n    ) : StatusNotification {\n\n        override val createAt: Instant\n            get() = status.status.createAt\n\n        override val locator: PlatformLocator\n            get() = status.locator\n\n        override val formattingDisplayTime: FormattingTime by lazy {\n            FormattingTime(createAt)\n        }\n    }\n\n    @Serializable\n    data class Repost(\n        override val id: String,\n        val author: BlogAuthor,\n        val blog: Blog,\n        override val locator: PlatformLocator,\n        override val createAt: Instant,\n        override val unread: Boolean,\n    ) : StatusNotification {\n\n        override val status: StatusUiState? = null\n\n        override val formattingDisplayTime: FormattingTime by lazy {\n            FormattingTime(createAt)\n        }\n    }\n\n    @Serializable\n    data class Quote(\n        override val id: String,\n        val author: BlogAuthor,\n        val quote: StatusUiState,\n        override val unread: Boolean,\n    ) : StatusNotification {\n\n        override val createAt: Instant\n            get() = quote.status.createAt\n\n        override val status: StatusUiState get() = quote\n\n        override val locator: PlatformLocator\n            get() = quote.locator\n\n        override val formattingDisplayTime: FormattingTime by lazy {\n            FormattingTime(createAt)\n        }\n    }\n\n    @Serializable\n    data class QuoteUpdate(\n        override val id: String,\n        val author: BlogAuthor,\n        val quote: StatusUiState,\n        override val unread: Boolean,\n        override val createAt: Instant,\n    ) : StatusNotification {\n\n        override val status: StatusUiState get() = quote\n\n        override val locator: PlatformLocator\n            get() = quote.locator\n\n        override val formattingDisplayTime: FormattingTime by lazy {\n            FormattingTime(createAt)\n        }\n    }\n\n    @Serializable\n    data class Reply(\n        override val id: String,\n        val author: BlogAuthor,\n        val reply: StatusUiState,\n        override val unread: Boolean,\n    ) : StatusNotification {\n\n        override val createAt: Instant\n            get() = reply.status.createAt\n\n        override val status: StatusUiState get() = reply\n\n        override val locator: PlatformLocator\n            get() = reply.locator\n\n        override val formattingDisplayTime: FormattingTime by lazy {\n            FormattingTime(createAt)\n        }\n    }\n\n    @Serializable\n    data class NewStatus(\n        override val status: StatusUiState,\n        override val unread: Boolean,\n    ) : StatusNotification {\n        override val id: String\n            get() = status.status.id\n\n        override val createAt: Instant\n            get() = status.status.createAt\n\n        override val locator: PlatformLocator\n            get() = status.locator\n\n        override val formattingDisplayTime: FormattingTime by lazy {\n            FormattingTime(createAt)\n        }\n    }\n\n    @Serializable\n    data class FollowRequest(\n        override val id: String,\n        override val locator: PlatformLocator,\n        override val createAt: Instant,\n        val author: BlogAuthor,\n        override val unread: Boolean,\n    ) : StatusNotification {\n\n        override val status: StatusUiState?\n            get() = null\n\n        override val formattingDisplayTime: FormattingTime by lazy {\n            FormattingTime(createAt)\n        }\n    }\n\n    @Serializable\n    data class Poll(\n        override val id: String,\n        override val createAt: Instant,\n        val blog: Blog,\n        override val locator: PlatformLocator,\n        override val unread: Boolean,\n    ) : StatusNotification {\n\n        override val status: StatusUiState? = null\n\n        override val formattingDisplayTime: FormattingTime by lazy {\n            FormattingTime(createAt)\n        }\n    }\n\n    @Serializable\n    data class Update(\n        override val id: String,\n        override val createAt: Instant,\n        override val status: StatusUiState,\n        override val unread: Boolean,\n    ) : StatusNotification {\n\n        override val locator: PlatformLocator\n            get() = status.locator\n\n        override val formattingDisplayTime: FormattingTime by lazy {\n            FormattingTime(createAt)\n        }\n    }\n\n    @Serializable\n    data class SeveredRelationships(\n        override val id: String,\n        override val createAt: Instant,\n        override val locator: PlatformLocator,\n        val author: BlogAuthor,\n        val reason: String,\n        override val unread: Boolean,\n    ) : StatusNotification {\n\n        override val status: StatusUiState? = null\n\n        override val formattingDisplayTime: FormattingTime by lazy {\n            FormattingTime(createAt)\n        }\n    }\n\n    @Serializable\n    data class Unknown(\n        override val id: String,\n        override val locator: PlatformLocator,\n        val message: String,\n        override val createAt: Instant,\n        override val unread: Boolean,\n    ) : StatusNotification {\n\n        override val status: StatusUiState? = null\n\n        override val formattingDisplayTime: FormattingTime by lazy {\n            FormattingTime(createAt)\n        }\n    }\n}\n"
  },
  {
    "path": "bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/platform/BlogPlatform.kt",
    "content": "package com.zhangke.fread.status.platform\n\nimport com.zhangke.framework.network.FormalBaseUrl\nimport com.zhangke.framework.utils.Parcelize\nimport com.zhangke.framework.utils.PlatformParcelable\nimport com.zhangke.framework.utils.PlatformSerializable\nimport com.zhangke.fread.status.model.StatusProviderProtocol\nimport kotlinx.serialization.Serializable\n\n@Serializable\n@Parcelize\ndata class BlogPlatform(\n    val uri: String,\n    val name: String,\n    val description: String,\n    val baseUrl: FormalBaseUrl,\n    val protocol: StatusProviderProtocol,\n    val thumbnail: String?,\n    val supportsQuotePost: Boolean? = null,\n) : PlatformParcelable, PlatformSerializable\n"
  },
  {
    "path": "bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/platform/PlatformResolver.kt",
    "content": "package com.zhangke.fread.status.platform\n\nimport com.zhangke.framework.collections.mapFirst\n\nclass PlatformResolver(\n    private val resolverList: List<IPlatformResolver>,\n) {\n\n    suspend fun resolve(blogSnapshot: PlatformSnapshot): Result<BlogPlatform> {\n        return resolverList.mapFirst { it.resolve(blogSnapshot) }\n    }\n\n    suspend fun getSuggestedPlatformList(): List<PlatformSnapshot> {\n        return resolverList.map { it.getSuggestedPlatformSnapshotList() }.flatten()\n    }\n}\n\ninterface IPlatformResolver {\n\n    suspend fun resolve(blogSnapshot: PlatformSnapshot): Result<BlogPlatform>?\n\n    suspend fun getSuggestedPlatformSnapshotList(): List<PlatformSnapshot>\n}\n"
  },
  {
    "path": "bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/platform/PlatformSnapshot.kt",
    "content": "package com.zhangke.fread.status.platform\n\nimport com.zhangke.fread.status.model.StatusProviderProtocol\n\ndata class PlatformSnapshot(\n    val uri: String? = null,\n    val name: String? = null,\n    val domain: String,\n    val description: String,\n    val thumbnail: String,\n    val protocol: StatusProviderProtocol,\n    val priority: Int = 0,\n)\n"
  },
  {
    "path": "bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/publish/PublishBlogManager.kt",
    "content": "package com.zhangke.fread.status.publish\n\nimport com.zhangke.fread.status.account.LoggedAccount\nimport com.zhangke.fread.status.model.PublishBlogRules\n\nclass PublishBlogManager(\n    private val managerList: List<IPublishBlogManager>,\n) {\n\n    suspend fun getPublishBlogRules(account: LoggedAccount): Result<PublishBlogRules> {\n        return managerList.firstNotNullOf { it.getPublishBlogRules(account) }\n    }\n\n    suspend fun publish(account: LoggedAccount, post: PublishingPost): Result<Unit> {\n        return managerList.firstNotNullOf { it.publish(account, post) }\n    }\n}\n\ninterface IPublishBlogManager {\n\n    suspend fun getPublishBlogRules(account: LoggedAccount): Result<PublishBlogRules>?\n\n    suspend fun publish(account: LoggedAccount, post: PublishingPost): Result<Unit>?\n}\n"
  },
  {
    "path": "bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/publish/PublishingPost.kt",
    "content": "package com.zhangke.fread.status.publish\n\nimport com.zhangke.framework.utils.ContentProviderFile\nimport com.zhangke.fread.status.model.PostInteractionSetting\nimport com.zhangke.fread.status.model.StatusVisibility\n\ndata class PublishingPost(\n    val content: String,\n    val sensitive: Boolean,\n    val warningText: String?,\n    val languageCode: String,\n    val visibility: StatusVisibility,\n    val interactionSetting: PostInteractionSetting,\n    val medias: List<PublishingMedia>,\n)\n\ndata class PublishingMedia(\n    val file: ContentProviderFile,\n    val alt: String,\n    val isVideo: Boolean,\n)\n"
  },
  {
    "path": "bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/richtext/OnLinkTargetClick.kt",
    "content": "package com.zhangke.fread.status.richtext\n\nimport com.zhangke.fread.status.richtext.model.RichLinkTarget\n\ntypealias OnLinkTargetClick = (RichLinkTarget) -> Unit"
  },
  {
    "path": "bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/richtext/RichText.kt",
    "content": "package com.zhangke.fread.status.richtext\n\nimport androidx.compose.ui.text.AnnotatedString\nimport com.zhangke.framework.utils.PlatformSerializable\nimport com.zhangke.framework.utils.PlatformTransient\nimport com.zhangke.fread.status.model.Emoji\nimport com.zhangke.fread.status.model.Facet\nimport com.zhangke.fread.status.model.HashtagInStatus\nimport com.zhangke.fread.status.model.Mention\nimport com.zhangke.fread.status.richtext.parser.HtmlParser\nimport kotlinx.serialization.Contextual\nimport kotlinx.serialization.Serializable\n\n@Serializable\nclass RichText(\n    @Suppress(\"MemberVisibilityCanBePrivate\")\n    val document: String,\n    private val mentions: List<Mention> = emptyList(),\n    private val hashTags: List<HashtagInStatus> = emptyList(),\n    val emojis: List<Emoji> = emptyList(),\n    val facets: List<Facet> = emptyList(),\n    val type: RichTextType,\n) : PlatformSerializable {\n\n    @PlatformTransient\n    private var clickableDelegate: OnLinkTargetClick = { target ->\n        onLinkTargetClick?.invoke(target)\n    }\n\n    @PlatformTransient\n    var onLinkTargetClick: OnLinkTargetClick? = null\n\n    @Contextual\n    @PlatformTransient\n    private var richText: AnnotatedString? = null\n\n    fun parse(): AnnotatedString {\n        richText?.let { return it }\n        return HtmlParser.parse(\n            document = document,\n            type = type,\n            emojis = emojis,\n            mentions = mentions,\n            hashTags = hashTags,\n            facets = facets,\n            onLinkTargetClick = clickableDelegate,\n        ).also {\n            richText = it\n        }\n    }\n\n    companion object {\n        val empty by lazy { buildRichText(\"\") }\n    }\n}\n\n@Serializable\nenum class RichTextType {\n\n    HTML,\n    PLAINTEXT,\n    UNKNOWN,\n}\n"
  },
  {
    "path": "bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/richtext/RichTextBuilder.kt",
    "content": "package com.zhangke.fread.status.richtext\n\nimport com.zhangke.fread.status.blog.Blog\nimport com.zhangke.fread.status.model.Emoji\nimport com.zhangke.fread.status.model.Facet\nimport com.zhangke.fread.status.model.HashtagInStatus\nimport com.zhangke.fread.status.model.Mention\nimport com.zhangke.fread.status.model.StatusUiState\nimport com.zhangke.fread.status.status.model.Status\n\nfun buildRichText(\n    document: String,\n    type: RichTextType = RichTextType.UNKNOWN,\n    mentions: List<Mention> = emptyList(),\n    hashTags: List<HashtagInStatus> = emptyList(),\n    emojis: List<Emoji> = emptyList(),\n    facets: List<Facet> = emptyList(),\n): RichText {\n    return RichText(\n        document = document,\n        type = type,\n        mentions = mentions,\n        hashTags = hashTags,\n        emojis = emojis,\n        facets = facets,\n    )\n}\n\nsuspend fun Blog.preParseBlog() {\n    author.humanizedName.parse()\n    humanizedContent.parse()\n    humanizedSpoilerText.parse()\n    humanizedDescription.parse()\n    formattingDisplayTime.parse()\n}\n\nsuspend fun Status.preParseStatus() {\n    triggerAuthor.humanizedName.parse()\n    intrinsicBlog.preParseBlog()\n}\n\nsuspend fun StatusUiState.preParseStatusUiState() {\n    status.triggerAuthor.humanizedName.parse()\n    status.intrinsicBlog.preParseBlog()\n}\n\nsuspend fun List<Status>.preParseStatusList() {\n    forEach {\n        it.preParseStatus()\n    }\n}\n\nsuspend fun List<StatusUiState>.preParse() {\n    forEach {\n        it.preParseStatusUiState()\n    }\n}\n"
  },
  {
    "path": "bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/richtext/model/RichLinkTarget.kt",
    "content": "package com.zhangke.fread.status.richtext.model\n\nimport com.zhangke.fread.status.model.HashtagInStatus\nimport com.zhangke.fread.status.model.Mention\n\nsealed interface RichLinkTarget {\n\n    data class UrlTarget(val url: String) : RichLinkTarget\n\n    data class MentionTarget(val mention: Mention) : RichLinkTarget\n\n    data class MentionDidTarget(val did: String) : RichLinkTarget\n\n    data class HashtagTarget(val hashtag: HashtagInStatus) : RichLinkTarget\n\n    data class MaybeHashtagTarget(val hashtag: String) : RichLinkTarget\n}"
  },
  {
    "path": "bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/richtext/parser/HtmlParser.kt",
    "content": "package com.zhangke.fread.status.richtext.parser\n\nimport androidx.compose.foundation.text.appendInlineContent\nimport androidx.compose.ui.text.AnnotatedString\nimport androidx.compose.ui.text.LinkAnnotation\nimport androidx.compose.ui.text.SpanStyle\nimport androidx.compose.ui.text.TextLinkStyles\nimport androidx.compose.ui.text.buildAnnotatedString\nimport androidx.compose.ui.text.style.TextDecoration\nimport com.fleeksoft.ksoup.Ksoup\nimport com.fleeksoft.ksoup.nodes.Element\nimport com.fleeksoft.ksoup.nodes.Node\nimport com.fleeksoft.ksoup.nodes.TextNode\nimport com.fleeksoft.ksoup.select.NodeVisitor\nimport com.zhangke.framework.architect.theme.primaryLight\nimport com.zhangke.framework.network.SimpleUri\nimport com.zhangke.framework.utils.WebFinger\nimport com.zhangke.fread.status.model.Emoji\nimport com.zhangke.fread.status.model.Facet\nimport com.zhangke.fread.status.model.HashtagInStatus\nimport com.zhangke.fread.status.model.Mention\nimport com.zhangke.fread.status.richtext.OnLinkTargetClick\nimport com.zhangke.fread.status.richtext.RichTextType\nimport com.zhangke.fread.status.richtext.model.RichLinkTarget\n\nobject HtmlParser {\n\n    private const val QUOTE_INLINE_CLASS = \"quote-inline\"\n    private val simpleHtmlRegex = \"<[^>]+>\".toRegex()\n\n    fun parse(\n        document: String,\n        type: RichTextType,\n        emojis: List<Emoji> = emptyList(),\n        mentions: List<Mention> = emptyList(),\n        hashTags: List<HashtagInStatus> = emptyList(),\n        facets: List<Facet> = emptyList(),\n        onLinkTargetClick: OnLinkTargetClick = {},\n    ): AnnotatedString {\n        var isPlaintext = type == RichTextType.PLAINTEXT || facets.isNotEmpty()\n        if (type == RichTextType.UNKNOWN) {\n            if (!simpleHtmlRegex.containsMatchIn(document)) {\n                isPlaintext = true\n            }\n        }\n        if (isPlaintext) {\n            return PlaintextParser.parse(document, facets, onLinkTargetClick)\n        }\n        return buildAnnotatedString {\n            Ksoup.parseBodyFragment(document)\n                .body()\n                .traverse(\n                    ParseVisitor(\n                        spanBuilder = this,\n                        emojis = emojis.associateBy { it.shortcode },\n                        mentions = mentions,\n                        hashTags = hashTags.map { it.copy(name = it.name.lowercase()) },\n                        onLinkTargetClick = onLinkTargetClick,\n                    )\n                )\n        }\n    }\n\n    private class ParseVisitor(\n        private val spanBuilder: AnnotatedString.Builder,\n        private val emojis: Map<String, Emoji>,\n        private val mentions: List<Mention>,\n        private val hashTags: List<HashtagInStatus>,\n        private val onLinkTargetClick: OnLinkTargetClick,\n    ) : NodeVisitor {\n\n        private val popQueue = ArrayDeque<Int>()\n\n        private var skip = false\n        private var inQuoteInline = false\n\n        override fun head(node: Node, depth: Int) {\n            if (skip) return\n            if (inQuoteInline) return\n            if (node is TextNode) {\n                spanBuilder.appendWithEmoji(node.text(), emojis)\n                return\n            }\n            if (node is Element) {\n                when (node.tagName()) {\n                    \"br\" -> spanBuilder.appendLine()\n\n                    \"p\" -> {\n                        if (node.hasClass(QUOTE_INLINE_CLASS)) {\n                            inQuoteInline = true\n                        }\n                    }\n\n                    \"a\" -> {\n                        val href = node.attr(\"href\")\n                        var linkTarget: RichLinkTarget? = null\n                        if (node.hasClass(\"hashtag\")) {\n                            val text = node.text()\n                            if (text.startsWith(\"#\")) {\n                                val hashtagText = text.substring(1).lowercase()\n                                val hashTag = hashTags.firstOrNull { it.name == hashtagText }\n                                linkTarget = if (hashTag != null) {\n                                    RichLinkTarget.HashtagTarget(hashTag)\n                                } else {\n                                    RichLinkTarget.MaybeHashtagTarget(text.substring(1))\n                                }\n                            } else {\n                                if (href.isNotEmpty()) {\n                                    linkTarget = RichLinkTarget.UrlTarget(href)\n                                }\n                            }\n                        } else if (node.hasClass(\"mention\")) {\n                            val id = mentions.firstOrNull { it.url == href }?.id\n                            if (id != null) {\n                                val mention = mentions.firstOrNull { it.id == id }\n                                if (mention != null) {\n                                    linkTarget = RichLinkTarget.MentionTarget(mention)\n                                }\n                            } else {\n                                if (href.isNotEmpty()) {\n                                    linkTarget = RichLinkTarget.UrlTarget(href)\n                                }\n                            }\n                        } else if (href.isNotEmpty()) {\n                            linkTarget = RichLinkTarget.UrlTarget(href)\n                        }\n                        if (linkTarget != null) {\n                            popQueue.addLast(\n                                spanBuilder.pushLink(\n                                    LinkAnnotation.Clickable(\n                                        tag = when (linkTarget) {\n                                            is RichLinkTarget.UrlTarget -> linkTarget.url\n                                            is RichLinkTarget.MentionTarget -> linkTarget.mention.id\n                                            is RichLinkTarget.MentionDidTarget -> linkTarget.did\n                                            is RichLinkTarget.HashtagTarget -> linkTarget.hashtag.name\n                                            is RichLinkTarget.MaybeHashtagTarget -> linkTarget.hashtag\n                                        },\n                                        styles = TextLinkStyles(\n                                            style = SpanStyle(color = primaryLight),\n                                            hoveredStyle = SpanStyle(textDecoration = TextDecoration.Underline),\n                                        ),\n                                        linkInteractionListener = {\n                                            onLinkTargetClick(linkTarget)\n                                        },\n                                    )\n                                )\n                            )\n                        } else {\n                            // no href\n                        }\n                    }\n\n                    \"span\" -> {\n                        if (node.hasClass(\"invisible\")) {\n                            skip = true\n                        }\n                    }\n                }\n            }\n        }\n\n        override fun tail(node: Node, depth: Int) {\n            if (node is Element) {\n                when (node.tagName()) {\n                    \"a\" -> {\n                        if (popQueue.isNotEmpty()) {\n                            spanBuilder.pop(popQueue.removeLast())\n                        }\n                    }\n\n                    \"p\" -> {\n                        if (node.hasClass(QUOTE_INLINE_CLASS)) {\n                            inQuoteInline = false\n                        } else if (node.hasNextNonBlankSibling()) {\n                            spanBuilder.appendParagraphBreak()\n                        }\n                    }\n\n                    \"span\" -> {\n                        skip = false\n                    }\n                }\n            }\n        }\n    }\n\n    fun parseToPlainText(document: String): String {\n        return buildString {\n            Ksoup.parseBodyFragment(document)\n                .body()\n                .traverse(ParseToPlainVisitor(this))\n        }\n    }\n\n    class ParseToPlainVisitor(private val builder: StringBuilder) : NodeVisitor {\n\n        private var inQuoteInline = false\n\n        private fun Element?.isMention(): Boolean {\n            if (this == null) return false\n            if (hasClass(\"hashtag\")) return false\n            if (hasClass(\"mention\")) return true\n            return parent().isMention()\n        }\n\n        private fun Element?.mentionHref(): String? {\n            if (this == null) return null\n            if (hasClass(\"mention\")) {\n                return this.attr(\"href\")\n            }\n            return parent().mentionHref()\n        }\n\n        override fun head(node: Node, depth: Int) {\n            if (inQuoteInline) return\n            if (node is Element) {\n                if (node.tagName() == \"p\" && node.hasClass(QUOTE_INLINE_CLASS)) {\n                    inQuoteInline = true\n                    return\n                }\n                if (node.tagName() == \"br\") {\n                    builder.appendLine()\n                }\n                if (node.isMention()) {\n                    if (node.tagName() != \"a\") return\n                    val text = node.text()\n                    val href = node.mentionHref()\n                    builder.append(buildMentionText(text, href))\n                    return\n                }\n            }\n            if (node is TextNode) {\n                if ((node.parent() as? Element)?.isMention() == true) return\n                builder.append(node.text())\n            }\n        }\n\n        override fun tail(node: Node, depth: Int) {\n            if (node is Element && node.tagName() == \"p\") {\n                if (node.hasClass(QUOTE_INLINE_CLASS)) {\n                    inQuoteInline = false\n                } else if (node.hasNextNonBlankSibling()) {\n                    builder.appendParagraphBreak()\n                }\n            }\n        }\n\n        private fun buildMentionText(text: String, href: String?): String {\n            if (href.isNullOrBlank()) return text\n            val url = SimpleUri.parse(href) ?: return text\n            val textAsWebFinger = WebFinger.create(text)\n            if (textAsWebFinger != null) return text\n            return \"$text@${url.host}\"\n        }\n    }\n\n}\n\nprivate val EMOJI_CODE_PATTERN = (\":(\\\\w+):\").toRegex()\n\nprivate fun AnnotatedString.Builder.appendParagraphBreak() {\n    appendLine()\n    appendLine()\n}\n\nprivate fun StringBuilder.appendParagraphBreak() {\n    appendLine()\n    appendLine()\n}\n\nprivate fun Node.hasNextNonBlankSibling(): Boolean {\n    var sibling = nextSibling()\n    while (sibling != null) {\n        when (sibling) {\n            is TextNode -> {\n                if (sibling.text().isNotBlank()) return true\n            }\n            is Element -> return true\n        }\n        sibling = sibling.nextSibling()\n    }\n    return false\n}\n\ninternal fun AnnotatedString.Builder.appendWithEmoji(\n    text: String,\n    emojis: Map<String, Emoji>,\n) {\n    if (text.isEmpty()) {\n        return\n    }\n    if (emojis.isEmpty()) {\n        append(text)\n        return\n    }\n    val results = EMOJI_CODE_PATTERN.findAll(text)\n\n    var index = 0\n    results.iterator().forEach {\n        if (it.range.first > index) {\n            append(text.substring(index, it.range.first))\n        }\n\n        val emojiCode = it.groups[1]?.value\n        if (emojiCode != null) {\n            val emoji = emojis[emojiCode]\n            if (emoji != null) {\n                appendInlineContent(\"emoji\", \":${emoji.shortcode}:\")\n            } else {\n                append(it.value)\n            }\n        } else {\n            append(it.value)\n        }\n        index = it.range.last + 1\n    }\n    if (index < text.length) {\n        append(text.substring(index))\n    }\n}\n"
  },
  {
    "path": "bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/richtext/parser/PlaintextParser.kt",
    "content": "package com.zhangke.fread.status.richtext.parser\n\nimport androidx.compose.ui.text.AnnotatedString\nimport androidx.compose.ui.text.LinkAnnotation\nimport androidx.compose.ui.text.SpanStyle\nimport androidx.compose.ui.text.TextLinkStyles\nimport androidx.compose.ui.text.style.TextDecoration\nimport com.zhangke.framework.architect.theme.primaryLight\nimport com.zhangke.fread.status.model.Facet\nimport com.zhangke.fread.status.model.FacetFeatureUnion\nimport com.zhangke.fread.status.richtext.OnLinkTargetClick\nimport com.zhangke.fread.status.richtext.model.RichLinkTarget\nimport okio.utf8Size\n\nobject PlaintextParser {\n\n    fun parse(\n        document: String,\n        facets: List<Facet>,\n        onLinkTargetClick: OnLinkTargetClick,\n    ): AnnotatedString {\n        val annotatedStringBuilder = AnnotatedString.Builder()\n        annotatedStringBuilder.append(document)\n        val legalRange = 0..document.length\n        for (facet in facets) {\n            val start = calculateUtf8Index(facet.byteStart, document)\n            val end = calculateUtf8Index(facet.byteEnd, document)\n            if (start > end) continue\n            if (!legalRange.contains(start) || !legalRange.contains(end)) {\n                continue\n            }\n            for (feature in facet.features) {\n                var linkTarget: RichLinkTarget? = null\n                var linkTag: String? = null\n                when (feature) {\n                    is FacetFeatureUnion.Mention -> {\n                        linkTag = feature.did\n                        linkTarget = RichLinkTarget.MentionDidTarget(feature.did)\n                    }\n\n                    is FacetFeatureUnion.Link -> {\n                        linkTag = feature.uri\n                        linkTarget = RichLinkTarget.UrlTarget(feature.uri)\n                    }\n\n                    is FacetFeatureUnion.Tag -> {\n                        linkTag = feature.tag\n                        linkTarget = RichLinkTarget.MaybeHashtagTarget(feature.tag)\n                    }\n                }\n                annotatedStringBuilder.addLink(\n                    clickable = LinkAnnotation.Clickable(\n                        tag = linkTag,\n                        styles = TextLinkStyles(\n                            style = SpanStyle(color = primaryLight),\n                            hoveredStyle = SpanStyle(textDecoration = TextDecoration.Underline),\n                        ),\n                        linkInteractionListener = {\n                            onLinkTargetClick(linkTarget)\n                        },\n                    ),\n                    start = start.toInt(),\n                    end = end.toInt(),\n                )\n            }\n        }\n        return annotatedStringBuilder.toAnnotatedString()\n    }\n\n    private fun calculateUtf8Index(\n        index: Long,\n        text: String,\n    ): Long {\n        if (index > text.length) {\n            return text.length.toLong()\n        }\n        val utf8Index = text.utf8Size(0, index.toInt())\n        if (utf8Index == index) return index\n        val diff = utf8Index - index\n        if (diff > 0) {\n            return index - diff\n        }\n        return index\n    }\n}\n"
  },
  {
    "path": "bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/screen/StatusScreenProvider.kt",
    "content": "package com.zhangke.fread.status.screen\n\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.nav.Tab\nimport com.zhangke.framework.network.FormalBaseUrl\nimport com.zhangke.framework.utils.WebFinger\nimport com.zhangke.fread.status.account.LoggedAccount\nimport com.zhangke.fread.status.blog.Blog\nimport com.zhangke.fread.status.model.FreadContent\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.model.StatusProviderProtocol\nimport com.zhangke.fread.status.platform.BlogPlatform\nimport com.zhangke.fread.status.uri.FormalUri\n\nclass StatusScreenProvider(\n    private val providerList: List<IStatusScreenProvider>\n) {\n\n    fun getReplyBlogScreen(locator: PlatformLocator, blog: Blog): NavKey? {\n        return providerList.firstNotNullOfOrNull {\n            it.getReplyBlogScreen(locator, blog)\n        }\n    }\n\n    fun getEditBlogScreen(locator: PlatformLocator, blog: Blog): NavKey? {\n        return providerList.firstNotNullOfOrNull {\n            it.getEditBlogScreen(locator, blog)\n        }\n    }\n\n    fun getQuoteBlogScreen(locator: PlatformLocator, blog: Blog): NavKey? {\n        return providerList.firstNotNullOfOrNull { it.getQuoteBlogScreen(locator, blog) }\n    }\n\n    fun getContentScreen(content: FreadContent, isLatestTab: Boolean): Tab {\n        return providerList.firstNotNullOf {\n            it.getContentScreen(content, isLatestTab)\n        }\n    }\n\n    fun getEditContentConfigScreenScreen(content: FreadContent): NavKey? {\n        return providerList.firstNotNullOfOrNull {\n            it.getEditContentConfigScreenScreen(content)\n        }\n    }\n\n    fun getUserDetailScreen(\n        locator: PlatformLocator,\n        uri: FormalUri,\n        userId: String?,\n    ): NavKey? {\n        return providerList.firstNotNullOfOrNull { it.getUserDetailScreen(locator, uri, userId) }\n    }\n\n    fun getUserDetailScreen(\n        locator: PlatformLocator,\n        webFinger: WebFinger,\n        protocol: StatusProviderProtocol,\n    ): NavKey? {\n        return providerList.firstNotNullOfOrNull {\n            it.getUserDetailScreen(locator, webFinger, protocol)\n        }\n    }\n\n    fun getUserDetailScreen(\n        locator: PlatformLocator,\n        did: String,\n        protocol: StatusProviderProtocol,\n    ): NavKey? {\n        return providerList.firstNotNullOfOrNull {\n            it.getUserDetailScreen(locator, did, protocol)\n        }\n    }\n\n    fun getUserDetailScreenWithoutAccount(uri: FormalUri): NavKey? {\n        return providerList.firstNotNullOfOrNull {\n            it.getUserDetailScreenWithoutAccount(uri)\n        }\n    }\n\n    fun getTagTimelineScreen(\n        locator: PlatformLocator,\n        tag: String,\n        protocol: StatusProviderProtocol,\n    ): NavKey? {\n        return providerList.firstNotNullOfOrNull {\n            it.getTagTimelineScreen(\n                locator,\n                tag,\n                protocol\n            )\n        }\n    }\n\n    fun getBlogFavouritedScreen(\n        locator: PlatformLocator,\n        blog: Blog,\n        protocol: StatusProviderProtocol,\n    ): NavKey? {\n        return providerList.firstNotNullOfOrNull {\n            it.getBlogFavouritedScreen(locator, blog, protocol)\n        }\n    }\n\n    fun getBlogBoostedScreen(\n        locator: PlatformLocator,\n        blog: Blog,\n        protocol: StatusProviderProtocol,\n    ): NavKey? {\n        return providerList.firstNotNullOfOrNull {\n            it.getBlogBoostedScreen(\n                locator,\n                blog,\n                protocol\n            )\n        }\n    }\n\n    fun getInstanceDetailScreen(\n        locator: PlatformLocator,\n        protocol: StatusProviderProtocol,\n        baseUrl: FormalBaseUrl,\n    ): NavKey? {\n        return providerList.firstNotNullOfOrNull {\n            it.getInstanceDetailScreen(locator, protocol, baseUrl)\n        }\n    }\n\n    fun getExplorerTab(locator: PlatformLocator, platform: BlogPlatform): Tab? {\n        return providerList.firstNotNullOfOrNull { it.getExplorerTab(locator, platform) }\n    }\n\n    fun getAddContentScreen(protocol: StatusProviderProtocol): NavKey {\n        return providerList.firstNotNullOf {\n            it.getAddContentScreen(protocol)\n        }\n    }\n\n    fun getPublishScreen(\n        account: LoggedAccount,\n        text: String,\n    ): NavKey? {\n        return providerList.firstNotNullOfOrNull { it.getPublishScreen(account, text) }\n    }\n}\n\ninterface IStatusScreenProvider {\n\n    fun getReplyBlogScreen(locator: PlatformLocator, blog: Blog): NavKey?\n\n    fun getEditBlogScreen(locator: PlatformLocator, blog: Blog): NavKey?\n\n    fun getQuoteBlogScreen(locator: PlatformLocator, blog: Blog): NavKey?\n\n    fun getContentScreen(content: FreadContent, isLatestTab: Boolean): Tab?\n\n    fun getEditContentConfigScreenScreen(content: FreadContent): NavKey?\n\n    fun getUserDetailScreen(locator: PlatformLocator, uri: FormalUri, userId: String?): NavKey?\n\n    fun getUserDetailScreenWithoutAccount(uri: FormalUri): NavKey? {\n        return null\n    }\n\n    fun getUserDetailScreen(\n        locator: PlatformLocator,\n        webFinger: WebFinger,\n        protocol: StatusProviderProtocol\n    ): NavKey?\n\n    fun getUserDetailScreen(\n        locator: PlatformLocator,\n        did: String,\n        protocol: StatusProviderProtocol\n    ): NavKey?\n\n    fun getTagTimelineScreen(\n        locator: PlatformLocator,\n        tag: String,\n        protocol: StatusProviderProtocol\n    ): NavKey?\n\n    fun getBlogFavouritedScreen(\n        locator: PlatformLocator,\n        blog: Blog,\n        protocol: StatusProviderProtocol,\n    ): NavKey?\n\n    fun getBlogBoostedScreen(\n        locator: PlatformLocator,\n        blog: Blog,\n        protocol: StatusProviderProtocol,\n    ): NavKey?\n\n    fun getInstanceDetailScreen(\n        locator: PlatformLocator,\n        protocol: StatusProviderProtocol,\n        baseUrl: FormalBaseUrl,\n    ): NavKey? {\n        return null\n    }\n\n    fun getExplorerTab(locator: PlatformLocator, platform: BlogPlatform): Tab?\n\n    fun getAddContentScreen(protocol: StatusProviderProtocol): NavKey? {\n        return null\n    }\n\n    fun getPublishScreen(account: LoggedAccount, text: String): NavKey? {\n        return null\n    }\n}\n"
  },
  {
    "path": "bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/search/SearchContentResult.kt",
    "content": "package com.zhangke.fread.status.search\n\nimport com.zhangke.fread.status.model.StatusProviderProtocol\nimport com.zhangke.fread.status.platform.BlogPlatform\nimport com.zhangke.fread.status.platform.PlatformSnapshot\nimport com.zhangke.fread.status.source.StatusSource\n\nsealed interface SearchContentResult {\n\n    data class SearchedPlatformSnapshot(val platform: PlatformSnapshot) : SearchContentResult\n\n    data class Platform(val platform: BlogPlatform) : SearchContentResult\n\n    data class Source(val source: StatusSource) : SearchContentResult\n\n    val protocol: StatusProviderProtocol\n        get() = when (this) {\n            is SearchedPlatformSnapshot -> platform.protocol\n            is Source -> source.protocol\n            is Platform -> platform.protocol\n        }\n}\n\nsealed interface SearchedPlatform {\n\n    data class Snapshot(val snapshot: PlatformSnapshot) : SearchedPlatform\n\n    data class Platform(val platform: BlogPlatform) : SearchedPlatform\n}\n"
  },
  {
    "path": "bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/search/SearchEngine.kt",
    "content": "package com.zhangke.fread.status.search\n\nimport com.zhangke.fread.status.author.BlogAuthor\nimport com.zhangke.fread.status.model.Hashtag\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.model.StatusProviderProtocol\nimport com.zhangke.fread.status.model.StatusUiState\nimport com.zhangke.fread.status.model.isRss\nimport com.zhangke.fread.status.source.StatusSource\nimport com.zhangke.fread.status.utils.collect\nimport kotlinx.coroutines.flow.Flow\n\nclass SearchEngine(\n    private val engineList: List<ISearchEngine>,\n) {\n\n    suspend fun search(locator: PlatformLocator, query: String): Result<List<SearchResult>> {\n        return engineList.map { it.search(locator, query.trim()) }.collect()\n    }\n\n    suspend fun searchStatus(\n        locator: PlatformLocator,\n        query: String,\n        maxId: String?\n    ): Result<List<StatusUiState>> {\n        return engineList.map { it.searchStatus(locator, query, maxId) }.collect()\n    }\n\n    suspend fun searchHashtag(\n        locator: PlatformLocator,\n        query: String,\n        offset: Int?\n    ): Result<List<Hashtag>> {\n        return engineList.map { it.searchHashtag(locator, query, offset) }.collect()\n    }\n\n    suspend fun searchAuthor(\n        locator: PlatformLocator,\n        query: String,\n        offset: Int?\n    ): Result<List<BlogAuthor>> {\n        return engineList.map { it.searchAuthor(locator, query, offset) }.collect()\n    }\n\n    suspend fun searchSourceNoToken(query: String): Result<List<StatusSource>> {\n        return engineList.map { it.searchSourceNoToken(query.trim()) }.collect()\n            .map { list -> list.sortedBy { if (it.protocol.isRss) 0 else 1 } }\n    }\n\n    suspend fun searchPlatform(\n        locator: PlatformLocator,\n        query: String,\n    ): Flow<List<SearchedPlatform>> {\n        return engineList.firstNotNullOf { it.searchPlatform(locator, query) }\n    }\n\n    suspend fun searchStatusByUrl(\n        protocol: StatusProviderProtocol,\n        locator: PlatformLocator,\n        url: String,\n    ): Result<StatusUiState?> {\n        return engineList.firstNotNullOf { it.searchStatusByUrl(protocol, locator, url) }\n    }\n}\n\ninterface ISearchEngine {\n\n    suspend fun search(locator: PlatformLocator, query: String): Result<List<SearchResult>>\n\n    suspend fun searchStatus(\n        locator: PlatformLocator,\n        query: String,\n        maxId: String?,\n    ): Result<List<StatusUiState>>\n\n    suspend fun searchHashtag(\n        locator: PlatformLocator,\n        query: String,\n        offset: Int?,\n    ): Result<List<Hashtag>>\n\n    suspend fun searchAuthor(\n        locator: PlatformLocator,\n        query: String,\n        offset: Int?,\n    ): Result<List<BlogAuthor>>\n\n    suspend fun searchSourceNoToken(query: String): Result<List<StatusSource>>\n\n    suspend fun searchPlatform(\n        locator: PlatformLocator,\n        query: String,\n    ): Flow<List<SearchedPlatform>>? {\n        return null\n    }\n\n    suspend fun searchStatusByUrl(\n        protocol: StatusProviderProtocol,\n        locator: PlatformLocator,\n        url: String,\n    ): Result<StatusUiState?>? {\n        return null\n    }\n}\n"
  },
  {
    "path": "bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/search/SearchResult.kt",
    "content": "package com.zhangke.fread.status.search\n\nimport com.zhangke.fread.status.author.BlogAuthor\nimport com.zhangke.fread.status.model.Hashtag\nimport com.zhangke.fread.status.model.StatusUiState\nimport com.zhangke.fread.status.platform.BlogPlatform\nimport com.zhangke.fread.status.status.model.Status\n\nsealed interface SearchResult {\n\n    data class Author(val user: BlogAuthor) : SearchResult\n\n    data class Platform(val platform: BlogPlatform) : SearchResult\n\n    data class SearchedStatus(val status: StatusUiState) : SearchResult\n\n    data class SearchedHashtag(val hashtag: Hashtag) : SearchResult\n}\n"
  },
  {
    "path": "bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/source/StatusSource.kt",
    "content": "package com.zhangke.fread.status.source\n\nimport com.zhangke.framework.utils.Parcelize\nimport com.zhangke.framework.utils.PlatformParcelable\nimport com.zhangke.framework.utils.PlatformSerializable\nimport com.zhangke.fread.status.model.StatusProviderProtocol\nimport com.zhangke.fread.status.uri.FormalUri\n\n@Parcelize\ndata class StatusSource(\n    val uri: FormalUri,\n    val name: String,\n    val handle: String,\n    val description: String,\n    val thumbnail: String?,\n    val protocol: StatusProviderProtocol,\n): PlatformParcelable, PlatformSerializable\n"
  },
  {
    "path": "bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/source/StatusSourceResolver.kt",
    "content": "package com.zhangke.fread.status.source\n\nimport com.zhangke.framework.collections.mapFirst\nimport com.zhangke.fread.status.uri.FormalUri\n\nclass StatusSourceResolver(\n    private val resolverList: List<IStatusSourceResolver>,\n) {\n\n    suspend fun resolveSourceByUri(uri: FormalUri): Result<StatusSource?> {\n        resolverList.forEach {\n            val result = it.resolveSourceByUri(uri)\n            if (result.getOrNull() != null) return result\n        }\n        return Result.success(null)\n    }\n\n    suspend fun resolveRssSource(rssUrl: String): Result<StatusSource> {\n        return resolverList.mapFirst { it.resolveRssSource(rssUrl) }\n    }\n}\n\ninterface IStatusSourceResolver {\n\n    suspend fun resolveSourceByUri(uri: FormalUri): Result<StatusSource?>\n\n    suspend fun resolveRssSource(rssUrl: String): Result<StatusSource>?\n}\n"
  },
  {
    "path": "bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/status/StatusResolver.kt",
    "content": "package com.zhangke.fread.status.status\n\nimport com.zhangke.framework.collections.mapFirst\nimport com.zhangke.fread.status.author.BlogAuthor\nimport com.zhangke.fread.status.blog.Blog\nimport com.zhangke.fread.status.blog.BlogPoll\nimport com.zhangke.fread.status.blog.BlogTranslation\nimport com.zhangke.fread.status.model.PagedData\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.model.StatusActionType\nimport com.zhangke.fread.status.model.StatusUiState\nimport com.zhangke.fread.status.platform.BlogPlatform\nimport com.zhangke.fread.status.status.model.Status\nimport com.zhangke.fread.status.status.model.StatusContext\nimport com.zhangke.fread.status.uri.FormalUri\n\nclass StatusResolver(\n    private val resolverList: List<IStatusResolver>,\n) {\n\n    suspend fun getStatus(\n        locator: PlatformLocator,\n        blogId: String?,\n        blogUri: String?,\n        platform: BlogPlatform,\n    ): Result<StatusUiState> {\n        return resolverList.mapFirst { it.getStatus(locator, blogId, blogUri, platform) }\n    }\n\n    suspend fun getStatusList(\n        uri: FormalUri,\n        limit: Int,\n        maxId: String? = null,\n    ): Result<PagedData<StatusUiState>> {\n        for (statusResolver in resolverList) {\n            val result = statusResolver.getStatusList(\n                uri = uri,\n                limit = limit,\n                maxId = maxId,\n            )\n            if (result != null) return result\n        }\n        return Result.failure(IllegalArgumentException(\"Unsupported uri:$uri!\"))\n    }\n\n    suspend fun interactive(\n        locator: PlatformLocator,\n        status: Status,\n        type: StatusActionType,\n    ): Result<Status?> {\n        return resolverList.mapFirst { it.interactive(locator, status, type) }\n    }\n\n    suspend fun follow(\n        locator: PlatformLocator,\n        target: BlogAuthor,\n    ): Result<Unit> {\n        return resolverList.mapFirst { it.follow(locator, target) }\n    }\n\n    suspend fun unfollow(\n        locator: PlatformLocator,\n        target: BlogAuthor,\n    ): Result<Unit> {\n        return resolverList.mapFirst { it.unfollow(locator, target) }\n    }\n\n    suspend fun votePoll(\n        locator: PlatformLocator,\n        blog: Blog,\n        votedOption: List<BlogPoll.Option>,\n    ): Result<Status> {\n        return resolverList.mapFirst { it.votePoll(locator, blog, votedOption) }\n    }\n\n    suspend fun getStatusContext(locator: PlatformLocator, status: Status): Result<StatusContext> {\n        return resolverList.firstNotNullOf { it.getStatusContext(locator, status) }\n    }\n\n    suspend fun isFollowing(\n        locator: PlatformLocator,\n        target: BlogAuthor,\n    ): Result<Boolean>? {\n        return resolverList.firstNotNullOfOrNull { it.isFollowing(locator, target) }\n    }\n\n    suspend fun translate(\n        locator: PlatformLocator,\n        status: Status,\n        lan: String,\n    ): Result<BlogTranslation> {\n        return resolverList.firstNotNullOf { it.translate(locator, status, lan) }\n    }\n}\n\ninterface IStatusResolver {\n\n    suspend fun getStatus(\n        locator: PlatformLocator,\n        blogId: String?,\n        blogUri: String?,\n        platform: BlogPlatform,\n    ): Result<StatusUiState>?\n\n    /**\n     * @return null if un-support\n     */\n    suspend fun getStatusList(\n        uri: FormalUri,\n        limit: Int,\n        maxId: String?\n    ): Result<PagedData<StatusUiState>>?\n\n    suspend fun interactive(\n        locator: PlatformLocator,\n        status: Status,\n        type: StatusActionType,\n    ): Result<Status?>?\n\n    suspend fun votePoll(\n        locator: PlatformLocator,\n        blog: Blog,\n        votedOption: List<BlogPoll.Option>,\n    ): Result<Status>?\n\n    suspend fun getStatusContext(locator: PlatformLocator, status: Status): Result<StatusContext>?\n\n    suspend fun follow(\n        locator: PlatformLocator,\n        target: BlogAuthor,\n    ): Result<Unit>?\n\n    suspend fun unfollow(\n        locator: PlatformLocator,\n        target: BlogAuthor,\n    ): Result<Unit>?\n\n    suspend fun isFollowing(\n        locator: PlatformLocator,\n        target: BlogAuthor,\n    ): Result<Boolean>?\n\n    suspend fun translate(\n        locator: PlatformLocator,\n        status: Status,\n        lan: String,\n    ): Result<BlogTranslation>?\n}\n"
  },
  {
    "path": "bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/status/model/Status.kt",
    "content": "package com.zhangke.fread.status.status.model\n\nimport com.zhangke.framework.datetime.Instant\nimport com.zhangke.framework.utils.PlatformSerializable\nimport com.zhangke.fread.status.author.BlogAuthor\nimport com.zhangke.fread.status.blog.Blog\nimport com.zhangke.fread.status.platform.BlogPlatform\nimport kotlinx.serialization.Serializable\n\n@Serializable\nsealed class Status : PlatformSerializable {\n\n    abstract val createAt: Instant\n\n    /**\n     * identify of this status, must non-empty\n     */\n    abstract val id: String\n\n    /**\n     * 该 Platform 表示获取到该 Status 时使用的 Platform，而不是 Status 本身所属的 Platform。\n     */\n    abstract val platform: BlogPlatform\n\n    /**\n     * 触发该 Status 的用户\n     */\n    val triggerAuthor: BlogAuthor\n        get() = when (this) {\n            is NewBlog -> blog.author\n            is Reblog -> author\n        }\n\n    val intrinsicBlog: Blog\n        get() = when (this) {\n            is NewBlog -> blog\n            is Reblog -> reblog\n        }\n\n    @Serializable\n    data class NewBlog(\n        val blog: Blog,\n    ) : Status(), PlatformSerializable {\n\n        override val id: String get() = blog.id\n\n        override val createAt: Instant get() = blog.createAt\n\n        override val platform: BlogPlatform\n            get() = blog.platform\n    }\n\n    @Serializable\n    data class Reblog(\n        val author: BlogAuthor,\n        override val id: String,\n        override val createAt: Instant,\n        val reblog: Blog,\n    ) : Status(), PlatformSerializable {\n\n        override val platform: BlogPlatform\n            get() = reblog.platform\n    }\n}\n"
  },
  {
    "path": "bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/status/model/StatusContext.kt",
    "content": "package com.zhangke.fread.status.status.model\n\nimport com.zhangke.fread.status.model.StatusUiState\n\ndata class StatusContext(\n    val ancestors: List<StatusUiState>,\n    val status: StatusUiState?,\n    val descendants: List<DescendantStatus>,\n)\n\ndata class DescendantStatus(\n    val status: StatusUiState,\n    val descendantStatus: DescendantStatus?,\n)\n"
  },
  {
    "path": "bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/uri/FormalUri.kt",
    "content": "package com.zhangke.fread.status.uri\n\nimport com.zhangke.framework.utils.Parcelize\nimport com.zhangke.framework.utils.PlatformIgnoredOnParcel\nimport com.zhangke.framework.utils.PlatformParcelable\nimport com.zhangke.framework.utils.PlatformSerializable\nimport com.zhangke.framework.utils.UrlEncoder\nimport com.zhangke.framework.utils.uriString\nimport kotlinx.serialization.Serializable\n\n@Parcelize\n@Serializable\nclass FormalUri private constructor(\n    val host: String,\n    /**\n     * format like \"/xxx\"\n     */\n    private val rawPath: String,\n    val queries: Map<String, String>,\n) : PlatformParcelable, PlatformSerializable {\n\n    @PlatformIgnoredOnParcel\n    @Suppress(\"MemberVisibilityCanBePrivate\")\n    val scheme = SCHEME\n\n    @PlatformIgnoredOnParcel\n    val path = fixPath(rawPath)\n\n    override fun toString(): String {\n        return uriString(\n            scheme = scheme,\n            host = host,\n            path = path,\n            queries = queries,\n        )\n    }\n\n    /**\n     * Not encode string\n     */\n    fun toRawString(): String {\n        return uriString(\n            scheme = scheme,\n            host = host,\n            path = path,\n            queries = queries,\n            encode = false,\n        )\n    }\n\n    private fun fixPath(path: String): String {\n        var newPath = path\n        if (!newPath.startsWith('/')) {\n            newPath = \"/$newPath\"\n        }\n        if (newPath.endsWith('/')) {\n            newPath = newPath.substring(0, newPath.length - 1)\n        }\n        return newPath\n    }\n\n    override fun hashCode(): Int {\n        var result = host.hashCode()\n        result = 31 * result + path.hashCode()\n        result = 31 * result + queries.hashCode()\n        return result\n    }\n\n    override fun equals(other: Any?): Boolean {\n        if (other === this) return true\n        if (other == null) return false\n        if (other !is FormalUri) return false\n        return (host == other.host) && (path == other.path) && (queries == other.queries)\n    }\n\n    companion object {\n\n        const val SCHEME = \"freadapp\"\n\n        fun create(host: String, path: String, queries: Map<String, String>): FormalUri {\n            return FormalUri(\n                host = host,\n                rawPath = path,\n                queries = queries,\n            )\n        }\n\n        fun from(uri: String): FormalUri? {\n            val (host, path, queries) = FormalUriParser().parse(uri) ?: return null\n            return FormalUri(host, path, queries)\n        }\n    }\n}\n"
  },
  {
    "path": "bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/uri/FormalUriParser.kt",
    "content": "package com.zhangke.fread.status.uri\n\nimport io.ktor.http.decodeURLQueryComponent\n\nprivate val defaultUriDecoder: (String) -> String = { url ->\n    url.decodeURLQueryComponent()\n}\n\nclass FormalUriParser(\n    val uriDecoder: (String) -> String = defaultUriDecoder,\n) {\n\n    fun parse(uriString: String): Triple<String, String, Map<String, String>>? {\n        val fixedUriString = uriString.trim()\n        val scheme = findScheme(fixedUriString)\n        if (scheme != FormalUri.SCHEME) return null\n        val host = findHost(fixedUriString)\n        if (host.isNullOrEmpty()) return null\n        val path = findPath(fixedUriString)\n        if (path.isNullOrEmpty()) return null\n        val queries = findQueries(fixedUriString)\n        if (queries.isEmpty()) return null\n        return Triple(host, path, queries)\n    }\n\n    private fun findScheme(uri: String): String? {\n        return uri.split(\"://\")\n            .takeIf { it.size >= 2 }\n            ?.getOrNull(0)\n    }\n\n    private fun findHost(uri: String): String? {\n        val scheme = findScheme(uri) ?: return null\n        val uriNoSchemeAndQuery = uri.removePrefix(\"$scheme://\").substringBefore('?')\n        if (!uriNoSchemeAndQuery.contains('/')) return null\n        return uriNoSchemeAndQuery\n            .split(\"/\")\n            .getOrNull(0)\n    }\n\n    private fun findPath(uri: String): String? {\n        val scheme = findScheme(uri) ?: return null\n        val host = findHost(uri) ?: return null\n        return uri.removePrefix(\"$scheme://$host/\").substringBefore('?')\n            .takeIf { it.isNotEmpty() }\n            ?.let { \"/$it\" }\n    }\n\n    private fun findQueries(uri: String): Map<String, String> {\n        val queryPart = uri.split('?').getOrNull(1)\n            .takeIf { !it.isNullOrEmpty() } ?: return emptyMap()\n        return queryPart.split('&')\n            .associate {\n                val array = it.split('=')\n                array[0] to uriDecoder(array.getOrElse(1) { \"\" })\n            }\n    }\n}\n"
  },
  {
    "path": "bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/utils/DateTimeFormatter.kt",
    "content": "package com.zhangke.fread.status.utils\n\nimport androidx.compose.runtime.Composable\nimport com.zhangke.fread.localization.LocalizedString\nimport kotlinx.datetime.Clock\nimport kotlinx.datetime.Instant\nimport kotlinx.datetime.format\nimport kotlinx.datetime.format.DateTimeComponents\nimport kotlinx.datetime.format.Padding\nimport kotlinx.datetime.format.char\nimport org.jetbrains.compose.resources.getString\nimport org.jetbrains.compose.resources.stringResource\nimport kotlin.time.Duration\nimport kotlin.time.Duration.Companion.days\nimport kotlin.time.Duration.Companion.hours\nimport kotlin.time.Duration.Companion.minutes\nimport kotlin.time.Duration.Companion.seconds\nimport kotlin.time.DurationUnit\nimport kotlin.time.ExperimentalTime\n\nobject DateTimeFormatter {\n\n    suspend fun format(\n        datetime: Long,\n    ): String {\n        return format(\n            datetime = datetime,\n            config = defaultFormatConfig(),\n        )\n    }\n\n    @OptIn(ExperimentalTime::class)\n    fun format(\n        datetime: Long,\n        config: DatetimeFormatConfig,\n    ): String {\n        val instant = Instant.fromEpochMilliseconds(datetime)\n        val duration = Clock.System.now() - instant\n        if (duration > 3.days) {\n            // TODO: format with locale\n            return instant.format(\n                DateTimeComponents.Format {\n                    year()\n                    char('-')\n                    monthNumber(Padding.ZERO)\n                    char('-')\n                    dayOfMonth()\n                }\n            )\n        }\n        return \"${config.agoPrefix}${formatDuration(config, duration)}${config.agoSuffix}\"\n    }\n\n    private fun formatDuration(config: DatetimeFormatConfig, duration: Duration): String {\n        if (duration.isInfinite()) return \"\"\n        var leftDuration = duration\n        val day = (leftDuration).inWholeDays.days\n        leftDuration -= day\n        if (day > 0.days) {\n            return \"${day.toInt(DurationUnit.DAYS)} ${config.day}\"\n        }\n        val hours = leftDuration.inWholeHours.hours\n        leftDuration -= hours\n        if (hours > 0.hours) {\n            return \"${hours.toInt(DurationUnit.HOURS)} ${config.hour}\"\n        }\n        val minutes = leftDuration.inWholeMinutes.minutes\n        leftDuration -= minutes\n        if (minutes > 0.minutes) {\n            return \"${minutes.toInt(DurationUnit.MINUTES)} ${config.minutes}\"\n        }\n        val seconds = leftDuration.inWholeSeconds.seconds.coerceAtLeast(1.seconds)\n        return \"${seconds.toInt(DurationUnit.SECONDS)} ${config.second}\"\n    }\n}\n\ndata class DatetimeFormatConfig(\n    val agoPrefix: String,\n    val agoSuffix: String,\n    val day: String,\n    val hour: String,\n    val minutes: String,\n    val second: String,\n)\n\nsuspend fun defaultFormatConfig() = DatetimeFormatConfig(\n    agoPrefix = getString(LocalizedString.date_time_ago_prefix),\n    agoSuffix = getString(LocalizedString.date_time_ago_suffix),\n    day = getString(LocalizedString.date_time_day),\n    hour = getString(LocalizedString.date_time_hour),\n    minutes = getString(LocalizedString.date_time_minute),\n    second = getString(LocalizedString.date_time_second),\n)\n\n@Composable\nfun defaultUiFormatConfig() = DatetimeFormatConfig(\n    agoPrefix = stringResource(LocalizedString.date_time_ago_prefix),\n    agoSuffix = stringResource(LocalizedString.date_time_ago_suffix),\n    day = stringResource(LocalizedString.date_time_day),\n    hour = stringResource(LocalizedString.date_time_hour),\n    minutes = stringResource(LocalizedString.date_time_minute),\n    second = stringResource(LocalizedString.date_time_second),\n)\n"
  },
  {
    "path": "bizframework/status-provider/src/commonMain/kotlin/com/zhangke/fread/status/utils/ResultKtx.kt",
    "content": "package com.zhangke.fread.status.utils\n\nfun <T> List<Result<List<T>>>.collect(): Result<List<T>> {\n    if (this.isEmpty()) return Result.success(emptyList())\n    if (isNotEmpty() && firstOrNull { it.isSuccess } == null) {\n        return first()\n    }\n    return mapNotNull {\n        it.getOrNull()\n    }.reduce { list1, list2 ->\n        mutableListOf<T>().apply {\n            addAll(list1)\n            addAll(list2)\n        }\n    }.let { Result.success(it) }\n}\n"
  },
  {
    "path": "bizframework/status-provider/src/commonTest/kotlin/com/zhangke/fread/status/richtext/parser/HtmlParserTest.kt",
    "content": "package com.zhangke.fread.status.richtext.parser\n\nimport androidx.compose.foundation.text.appendInlineContent\nimport androidx.compose.ui.text.buildAnnotatedString\nimport com.zhangke.fread.status.model.Emoji\nimport kotlin.test.Test\nimport kotlin.test.assertEquals\n\nclass HtmlParserTest {\n\n    @Test\n    fun testAppendWithEmoji() {\n        val html = \"Yeah, you can disable comments on your :pixelfed: posts\"\n        val emojis = mapOf(\n            \"pixelfed\" to Emoji(\"pixelfed\", \"https://files.mastodon.social/custom_emojis/images/000/068/773/static/pixelfed-icon-color.png\", \"\"),\n        )\n        assertEquals(\n            buildAnnotatedString {\n                appendWithEmoji(\n                    html,\n                    emojis,\n                )\n            },\n            buildAnnotatedString {\n                append(\"Yeah, you can disable comments on your \")\n                appendInlineContent(\"emoji\", \"https://files.mastodon.social/custom_emojis/images/000/068/773/static/pixelfed-icon-color.png\")\n                append(\" posts\")\n            }\n        )\n    }\n}"
  },
  {
    "path": "bizframework/status-provider/src/commonTest/kotlin/com/zhangke/fread/status/uri/FormalUriTest.kt",
    "content": "package com.zhangke.fread.status.uri\n\nimport kotlin.test.Test\nimport kotlin.test.assertEquals\nimport kotlin.test.assertTrue\n\nclass FormalUriTest {\n\n    @Test\n    fun `should throw when empty host`() {\n        try {\n            FormalUri.create(\"\", \"\", emptyMap())\n        } catch (e: Throwable) {\n            e.printStackTrace()\n        }\n    }\n\n    @Test\n    fun `should return uri when params is regular`() {\n        val host = \"activity.pub\"\n        val path = \"user\"\n        val queries = mapOf(\"name\" to \"zhangke\")\n        val uri = FormalUri.create(host, path, queries)\n        assertEquals(uri.host, host)\n        assertEquals(uri.path, \"/$path\")\n        assertEquals(uri.queries, queries)\n    }\n\n    @Test\n    fun `should return uri when have multiple path is regular`() {\n        val host = \"activity.pub\"\n        val path = \"user/name\"\n        val queries = mapOf(\"name\" to \"zhangke\")\n        val uri = FormalUri.create(host, path, queries)\n        assertEquals(uri.host, host)\n        assertEquals(uri.path, \"/$path\")\n        assertEquals(uri.queries, queries)\n    }\n\n    @Test\n    fun `should return uri when have single path and path have divider`() {\n        val host = \"activity.pub\"\n        val path = \"user/\"\n        val queries = mapOf(\"name\" to \"zhangke\")\n        val uri = FormalUri.create(host, path, queries)\n        assertEquals(uri.host, host)\n        assertEquals(uri.path, \"/user\")\n        assertEquals(uri.queries, queries)\n    }\n\n    @Test\n    fun `should return uri when have multiple path and path have divider`() {\n        val host = \"activity.pub\"\n        val path = \"user/name/as/\"\n        val queries = mapOf(\"name\" to \"zhangke\")\n        val uri = FormalUri.create(host, path, queries)\n        assertEquals(uri.host, host)\n        assertEquals(uri.path, \"/user/name/as\")\n        assertEquals(uri.queries, queries)\n    }\n\n    @Test\n    fun `should return null when scheme is bad`() {\n        val uriString = \"badScheme://activity/user?query=zhang\"\n        val uri = FormalUri.from(uriString)\n        assertEquals(uri, null)\n    }\n\n    @Test\n    fun `should return null when scheme is empty`() {\n        val uriString = \"://activity/user?query=zhang\"\n        val uri = FormalUri.from(uriString)\n        assertEquals(uri, null)\n    }\n\n    @Test\n    fun `should return null when scheme and divider is empty`() {\n        val uriString = \"activity/user?query=zhang\"\n        val uri = FormalUri.from(uriString)\n        assertEquals(uri, null)\n    }\n\n    @Test\n    fun `should return uri when scheme is ok`() {\n        val uriString = \"${FormalUri.SCHEME}://activity/user?query=zhang\"\n        val uri = FormalUri.from(uriString)\n        assertTrue(uri != null)\n    }\n\n    @Test\n    fun `should return null when host is empty`() {\n        val uriString = \"${FormalUri.SCHEME}:///user?query=zhang\"\n        val uri = FormalUri.from(uriString)\n        assertEquals(uri, null)\n    }\n\n    @Test\n    fun `should return null when host is null`() {\n        val uriString = \"${FormalUri.SCHEME}://user?query=zhang\"\n        val uri = FormalUri.from(uriString)\n        assertEquals(uri, null)\n    }\n\n    @Test\n    fun `should return null when path is empty`() {\n        val uriString = \"${FormalUri.SCHEME}://activity_pub/?query=zhang\"\n        val uri = FormalUri.from(uriString)\n        assertEquals(uri, null)\n    }\n\n    @Test\n    fun `should return null when path is null`() {\n        val uriString = \"${FormalUri.SCHEME}://activity_pub/?query=zhang\"\n        val uri = FormalUri.from(uriString)\n        assertEquals(uri, null)\n    }\n\n    @Test\n    fun `should return null when path and divider is null`() {\n        val uriString = \"${FormalUri.SCHEME}://activity_pub?query=zhang\"\n        val uri = FormalUri.from(uriString)\n        assertEquals(uri, null)\n    }\n\n    @Test\n    fun `should return null when query is null`() {\n        val uriString = \"${FormalUri.SCHEME}://activity_pub/user\"\n        val uri = FormalUri.from(uriString)\n        assertEquals(uri, null)\n    }\n\n    @Test\n    fun `should return uri when query is empty`() {\n        val uriString = \"${FormalUri.SCHEME}://activity_pub/user?query=\"\n        val uri = FormalUri.from(uriString)\n        assertTrue(uri != null)\n    }\n\n    @Test\n    fun `should return uri and empty query when query is empty`() {\n        val uriString = \"${FormalUri.SCHEME}://activity_pub/user?query=\"\n        val uri = FormalUri.from(uriString)\n        assertEquals(uri!!.host, \"activity_pub\")\n        assertEquals(uri.path, \"/user\")\n        assertEquals(uri.queries[\"query\"], \"\")\n    }\n\n    @Test\n    fun `should return uri when uri is ok`() {\n        val uriString = \"${FormalUri.SCHEME}://activity_pub/user?query=zhangke\"\n        val uri = FormalUri.from(uriString)\n        assertEquals(uri!!.host, \"activity_pub\")\n        assertEquals(uri.path, \"/user\")\n        assertEquals(uri.queries[\"query\"], \"zhangke\")\n    }\n\n    @Test\n    fun `should return uri when have multiple path`() {\n        val uriString = \"${FormalUri.SCHEME}://activity_pub/user/name?query=zhangke\"\n        val uri = FormalUri.from(uriString)\n        assertEquals(uri!!.host, \"activity_pub\")\n        assertEquals(uri.path, \"/user/name\")\n        assertEquals(uri.queries[\"query\"], \"zhangke\")\n    }\n}"
  },
  {
    "path": "build-logic/README.md",
    "content": "# Convention Plugins\n\nThe `build-logic` folder defines project-specific convention plugins, used to keep a single\nsource of truth for common module configurations.\n\nThis approach is heavily based on\n[https://developer.squareup.com/blog/herding-elephants/](https://developer.squareup.com/blog/herding-elephants/)\nand\n[https://github.com/jjohannes/idiomatic-gradle](https://github.com/jjohannes/idiomatic-gradle).\n\nBy setting up convention plugins in `build-logic`, we can avoid duplicated build script setup,\nmessy `subproject` configurations, without the pitfalls of the `buildSrc` directory.\n\n`build-logic` is an included build, as configured in the root\n[`settings.gradle.kts`](../settings.gradle.kts).\n\nInside `build-logic` is a `convention` module, which defines a set of plugins that all normal\nmodules can use to configure themselves.\n\n`build-logic` also includes a set of `Kotlin` files used to share logic between plugins themselves,\nwhich is most useful for configuring Android components (libraries vs applications) with shared\ncode.\n\nThese plugins are *additive* and *composable*, and try to only accomplish a single responsibility.\nModules can then pick and choose the configurations they need.\nIf there is one-off logic for a module without shared code, it's preferable to define that directly\nin the module's `build.gradle`, as opposed to creating a convention plugin with module-specific\nsetup.\n\nCurrent list of convention plugins:\n\n- [`nowinandroid.android.application`](convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt),\n  [`nowinandroid.android.library`](convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt),\n  [`nowinandroid.android.test`](convention/src/main/kotlin/AndroidTestConventionPlugin.kt):\n  Configures common Android and Kotlin options.\n- [`nowinandroid.android.application.compose`](convention/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt),\n  [`nowinandroid.android.library.compose`](convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt):\n  Configures Jetpack Compose options\n"
  },
  {
    "path": "build-logic/convention/build.gradle.kts",
    "content": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport org.jetbrains.kotlin.gradle.dsl.JvmTarget\nimport org.jetbrains.kotlin.gradle.tasks.KotlinCompile\n\nplugins {\n    `kotlin-dsl`\n}\n\ngroup = \"com.zhangke.fread.buildlogic\"\n\n// Configure the build-logic plugins to target JDK 17\n// This matches the JDK used to build the project, and is not related to what is running on device.\njava {\n    sourceCompatibility = JavaVersion.VERSION_17\n    targetCompatibility = JavaVersion.VERSION_17\n}\ntasks.withType<KotlinCompile>().configureEach {\n    compilerOptions {\n        jvmTarget.set(JvmTarget.JVM_17)\n    }\n}\n\ndependencies {\n    compileOnly(libs.ksp.gradlePlugin)\n    compileOnly(libs.android.gradlePlugin)\n    compileOnly(libs.kotlin.gradlePlugin)\n    compileOnly(libs.compose.gradlePlugin)\n    compileOnly(libs.composeCompiler.gradlePlugin)\n}\n\ngradlePlugin {\n    plugins {\n        register(\"androidApplication\") {\n            id = \"fread.android.application\"\n            implementationClass = \"AndroidApplicationConventionPlugin\"\n        }\n        register(\"androidLibrary\") {\n            id = \"fread.android.library\"\n            implementationClass = \"AndroidLibraryConventionPlugin\"\n        }\n        register(\"kotlinMultiplatformLibrary\") {\n            id = \"fread.kmp.library\"\n            implementationClass = \"KotlinMultiplatformLibraryConventionPlugin\"\n        }\n        register(\"composeMultiplatform\") {\n            id = \"fread.compose.multiplatform\"\n            implementationClass = \"ComposeMultiPlatformConventionPlugin\"\n        }\n        register(\"projectFrameworkKmp\") {\n            id = \"fread.project.framework.kmp\"\n            implementationClass = \"ProjectFrameworkKmpConventionPlugin\"\n        }\n        register(\"projectFeatureKmp\") {\n            id = \"fread.project.feature.kmp\"\n            implementationClass = \"ProjectFeatureKmpConventionPlugin\"\n        }\n    }\n}\n"
  },
  {
    "path": "build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt",
    "content": "/*\n * Copyright 2022 The Android Open Source Project\n *\n *   Licensed under the Apache License, Version 2.0 (the \"License\");\n *   you may not use this file except in compliance with the License.\n *   You may obtain a copy of the License at\n *\n *       https://www.apache.org/licenses/LICENSE-2.0\n *\n *   Unless required by applicable law or agreed to in writing, software\n *   distributed under the License is distributed on an \"AS IS\" BASIS,\n *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *   See the License for the specific language governing permissions and\n *   limitations under the License.\n */\n\nimport com.zhangke.fread.applicationComponentsExtension\nimport com.zhangke.fread.applicationExtension\nimport com.zhangke.fread.configureKotlinAndroid\nimport com.zhangke.fread.configurePrintApksTask\nimport org.gradle.api.Plugin\nimport org.gradle.api.Project\nimport org.gradle.kotlin.dsl.extra\n\nclass AndroidApplicationConventionPlugin : Plugin<Project> {\n    override fun apply(target: Project) {\n        with(target) {\n            with(pluginManager) {\n                apply(\"com.android.application\")\n                apply(\"org.jetbrains.kotlin.android\")\n                apply(\"org.jetbrains.kotlin.plugin.serialization\")\n                if (gradle.extra[\"enableFirebaseModule\"] == true) {\n                    println(\"Find the Firebase configuration file, add the Firebase plugin.\")\n                    apply(\"com.google.gms.google-services\")\n                    apply(\"com.google.firebase.crashlytics\")\n                }\n            }\n\n            applicationExtension {\n                configureKotlinAndroid(this)\n                defaultConfig.targetSdk = 36\n            }\n            applicationComponentsExtension {\n                configurePrintApksTask(this)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt",
    "content": "/*\n * Copyright 2022 The Android Open Source Project\n *\n *   Licensed under the Apache License, Version 2.0 (the \"License\");\n *   you may not use this file except in compliance with the License.\n *   You may obtain a copy of the License at\n *\n *       https://www.apache.org/licenses/LICENSE-2.0\n *\n *   Unless required by applicable law or agreed to in writing, software\n *   distributed under the License is distributed on an \"AS IS\" BASIS,\n *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *   See the License for the specific language governing permissions and\n *   limitations under the License.\n */\n\nimport com.zhangke.fread.configureKotlinAndroid\nimport com.zhangke.fread.configurePrintApksTask\nimport com.zhangke.fread.libraryComponentsExtension\nimport com.zhangke.fread.libraryExtension\nimport org.gradle.api.Plugin\nimport org.gradle.api.Project\n\nclass AndroidLibraryConventionPlugin : Plugin<Project> {\n    override fun apply(target: Project) {\n        with(target) {\n            with(pluginManager) {\n                apply(\"com.android.library\")\n                apply(\"org.jetbrains.kotlin.android\")\n                apply(\"org.jetbrains.kotlin.plugin.serialization\")\n            }\n\n            libraryExtension {\n                configureKotlinAndroid(this)\n            }\n            libraryComponentsExtension {\n                configurePrintApksTask(this)\n            }\n        }\n    }\n}"
  },
  {
    "path": "build-logic/convention/src/main/kotlin/ComposeMultiPlatformConventionPlugin.kt",
    "content": "/*\n * Copyright 2022 The Android Open Source Project\n *\n *   Licensed under the Apache License, Version 2.0 (the \"License\");\n *   you may not use this file except in compliance with the License.\n *   You may obtain a copy of the License at\n *\n *       https://www.apache.org/licenses/LICENSE-2.0\n *\n *   Unless required by applicable law or agreed to in writing, software\n *   distributed under the License is distributed on an \"AS IS\" BASIS,\n *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *   See the License for the specific language governing permissions and\n *   limitations under the License.\n */\nimport com.zhangke.fread.applicationExtension\nimport com.zhangke.fread.libraryExtension\nimport org.gradle.api.Plugin\nimport org.gradle.api.Project\nimport org.gradle.kotlin.dsl.configure\nimport org.jetbrains.kotlin.compose.compiler.gradle.ComposeCompilerGradlePluginExtension\n\nclass ComposeMultiPlatformConventionPlugin : Plugin<Project> {\n    override fun apply(target: Project) {\n        with(target) {\n            with(pluginManager) {\n                apply(\"org.jetbrains.compose\")\n                apply(\"org.jetbrains.kotlin.plugin.compose\")\n            }\n            if (pluginManager.hasPlugin(\"com.android.application\")) {\n                applicationExtension {\n                    buildFeatures {\n                        compose = true\n                    }\n                }\n            } else if (pluginManager.hasPlugin(\"com.android.library\")) {\n                libraryExtension {\n                    buildFeatures {\n                        compose = true\n                    }\n                }\n            }\n            composeCompiler {\n                // Enable 'strong skipping'\n                // https://medium.com/androiddevelopers/jetpack-compose-strong-skipping-mode-explained-cbdb2aa4b900\n\n                if (project.providers.gradleProperty(\"myapp.enableComposeCompilerReports\").isPresent) {\n                    val composeReports = layout.buildDirectory.map { it.dir(\"reports\").dir(\"compose\") }\n                    reportsDestination.set(composeReports)\n                    metricsDestination.set(composeReports)\n                }\n            }\n        }\n    }\n\n    private fun Project.composeCompiler(block: ComposeCompilerGradlePluginExtension.() -> Unit) {\n        extensions.configure<ComposeCompilerGradlePluginExtension>(block)\n    }\n}\n"
  },
  {
    "path": "build-logic/convention/src/main/kotlin/KotlinMultiplatformLibraryConventionPlugin.kt",
    "content": "/*\n * Copyright 2022 The Android Open Source Project\n *\n *   Licensed under the Apache License, Version 2.0 (the \"License\");\n *   you may not use this file except in compliance with the License.\n *   You may obtain a copy of the License at\n *\n *       https://www.apache.org/licenses/LICENSE-2.0\n *\n *   Unless required by applicable law or agreed to in writing, software\n *   distributed under the License is distributed on an \"AS IS\" BASIS,\n *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *   See the License for the specific language governing permissions and\n *   limitations under the License.\n */\n\nimport com.zhangke.fread.configureKotlinAndroid\nimport com.zhangke.fread.configurePrintApksTask\nimport com.zhangke.fread.kotlin\nimport com.zhangke.fread.kotlinCompile\nimport com.zhangke.fread.kotlinMultiplatform\nimport com.zhangke.fread.libraryComponentsExtension\nimport com.zhangke.fread.libraryExtension\nimport org.gradle.api.Plugin\nimport org.gradle.api.Project\n\nclass KotlinMultiplatformLibraryConventionPlugin : Plugin<Project> {\n    override fun apply(target: Project) {\n        with(target) {\n            with(pluginManager) {\n                apply(\"com.android.library\")\n                apply(\"org.jetbrains.kotlin.multiplatform\")\n                apply(\"org.jetbrains.kotlin.plugin.serialization\")\n            }\n            libraryExtension {\n                configureKotlinAndroid(this)\n            }\n            libraryComponentsExtension {\n                configurePrintApksTask(this)\n            }\n            kotlinMultiplatform {\n                compilerOptions {\n                    freeCompilerArgs.add(\"-Xcontext-parameters\")\n                }\n                androidTarget()\n                iosX64()\n                iosArm64()\n                iosSimulatorArm64()\n                sourceSets.apply {\n                    targets.configureEach {\n                        compilations.configureEach {\n                            compileTaskProvider.configure {\n                                compilerOptions {\n                                    // https://youtrack.jetbrains.com/issue/KT-61573\n                                    freeCompilerArgs.add(\"-Xexpect-actual-classes\")\n                                    freeCompilerArgs.add(\"-Xcontext-parameters\")\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "build-logic/convention/src/main/kotlin/Project.kt",
    "content": "import org.gradle.kotlin.dsl.DependencyHandlerScope\nimport org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension\n\nfun DependencyHandlerScope.kspAll(dependencyNotation: Any) {\n    add(\"kspAndroid\", dependencyNotation)\n    add(\"kspIosSimulatorArm64\", dependencyNotation)\n    add(\"kspIosX64\", dependencyNotation)\n    add(\"kspIosArm64\", dependencyNotation)\n}\n\nfun KotlinMultiplatformExtension.configureCommonMainKsp() {\n    sourceSets.named(\"commonMain\").configure {\n        kotlin.srcDir(\"build/generated/ksp/metadata/commonMain/kotlin\")\n    }\n}"
  },
  {
    "path": "build-logic/convention/src/main/kotlin/ProjectFeatureKmpConventionPlugin.kt",
    "content": "import org.gradle.api.Plugin\nimport org.gradle.api.Project\n\nclass ProjectFeatureKmpConventionPlugin: Plugin<Project> {\n\n    override fun apply(target: Project) {\n        with(target) {\n            with(pluginManager) {\n                apply(\"fread.project.framework.kmp\")\n            }\n        }\n    }\n}"
  },
  {
    "path": "build-logic/convention/src/main/kotlin/ProjectFrameworkKmpConventionPlugin.kt",
    "content": "import com.zhangke.fread.compose\nimport com.zhangke.fread.kotlinMultiplatform\nimport com.zhangke.fread.libs\nimport org.gradle.api.Plugin\nimport org.gradle.api.Project\nimport org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType\nimport kotlin.text.get\n\nclass ProjectFrameworkKmpConventionPlugin : Plugin<Project> {\n\n    override fun apply(target: Project) {\n        with(target) {\n            with(pluginManager) {\n                apply(\"fread.kmp.library\")\n                apply(\"fread.compose.multiplatform\")\n            }\n            kotlinMultiplatform {\n                sourceSets.apply {\n                    commonMain {\n                        dependencies {\n                            implementation(compose.runtime)\n                            implementation(compose.ui)\n                            implementation(compose.foundation)\n                            implementation(compose.material)\n                            implementation(compose.materialIconsExtended)\n                            implementation(compose.material3)\n\n                            implementation(libs.findLibrary(\"koin-core\").get())\n                            implementation(libs.findLibrary(\"koin-compose\").get())\n                            implementation(libs.findLibrary(\"koin-compose-viewmodel\").get())\n                            implementation(libs.findLibrary(\"koin-compose-nav3\").get())\n\n                            implementation(libs.findLibrary(\"kotlinx-datetime\").get())\n                            implementation(libs.findBundle(\"androidx-nav3\").get())\n                        }\n                    }\n                    androidMain {\n                        dependencies {\n                            implementation(libs.findLibrary(\"koin-android\").get())\n                        }\n                    }\n                    targets.configureEach {\n                        val isAndroidTarget = platformType == KotlinPlatformType.androidJvm\n                        compilations.configureEach {\n                            compileTaskProvider.configure {\n                                compilerOptions {\n                                    if (isAndroidTarget) {\n                                        freeCompilerArgs.addAll(\n                                            \"-P\",\n                                            \"plugin:org.jetbrains.kotlin.parcelize:additionalAnnotation=com.zhangke.framework.utils.Parcelize\",\n                                        )\n                                    }\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "build-logic/convention/src/main/kotlin/com/zhangke/fread/KotlinAndroid.kt",
    "content": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.zhangke.fread\n\nimport com.android.build.api.dsl.CommonExtension\nimport org.gradle.api.JavaVersion\nimport org.gradle.api.Project\nimport org.gradle.api.file.DuplicatesStrategy\nimport org.gradle.jvm.tasks.Jar\nimport org.gradle.kotlin.dsl.assign\nimport org.gradle.kotlin.dsl.provideDelegate\nimport org.gradle.kotlin.dsl.withType\nimport org.gradle.language.jvm.tasks.ProcessResources\nimport org.jetbrains.kotlin.gradle.dsl.JvmTarget\nimport org.jetbrains.kotlin.gradle.dsl.KotlinVersion\n\n/**\n * Configure base Kotlin with Android options\n */\ninternal fun Project.configureKotlinAndroid(\n    commonExtension: CommonExtension<*, *, *, *, *, *>,\n) {\n    commonExtension.apply {\n        compileSdk = 36\n\n        defaultConfig {\n            minSdk = 24\n        }\n\n        compileOptions {\n            // Up to Java 11 APIs are available through desugaring\n            // https://developer.android.com/studio/write/java11-minimal-support-table\n            sourceCompatibility = JavaVersion.VERSION_11\n            targetCompatibility = JavaVersion.VERSION_11\n\n            // https://developer.android.com/studio/write/java8-support-table\n            isCoreLibraryDesugaringEnabled = true\n        }\n    }\n    dependencies.apply {\n        add(\"coreLibraryDesugaring\", \"com.android.tools:desugar_jdk_libs:2.0.4\")\n    }\n    configureKotlin()\n}\n\n/**\n * Configure base Kotlin options for JVM (non-Android)\n */\ninternal fun Project.configureKotlinJvm() {\n    java {\n        // Up to Java 11 APIs are available through desugaring\n        // https://developer.android.com/studio/write/java11-minimal-support-table\n        sourceCompatibility = JavaVersion.VERSION_11\n        targetCompatibility = JavaVersion.VERSION_11\n    }\n    configureKotlin()\n}\n\n/**\n * Configure base Kotlin options\n */\nprivate fun Project.configureKotlin() {\n    kotlinCompile {\n        compilerOptions {\n            jvmTarget = JvmTarget.JVM_11\n            languageVersion = KotlinVersion.KOTLIN_2_2\n            // Treat all Kotlin warnings as errors (disabled by default)\n            // Override by setting warningsAsErrors=true in your ~/.gradle/gradle.properties\n            val warningsAsErrors: String? by project\n            allWarningsAsErrors = warningsAsErrors.toBoolean()\n            freeCompilerArgs.add(\"-Xcontext-parameters\")\n        }\n    }\n    kotlin {\n        sourceSets.all {\n            languageSettings {\n                languageVersion = KotlinVersion.KOTLIN_2_2.version\n                enableLanguageFeature(\"ExplicitBackingFields\")\n                optIn(\"kotlin.RequiresOptIn\")\n                optIn(\"kotlinx.coroutines.ExperimentalCoroutinesApi\")\n                optIn(\"kotlinx.coroutines.FlowPreview\")\n                optIn(\"androidx.compose.foundation.ExperimentalFoundationApi\")\n            }\n        }\n        sourceSets.findByName(\"main\")?.apply {\n            kotlin.srcDir(\"build/generated/ksp/main/kotlin\")\n            resources.srcDir(\"build/generated/ksp/main/resources\")\n        }\n        sourceSets.findByName(\"debug\")?.apply {\n            kotlin.srcDir(\"build/generated/ksp/debug/kotlin\")\n            resources.srcDir(\"build/generated/ksp/debug/resources\")\n        }\n        sourceSets.findByName(\"release\")?.apply {\n            kotlin.srcDir(\"build/generated/ksp/release/kotlin\")\n            resources.srcDir(\"build/generated/ksp/release/resources\")\n        }\n    }\n    tasks.withType<ProcessResources>{\n        duplicatesStrategy = DuplicatesStrategy.INCLUDE\n    }\n    tasks.withType<Jar>{\n        duplicatesStrategy = DuplicatesStrategy.INCLUDE\n    }\n}\n"
  },
  {
    "path": "build-logic/convention/src/main/kotlin/com/zhangke/fread/PrintTestApks.kt",
    "content": "/*\n * Copyright 2022 The Android Open Source Project\n *\n *   Licensed under the Apache License, Version 2.0 (the \"License\");\n *   you may not use this file except in compliance with the License.\n *   You may obtain a copy of the License at\n *\n *       https://www.apache.org/licenses/LICENSE-2.0\n *\n *   Unless required by applicable law or agreed to in writing, software\n *   distributed under the License is distributed on an \"AS IS\" BASIS,\n *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *   See the License for the specific language governing permissions and\n *   limitations under the License.\n */\n\npackage com.zhangke.fread\n\nimport com.android.build.api.artifact.SingleArtifact\nimport com.android.build.api.variant.AndroidComponentsExtension\nimport com.android.build.api.variant.BuiltArtifactsLoader\nimport com.android.build.api.variant.HasAndroidTest\nimport org.gradle.api.DefaultTask\nimport org.gradle.api.Project\nimport org.gradle.api.file.Directory\nimport org.gradle.api.file.DirectoryProperty\nimport org.gradle.api.provider.ListProperty\nimport org.gradle.api.provider.Property\nimport org.gradle.api.tasks.Input\nimport org.gradle.api.tasks.InputDirectory\nimport org.gradle.api.tasks.InputFiles\nimport org.gradle.api.tasks.Internal\nimport org.gradle.api.tasks.TaskAction\nimport java.io.File\n\ninternal fun Project.configurePrintApksTask(extension: AndroidComponentsExtension<*, *, *>) {\n    extension.onVariants { variant ->\n        if (variant is HasAndroidTest) {\n            val loader = variant.artifacts.getBuiltArtifactsLoader()\n            val artifact = variant.androidTest?.artifacts?.get(SingleArtifact.APK)\n            val javaSources = variant.androidTest?.sources?.java?.all\n            val kotlinSources = variant.androidTest?.sources?.kotlin?.all\n\n            val testSources = if (javaSources != null && kotlinSources != null) {\n                javaSources.zip(kotlinSources) { javaDirs, kotlinDirs ->\n                    javaDirs + kotlinDirs\n                }\n            } else javaSources ?: kotlinSources\n\n            if (artifact != null && testSources != null) {\n                tasks.register(\n                    \"${variant.name}PrintTestApk\",\n                    PrintApkLocationTask::class.java\n                ) {\n                    apkFolder.set(artifact)\n                    builtArtifactsLoader.set(loader)\n                    variantName.set(variant.name)\n                    sources.set(testSources)\n                }\n            }\n        }\n    }\n}\n\ninternal abstract class PrintApkLocationTask : DefaultTask() {\n    @get:InputDirectory\n    abstract val apkFolder: DirectoryProperty\n\n    @get:InputFiles\n    abstract val sources: ListProperty<Directory>\n\n    @get:Internal\n    abstract val builtArtifactsLoader: Property<BuiltArtifactsLoader>\n\n    @get:Input\n    abstract val variantName: Property<String>\n\n    @TaskAction\n    fun taskAction() {\n        val hasFiles = sources.orNull?.any { directory ->\n            directory.asFileTree.files.any {\n                it.isFile && it.parentFile.path.contains(\"build${File.separator}generated\").not()\n            }\n        } ?: throw RuntimeException(\"Cannot check androidTest sources\")\n\n        // Don't print APK location if there are no androidTest source files\n        if (!hasFiles) {\n            return\n        }\n\n        val builtArtifacts = builtArtifactsLoader.get().load(apkFolder.get())\n            ?: throw RuntimeException(\"Cannot load APKs\")\n        if (builtArtifacts.elements.size != 1)\n            throw RuntimeException(\"Expected one APK !\")\n        val apk = File(builtArtifacts.elements.single().outputFile).toPath()\n        println(apk)\n    }\n}"
  },
  {
    "path": "build-logic/convention/src/main/kotlin/com/zhangke/fread/ProjectExt.kt",
    "content": "package com.zhangke.fread\n\nimport com.android.build.api.dsl.ApplicationExtension\nimport com.android.build.api.dsl.LibraryExtension\nimport com.android.build.api.variant.ApplicationAndroidComponentsExtension\nimport com.android.build.api.variant.LibraryAndroidComponentsExtension\nimport org.gradle.api.Project\nimport org.gradle.api.artifacts.VersionCatalog\nimport org.gradle.api.artifacts.VersionCatalogsExtension\nimport org.gradle.api.artifacts.dsl.DependencyHandler\nimport org.gradle.api.plugins.JavaPluginExtension\nimport org.gradle.kotlin.dsl.configure\nimport org.gradle.kotlin.dsl.getByType\nimport org.gradle.kotlin.dsl.withType\nimport org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension\nimport org.jetbrains.kotlin.gradle.dsl.KotlinProjectExtension\nimport org.jetbrains.kotlin.gradle.tasks.KotlinCompile\nimport org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile\n\ninternal val Project.compose\n    get() = org.jetbrains.compose.ComposePlugin.Dependencies(this)\n\ninternal fun Project.libraryExtension(action: LibraryExtension.() -> Unit) =\n    extensions.configure<LibraryExtension>(action)\n\ninternal fun Project.applicationExtension(action: ApplicationExtension.() -> Unit) =\n    extensions.configure<ApplicationExtension>(action)\n\ninternal fun Project.libraryComponentsExtension(action: LibraryAndroidComponentsExtension.() -> Unit) =\n    extensions.configure<LibraryAndroidComponentsExtension>(action)\n\ninternal fun Project.applicationComponentsExtension(action: ApplicationAndroidComponentsExtension.() -> Unit) =\n    extensions.configure<ApplicationAndroidComponentsExtension>(action)\n\ninternal fun Project.kotlinCompile(action: KotlinJvmCompile.() -> Unit) =\n    tasks.withType<KotlinCompile>().all { action(this) }\n\ninternal fun Project.kotlin(action: KotlinProjectExtension.() -> Unit) =\n    extensions.configure<KotlinProjectExtension>(action)\n\ninternal fun Project.kotlinMultiplatform(action: KotlinMultiplatformExtension.() -> Unit) =\n    extensions.configure<KotlinMultiplatformExtension>(action)\n\ninternal fun Project.java(action: JavaPluginExtension.() -> Unit) =\n    extensions.configure<JavaPluginExtension>(action)\n\ninternal val Project.libs: VersionCatalog\n    get() = extensions.getByType<VersionCatalogsExtension>().named(\"libs\")\n\ninternal fun DependencyHandler.implementation(dependency: Any) =\n    add(\"implementation\", dependency)\n"
  },
  {
    "path": "build-logic/gradle.properties",
    "content": "# Gradle properties are not passed to included builds https://github.com/gradle/gradle/issues/2534\norg.gradle.parallel=true\norg.gradle.caching=true\norg.gradle.configureondemand=true\n"
  },
  {
    "path": "build-logic/settings.gradle.kts",
    "content": "/*\n * Copyright 2022 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\ndependencyResolutionManagement {\n    repositories {\n        google()\n        mavenCentral()\n    }\n    versionCatalogs {\n        create(\"libs\") {\n            from(files(\"../gradle/libs.versions.toml\"))\n        }\n    }\n}\n\nrootProject.name = \"build-logic\"\ninclude(\":convention\")\n"
  },
  {
    "path": "build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.android.application) apply false\n    alias(libs.plugins.android.library) apply false\n    alias(libs.plugins.kotlin.android) apply false\n    alias(libs.plugins.kotlin.jvm) apply false\n    alias(libs.plugins.kotlin.multiplatform) apply false\n    alias(libs.plugins.ksp) apply false\n    alias(libs.plugins.kotlinx.serialization) apply false\n    alias(libs.plugins.jetbrains.compose) apply false\n    alias(libs.plugins.jetbrains.compose.compiler) apply false\n    alias(libs.plugins.google.service) apply false\n    alias(libs.plugins.firebase.crashlytics) apply false\n}\n"
  },
  {
    "path": "commonbiz/analytics/.gitignore",
    "content": "/build"
  },
  {
    "path": "commonbiz/analytics/build.gradle.kts",
    "content": "plugins {\n    id(\"fread.project.framework.kmp\")\n    id(\"com.google.devtools.ksp\")\n    id(\"kotlin-parcelize\")\n}\n\nandroid {\n    namespace = \"com.zhangke.fread.analytics\"\n    sourceSets {\n        getByName(\"main\") {\n            res.srcDirs(\"src/androidMain/res\")\n            manifest.srcFile(\"src/androidMain/AndroidManifest.xml\")\n        }\n    }\n}\n\nkotlin {\n    sourceSets {\n        commonMain {\n            dependencies {\n                implementation(project(path = \":framework\"))\n                implementation(project(path = \":commonbiz:common\"))\n\n                implementation(libs.krouter.runtime)\n            }\n        }\n        commonTest {\n            dependencies {\n                implementation(kotlin(\"test\"))\n            }\n        }\n        androidMain {\n            dependencies {\n                implementation(libs.androidx.core.ktx)\n                implementation(libs.auto.service.annotations)\n            }\n        }\n    }\n    configureCommonMainKsp()\n}\n\ndependencies {\n    kspAll(libs.krouter.collecting.compiler)\n}"
  },
  {
    "path": "commonbiz/analytics/consumer-rules.pro",
    "content": ""
  },
  {
    "path": "commonbiz/analytics/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguardFiles setting in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n\n# Uncomment this to preserve the line number information for\n# debugging stack traces.\n#-keepattributes SourceFile,LineNumberTable\n\n# If you keep the line number information, uncomment this to\n# hide the original source file name.\n#-renamesourcefileattribute SourceFile"
  },
  {
    "path": "commonbiz/analytics/src/commonMain/kotlin/com/zhangke/fread/analytics/Analytics.kt",
    "content": "package com.zhangke.fread.analytics\n\nimport com.zhangke.krouter.KRouter\n\ninterface LogReporter {\n\n    fun report(eventName: String, parameters: Map<String, String>)\n}\n\nprivate val loggerList: List<LogReporter> by lazy {\n    KRouter.getServices()\n}\n\nfun reportPageShow(\n    pageName: String,\n    paramsBuilder: (TrackingEventDataBuilder.() -> Unit),\n) {\n    reportToLogger(EventNames.PAGE_SHOW) {\n        put(EventParamsName.PAGE_NAME, pageName)\n        paramsBuilder()\n    }\n}\n\nfun reportInfo(\n    paramsBuilder: (TrackingEventDataBuilder.() -> Unit),\n) {\n    reportToLogger(EventNames.INFO, paramsBuilder)\n}\n\nfun reportToLogger(\n    eventName: String,\n    paramsBuilder: (TrackingEventDataBuilder.() -> Unit) = {},\n) {\n    for (reporter in loggerList) {\n        reporter.report(eventName, TrackingEventDataBuilder().apply(paramsBuilder).build())\n    }\n}"
  },
  {
    "path": "commonbiz/analytics/src/commonMain/kotlin/com/zhangke/fread/analytics/EventNames.kt",
    "content": "package com.zhangke.fread.analytics\n\nobject EventNames {\n\n    const val PAGE_SHOW = \"page_show\"\n    const val CLICK = \"click\"\n    const val INFO = \"info\"\n}\n\nobject EventParamsName {\n\n    const val PAGE_NAME = \"page_name\"\n    const val ELEMENT = \"element\"\n}\n"
  },
  {
    "path": "commonbiz/analytics/src/commonMain/kotlin/com/zhangke/fread/analytics/TrackingEventDataBuilder.kt",
    "content": "package com.zhangke.fread.analytics\n\n\nclass TrackingEventDataBuilder : MutableMap<String, String> by mutableMapOf() {\n\n    fun putIfNotNull(key: String, value: String?) {\n        value?.let { put(key, it) }\n    }\n\n    fun build(): Map<String, String> = toMap()\n}\n"
  },
  {
    "path": "commonbiz/common/.gitignore",
    "content": "/build"
  },
  {
    "path": "commonbiz/common/build.gradle.kts",
    "content": "plugins {\n    id(\"fread.project.framework.kmp\")\n    id(\"com.google.devtools.ksp\")\n    id(\"kotlin-parcelize\")\n    alias(libs.plugins.room)\n}\n\nandroid {\n    namespace = \"com.zhangke.fread.commonbiz\"\n    sourceSets {\n        getByName(\"main\") {\n            res.srcDirs(\"src/androidMain/res\")\n            manifest.srcFile(\"src/androidMain/AndroidManifest.xml\")\n        }\n    }\n}\n\nkotlin {\n    sourceSets {\n        all {\n            languageSettings {\n                optIn(\"com.russhwolf.settings.ExperimentalSettingsApi\")\n            }\n        }\n        commonMain {\n            dependencies {\n                implementation(project(path = \":framework\"))\n                implementation(project(path = \":bizframework:status-provider\"))\n\n                implementation(compose.components.resources)\n\n                implementation(libs.arrow.core)\n\n                implementation(libs.androidx.annotation)\n                implementation(libs.bundles.androidx.datastore)\n\n                implementation(libs.kotlinx.serialization.core)\n                implementation(libs.kotlinx.serialization.json)\n\n                implementation(libs.jetbrains.lifecycle.runtime)\n                implementation(libs.jetbrains.lifecycle.viewmodel)\n                implementation(libs.jetbrains.lifecycle.viewmodel.compose)\n\n                implementation(libs.androidx.room)\n                implementation(libs.uri.kmp)\n\n                implementation(libs.imageLoader)\n\n                implementation(libs.krouter.runtime)\n\n                implementation(libs.multiplatformsettings.core)\n                implementation(libs.multiplatformsettings.coroutines)\n\n                implementation(libs.ktor.client.core)\n                implementation(libs.ktor.client.serialization.kotlinx.json)\n\n                implementation(libs.compose.media.player)\n            }\n        }\n        commonTest {\n            dependencies {\n                implementation(project(path = \":framework\"))\n                implementation(project(path = \":bizframework:status-provider\"))\n\n                implementation(kotlin(\"test\"))\n\n                implementation(libs.kotlin.coroutine.core)\n                implementation(libs.kotlin.coroutine.test)\n                implementation(libs.kotlinx.datetime)\n            }\n        }\n        androidMain {\n            dependencies {\n                implementation(libs.bundles.androidx.activity)\n                implementation(libs.androidx.appcompat)\n\n                implementation(libs.androidx.browser)\n\n                implementation(libs.multiplatformsettings.datastore)\n            }\n        }\n        iosMain {\n            dependencies {\n                implementation(libs.androidx.sqlite.bundled)\n            }\n        }\n    }\n    configureCommonMainKsp()\n}\n\ndependencies {\n    kspAll(libs.androidx.room.compiler)\n    kspAll(libs.krouter.collecting.compiler)\n}\n\ncompose {\n    resources {\n        publicResClass = true\n        packageOfResClass = \"com.zhangke.fread.commonbiz\"\n        generateResClass = always\n    }\n}\n\nroom {\n    schemaDirectory(\"$projectDir/schemas\")\n}\n"
  },
  {
    "path": "commonbiz/common/src/androidMain/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest>\n\n</manifest>"
  },
  {
    "path": "commonbiz/common/src/androidMain/kotlin/com/zhangke/fread/common/AndroidCommonModule.kt",
    "content": "package com.zhangke.fread.common\n\nimport android.content.Context\nimport androidx.datastore.core.DataStore\nimport androidx.datastore.preferences.core.Preferences\nimport androidx.datastore.preferences.preferencesDataStore\nimport androidx.room.Room\nimport com.russhwolf.settings.ExperimentalSettingsImplementation\nimport com.russhwolf.settings.coroutines.FlowSettings\nimport com.russhwolf.settings.datastore.DataStoreSettings\nimport com.zhangke.framework.module.ModuleStartup\nimport com.zhangke.fread.common.browser.AndroidSystemBrowserLauncher\nimport com.zhangke.fread.common.browser.OAuthHandler\nimport com.zhangke.fread.common.browser.SystemBrowserLauncher\nimport com.zhangke.fread.common.db.ContentConfigDatabases\nimport com.zhangke.fread.common.db.FreadContentDatabase\nimport com.zhangke.fread.common.db.MixedStatusDatabases\nimport com.zhangke.fread.common.db.old.OldFreadContentDatabase\nimport com.zhangke.fread.common.handler.TextHandler\nimport com.zhangke.fread.common.language.ActivityLanguageHelper\nimport com.zhangke.fread.common.language.LanguageHelper\nimport com.zhangke.fread.common.startup.LanguageModuleStartup\nimport com.zhangke.fread.common.utils.MediaFileHelper\nimport com.zhangke.fread.common.utils.PlatformUriHelper\nimport com.zhangke.fread.common.utils.StorageHelper\nimport com.zhangke.fread.common.utils.ToastHelper\nimport org.koin.android.ext.koin.androidContext\nimport org.koin.core.module.Module\nimport org.koin.core.module.dsl.factoryOf\nimport org.koin.core.module.dsl.singleOf\nimport org.koin.dsl.bind\n\n@OptIn(ExperimentalSettingsImplementation::class)\nactual fun Module.createPlatformModule() {\n    single<ContentConfigDatabases> {\n        Room.databaseBuilder(\n            androidContext(),\n            ContentConfigDatabases::class.java,\n            ContentConfigDatabases.DB_NAME,\n        ).build()\n    }\n    single<FreadContentDatabase> {\n        Room.databaseBuilder(\n            androidContext(),\n            FreadContentDatabase::class.java,\n            FreadContentDatabase.DB_NAME,\n        ).build()\n    }\n    single<OldFreadContentDatabase> {\n        Room.databaseBuilder(\n            androidContext(),\n            OldFreadContentDatabase::class.java,\n            OldFreadContentDatabase.DB_NAME,\n        ).build()\n    }\n    single<MixedStatusDatabases> {\n        Room.databaseBuilder(\n            androidContext(),\n            MixedStatusDatabases::class.java,\n            MixedStatusDatabases.DB_NAME,\n        ).addMigrations(MixedStatusDatabases.MIGRATION_1_2).build()\n    }\n\n    singleOf(::MediaFileHelper)\n    singleOf(::PlatformUriHelper)\n    singleOf(::StorageHelper)\n    singleOf(::ActivityLanguageHelper)\n    singleOf(::LanguageHelper)\n    singleOf(::TextHandler)\n    singleOf(::ToastHelper)\n    singleOf(::OAuthHandler)\n    singleOf(::AndroidSystemBrowserLauncher) bind SystemBrowserLauncher::class\n    factoryOf(::LanguageModuleStartup) bind ModuleStartup::class\n    single<FlowSettings> {\n        DataStoreSettings(androidContext().localConfig)\n    } bind FlowSettings::class\n}\n\nval Context.localConfig: DataStore<Preferences> by preferencesDataStore(name = \"local_config\")\n"
  },
  {
    "path": "commonbiz/common/src/androidMain/kotlin/com/zhangke/fread/common/browser/AndroidSystemBrowserLauncher.kt",
    "content": "package com.zhangke.fread.common.browser\n\nimport android.content.Context\nimport android.content.Intent\nimport androidx.browser.customtabs.CustomTabsIntent\nimport com.zhangke.framework.activity.TopActivityManager\nimport com.zhangke.framework.utils.PlatformUri\nimport com.zhangke.framework.utils.extractActivity\nimport com.zhangke.framework.utils.startActivityCompat\nimport com.zhangke.framework.utils.toAndroidUri\n\nclass AndroidSystemBrowserLauncher(\n    private val context: Context,\n) : SystemBrowserLauncher {\n\n    override fun launchBySystemBrowser(uri: PlatformUri) {\n        val intent = Intent(Intent.ACTION_VIEW, uri.toAndroidUri())\n        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)\n        try {\n            context.startActivityCompat(intent)\n        } catch (_: Throwable) {\n            // ignore\n        }\n    }\n\n    override fun launchWebTabInApp(uri: PlatformUri) {\n        try {\n            CustomTabsIntent.Builder().build()\n                .launchUrl(TopActivityManager.topActiveActivity!!, uri.toAndroidUri())\n        } catch (_: Throwable) {\n            // ignore\n        }\n    }\n}\n"
  },
  {
    "path": "commonbiz/common/src/androidMain/kotlin/com/zhangke/fread/common/browser/OAuthLauncher.android.kt",
    "content": "package com.zhangke.fread.common.browser\n\nimport android.content.Intent\nimport androidx.browser.customtabs.CustomTabsIntent\nimport androidx.core.net.toUri\nimport com.zhangke.fread.common.di.ApplicationContext\nimport kotlinx.coroutines.CompletableDeferred\n\nactual class OAuthHandler (\n    private val context: ApplicationContext,\n) {\n\n    private var oauthCodeCompletable: CompletableDeferred<String>? = null\n\n    actual suspend fun startOAuth(url: String): String {\n        val customTabsIntent = CustomTabsIntent.Builder()\n            .build()\n        customTabsIntent.intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)\n        customTabsIntent.launchUrl(context, url.toUri())\n\n        val code = CompletableDeferred<String>().also { oauthCodeCompletable = it }.await()\n        return code\n    }\n\n    fun onOauthSuccess(code: String) {\n        oauthCodeCompletable?.let {\n            it.complete(code)\n            oauthCodeCompletable = null\n        }\n    }\n}"
  },
  {
    "path": "commonbiz/common/src/androidMain/kotlin/com/zhangke/fread/common/daynight/DayNightPlatformHelper.android.kt",
    "content": "package com.zhangke.fread.common.daynight\n\nimport androidx.appcompat.app.AppCompatDelegate\nimport com.zhangke.framework.activity.TopActivityManager\n\nactual class DayNightPlatformHelper {\n\n    actual fun setDefaultMode(modeValue: DayNightMode) {\n        AppCompatDelegate.setDefaultNightMode(modeValue.modeValue)\n    }\n\n    actual fun setMode(mode: DayNightMode) {\n        AppCompatDelegate.setDefaultNightMode(mode.modeValue)\n        TopActivityManager.topActiveActivity?.recreate()\n    }\n\n    actual fun setAmoledMode(enabled: Boolean) {\n        TopActivityManager.topActiveActivity?.recreate()\n    }\n\n    private val DayNightMode.modeValue: Int\n        get() = when (this) {\n            DayNightMode.DAY -> AppCompatDelegate.MODE_NIGHT_NO\n            DayNightMode.NIGHT -> AppCompatDelegate.MODE_NIGHT_YES\n            DayNightMode.FOLLOW_SYSTEM -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM\n        }\n\n}\n"
  },
  {
    "path": "commonbiz/common/src/androidMain/kotlin/com/zhangke/fread/common/di/ApplicationContext.kt",
    "content": "package com.zhangke.fread.common.di\n\ntypealias ApplicationContext = android.content.Context\n"
  },
  {
    "path": "commonbiz/common/src/androidMain/kotlin/com/zhangke/fread/common/handler/TextHandler.android.kt",
    "content": "package com.zhangke.fread.common.handler\n\nimport android.content.Context\nimport android.content.Intent\nimport androidx.core.net.toUri\nimport com.zhangke.framework.utils.SystemPageUtils\nimport com.zhangke.framework.utils.SystemUtils\nimport com.zhangke.framework.utils.startActivityCompat\nimport com.zhangke.fread.common.config.AppCommonConfig\nimport com.zhangke.fread.common.utils.ShareHelper\n\nactual class TextHandler (\n    private val context: Context,\n) {\n    actual val packageName: String\n        get() = context.packageName\n\n    actual val versionName: String\n        get() = SystemUtils.getAppVersionName(context)\n\n    actual val versionCode: String\n        get() = SystemUtils.getAppVersionCode(context)\n\n    actual fun copyText(text: String) {\n        SystemUtils.copyText(context, text)\n    }\n\n    actual fun shareUrl(url: String, text: String) {\n        ShareHelper.shareUrl(context, url, text)\n    }\n\n    actual fun openSendEmail() {\n        SystemUtils.copyText(context, AppCommonConfig.AUTHOR_EMAIL)\n        val intent = Intent(Intent.ACTION_SEND)\n        intent.data = \"mailto:\".toUri()\n        intent.putExtra(Intent.EXTRA_EMAIL, AppCommonConfig.AUTHOR_EMAIL)\n        if (intent.resolveActivity(context.packageManager) != null) {\n            context.startActivityCompat(intent)\n        }\n    }\n\n    actual fun openAppMarket() {\n        SystemPageUtils.openAppMarket(context)\n    }\n}"
  },
  {
    "path": "commonbiz/common/src/androidMain/kotlin/com/zhangke/fread/common/language/ActivityLanguageHelper.android.kt",
    "content": "package com.zhangke.fread.common.language\n\nimport android.content.Context\nimport android.os.Build.VERSION\nimport android.os.LocaleList\nimport com.zhangke.fread.localization.locale\nimport java.util.Locale\n\nactual class ActivityLanguageHelper(\n    private val languageHelper: LanguageHelper,\n) {\n\n    actual val currentLanguage get() = languageHelper.currentLanguage\n\n    actual fun initialize() {\n        languageHelper.init()\n    }\n\n    actual fun setLanguage(item: LanguageSettingItem) {\n        languageHelper.setLanguage(item)\n        languageHelper.topActiveActivity?.let {\n            it.changeLanguage(item)\n            it.recreate()\n        }\n    }\n}\n\ninternal fun Context.changeLanguage(item: LanguageSettingItem) {\n    val metrics = resources.displayMetrics\n    val configuration = resources.configuration\n\n    val targetLocale = item.locale\n    Locale.setDefault(targetLocale)\n    if (VERSION.SDK_INT >= 24) {\n        configuration.setLocales(LocaleList(targetLocale))\n    } else {\n        configuration.setLocale(targetLocale)\n    }\n\n    @Suppress(\"DEPRECATION\")\n    resources.updateConfiguration(configuration, metrics)\n}\n\nprivate val LanguageSettingItem.locale: Locale\n    get() {\n        return when (this) {\n            is LanguageSettingItem.FollowSystem -> Locale.getDefault()\n            is LanguageSettingItem.Language -> code.locale\n        }\n    }\n"
  },
  {
    "path": "commonbiz/common/src/androidMain/kotlin/com/zhangke/fread/common/language/LanguageHelper.android.kt",
    "content": "package com.zhangke.fread.common.language\n\nimport android.app.Activity\nimport android.app.Application\nimport android.content.Context\nimport android.os.Bundle\nimport com.zhangke.framework.architect.coroutines.ApplicationScope\nimport com.zhangke.framework.utils.ActivityLifecycleCallbacksAdapter\nimport com.zhangke.fread.common.config.LocalConfigManager\nimport com.zhangke.fread.localization.LanguageCode\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.runBlocking\nimport java.lang.ref.WeakReference\n\nprivate const val OLD_LANGUAGE_SETTING = \"app_language_setting\"\n\nclass LanguageHelper (\n    private val localConfigManager: LocalConfigManager,\n    private val context: Context,\n) {\n\n    private var activeActivity: WeakReference<Activity?>? = null\n\n    val topActiveActivity: Activity? get() = activeActivity?.get()\n\n    private val pausedActivityList = mutableListOf<WeakReference<Activity>>()\n\n    var currentLanguage: LanguageSettingItem = readLocalLanguageCode()\n        private set\n\n    private val application = context.applicationContext as Application\n\n    fun init() {\n        application.changeLanguage(currentLanguage)\n        application.registerActivityLifecycleCallbacks(object :\n            ActivityLifecycleCallbacksAdapter() {\n\n            override fun onActivityResumed(activity: Activity) {\n                super.onActivityResumed(activity)\n                activeActivity = WeakReference(activity)\n            }\n\n            override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {\n                activity.changeLanguage(currentLanguage)\n                super.onActivityCreated(activity, savedInstanceState)\n            }\n\n            override fun onActivityPaused(activity: Activity) {\n                val savedValue = pausedActivityList.findLast { it.get() == activity }\n                if (savedValue == null) {\n                    pausedActivityList += WeakReference(activity)\n                }\n                activeActivity?.clear()\n                activeActivity = null\n                super.onActivityPaused(activity)\n            }\n\n            override fun onActivityDestroyed(activity: Activity) {\n                pausedActivityList.removeAll {\n                    it.get() == activity\n                }\n                activeActivity?.clear()\n                activeActivity = null\n                super.onActivityDestroyed(activity)\n            }\n        })\n    }\n\n    private fun readLocalLanguageCode(): LanguageSettingItem {\n        return runBlocking {\n            val id = localConfigManager.getString(LOCAL_KEY_LANGUAGE)\n                ?.let { LanguageSettingItem.fromLocalId(it) }\n            if (id != null) return@runBlocking id\n            tryReadOldConfigAsLanguageCode() ?: LanguageSettingItem.FollowSystem\n        }\n    }\n\n    private suspend fun tryReadOldConfigAsLanguageCode(): LanguageSettingItem? {\n        val type = localConfigManager.getInt(OLD_LANGUAGE_SETTING) ?: return null\n        val code = when (type) {\n            LanguageSettingType.CN.value -> LanguageSettingItem.Language(LanguageCode.ZH_CN)\n            LanguageSettingType.EN.value -> LanguageSettingItem.Language(LanguageCode.EN_US)\n            LanguageSettingType.SYSTEM.value -> LanguageSettingItem.FollowSystem\n            else -> null\n        }\n        if (code == null) return null\n        saveLocalToStorage(code)\n        localConfigManager.removeKey(OLD_LANGUAGE_SETTING)\n        return code\n    }\n\n    private fun saveLocalToStorage(item: LanguageSettingItem) {\n        ApplicationScope.launch {\n            localConfigManager.putString(LOCAL_KEY_LANGUAGE, item.localId)\n        }\n    }\n\n    private fun notifyOtherActivityConfig() {\n        pausedActivityList.mapNotNull { it.get() }.forEach { it.recreate() }\n    }\n\n    fun setLanguage(item: LanguageSettingItem) {\n        currentLanguage = item\n        saveLocalToStorage(item)\n        application.changeLanguage(item)\n        notifyOtherActivityConfig()\n    }\n}"
  },
  {
    "path": "commonbiz/common/src/androidMain/kotlin/com/zhangke/fread/common/page/BasePagerTabHookManager.android.kt",
    "content": "package com.zhangke.fread.common.page\n\nimport com.zhangke.fread.status.utils.findImplementers\n\ninternal actual fun findBasePagerTabImplementers(): List<BasePagerTabHook> {\n    return findImplementers()\n}"
  },
  {
    "path": "commonbiz/common/src/androidMain/kotlin/com/zhangke/fread/common/startup/LanguageModuleStartup.kt",
    "content": "package com.zhangke.fread.common.startup\n\nimport com.zhangke.framework.module.ModuleStartup\nimport com.zhangke.fread.common.language.LanguageHelper\n\nclass LanguageModuleStartup (\n    private val languageHelper: LanguageHelper,\n): ModuleStartup {\n\n    override fun onAppCreate() {\n        languageHelper.init()\n    }\n}"
  },
  {
    "path": "commonbiz/common/src/androidMain/kotlin/com/zhangke/fread/common/update/AppPlatformUpdater.android.kt",
    "content": "package com.zhangke.fread.common.update\n\nimport android.widget.Toast\nimport androidx.browser.customtabs.CustomTabsIntent\nimport androidx.core.net.toUri\nimport com.zhangke.framework.activity.TopActivityManager\nimport com.zhangke.framework.utils.SystemPageUtils\nimport com.zhangke.framework.utils.appContext\nimport com.zhangke.framework.utils.getApkSignatureSha1\nimport com.zhangke.fread.common.config.AppCommonConfig\n\nactual class AppPlatformUpdater {\n\n    companion object {\n\n        private const val F_DROID_SIGN_PUB_KEY =\n            \"B9:7E:73:42:BD:41:63:A5:D7:D7:1C:5E:35:DF:DF:D1:A9:13:8C:E8\"\n    }\n\n    actual val platformName: String = \"android\"\n\n    actual val signingForFDroid: Boolean\n        get() {\n            val apkSignatureSha1 = appContext.getApkSignatureSha1()\n            return apkSignatureSha1 == F_DROID_SIGN_PUB_KEY\n        }\n\n    actual fun getAppVersionCode(): Long {\n        return appContext.packageManager\n            .getPackageInfo(appContext.packageName, 0)\n            .versionCode\n            .toLong()\n    }\n\n    actual fun triggerUpdate(releaseInfo: AppReleaseInfo) {\n        val builtForFDroid = signingForFDroid\n        if (builtForFDroid) {\n            if (!SystemPageUtils.openSystemViewPage(appContext, AppCommonConfig.F_DROID_URI)) {\n                Toast.makeText(appContext, \"F-Droid not found\", Toast.LENGTH_SHORT).show()\n            }\n            return\n        }\n        if (SystemPageUtils.openAppMarket(appContext)) return\n        val activity = TopActivityManager.topActiveActivity ?: return\n        val customTabsIntent = CustomTabsIntent.Builder()\n            .build()\n        customTabsIntent.launchUrl(activity, AppCommonConfig.WEBSITE.toUri())\n    }\n}\n"
  },
  {
    "path": "commonbiz/common/src/androidMain/kotlin/com/zhangke/fread/common/utils/ActivityResultUtils.kt",
    "content": "package com.zhangke.fread.common.utils\n\nimport android.content.Intent\n\ntypealias ActivityResultCallback = (resultCode: Int, Intent?) -> Unit\n\ninterface CallbackableActivity {\n\n    fun registerCallback(requestCode: Int, callback: ActivityResultCallback)\n}\n"
  },
  {
    "path": "commonbiz/common/src/androidMain/kotlin/com/zhangke/fread/common/utils/MediaFileHelper.android.kt",
    "content": "package com.zhangke.fread.common.utils\n\nimport com.zhangke.framework.media.MediaFileUtil\nimport com.zhangke.framework.toast.showFileSaveFailedToast\nimport com.zhangke.framework.toast.showFileSaveSuccessToast\nimport com.zhangke.framework.toast.showFileSavingToast\nimport com.zhangke.fread.common.di.ApplicationContext\nimport com.zhangke.fread.common.di.ApplicationCoroutineScope\nimport kotlinx.coroutines.launch\n\nactual class MediaFileHelper (\n    private val context: ApplicationContext,\n    private val applicationCoroutineScope: ApplicationCoroutineScope,\n) {\n    actual fun saveImageToGallery(url: String) {\n        applicationCoroutineScope.launch {\n            showFileSavingToast()\n            if (MediaFileUtil.saveImageToGallery(context, url)) {\n                showFileSaveSuccessToast()\n            } else {\n                showFileSaveFailedToast()\n            }\n        }\n    }\n\n    actual fun saveVideoToGallery(url: String) {\n        applicationCoroutineScope.launch {\n            showFileSavingToast()\n            if (MediaFileUtil.saveVideoToGallery(context, url)) {\n                showFileSaveSuccessToast()\n            } else {\n                showFileSaveFailedToast()\n            }\n        }\n    }\n}"
  },
  {
    "path": "commonbiz/common/src/androidMain/kotlin/com/zhangke/fread/common/utils/PlatformUriHelper.android.kt",
    "content": "package com.zhangke.fread.common.utils\n\nimport com.zhangke.framework.media.MediaFileUtil\nimport com.zhangke.framework.utils.ContentProviderFile\nimport com.zhangke.framework.utils.PlatformUri\nimport com.zhangke.framework.utils.toAndroidUri\nimport com.zhangke.framework.utils.toContentProviderFile\nimport com.zhangke.fread.common.di.ApplicationContext\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\n\nactual class PlatformUriHelper (\n    private val context: ApplicationContext,\n) {\n    actual suspend fun read(uri: PlatformUri): ContentProviderFile? {\n        val contentFile = withContext(Dispatchers.IO) {\n            uri.toAndroidUri().toContentProviderFile(context)\n        }\n        return contentFile\n    }\n\n    actual suspend fun readBytes(uri: PlatformUri): ByteArray? {\n        return context.contentResolver.openInputStream(uri.toAndroidUri())?.use {\n            it.readBytes()\n        }\n    }\n\n    actual fun queryFileName(uri: PlatformUri): String? {\n        return MediaFileUtil.queryFileName(context, uri.toAndroidUri())\n    }\n}"
  },
  {
    "path": "commonbiz/common/src/androidMain/kotlin/com/zhangke/fread/common/utils/RandomIdGenerator.android.kt",
    "content": "package com.zhangke.fread.common.utils\n\nimport java.util.UUID\n\nactual class RandomIdGenerator {\n\n    actual fun generateId(): String {\n        return UUID.randomUUID().toString()\n    }\n}\n"
  },
  {
    "path": "commonbiz/common/src/androidMain/kotlin/com/zhangke/fread/common/utils/ShareHelper.kt",
    "content": "package com.zhangke.fread.common.utils\n\nimport android.content.Context\nimport android.content.Intent\nimport androidx.core.text.HtmlCompat\nimport com.zhangke.framework.utils.startActivityCompat\n\nobject ShareHelper {\n\n    fun shareUrl(\n        context: Context,\n        url: String,\n        text: String,\n    ) {\n        val intent = Intent().apply {\n            action = Intent.ACTION_SEND\n            type = \"text/plain\"\n            putExtra(Intent.EXTRA_TITLE, HtmlCompat.fromHtml(text, HtmlCompat.FROM_HTML_MODE_LEGACY).toString())\n            putExtra(Intent.EXTRA_TEXT, url)\n            addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)\n        }\n        context.startActivityCompat(Intent.createChooser(intent, null))\n    }\n}\n"
  },
  {
    "path": "commonbiz/common/src/androidMain/kotlin/com/zhangke/fread/common/utils/StorageHelper.android.kt",
    "content": "package com.zhangke.fread.common.utils\n\nimport com.zhangke.fread.common.di.ApplicationContext\nimport okio.Path\nimport okio.Path.Companion.toOkioPath\n\nactual class StorageHelper (\n    private val applicationContext: ApplicationContext,\n) {\n    actual val cacheDir: Path\n        get() = applicationContext.cacheDir.toOkioPath()\n}"
  },
  {
    "path": "commonbiz/common/src/androidMain/kotlin/com/zhangke/fread/common/utils/ToastHelper.android.kt",
    "content": "package com.zhangke.fread.common.utils\n\nimport android.view.Gravity\nimport android.widget.Toast\nimport com.zhangke.fread.common.di.ApplicationContext\n\nactual class ToastHelper(\n    private val context: ApplicationContext,\n) {\n    actual fun showToast(content: String) {\n        val toast = Toast.makeText(context, content, Toast.LENGTH_SHORT)\n        toast.setGravity(Gravity.CENTER, 0, 0)\n        toast.show()\n    }\n}\n"
  },
  {
    "path": "commonbiz/common/src/androidMain/res/anim/fade_in.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<alpha xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:duration=\"300\"\n    android:fromAlpha=\"0\"\n    android:toAlpha=\"1\" />"
  },
  {
    "path": "commonbiz/common/src/androidMain/res/anim/fade_out.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<alpha xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:duration=\"300\"\n    android:fromAlpha=\"1\"\n    android:toAlpha=\"0\" />"
  },
  {
    "path": "commonbiz/common/src/androidMain/res/drawable/ic_logo_skeleton.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"157dp\"\n    android:height=\"117dp\"\n    android:viewportWidth=\"157\"\n    android:viewportHeight=\"117\">\n  <path\n      android:pathData=\"M0.72,33.52C-2.32,55.28 4.36,81.47 18.73,86.51C30.74,90.73 40.88,85.93 45.32,82.06C49.78,84.78 54.96,86.89 60.82,88.28C62.51,95.83 65.91,102.92 70.16,107.58C78.99,117.28 90.99,118.94 106.18,112.55L106.18,112.51L106.18,112.55C107.57,112.12 109.01,111.3 112.21,113.5C113.82,114.61 116.3,115.79 119.29,113.92C122.41,111.97 122.31,108.08 120.79,106.2C119.09,104.07 118.31,104.01 117.54,102.18C116.76,100.36 117.85,100.74 118.99,98.02C120.14,95.3 119.34,92.65 117.96,91.17C115.86,88.9 111.79,88.28 109.48,91.98C108.66,93.29 108.23,95.28 105.91,96.1C102.35,97.24 99.56,96.76 97.53,94.66C96.06,93.13 95.91,90.79 96.32,88.14C102.07,86.73 107.14,84.62 111.52,81.91C115.86,85.81 126.11,90.78 138.27,86.51C152.64,81.47 159.32,55.28 156.28,33.52C154.97,21.09 147.93,-2.18 118.94,0.16C114.27,0.54 108.7,2.54 104.45,6.25C96.74,2.63 87.81,0.61 78.3,0.61C68.89,0.61 60.06,2.58 52.41,6.12C48.17,2.5 42.68,0.54 38.06,0.16C9.07,-2.18 2.03,21.09 0.72,33.52ZM90.17,44.36C90.17,38.84 93.76,34.36 98.17,34.36C102.59,34.37 106.17,38.84 106.17,44.36C106.17,49.89 102.59,54.36 98.17,54.36C93.76,54.36 90.17,49.89 90.17,44.36ZM90.14,26.49C89.7,24.85 91.11,23.05 93.29,22.47C95.47,21.88 97.6,22.74 98.03,24.37C98.47,26.01 97.06,27.81 94.88,28.39C92.7,28.98 90.57,28.12 90.14,26.49ZM52.17,44.32C52.17,38.8 55.76,34.32 60.17,34.32C64.59,34.32 68.17,38.8 68.17,44.32C68.17,49.84 64.59,54.32 60.17,54.32C55.76,54.32 52.17,49.84 52.17,44.32ZM63.29,28.34C61.11,27.76 59.7,25.96 60.14,24.33C60.57,22.69 62.7,21.84 64.88,22.42C67.06,23.01 68.47,24.81 68.03,26.44C67.6,28.08 65.47,28.93 63.29,28.34Z\"\n      android:fillColor=\"#000000\"/>\n</vector>\n"
  },
  {
    "path": "commonbiz/common/src/androidMain/res/values/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"color_logo_background\">#FF00A5FF</color>\n</resources>\n"
  },
  {
    "path": "commonbiz/common/src/androidMain/res/values/theme.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n\n    <style name=\"DialogActivity\" parent=\"Theme.AppCompat.Dialog\">\n        <item name=\"android:windowBackground\">@android:color/transparent</item>\n        <item name=\"android:excludeFromRecents\">true</item>\n        <item name=\"android:windowNoTitle\">true</item>\n        <item name=\"windowNoTitle\">true</item>\n        <item name=\"android:windowActionBar\">false</item>\n        <item name=\"windowActionBar\">false</item>\n    </style>\n\n</resources>\n"
  },
  {
    "path": "commonbiz/common/src/commonMain/composeResources/drawable/bluesky_logo.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"600dp\"\n    android:height=\"530dp\"\n    android:viewportWidth=\"600\"\n    android:viewportHeight=\"530\">\n  <path\n      android:pathData=\"m135.72,44.03c66.5,49.92 138.02,151.14 164.28,205.46 26.26,-54.32 97.78,-155.54 164.28,-205.46 47.98,-36.02 125.72,-63.89 125.72,24.8 0,17.71 -10.15,148.79 -16.11,170.07 -20.7,73.98 -96.14,92.85 -163.25,81.43 117.3,19.96 147.14,86.09 82.7,152.22 -122.39,125.59 -175.91,-31.51 -189.63,-71.77 -2.51,-7.38 -3.69,-10.83 -3.71,-7.9 -0.02,-2.94 -1.19,0.52 -3.71,7.9 -13.71,40.26 -67.23,197.36 -189.63,71.77 -64.44,-66.13 -34.6,-132.26 82.7,-152.22 -67.11,11.42 -142.55,-7.45 -163.25,-81.43 -5.96,-21.28 -16.11,-152.36 -16.11,-170.07 0,-88.69 77.74,-60.82 125.72,-24.8z\"\n      android:fillColor=\"#1185fe\"/>\n</vector>\n"
  },
  {
    "path": "commonbiz/common/src/commonMain/composeResources/drawable/ic_explorer.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"25dp\"\n    android:height=\"26dp\"\n    android:viewportWidth=\"25\"\n    android:viewportHeight=\"26\">\n  <path\n      android:pathData=\"M7.604,5.204C11.789,2.787 17.197,4.318 19.682,8.623C22.167,12.927 20.789,18.376 16.604,20.792C12.419,23.208 7.012,21.677 4.526,17.373C2.041,13.068 3.419,7.62 7.604,5.204ZM8.604,6.936C5.387,8.793 4.319,13.014 6.258,16.373C8.198,19.732 12.387,20.918 15.604,19.06C18.822,17.202 19.889,12.982 17.95,9.623C16.011,6.264 11.821,5.078 8.604,6.936Z\"\n      android:fillColor=\"#828FB0\"/>\n  <path\n      android:pathData=\"M17.652,7.256C20.026,6.879 21.755,7.138 22.209,8.113C23.052,9.921 19.193,13.505 13.589,16.118C7.985,18.731 2.759,19.384 1.916,17.576C1.435,16.544 2.485,14.934 4.517,13.256\"\n      android:strokeWidth=\"2\"\n      android:fillColor=\"#00000000\"\n      android:strokeColor=\"#828FB0\"/>\n</vector>\n"
  },
  {
    "path": "commonbiz/common/src/commonMain/composeResources/drawable/ic_not_found_404.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n        android:width=\"256dp\"\n        android:height=\"256dp\"\n        android:viewportWidth=\"256.0\"\n        android:viewportHeight=\"256.0\">\n    <path\n        android:pathData=\"M128,128m-112,0a112,112 0,1 1,224 0a112,112 0,1 1,-224 0\"\n        android:strokeColor=\"#CFD8DC\"\n        android:fillColor=\"#ECEFF1\"\n        android:strokeWidth=\"8\"/>\n    <path\n        android:pathData=\"M80,68H156L192,104V188C192,195.732 185.732,202 178,202H80C72.268,202 66,195.732 66,188V82C66,74.268 72.268,68 80,68Z\"\n        android:strokeColor=\"#B0BEC5\"\n        android:fillColor=\"#FFFFFF\"\n        android:strokeWidth=\"6\"/>\n    <path\n        android:pathData=\"M156,68V104H192\"\n        android:strokeColor=\"#B0BEC5\"\n        android:fillColor=\"#FFFFFF\"\n        android:strokeWidth=\"6\"/>\n    <path\n        android:pathData=\"M99,124h6v36h-6z\"\n        android:fillColor=\"#607D8B\"/>\n    <path\n        android:pathData=\"M81,140h24v6h-24z\"\n        android:fillColor=\"#607D8B\"/>\n    <path\n        android:pathData=\"M81,124h6v20h-6z\"\n        android:fillColor=\"#607D8B\"/>\n    <path\n        android:pathData=\"M113,124h24v36h-24z\"\n        android:fillColor=\"#607D8B\"/>\n    <path\n        android:pathData=\"M119,130h12v24h-12z\"\n        android:fillColor=\"#FFFFFF\"/>\n    <path\n        android:pathData=\"M163,124h6v36h-6z\"\n        android:fillColor=\"#607D8B\"/>\n    <path\n        android:pathData=\"M145,140h24v6h-24z\"\n        android:fillColor=\"#607D8B\"/>\n    <path\n        android:pathData=\"M145,124h6v20h-6z\"\n        android:fillColor=\"#607D8B\"/>\n</vector>\n"
  },
  {
    "path": "commonbiz/common/src/commonMain/composeResources/drawable/mastodon_black_text.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:aapt=\"http://schemas.android.com/aapt\"\n    android:width=\"314dp\"\n    android:height=\"80dp\"\n    android:viewportWidth=\"314\"\n    android:viewportHeight=\"80\">\n  <path\n      android:pathData=\"M73.45,17.7C72.32,9.2 65,2.49 56.35,1.2C54.88,0.98 49.35,0.18 36.52,0.18H36.42C23.59,0.18 20.84,0.98 19.37,1.2C10.94,2.46 3.26,8.48 1.38,17.09C0.5,21.32 0.4,26.02 0.57,30.33C0.81,36.52 0.86,42.68 1.41,48.84C1.79,52.93 2.45,56.99 3.4,60.98C5.18,68.36 12.36,74.5 19.4,76.99C26.93,79.6 35.03,80.04 42.79,78.25C43.64,78.04 44.48,77.81 45.32,77.54C47.21,76.93 49.42,76.25 51.05,75.06C51.07,75.04 51.09,75.02 51.1,74.99C51.11,74.97 51.12,74.95 51.12,74.91V68.94C51.12,68.94 51.12,68.89 51.1,68.87C51.1,68.85 51.07,68.82 51.05,68.81C51.03,68.8 51,68.79 50.98,68.77C50.95,68.77 50.93,68.77 50.91,68.77C45.93,69.98 40.83,70.59 35.73,70.57C26.93,70.57 24.56,66.34 23.89,64.58C23.35,63.06 23,61.47 22.86,59.86C22.86,59.84 22.86,59.81 22.87,59.79C22.87,59.76 22.89,59.74 22.92,59.73C22.94,59.71 22.96,59.7 22.99,59.69H23.07C27.96,60.88 32.98,61.49 38.01,61.49C39.22,61.49 40.42,61.49 41.64,61.46C46.69,61.31 52.02,61.05 57.01,60.07C57.13,60.04 57.26,60.02 57.37,59.99C65.22,58.46 72.69,53.66 73.45,41.51C73.47,41.04 73.54,36.5 73.54,36.01C73.54,34.32 74.08,24.04 73.46,17.72L73.45,17.7Z\">\n    <aapt:attr name=\"android:fillColor\">\n      <gradient \n          android:startX=\"37.12\"\n          android:startY=\"0.18\"\n          android:endX=\"37.12\"\n          android:endY=\"79.32\"\n          android:type=\"linear\">\n        <item android:offset=\"0\" android:color=\"#FF6364FF\"/>\n        <item android:offset=\"1\" android:color=\"#FF563ACC\"/>\n      </gradient>\n    </aapt:attr>\n  </path>\n  <path\n      android:pathData=\"M15.31,22.45C15.31,19.96 17.27,17.96 19.7,17.96C22.12,17.96 24.08,19.97 24.08,22.45C24.08,24.92 22.12,26.93 19.7,26.93C17.27,26.93 15.31,24.92 15.31,22.45Z\"\n      android:fillColor=\"#ffffff\"/>\n  <path\n      android:pathData=\"M80.52,26.3V46.91H72.53V26.91C72.53,22.7 70.8,20.57 67.33,20.57C63.5,20.57 61.57,23.11 61.57,28.12V39.06H53.64V28.12C53.64,23.09 51.74,20.57 47.88,20.57C44.42,20.57 42.68,22.7 42.68,26.91V46.89H34.71V26.3C34.71,22.1 35.75,18.76 37.85,16.27C40.03,13.79 42.87,12.53 46.39,12.53C50.47,12.53 53.57,14.14 55.61,17.36L57.61,20.76L59.6,17.36C61.64,14.15 64.73,12.53 68.82,12.53C72.34,12.53 75.19,13.8 77.36,16.27C79.46,18.76 80.51,22.08 80.51,26.3H80.52ZM107.99,36.54C109.65,34.75 110.43,32.54 110.43,29.84C110.43,27.13 109.64,24.89 107.99,23.19C106.41,21.4 104.39,20.55 101.95,20.55C99.52,20.55 97.51,21.4 95.91,23.19C94.33,24.89 93.54,27.13 93.54,29.84C93.54,32.54 94.33,34.78 95.91,36.54C97.5,38.25 99.52,39.11 101.95,39.11C104.39,39.11 106.4,38.26 107.99,36.54ZM110.43,13.36H118.3V46.31H110.43V42.43C108.05,45.65 104.76,47.25 100.49,47.25C96.21,47.25 92.92,45.6 90.01,42.24C87.14,38.88 85.68,34.73 85.68,29.86C85.68,24.99 87.15,20.9 90.01,17.54C92.94,14.18 96.42,12.47 100.49,12.47C104.56,12.47 108.05,14.06 110.43,17.27V13.39V13.36ZM144.76,29.21C147.08,31 148.23,33.5 148.17,36.65C148.17,40.01 147.02,42.66 144.64,44.48C142.26,46.27 139.39,47.19 135.91,47.19C129.63,47.19 125.37,44.54 123.11,39.36L129.93,35.21C130.84,38.03 132.85,39.49 135.91,39.49C138.72,39.49 140.12,38.58 140.12,36.67C140.12,35.28 138.29,34.02 134.57,33.05C133.16,32.66 132,32.26 131.1,31.93C129.81,31.41 128.72,30.81 127.81,30.08C125.55,28.29 124.4,25.93 124.4,22.9C124.4,19.67 125.49,17.1 127.69,15.25C129.95,13.34 132.69,12.42 135.98,12.42C141.23,12.42 145.06,14.73 147.57,19.4L140.87,23.35C139.9,21.11 138.24,19.99 135.98,19.99C133.61,19.99 132.45,20.9 132.45,22.69C132.45,24.08 134.28,25.33 138,26.3C140.87,26.96 143.13,27.95 144.76,29.21H144.77H144.76ZM169.76,21.52H162.87V35.23C162.87,36.87 163.48,37.87 164.65,38.32C165.5,38.65 167.21,38.71 169.78,38.59V46.3C164.48,46.95 160.64,46.42 158.38,44.65C156.12,42.95 155.03,39.77 155.03,35.24V21.52H149.73V13.35H155.03V6.7L162.89,4.13V13.36H169.79V21.53H169.78L169.76,21.52ZM194.84,36.35C196.42,34.65 197.21,32.47 197.21,29.82C197.21,27.18 196.42,25.03 194.84,23.3C193.24,21.59 191.29,20.73 188.92,20.73C186.54,20.73 184.59,21.58 183,23.3C181.47,25.09 180.68,27.24 180.68,29.82C180.68,32.4 181.47,34.56 183,36.35C184.58,38.05 186.54,38.92 188.92,38.92C191.29,38.92 193.24,38.07 194.84,36.35ZM177.46,42.21C174.35,38.85 172.83,34.77 172.83,29.82C172.83,24.88 174.35,20.86 177.46,17.5C180.57,14.14 184.41,12.44 188.92,12.44C193.42,12.44 197.27,14.14 200.37,17.5C203.47,20.86 205.07,25.01 205.07,29.82C205.07,34.63 203.47,38.85 200.37,42.21C197.26,45.57 193.48,47.21 188.92,47.21C184.35,47.21 180.56,45.57 177.46,42.21ZM231.36,36.53C232.95,34.74 233.74,32.53 233.74,29.82C233.74,27.12 232.95,24.88 231.36,23.18C229.78,21.39 227.76,20.53 225.32,20.53C222.89,20.53 220.87,21.39 219.23,23.18C217.64,24.88 216.85,27.12 216.85,29.82C216.85,32.53 217.64,34.77 219.23,36.53C220.88,38.24 222.95,39.1 225.32,39.1C227.7,39.1 229.77,38.25 231.36,36.53ZM233.74,0.16H241.61V46.3H233.74V42.41C231.43,45.64 228.13,47.23 223.86,47.23C219.59,47.23 216.25,45.59 213.3,42.23C210.43,38.87 208.98,34.72 208.98,29.85C208.98,24.98 210.45,20.89 213.3,17.53C216.22,14.17 219.76,12.46 223.86,12.46C227.96,12.46 231.43,14.04 233.74,17.26V0.18V0.16ZM269.24,36.31C270.82,34.61 271.62,32.43 271.62,29.79C271.62,27.14 270.82,24.99 269.24,23.26C267.65,21.56 265.71,20.69 263.32,20.69C260.93,20.69 259,21.54 257.4,23.26C255.87,25.05 255.08,27.21 255.08,29.79C255.08,32.37 255.87,34.52 257.4,36.31C258.98,38.02 260.94,38.88 263.32,38.88C265.7,38.88 267.64,38.03 269.24,36.31ZM251.86,42.17C248.76,38.81 247.23,34.73 247.23,29.79C247.23,24.84 248.75,20.83 251.86,17.47C254.97,14.1 258.82,12.4 263.32,12.4C267.82,12.4 271.68,14.1 274.77,17.47C277.88,20.83 279.47,24.98 279.47,29.79C279.47,34.6 277.88,38.81 274.77,42.17C271.66,45.53 267.88,47.17 263.32,47.17C258.76,47.17 254.96,45.53 251.86,42.17ZM313.5,26.02V46.26H305.64V27.08C305.64,24.9 305.09,23.26 303.98,22.02C302.95,20.9 301.48,20.31 299.59,20.31C295.15,20.31 292.89,23.02 292.89,28.48V46.27H285.03V13.35H292.89V17.05C294.78,13.96 297.78,12.44 301.97,12.44C305.32,12.44 308.07,13.62 310.21,16.05C312.41,18.49 313.5,21.79 313.5,26.06\"\n      android:fillColor=\"#000000\"/>\n</vector>\n"
  },
  {
    "path": "commonbiz/common/src/commonMain/composeResources/drawable/mastodon_logo.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"61.08dp\"\n    android:height=\"65.48dp\"\n    android:viewportWidth=\"216.41\"\n    android:viewportHeight=\"232.01\">\n  <path\n      android:pathData=\"M211.81,139.09c-3.18,16.37 -28.49,34.28 -57.56,37.75 -15.16,1.81 -30.08,3.47 -46,2.74 -26.03,-1.19 -46.56,-6.21 -46.56,-6.21 0,2.53 0.16,4.95 0.47,7.2 3.38,25.69 25.47,27.23 46.39,27.94 21.12,0.72 39.92,-5.21 39.92,-5.21l0.87,19.09s-14.77,7.93 -41.08,9.39c-14.51,0.8 -32.52,-0.37 -53.51,-5.92C9.23,213.82 1.41,165.31 0.21,116.09c-0.37,-14.61 -0.14,-28.39 -0.14,-39.92 0,-50.33 32.98,-65.08 32.98,-65.08C49.67,3.45 78.2,0.24 107.86,0h0.73c29.66,0.24 58.21,3.45 74.84,11.09 0,0 32.97,14.75 32.97,65.08 0,0 0.41,37.13 -4.6,62.92\"\n      android:fillColor=\"#2b90d9\"/>\n  <path\n      android:pathData=\"M177.51,80.08v60.94h-24.14v-59.15c0,-12.47 -5.25,-18.8 -15.74,-18.8 -11.6,0 -17.42,7.51 -17.42,22.35v32.38H96.21V85.42c0,-14.85 -5.82,-22.35 -17.42,-22.35 -10.49,0 -15.74,6.33 -15.74,18.8v59.15H38.9V80.08c0,-12.45 3.17,-22.35 9.54,-29.67 6.57,-7.32 15.17,-11.08 25.85,-11.08 12.35,0 21.71,4.75 27.9,14.25l6.01,10.08 6.01,-10.08c6.18,-9.5 15.54,-14.25 27.9,-14.25 10.68,0 19.28,3.75 25.85,11.08 6.37,7.32 9.54,17.22 9.54,29.67\"\n      android:fillColor=\"#fff\"/>\n</vector>\n"
  },
  {
    "path": "commonbiz/common/src/commonMain/composeResources/drawable/mastodon_white_text.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:aapt=\"http://schemas.android.com/aapt\"\n    android:width=\"313dp\"\n    android:height=\"81dp\"\n    android:viewportWidth=\"313\"\n    android:viewportHeight=\"81\">\n  <path\n      android:pathData=\"M72.95,18.45C71.82,9.95 64.5,3.24 55.85,1.95C54.38,1.73 48.85,0.93 36.02,0.93H35.92C23.09,0.93 20.34,1.73 18.87,1.95C10.44,3.22 2.76,9.23 0.88,17.84C-0,22.08 -0.1,26.78 0.07,31.09C0.31,37.27 0.36,43.43 0.91,49.6C1.29,53.69 1.95,57.74 2.9,61.73C4.68,69.11 11.86,75.25 18.9,77.75C26.43,80.35 34.53,80.79 42.29,79C43.14,78.79 43.98,78.56 44.82,78.29C46.71,77.68 48.92,77 50.55,75.81C50.57,75.8 50.59,75.77 50.6,75.75C50.61,75.72 50.62,75.7 50.62,75.66V69.7C50.62,69.7 50.62,69.65 50.6,69.62C50.6,69.6 50.57,69.58 50.55,69.56C50.53,69.55 50.5,69.54 50.48,69.53C50.45,69.53 50.43,69.53 50.41,69.53C45.43,70.73 40.33,71.34 35.23,71.33C26.43,71.33 24.06,67.09 23.39,65.34C22.85,63.82 22.5,62.22 22.36,60.61C22.36,60.59 22.36,60.57 22.37,60.54C22.37,60.52 22.39,60.49 22.42,60.48C22.44,60.47 22.46,60.46 22.49,60.44H22.57C27.46,61.64 32.48,62.25 37.51,62.25C38.72,62.25 39.92,62.25 41.14,62.21C46.19,62.06 51.52,61.81 56.51,60.82C56.63,60.8 56.76,60.77 56.87,60.75C64.72,59.21 72.19,54.42 72.95,42.27C72.97,41.79 73.04,37.25 73.04,36.76C73.04,35.07 73.58,24.79 72.96,18.48L72.95,18.45Z\">\n    <aapt:attr name=\"android:fillColor\">\n      <gradient \n          android:startX=\"36.62\"\n          android:startY=\"0.93\"\n          android:endX=\"36.62\"\n          android:endY=\"80.07\"\n          android:type=\"linear\">\n        <item android:offset=\"0\" android:color=\"#FF6364FF\"/>\n        <item android:offset=\"1\" android:color=\"#FF563ACC\"/>\n      </gradient>\n    </aapt:attr>\n  </path>\n  <path\n      android:pathData=\"M14.81,23.2C14.81,20.72 16.77,18.72 19.2,18.72C21.62,18.72 23.58,20.73 23.58,23.2C23.58,25.67 21.62,27.68 19.2,27.68C16.77,27.68 14.81,25.67 14.81,23.2Z\"\n      android:fillColor=\"#ffffff\"/>\n  <path\n      android:pathData=\"M80.02,27.06V47.66H72.03V27.67C72.03,23.45 70.3,21.32 66.83,21.32C63,21.32 61.07,23.87 61.07,28.87V39.82H53.14V28.87C53.14,23.84 51.24,21.32 47.38,21.32C43.92,21.32 42.18,23.45 42.18,27.67V47.65H34.21V27.06C34.21,22.86 35.25,19.51 37.35,17.03C39.53,14.54 42.37,13.29 45.89,13.29C49.97,13.29 53.07,14.9 55.11,18.11L57.11,21.52L59.1,18.11C61.14,14.91 64.23,13.29 68.32,13.29C71.84,13.29 74.69,14.55 76.86,17.03C78.96,19.51 80.01,22.83 80.01,27.06H80.02ZM107.49,37.3C109.15,35.51 109.93,33.29 109.93,30.59C109.93,27.89 109.14,25.65 107.49,23.94C105.91,22.15 103.89,21.3 101.45,21.3C99.02,21.3 97.01,22.15 95.41,23.94C93.83,25.65 93.04,27.89 93.04,30.59C93.04,33.29 93.83,35.53 95.41,37.3C97,39 99.02,39.87 101.45,39.87C103.89,39.87 105.9,39.02 107.49,37.3ZM109.93,14.12H117.8V47.06H109.93V43.18C107.55,46.41 104.26,48 99.99,48C95.71,48 92.42,46.36 89.5,43C86.64,39.64 85.18,35.48 85.18,30.61C85.18,25.74 86.65,21.65 89.5,18.29C92.43,14.93 95.92,13.23 99.99,13.23C104.06,13.23 107.55,14.81 109.93,18.02V14.14V14.12ZM144.26,29.97C146.58,31.76 147.73,34.25 147.67,37.41C147.67,40.77 146.52,43.41 144.14,45.24C141.76,47.03 138.89,47.94 135.41,47.94C129.13,47.94 124.87,45.3 122.61,40.11L129.43,35.96C130.34,38.78 132.35,40.25 135.41,40.25C138.22,40.25 139.62,39.33 139.62,37.42C139.62,36.03 137.79,34.78 134.07,33.8C132.66,33.41 131.5,33.01 130.6,32.68C129.31,32.16 128.22,31.56 127.31,30.83C125.05,29.04 123.9,26.68 123.9,23.65C123.9,20.42 124.99,17.85 127.19,16C129.45,14.09 132.19,13.18 135.48,13.18C140.73,13.18 144.56,15.48 147.07,20.16L140.37,24.1C139.4,21.86 137.74,20.74 135.48,20.74C133.11,20.74 131.95,21.65 131.95,23.44C131.95,24.83 133.78,26.08 137.5,27.06C140.37,27.72 142.63,28.7 144.26,29.97H144.27H144.26ZM169.26,22.27H162.37V35.98C162.37,37.63 162.98,38.63 164.15,39.08C165,39.4 166.71,39.47 169.27,39.34V47.05C163.98,47.71 160.14,47.17 157.88,45.41C155.62,43.7 154.53,40.53 154.53,36V22.27H149.23V14.1H154.53V7.46L162.39,4.89V14.12H169.29V22.29H169.27L169.26,22.27ZM194.34,37.1C195.92,35.4 196.71,33.22 196.71,30.58C196.71,27.94 195.92,25.78 194.34,24.05C192.74,22.35 190.79,21.48 188.42,21.48C186.04,21.48 184.09,22.33 182.49,24.05C180.97,25.84 180.18,28 180.18,30.58C180.18,33.16 180.97,35.31 182.49,37.1C184.08,38.81 186.04,39.67 188.42,39.67C190.79,39.67 192.74,38.82 194.34,37.1ZM176.96,42.96C173.85,39.6 172.32,35.52 172.32,30.58C172.32,25.63 173.85,21.62 176.96,18.26C180.07,14.9 183.91,13.19 188.42,13.19C192.92,13.19 196.77,14.9 199.87,18.26C202.97,21.62 204.57,25.77 204.57,30.58C204.57,35.39 202.97,39.6 199.87,42.96C196.76,46.32 192.98,47.96 188.42,47.96C183.85,47.96 180.06,46.32 176.96,42.96ZM230.86,37.29C232.45,35.5 233.24,33.28 233.24,30.58C233.24,27.87 232.45,25.63 230.86,23.93C229.28,22.14 227.26,21.29 224.82,21.29C222.39,21.29 220.37,22.14 218.73,23.93C217.14,25.63 216.35,27.87 216.35,30.58C216.35,33.28 217.14,35.52 218.73,37.29C220.38,38.99 222.45,39.86 224.82,39.86C227.2,39.86 229.27,39 230.86,37.29ZM233.24,0.92H241.11V47.05H233.24V43.17C230.93,46.39 227.63,47.99 223.36,47.99C219.09,47.99 215.75,46.35 212.8,42.98C209.93,39.62 208.48,35.47 208.48,30.6C208.48,25.73 209.95,21.64 212.8,18.28C215.72,14.92 219.26,13.22 223.36,13.22C227.45,13.22 230.93,14.8 233.24,18.01V0.93V0.92ZM268.74,37.07C270.32,35.36 271.12,33.18 271.12,30.54C271.12,27.9 270.32,25.74 268.74,24.01C267.15,22.31 265.21,21.45 262.82,21.45C260.43,21.45 258.5,22.3 256.9,24.01C255.37,25.8 254.58,27.96 254.58,30.54C254.58,33.12 255.37,35.28 256.9,37.07C258.48,38.77 260.44,39.64 262.82,39.64C265.2,39.64 267.14,38.78 268.74,37.07ZM251.36,42.92C248.26,39.56 246.73,35.48 246.73,30.54C246.73,25.6 248.25,21.58 251.36,18.22C254.47,14.86 258.32,13.15 262.82,13.15C267.32,13.15 271.18,14.86 274.27,18.22C277.38,21.58 278.97,25.73 278.97,30.54C278.97,35.35 277.38,39.56 274.27,42.92C271.16,46.28 267.38,47.93 262.82,47.93C258.26,47.93 254.46,46.28 251.36,42.92ZM313,26.78V47.01H305.14V27.84C305.14,25.66 304.59,24.01 303.48,22.77C302.45,21.65 300.98,21.07 299.09,21.07C294.65,21.07 292.39,23.77 292.39,29.24V47.03H284.53V14.1H292.39V17.81C294.28,14.71 297.28,13.19 301.47,13.19C304.82,13.19 307.57,14.37 309.71,16.81C311.91,19.24 313,22.54 313,26.82\"\n      android:fillColor=\"#ffffff\"/>\n</vector>\n"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/CommonModule.kt",
    "content": "package com.zhangke.fread.common\n\nimport com.zhangke.framework.architect.coroutines.ApplicationScope\nimport com.zhangke.framework.module.ModuleStartup\nimport com.zhangke.framework.nav.NavEntryProvider\nimport com.zhangke.fread.common.account.ActiveAccountsSynchronizer\nimport com.zhangke.fread.common.adapter.StatusUiStateAdapter\nimport com.zhangke.fread.common.browser.BrowserLauncher\nimport com.zhangke.fread.common.browser.UrlRedirectViewModel\nimport com.zhangke.fread.common.bubble.BubbleManager\nimport com.zhangke.fread.common.config.FreadConfigManager\nimport com.zhangke.fread.common.config.LocalConfigManager\nimport com.zhangke.fread.common.content.FreadContentDbMigrateManager\nimport com.zhangke.fread.common.content.FreadContentRepo\nimport com.zhangke.fread.common.daynight.DayNightHelper\nimport com.zhangke.fread.common.deeplink.SelectAccountForPublishViewModel\nimport com.zhangke.fread.common.deeplink.SelectedContentSwitcher\nimport com.zhangke.fread.common.di.ApplicationCoroutineScope\nimport com.zhangke.fread.common.mixed.MixedStatusRepo\nimport com.zhangke.fread.common.onboarding.OnboardingComponent\nimport com.zhangke.fread.common.publish.PublishPostManager\nimport com.zhangke.fread.common.repo.LinkPreviewCardRepo\nimport com.zhangke.fread.common.review.FreadReviewManager\nimport com.zhangke.fread.common.startup.FeedsRepoModuleStartup\nimport com.zhangke.fread.common.startup.FreadConfigModuleStartup\nimport com.zhangke.fread.common.startup.StartupManager\nimport com.zhangke.fread.common.status.StatusIdGenerator\nimport com.zhangke.fread.common.status.StatusUpdater\nimport com.zhangke.fread.common.status.adapter.ContentConfigAdapter\nimport com.zhangke.fread.common.status.usecase.FormatStatusDisplayTimeUseCase\nimport com.zhangke.fread.common.update.AppUpdateManager\nimport org.koin.core.module.Module\nimport org.koin.core.module.dsl.factoryOf\nimport org.koin.core.module.dsl.singleOf\nimport org.koin.core.module.dsl.viewModel\nimport org.koin.core.module.dsl.viewModelOf\nimport org.koin.dsl.bind\nimport org.koin.dsl.module\n\nval commonModule = module {\n    createPlatformModule()\n    factoryOf(::CommonNavEntryProvider) bind NavEntryProvider::class\n    factory<ApplicationCoroutineScope> { ApplicationScope }\n    singleOf(::ActiveAccountsSynchronizer)\n    singleOf(::BubbleManager)\n    singleOf(::DayNightHelper)\n    singleOf(::FreadConfigManager)\n    singleOf(::FreadReviewManager)\n    singleOf(::LocalConfigManager)\n    singleOf(::OnboardingComponent)\n    singleOf(::PublishPostManager)\n    singleOf(::SelectedContentSwitcher)\n    singleOf(::StartupManager)\n    singleOf(::StatusUpdater)\n\n    factoryOf(::LinkPreviewCardRepo)\n    factoryOf(::AppUpdateManager)\n    factoryOf(::BrowserLauncher)\n    factoryOf(::ContentConfigAdapter)\n    factoryOf(::FreadContentDbMigrateManager)\n    factoryOf(::FreadContentRepo)\n    factoryOf(::FormatStatusDisplayTimeUseCase)\n    factoryOf(::MixedStatusRepo)\n    factoryOf(::StatusIdGenerator)\n    factoryOf(::StatusUiStateAdapter)\n\n    factoryOf(::CommonStartup) bind ModuleStartup::class\n    factoryOf(::FreadConfigModuleStartup) bind ModuleStartup::class\n    factoryOf(::FeedsRepoModuleStartup) bind ModuleStartup::class\n\n    viewModelOf(::SelectAccountForPublishViewModel)\n    viewModel { params ->\n        UrlRedirectViewModel(\n            browserInterceptorSet = getAll(),\n            browserLauncher = get(),\n            statusProvider = get(),\n            selectedContentSwitcher = get(),\n            uri = params.get(),\n            locator = params.getOrNull(),\n            isFromExternal = params.get(),\n        )\n    }\n}\n\nexpect fun Module.createPlatformModule()\n"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/CommonNavEntryProvider.kt",
    "content": "package com.zhangke.fread.common\n\nimport androidx.navigation3.runtime.EntryProviderScope\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.nav.dialogMetadata\nimport com.zhangke.framework.nav.NavEntryProvider\nimport com.zhangke.fread.common.browser.UrlRedirectScreen\nimport com.zhangke.fread.common.browser.UrlRedirectScreenKey\nimport com.zhangke.fread.common.deeplink.SelectAccountForPublishScreen\nimport com.zhangke.fread.common.deeplink.SelectAccountForPublishScreenKey\nimport kotlinx.serialization.modules.PolymorphicModuleBuilder\nimport kotlinx.serialization.modules.subclass\nimport org.koin.compose.viewmodel.koinViewModel\nimport org.koin.core.parameter.parametersOf\n\nclass CommonNavEntryProvider : NavEntryProvider {\n\n    override fun EntryProviderScope<NavKey>.build() {\n        entry<SelectAccountForPublishScreenKey>(\n            metadata = dialogMetadata(),\n        ) {\n            SelectAccountForPublishScreen(koinViewModel { parametersOf(it.text) })\n        }\n        entry<UrlRedirectScreenKey>(\n            metadata = dialogMetadata(),\n        ) {\n            UrlRedirectScreen(\n                uri = it.uri,\n                viewModel = koinViewModel {\n                    parametersOf(it.uri, it.locator, it.isFromExternal)\n                },\n            )\n        }\n    }\n\n    override fun PolymorphicModuleBuilder<NavKey>.polymorph() {\n        subclass(SelectAccountForPublishScreenKey::class)\n        subclass(UrlRedirectScreenKey::class)\n    }\n}\n"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/CommonStartup.kt",
    "content": "package com.zhangke.fread.common\n\nimport com.zhangke.framework.module.ModuleStartup\nimport com.zhangke.fread.common.account.ActiveAccountsSynchronizer\nimport com.zhangke.fread.common.content.FreadContentDbMigrateManager\nimport com.zhangke.fread.common.language.ActivityLanguageHelper\n\nclass CommonStartup (\n    private val freadContentDbMigrateManager: FreadContentDbMigrateManager,\n    private val activeAccountsSynchronizer: ActiveAccountsSynchronizer,\n    private val activityLanguageHelper: ActivityLanguageHelper,\n) : ModuleStartup {\n\n    override fun onAppCreate() {\n        freadContentDbMigrateManager.migrateOldDb()\n        activeAccountsSynchronizer.initialize()\n        activityLanguageHelper.initialize()\n    }\n}"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/MixedContentJsonBuilder.kt",
    "content": "package com.zhangke.fread.common\n\nimport com.zhangke.framework.architect.json.JsonModuleBuilder\nimport com.zhangke.fread.status.content.MixedContent\nimport com.zhangke.fread.status.model.FreadContent\nimport com.zhangke.krouter.annotation.Service\nimport kotlinx.serialization.modules.SerializersModuleBuilder\nimport kotlinx.serialization.serializer\n\n@Service\nclass MixedContentJsonBuilder : JsonModuleBuilder {\n\n    override fun SerializersModuleBuilder.buildSerializersModule() {\n        polymorphic(\n            baseClass = FreadContent::class,\n            actualClass = MixedContent::class,\n            actualSerializer = serializer(),\n        )\n    }\n}\n"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/account/ActiveAccountsSynchronizer.kt",
    "content": "package com.zhangke.fread.common.account\n\nimport com.zhangke.framework.architect.coroutines.ApplicationScope\nimport com.zhangke.fread.common.config.FreadConfigManager\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.launch\n\nclass ActiveAccountsSynchronizer (\n    private val freadConfigManager: FreadConfigManager,\n) {\n\n    private val _activeAccountUriFlow = MutableStateFlow<String?>(null)\n    val activeAccountUriFlow: StateFlow<String?> = _activeAccountUriFlow\n\n    fun initialize() {\n        ApplicationScope.launch {\n            freadConfigManager.getLastSelectedAccount()?.let {\n                _activeAccountUriFlow.value = it\n            }\n        }\n    }\n\n    suspend fun onAccountSelected(accountUri: String) {\n        _activeAccountUriFlow.value = accountUri\n        freadConfigManager.updateLastSelectedAccount(accountUri)\n    }\n}"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/action/ComposableActions.kt",
    "content": "package com.zhangke.fread.common.action\n\nimport androidx.compose.runtime.staticCompositionLocalOf\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.SharedFlow\n\nval LocalComposableActions = staticCompositionLocalOf { ComposableActions }\n\nobject ComposableActions {\n\n    private val _actionFlow = MutableSharedFlow<String>(replay = 1)\n    val actionFlow: SharedFlow<String> get() = _actionFlow\n\n    suspend fun post(uri: String) {\n        _actionFlow.emit(uri)\n    }\n\n    fun resetReplayCache() {\n        _actionFlow.resetReplayCache()\n    }\n}\n"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/action/RouteAction.kt",
    "content": "package com.zhangke.fread.common.action\n\ninterface RouteAction {\n\n    fun execute(): Boolean\n}\n"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/action/RouteActions.kt",
    "content": "package com.zhangke.fread.common.action\n\nobject RouteActions {\n\n    const val ACTION_KEY = \"route_action\"\n\n    const val BASE_URI = \"fread://fread.xyz/action\"\n\n}\n\nobject OpenNotificationPageAction {\n\n    const val URI = \"${RouteActions.BASE_URI}/open_notification_page\"\n\n    fun buildOpenNotificationPageRoute(): String {\n        return buildString {\n            append(URI)\n        }\n    }\n}\n"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/adapter/StatusUiStateAdapter.kt",
    "content": "package com.zhangke.fread.common.adapter\n\nimport com.zhangke.fread.status.model.BlogTranslationUiState\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.model.StatusUiState\nimport com.zhangke.fread.status.status.model.Status\n\nclass StatusUiStateAdapter () {\n\n    fun toStatusUiState(\n        statusUiStatus: StatusUiState,\n        status: Status,\n        blogTranslationState: BlogTranslationUiState? = null,\n    ): StatusUiState {\n        return StatusUiState(\n            status = status,\n            locator = statusUiStatus.locator,\n            logged = statusUiStatus.logged,\n            isOwner = statusUiStatus.isOwner,\n            blogTranslationState = blogTranslationState ?: BlogTranslationUiState(\n                support = status.intrinsicBlog.supportTranslate,\n                translating = false,\n                showingTranslation = false,\n                blogTranslation = null,\n            ),\n        )\n    }\n\n    fun toStatusUiStateSnapshot(\n        locator: PlatformLocator,\n        status: Status,\n        blogTranslationState: BlogTranslationUiState? = null,\n    ): StatusUiState {\n        return StatusUiState(\n            status = status,\n            locator = locator,\n            logged = false,\n            isOwner = false,\n            blogTranslationState = blogTranslationState ?: BlogTranslationUiState(\n                support = status.intrinsicBlog.supportTranslate,\n                translating = false,\n                showingTranslation = false,\n                blogTranslation = null,\n            ),\n        )\n    }\n}"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/browser/BrowserInterceptor.kt",
    "content": "package com.zhangke.fread.common.browser\n\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.fread.status.model.FreadContent\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.model.StatusProviderProtocol\n\ninterface BrowserInterceptor {\n\n    suspend fun intercept(\n        locator: PlatformLocator?,\n        url: String,\n        isFromExternal: Boolean,\n    ): InterceptorResult\n}\n\nsealed interface InterceptorResult {\n\n    data object CanNotIntercept : InterceptorResult\n\n    data class SuccessWithOpenNewScreen(val screen: NavKey) : InterceptorResult\n\n    data class SwitchHomeContent(val content: FreadContent) : InterceptorResult\n\n    data class RequireSelectAccount(val protocol: StatusProviderProtocol) : InterceptorResult\n}\n"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/browser/BrowserLauncher.kt",
    "content": "package com.zhangke.fread.common.browser\n\nimport androidx.compose.runtime.staticCompositionLocalOf\nimport com.zhangke.framework.utils.PlatformUri\nimport com.zhangke.framework.utils.toPlatformUri\nimport com.zhangke.fread.common.config.AppCommonConfig\nimport com.zhangke.fread.common.config.FreadConfigManager\nimport com.zhangke.fread.common.utils.GlobalScreenNavigation\nimport com.zhangke.fread.status.model.PlatformLocator\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.launch\n\nclass BrowserLauncher(\n    private val systemBrowserLauncher: SystemBrowserLauncher,\n    private val freadConfigManager: FreadConfigManager,\n) {\n\n    fun launchBySystemBrowser(url: String) {\n        launchBySystemBrowser(url.toPlatformUri())\n    }\n\n    fun launchBySystemBrowser(uri: PlatformUri) {\n        systemBrowserLauncher.launchBySystemBrowser(uri)\n    }\n\n    suspend fun launchWebTabInApp(\n        url: String,\n        locator: PlatformLocator? = null,\n        checkAppSupportPage: Boolean = true,\n        isFromExternal: Boolean = false,\n    ) {\n        launchWebTabInApp(\n            uri = url.toPlatformUri(),\n            locator = locator,\n            checkAppSupportPage = checkAppSupportPage,\n            isFromExternal = isFromExternal,\n        )\n    }\n\n    suspend fun launchWebTabInApp(\n        uri: PlatformUri,\n        locator: PlatformLocator? = null,\n        checkAppSupportPage: Boolean = true,\n        isFromExternal: Boolean = false,\n    ) {\n        if (checkAppSupportPage) {\n            GlobalScreenNavigation.navigate(\n                screen = UrlRedirectScreenKey(\n                    uri = uri.toString(),\n                    locator = locator,\n                    isFromExternal = isFromExternal,\n                ),\n            )\n        } else {\n            if (freadConfigManager.openUrlInAppBrowser) {\n                systemBrowserLauncher.launchWebTabInApp(uri)\n            } else {\n                systemBrowserLauncher.launchBySystemBrowser(uri)\n            }\n        }\n    }\n\n    suspend fun launchFreadLandingPage() {\n        launchWebTabInApp(AppCommonConfig.WEBSITE)\n    }\n\n    suspend fun launchAuthorWebsite() {\n        launchWebTabInApp(AppCommonConfig.AUTHOR_WEBSITE)\n    }\n}\n\nval LocalActivityBrowserLauncher = staticCompositionLocalOf<BrowserLauncher> {\n    error(\"No ActivityBrowserLauncher provided\")\n}\n\nfun BrowserLauncher.launchWebTabInApp(\n    scope: CoroutineScope,\n    url: PlatformUri,\n    locator: PlatformLocator? = null,\n    checkAppSupportPage: Boolean = true,\n) {\n    scope.launch { this@launchWebTabInApp.launchWebTabInApp(url, locator, checkAppSupportPage) }\n}\n\nfun BrowserLauncher.launchWebTabInApp(\n    scope: CoroutineScope,\n    url: String,\n    locator: PlatformLocator? = null,\n    checkAppSupportPage: Boolean = true,\n) {\n    scope.launch { this@launchWebTabInApp.launchWebTabInApp(url, locator, checkAppSupportPage) }\n}"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/browser/OAuthHandler.kt",
    "content": "package com.zhangke.fread.common.browser\n\nexpect class OAuthHandler {\n    suspend fun startOAuth(url: String): String\n}\n"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/browser/SystemBrowserLauncher.kt",
    "content": "package com.zhangke.fread.common.browser\n\nimport com.zhangke.framework.utils.PlatformUri\n\ninterface SystemBrowserLauncher {\n\n    fun launchBySystemBrowser(uri: PlatformUri)\n\n    fun launchWebTabInApp(uri: PlatformUri)\n}\n"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/browser/UrlRedirectScreen.kt",
    "content": "package com.zhangke.fread.common.browser\n\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material3.CircularProgressIndicator\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.dp\nimport androidx.lifecycle.ViewModel\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.composable.ConsumeFlow\nimport com.zhangke.framework.composable.currentOrThrow\nimport com.zhangke.framework.ktx.launchInViewModel\nimport com.zhangke.framework.nav.LocalNavBackStack\nimport com.zhangke.framework.nav.popIfNotRoot\nimport com.zhangke.fread.common.composable.SelectableAccount\nimport com.zhangke.fread.common.deeplink.SelectAccountForPublishScreenKey\nimport com.zhangke.fread.common.deeplink.SelectedContentSwitcher\nimport com.zhangke.fread.common.utils.GlobalScreenNavigation\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.StatusProvider\nimport com.zhangke.fread.status.account.LoggedAccount\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.model.StatusProviderProtocol\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asSharedFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.serialization.Serializable\nimport org.jetbrains.compose.resources.stringResource\n\n@Serializable\ndata class UrlRedirectScreenKey(\n    val uri: String,\n    val locator: PlatformLocator? = null,\n    val isFromExternal: Boolean = false,\n) : NavKey\n\n@Composable\nfun UrlRedirectScreen(uri: String, viewModel: UrlRedirectViewModel) {\n    val backStack = LocalNavBackStack.currentOrThrow\n    val browserLauncher = LocalActivityBrowserLauncher.current\n    ConsumeFlow(viewModel.finishPageFlow) { backStack.popIfNotRoot() }\n    ConsumeFlow(viewModel.openNewPageFlow) { GlobalScreenNavigation.navigate(it) }\n    ConsumeFlow(viewModel.finishAndOpenUrlTab) {\n        browserLauncher.launchWebTabInApp(\n            url = uri,\n            locator = null,\n            checkAppSupportPage = false,\n        )\n        backStack.popIfNotRoot()\n    }\n    ConsumeFlow(viewModel.finishAndOpenPublishScreen) {\n        backStack.popIfNotRoot()\n        GlobalScreenNavigation.navigate(SelectAccountForPublishScreenKey(uri))\n    }\n    val pageState by viewModel.pageState.collectAsState()\n    Box(\n        modifier = Modifier,\n        contentAlignment = Alignment.Center,\n    ) {\n        when (pageState) {\n            is UrlRedirectPageState.Loading -> {\n                Surface(\n                    modifier = Modifier,\n                    shape = RoundedCornerShape(16.dp),\n                ) {\n                    CircularProgressIndicator(\n                        modifier = Modifier\n                            .padding(vertical = 24.dp, horizontal = 64.dp)\n                            .size(80.dp)\n                    )\n                }\n            }\n\n            is UrlRedirectPageState.SelectAccount -> {\n                val accountList = (pageState as UrlRedirectPageState.SelectAccount).accounts\n                Surface(\n                    modifier = Modifier.padding(horizontal = 16.dp),\n                    shape = RoundedCornerShape(16.dp),\n                    shadowElevation = 2.dp,\n                ) {\n                    Column(\n                        modifier = Modifier.fillMaxWidth()\n                            .padding(16.dp)\n                            .verticalScroll(rememberScrollState()),\n                    ) {\n                        Text(\n                            modifier = Modifier,\n                            text = stringResource(LocalizedString.statusUiSwitchAccountDialogTitle),\n                            style = MaterialTheme.typography.titleMedium\n                                .copy(fontWeight = FontWeight.SemiBold),\n                        )\n                        for (account in accountList) {\n                            Spacer(modifier = Modifier.height(16.dp))\n                            SelectableAccount(\n                                account = account,\n                                onClick = { viewModel.onAccountSelected(account) },\n                            )\n                        }\n                    }\n                }\n            }\n        }\n    }\n    LaunchedEffect(Unit) { viewModel.onPageResumed() }\n}\n\nsealed interface UrlRedirectPageState {\n\n    data object Loading : UrlRedirectPageState\n\n    data class SelectAccount(val accounts: List<LoggedAccount>) : UrlRedirectPageState\n}\n\nclass UrlRedirectViewModel(\n    private val browserInterceptorSet: List<BrowserInterceptor>,\n    val browserLauncher: BrowserLauncher,\n    private val statusProvider: StatusProvider,\n    private val selectedContentSwitcher: SelectedContentSwitcher,\n    private val uri: String,\n    private val locator: PlatformLocator?,\n    private val isFromExternal: Boolean,\n) : ViewModel() {\n\n    private val _openNewPageFlow = MutableSharedFlow<NavKey>()\n    val openNewPageFlow = _openNewPageFlow.asSharedFlow()\n\n    private val _finishPageFlow = MutableSharedFlow<Unit>()\n    val finishPageFlow = _finishPageFlow.asSharedFlow()\n\n    private val _finishAndOpenUrlTab = MutableSharedFlow<String>()\n    val finishAndOpenUrlTab = _finishAndOpenUrlTab.asSharedFlow()\n\n    private val _finishAndOpenPublishScreen = MutableSharedFlow<String>()\n    val finishAndOpenPublishScreen = _finishAndOpenPublishScreen.asSharedFlow()\n\n    private val _pageState = MutableStateFlow<UrlRedirectPageState>(UrlRedirectPageState.Loading)\n    val pageState = _pageState.asStateFlow()\n\n    private var accountSelectCount = 0\n\n    fun onPageResumed() {\n        parseUrl(locator)\n    }\n\n    fun onAccountSelected(account: LoggedAccount) {\n        parseUrl(account.locator)\n    }\n\n    private fun parseUrl(locator: PlatformLocator?) {\n        launchInViewModel {\n            _pageState.emit(UrlRedirectPageState.Loading)\n            val result = browserInterceptorSet.firstNotNullOfOrNull { interceptor ->\n                interceptor.intercept(\n                    locator = locator,\n                    url = uri,\n                    isFromExternal = isFromExternal,\n                ).takeIf { it !is InterceptorResult.CanNotIntercept }\n            }\n            if (result == null) {\n                if (isFromExternal) {\n                    _finishAndOpenPublishScreen.emit(uri)\n                } else {\n                    _finishAndOpenUrlTab.emit(uri)\n                }\n            } else {\n                when (result) {\n                    is InterceptorResult.SuccessWithOpenNewScreen -> {\n                        _finishPageFlow.emit(Unit)\n                        _openNewPageFlow.emit(result.screen)\n                    }\n\n                    is InterceptorResult.SwitchHomeContent -> {\n                        selectedContentSwitcher.switchToContent(result.content)\n                        _finishPageFlow.emit(Unit)\n                    }\n\n                    is InterceptorResult.RequireSelectAccount -> {\n                        if (accountSelectCount > 1) {\n                            _finishPageFlow.emit(Unit)\n                        } else {\n                            accountSelectCount++\n                            val accounts = loadLoggedAccounts(result.protocol)\n                            if (accounts.isEmpty()) {\n                                _finishPageFlow.emit(Unit)\n                            } else {\n                                _pageState.emit(UrlRedirectPageState.SelectAccount(accounts))\n                            }\n                        }\n                    }\n\n                    else -> {\n                        _finishPageFlow.emit(Unit)\n                    }\n                }\n            }\n        }\n    }\n\n    private suspend fun loadLoggedAccounts(protocol: StatusProviderProtocol): List<LoggedAccount> {\n        return statusProvider.accountManager\n            .getAllLoggedAccount()\n            .filter { it.platform.protocol == protocol }\n    }\n}\n"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/bubble/Bubble.kt",
    "content": "package com.zhangke.fread.common.bubble\n\nimport androidx.compose.foundation.layout.ColumnScope\nimport androidx.compose.runtime.Composable\n\ninterface Bubble {\n\n    @Composable\n    fun ColumnScope.Content()\n}\n"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/bubble/BubbleManager.kt",
    "content": "package com.zhangke.fread.common.bubble\n\nimport androidx.compose.runtime.staticCompositionLocalOf\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asStateFlow\n\nclass BubbleManager () {\n\n    private val _bubbleListFlow = MutableStateFlow<List<Bubble>>(emptyList())\n    val bubbleListFlow get() = _bubbleListFlow.asStateFlow()\n\n    suspend fun addBubble(bubble: Bubble) {\n        _bubbleListFlow.emit(_bubbleListFlow.value + bubble)\n    }\n}\n\nval LocalBubbleManager = staticCompositionLocalOf<BubbleManager> {\n    error(\"No BubbleManager provided\")\n}"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/composable/EmptyContent.kt",
    "content": "package com.zhangke.fread.common.composable\n\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material3.Button\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.unit.dp\nimport com.zhangke.fread.commonbiz.Res\nimport com.zhangke.fread.commonbiz.img_empty_account\nimport com.zhangke.fread.commonbiz.img_empty_content\nimport com.zhangke.fread.commonbiz.img_empty_message\nimport com.zhangke.fread.localization.LocalizedString\nimport org.jetbrains.compose.resources.DrawableResource\nimport org.jetbrains.compose.resources.painterResource\nimport org.jetbrains.compose.resources.stringResource\n\nenum class EmptyContentType {\n\n    Content,\n    Message,\n    Account;\n\n    fun getDrawableResource(): DrawableResource {\n        return when (this) {\n            Content -> Res.drawable.img_empty_content\n            Message -> Res.drawable.img_empty_message\n            Account -> Res.drawable.img_empty_account\n        }\n    }\n}\n\n@Composable\nfun EmptyContent(\n    modifier: Modifier,\n    type: EmptyContentType = EmptyContentType.Content,\n    contentTitle: String = stringResource(LocalizedString.emptyContentHintTitle),\n    subtitle: String? = stringResource(LocalizedString.emptyContentHintDesc),\n    onClick: (() -> Unit)? = null,\n) {\n    Column(\n        modifier = modifier.verticalScroll(rememberScrollState()),\n        horizontalAlignment = Alignment.CenterHorizontally,\n        verticalArrangement = Arrangement.Center,\n    ) {\n        Column(\n            modifier = Modifier.fillMaxWidth(),\n            verticalArrangement = Arrangement.Center,\n            horizontalAlignment = Alignment.CenterHorizontally,\n        ) {\n            Image(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .padding(horizontal = 16.dp),\n                contentScale = ContentScale.Inside,\n                painter = painterResource(type.getDrawableResource()),\n                contentDescription = null,\n            )\n            Text(\n                modifier = Modifier.padding(start = 16.dp, top = 16.dp, end = 16.dp),\n                text = contentTitle,\n                textAlign = TextAlign.Center,\n                style = MaterialTheme.typography.bodyMedium,\n            )\n            if (!subtitle.isNullOrEmpty()) {\n                Text(\n                    modifier = Modifier.padding(start = 32.dp, top = 8.dp, end = 32.dp),\n                    text = subtitle,\n                    style = MaterialTheme.typography.labelMedium,\n                    color = MaterialTheme.colorScheme.onSurfaceVariant,\n                    maxLines = 2,\n                    textAlign = TextAlign.Center,\n                )\n            }\n        }\n        if (onClick != null) {\n            Button(\n                modifier = Modifier.padding(top = 32.dp),\n                onClick = onClick,\n            ) {\n                Text(text = stringResource(LocalizedString.feedsAddContent))\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/composable/ErrorContent.kt",
    "content": "package com.zhangke.fread.common.composable\n\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material3.Button\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.unit.dp\nimport com.zhangke.fread.commonbiz.Res\nimport com.zhangke.fread.commonbiz.img_error_state\nimport com.zhangke.fread.localization.LocalizedString\nimport org.jetbrains.compose.resources.painterResource\nimport org.jetbrains.compose.resources.stringResource\n\nenum class ErrorType {\n\n    Unknown,\n    Network,\n    NotFound,\n}\n\n@Composable\nfun ErrorContent(\n    modifier: Modifier,\n    errorMessage: String?,\n    onRetryClick: () -> Unit,\n    type: ErrorType = ErrorType.Unknown,\n) {\n    Column(\n        modifier = modifier.verticalScroll(rememberScrollState()),\n        horizontalAlignment = Alignment.CenterHorizontally,\n        verticalArrangement = Arrangement.Center,\n    ) {\n\n        Image(\n            modifier = Modifier\n                .fillMaxWidth()\n                .padding(horizontal = 16.dp),\n            contentScale = ContentScale.Inside,\n            painter = painterResource(Res.drawable.img_error_state),\n            contentDescription = null,\n        )\n\n        Text(\n            modifier = Modifier.padding(start = 16.dp, top = 16.dp, end = 16.dp),\n            text = stringResource(\n                when (type) {\n                    ErrorType.Unknown -> LocalizedString.unknownError\n                    ErrorType.Network -> LocalizedString.networkError\n                    ErrorType.NotFound -> LocalizedString.resourceNotFound\n                }\n            ),\n            textAlign = TextAlign.Center,\n            style = MaterialTheme.typography.bodyMedium,\n        )\n\n        if (!errorMessage.isNullOrEmpty()) {\n            Text(\n                modifier = Modifier.padding(start = 32.dp, top = 8.dp, end = 32.dp),\n                text = errorMessage,\n                color = MaterialTheme.colorScheme.onSurfaceVariant,\n                style = MaterialTheme.typography.labelMedium,\n                maxLines = 2,\n                textAlign = TextAlign.Center,\n            )\n        }\n\n        Spacer(modifier = Modifier.height(16.dp))\n\n        Button(onClick = onRetryClick) {\n            Text(text = stringResource(LocalizedString.retry))\n        }\n    }\n}\n"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/composable/SelectableAccount.kt",
    "content": "package com.zhangke.fread.common.composable\n\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport com.seiko.imageloader.model.ImageAction\nimport com.seiko.imageloader.model.ImageRequest\nimport com.seiko.imageloader.rememberImageActionPainter\nimport com.seiko.imageloader.ui.AutoSizeBox\nimport com.zhangke.framework.composable.freadPlaceholder\nimport com.zhangke.framework.composable.noRippleClick\nimport com.zhangke.fread.status.account.LoggedAccount\nimport com.zhangke.fread.status.richtext.RichText\n\n@Composable\ninternal fun SelectableAccount(\n    account: LoggedAccount,\n    onClick: (LoggedAccount) -> Unit,\n) {\n    Row(\n        modifier = Modifier.fillMaxWidth().noRippleClick { onClick(account) },\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        AutoSizeBox(\n            request = remember(account.avatar) {\n                ImageRequest(account.avatar.orEmpty())\n            },\n        ) { action ->\n            Image(\n                painter = rememberImageActionPainter(action),\n                contentDescription = \"Avatar\",\n                modifier = Modifier.size(42.dp)\n                    .clip(CircleShape)\n                    .freadPlaceholder(action !is ImageAction.Success)\n                    .clickable { onClick(account) },\n            )\n        }\n        Column(modifier = Modifier.padding(start = 16.dp).weight(1F)) {\n            Text(\n                text = account.humanizedName.document,\n                style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold),\n                maxLines = 1,\n                overflow = TextOverflow.Ellipsis,\n            )\n            Text(\n                text = account.prettyHandle,\n                style = MaterialTheme.typography.labelMedium,\n                color = MaterialTheme.colorScheme.onSurfaceVariant,\n            )\n        }\n    }\n}"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/config/AppCommonConfig.kt",
    "content": "package com.zhangke.fread.common.config\n\nobject AppCommonConfig {\n\n    const val APP_NAME = \"Fread\"\n\n    const val WEBSITE = \"https://fread.xyz\"\n\n    const val AUTHOR = \"Zhang Ke\"\n\n    const val AUTHOR_WEBSITE = \"https://zhangke.space\"\n\n    const val AUTHOR_EMAIL = \"kezhang404@gmail.com\"\n\n    const val PRIVACY_POLICY = \"https://fread.xyz/appprivacy.html\"\n\n    const val FEEDBACK_URL = \"https://github.com/0xZhangKe/Fread/issues/new\"\n\n    const val TELEGRAM_GROUP = \"https://t.me/+-SlbKcNbJSphNWI1\"\n\n    const val DONATE_KO_FI_LINK = \"https://ko-fi.com/zhangke\"\n\n    const val DONATE_AF_DIAN_LINK = \"https://afdian.com/a/_0cdc1\"\n\n    const val F_DROID_URI = \"https://f-droid.org/zh_Hans/packages/com.zhangke.fread\"\n}\n"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/config/FreadConfigManager.kt",
    "content": "package com.zhangke.fread.common.config\n\nimport androidx.compose.runtime.staticCompositionLocalOf\nimport com.zhangke.fread.common.theme.ThemeType\nimport com.zhangke.fread.common.utils.RandomIdGenerator\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.IO\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.withContext\n\nclass FreadConfigManager(\n    private val localConfigManager: LocalConfigManager,\n) {\n    companion object {\n        private const val LOCAL_KEY_AUTO_PLAY_INLINE_VIDEO = \"auto_play_inline_video\"\n        private const val LOCAL_KEY_STATUS_CONTENT_SIZE = \"fread_status_content_size\"\n        private const val LOCAL_KEY_STATUS_ALWAYS_SHOW_SENSITIVE =\n            \"fread_status_always_show_sensitive\"\n        private const val LOCAL_KEY_IMMERSIVE_NAV_BAR = \"immersiveNavBar\"\n        private const val LOCAL_KEY_HOME_TAB_NEXT_BUTTON_VISIBLE = \"home_tab_next_button_visible\"\n        private const val LOCAL_KEY_HOME_TAB_REFRESH_BUTTON_VISIBLE =\n            \"home_tab_refresh_button_visible\"\n        private const val LOCAL_KEY_DEVICE_ID = \"device_id\"\n        private const val LOCAL_KEY_IGNORE_UPDATE_VERSION = \"ignore_update_version\"\n        private const val LOCAL_KEY_BSKY_PUBLISH_LAN = \"bsky_publish_lan\"\n        private const val LOCAL_KEY_LAST_SELECTED_ACCOUNT = \"last_selected_account\"\n        private const val LOCAL_KEY_TIMELINE_DEFAULT_POSITION = \"timeline_default_position\"\n        private const val LOCAL_KEY_THEME_TYPE = \"theme_type\"\n        private const val LOCAL_KEY_OPEN_URL_IN_APP_BROWSER = \"open_url_in_app_browser\"\n        private const val LOCAL_KEY_ENABLE_BLUR_APP_BAR_STYLE = \"enable_blur_app_bar_style\"\n    }\n\n    private val _statusConfigFlow = MutableStateFlow(StatusConfig.default())\n    val statusConfigFlow get(): StateFlow<StatusConfig> = _statusConfigFlow\n\n    private val _themeTypeeFlow = MutableStateFlow(ThemeType.DEFAULT)\n    val themeTypeFlow get() = _themeTypeeFlow\n\n    private val _homeTabNextButtonVisibleFlow = MutableStateFlow(false)\n    val homeTabNextButtonVisibleFlow: StateFlow<Boolean> get() = _homeTabNextButtonVisibleFlow\n\n    private val _homeTabRefreshButtonVisibleFlow = MutableStateFlow(false)\n    val homeTabRefreshButtonVisibleFlow: StateFlow<Boolean> get() = _homeTabRefreshButtonVisibleFlow\n    private val _enableBlurAppBarStyleFlow = MutableStateFlow(true)\n    val enableBlurAppBarStyleFlow: StateFlow<Boolean> get() = _enableBlurAppBarStyleFlow\n\n    var autoPlayInlineVideo: Boolean = false\n        private set\n    var openUrlInAppBrowser: Boolean = true\n        private set\n\n    suspend fun initConfig() {\n        _statusConfigFlow.value = readLocalStatusConfig()\n        _themeTypeeFlow.value = getThemeType()\n        autoPlayInlineVideo =\n            localConfigManager.getBoolean(LOCAL_KEY_AUTO_PLAY_INLINE_VIDEO) ?: false\n        openUrlInAppBrowser =\n            localConfigManager.getBoolean(LOCAL_KEY_OPEN_URL_IN_APP_BROWSER) != false\n        _enableBlurAppBarStyleFlow.value =\n            localConfigManager.getBoolean(LOCAL_KEY_ENABLE_BLUR_APP_BAR_STYLE) != false\n        _homeTabNextButtonVisibleFlow.value =\n            localConfigManager.getBoolean(LOCAL_KEY_HOME_TAB_NEXT_BUTTON_VISIBLE) ?: false\n        _homeTabRefreshButtonVisibleFlow.value =\n            localConfigManager.getBoolean(LOCAL_KEY_HOME_TAB_REFRESH_BUTTON_VISIBLE) ?: false\n    }\n\n    private suspend fun readLocalStatusConfig(): StatusConfig {\n        val alwaysShowSensitiveContent =\n            localConfigManager.getBoolean(LOCAL_KEY_STATUS_ALWAYS_SHOW_SENSITIVE) == true\n        val contentSize = localConfigManager.getString(LOCAL_KEY_STATUS_CONTENT_SIZE)\n            ?.toContentSize()\n            ?: StatusContentSize.default()\n        val immersiveNavBar = localConfigManager.getBoolean(LOCAL_KEY_IMMERSIVE_NAV_BAR) != false\n        return StatusConfig(\n            alwaysShowSensitiveContent = alwaysShowSensitiveContent,\n            contentSize = contentSize,\n            immersiveNavBar = immersiveNavBar,\n        )\n    }\n\n    private fun String.toContentSize(): StatusContentSize? {\n        return runCatching { StatusContentSize.valueOf(this) }.getOrNull()\n    }\n\n    suspend fun updateAutoPlayInlineVideo(value: Boolean) {\n        autoPlayInlineVideo = value\n        withContext(Dispatchers.IO) {\n            localConfigManager.putBoolean(LOCAL_KEY_AUTO_PLAY_INLINE_VIDEO, value)\n        }\n    }\n\n    suspend fun updateOpenUrlInAppBrowser(value: Boolean) {\n        openUrlInAppBrowser = value\n        withContext(Dispatchers.IO) {\n            localConfigManager.putBoolean(LOCAL_KEY_OPEN_URL_IN_APP_BROWSER, value)\n        }\n    }\n\n    suspend fun updateEnableBlurAppBarStyle(value: Boolean) {\n        _enableBlurAppBarStyleFlow.value = value\n        withContext(Dispatchers.IO) {\n            localConfigManager.putBoolean(LOCAL_KEY_ENABLE_BLUR_APP_BAR_STYLE, value)\n        }\n    }\n\n    suspend fun updateStatusContentSize(contentSize: StatusContentSize) {\n        _statusConfigFlow.value = _statusConfigFlow.value.copy(contentSize = contentSize)\n        withContext(Dispatchers.IO) {\n            localConfigManager.putString(\n                LOCAL_KEY_STATUS_CONTENT_SIZE,\n                contentSize.name,\n            )\n        }\n    }\n\n    suspend fun updateAlwaysShowSensitiveContent(always: Boolean) {\n        _statusConfigFlow.value = _statusConfigFlow.value.copy(alwaysShowSensitiveContent = always)\n        withContext(Dispatchers.IO) {\n            localConfigManager.putBoolean(LOCAL_KEY_STATUS_ALWAYS_SHOW_SENSITIVE, always)\n        }\n    }\n\n    suspend fun updateImmersiveNavBar(immersive: Boolean) {\n        _statusConfigFlow.value = _statusConfigFlow.value.copy(immersiveNavBar = immersive)\n        withContext(Dispatchers.IO) {\n            localConfigManager.putBoolean(LOCAL_KEY_IMMERSIVE_NAV_BAR, immersive)\n        }\n    }\n\n    suspend fun updateHomeTabNextButtonVisible(visible: Boolean) {\n        _homeTabNextButtonVisibleFlow.value = visible\n        withContext(Dispatchers.IO) {\n            localConfigManager.putBoolean(LOCAL_KEY_HOME_TAB_NEXT_BUTTON_VISIBLE, visible)\n        }\n    }\n\n    suspend fun updateHomeTabRefreshButtonVisible(visible: Boolean) {\n        _homeTabRefreshButtonVisibleFlow.value = visible\n        withContext(Dispatchers.IO) {\n            localConfigManager.putBoolean(LOCAL_KEY_HOME_TAB_REFRESH_BUTTON_VISIBLE, visible)\n        }\n    }\n\n    suspend fun getDeviceId(): String {\n        return localConfigManager.getStringOrPut(LOCAL_KEY_DEVICE_ID) {\n            RandomIdGenerator().generateId()\n        }\n    }\n\n    suspend fun getIgnoreUpdateVersion(): Long? {\n        return localConfigManager.getLong(LOCAL_KEY_IGNORE_UPDATE_VERSION)\n    }\n\n    suspend fun updateIgnoreUpdateVersion(version: Long) {\n        withContext(Dispatchers.IO) {\n            localConfigManager.putLong(LOCAL_KEY_IGNORE_UPDATE_VERSION, version)\n        }\n    }\n\n    suspend fun getBskyPublishLanguage(): List<String> {\n        return localConfigManager.getString(LOCAL_KEY_BSKY_PUBLISH_LAN)?.split(\",\") ?: emptyList()\n    }\n\n    suspend fun updateBskyPublishLanguage(languages: List<String>) {\n        withContext(Dispatchers.IO) {\n            localConfigManager.putString(LOCAL_KEY_BSKY_PUBLISH_LAN, languages.joinToString(\",\"))\n        }\n    }\n\n    suspend fun getLastSelectedAccount(): String? {\n        return localConfigManager.getString(LOCAL_KEY_LAST_SELECTED_ACCOUNT)\n    }\n\n    suspend fun updateLastSelectedAccount(accountUri: String) {\n        localConfigManager.putString(LOCAL_KEY_LAST_SELECTED_ACCOUNT, accountUri)\n    }\n\n    suspend fun getTimelineDefaultPosition(): TimelineDefaultPosition {\n        return localConfigManager.getString(LOCAL_KEY_TIMELINE_DEFAULT_POSITION)\n            ?.let { TimelineDefaultPosition.valueOf(it) }\n            ?: TimelineDefaultPosition.NEWEST\n    }\n\n    suspend fun updateTimelineDefaultPosition(position: TimelineDefaultPosition) {\n        localConfigManager.putString(LOCAL_KEY_TIMELINE_DEFAULT_POSITION, position.name)\n    }\n\n    suspend fun getThemeType(): ThemeType {\n        return localConfigManager.getString(LOCAL_KEY_THEME_TYPE)\n            ?.let { ThemeType.valueOf(it) }\n            ?: ThemeType.DEFAULT\n    }\n\n    suspend fun updateThemeType(type: ThemeType) {\n        _themeTypeeFlow.value = type\n        localConfigManager.putString(LOCAL_KEY_THEME_TYPE, type.name)\n    }\n}\n\nval LocalFreadConfigManager =\n    staticCompositionLocalOf<FreadConfigManager> { error(\"No FreadConfigManager provided\") }\n"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/config/LocalConfigManager.kt",
    "content": "package com.zhangke.fread.common.config\n\nimport androidx.compose.runtime.staticCompositionLocalOf\nimport com.russhwolf.settings.coroutines.FlowSettings\n\nclass LocalConfigManager(private val configSettings: FlowSettings) {\n\n    suspend fun getString(key: String): String? {\n        return configSettings.getStringOrNull(key)\n    }\n\n    suspend fun getStringOrPut(key: String, block: () -> String): String {\n        val value = configSettings.getStringOrNull(key)\n        return value ?: block().also { configSettings.putString(key, it) }\n    }\n\n    suspend fun putString(key: String, value: String) {\n        configSettings.putString(key, value)\n    }\n\n    suspend fun getInt(key: String): Int? {\n        return configSettings.getIntOrNull(key)\n    }\n\n    suspend fun putInt(key: String, value: Int) {\n        configSettings.putInt(key, value)\n    }\n\n    suspend fun getLong(key: String): Long? {\n        return configSettings.getLongOrNull(key)\n    }\n\n    suspend fun putLong(key: String, value: Long) {\n        configSettings.putLong(key, value)\n    }\n\n    suspend fun getBoolean(key: String): Boolean? {\n        return configSettings.getBooleanOrNull(key)\n    }\n\n    suspend fun putBoolean(key: String, value: Boolean) {\n        configSettings.putBoolean(key, value)\n    }\n\n    suspend fun removeKey(key: String) {\n        configSettings.remove(key)\n    }\n}\n\nval LocalLocalConfigManager =\n    staticCompositionLocalOf<LocalConfigManager> { error(\"No LocalConfigManager provided\") }"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/config/StatusConfig.kt",
    "content": "package com.zhangke.fread.common.config\n\ndata class StatusConfig(\n    val alwaysShowSensitiveContent: Boolean,\n    val contentSize: StatusContentSize,\n    val immersiveNavBar: Boolean,\n) {\n\n    companion object {\n\n        fun default(\n            alwaysShowSensitiveContent: Boolean = false,\n            contentSize: StatusContentSize = StatusContentSize.default(),\n            immersiveNavBar: Boolean = true,\n        ) = StatusConfig(\n            alwaysShowSensitiveContent = alwaysShowSensitiveContent,\n            contentSize = contentSize,\n            immersiveNavBar = immersiveNavBar,\n        )\n    }\n}\n"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/config/StatusContentSize.kt",
    "content": "package com.zhangke.fread.common.config\n\nenum class StatusContentSize {\n\n    SMALL,\n    MEDIUM,\n    LARGE;\n\n    companion object {\n\n        fun default(): StatusContentSize = MEDIUM\n    }\n}\n"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/config/TimelineDefaultPosition.kt",
    "content": "package com.zhangke.fread.common.config\n\nenum class TimelineDefaultPosition {\n\n    NEWEST,\n    LAST_READ,\n}\n"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/content/FreadContentDbMigrateManager.kt",
    "content": "package com.zhangke.fread.common.content\n\nimport com.zhangke.framework.architect.coroutines.ApplicationScope\nimport com.zhangke.fread.common.db.ContentConfigDatabases\nimport com.zhangke.fread.common.status.adapter.ContentConfigAdapter\nimport com.zhangke.fread.status.StatusProvider\nimport com.zhangke.fread.status.content.MixedContent\nimport com.zhangke.fread.status.model.ContentConfig\nimport com.zhangke.fread.status.model.FreadContent\nimport kotlinx.coroutines.launch\n\nclass FreadContentDbMigrateManager (\n    private val contentRepo: FreadContentRepo,\n    private val statusProvider: StatusProvider,\n    private val contentConfigDatabases: ContentConfigDatabases,\n    private val contentConfigAdapter: ContentConfigAdapter,\n) {\n\n    internal fun migrateOldDb() {\n        ApplicationScope.launch {\n            migrateContentConfig()\n            migrateOldMixedContent()\n        }\n    }\n\n    private suspend fun migrateContentConfig() {\n        val contentConfigList = contentConfigDatabases.getContentConfigDao()\n            .queryAllContentConfig()\n        if (contentConfigList.isEmpty()) return\n        contentConfigList\n            .map(contentConfigAdapter::toContentConfig)\n            .mapNotNull { it.toContent() }\n            .let { contentRepo.insertAll(it) }\n        contentConfigDatabases.getContentConfigDao().deleteTable()\n    }\n\n    private fun ContentConfig.toContent(): FreadContent? {\n        if (this is ContentConfig.MixedContent) {\n            return MixedContent(\n                id = this.id.toString(),\n                order = this.order,\n                name = this.name,\n                sourceUriList = this.sourceUriList,\n            )\n        }\n        return statusProvider.contentManager.restoreContent(this)\n    }\n\n    private suspend fun migrateOldMixedContent() {\n        contentRepo.getAllOldContents()\n            .mapNotNull { it.second as? MixedContent }\n            .forEach { content ->\n                contentRepo.insertContent(content)\n                contentRepo.deleteOldContents(content.id)\n            }\n    }\n}"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/content/FreadContentRepo.kt",
    "content": "package com.zhangke.fread.common.content\n\nimport com.zhangke.fread.common.db.FreadContentDatabase\nimport com.zhangke.fread.common.db.FreadContentEntity\nimport com.zhangke.fread.common.db.old.OldFreadContentDatabase\nimport com.zhangke.fread.status.model.FreadContent\nimport kotlinx.coroutines.flow.map\nimport kotlinx.coroutines.flow.mapNotNull\n\nclass FreadContentRepo (\n    database: FreadContentDatabase,\n    private val oldContentDatabase: OldFreadContentDatabase,\n) {\n\n    private val dao = database.contentDao()\n\n    suspend fun getAllOldContents(): List<Pair<String, FreadContent>> {\n        return oldContentDatabase.contentDao().queryAll().map { it.id to it.content }\n    }\n\n    suspend fun deleteOldContents(id: String) {\n        oldContentDatabase.contentDao().delete(id)\n    }\n\n    fun getAllContentFlow() =\n        dao.queryAllFlow().map { list -> list.map { it.content }.sortedBy { it.order } }\n\n    fun getContentFlow(id: String) = dao.queryFlow(id).mapNotNull { it?.content }\n\n    suspend fun getAllContent(): List<FreadContent> {\n        return dao.queryAll().map { it.content }.sortedBy { it.order }\n    }\n\n    suspend fun getContent(id: String): FreadContent? {\n        return dao.query(id)?.content\n    }\n\n    suspend fun insertContent(content: FreadContent) {\n        dao.insert(content.toEntity())\n    }\n\n    suspend fun insertAll(content: List<FreadContent>) {\n        dao.insertAll(content.map { it.toEntity() })\n    }\n\n    suspend fun delete(id: String) {\n        dao.delete(id)\n    }\n\n    suspend fun getMaxOrder(): Int {\n        return getAllContent().maxOfOrNull { it.order } ?: 0\n    }\n\n    suspend fun checkNameExist(name: String): Boolean {\n        return getAllContent().any { it.name == name }\n    }\n\n    suspend fun reorderConfig(from: FreadContent, to: FreadContent) {\n        if (from == to) return\n        val pendingInsertList = mutableListOf<FreadContent>()\n        pendingInsertList += from.newOrder(to.order)\n        val allConfig = getAllContent()\n        if (from.order > to.order) {\n            // move up\n            allConfig.filter { it.order in to.order until from.order }\n                .map { it.newOrder(it.order + 1) }\n                .let { pendingInsertList += it }\n        } else {\n            // move down\n            allConfig.filter { it.order > from.order && it.order <= to.order }\n                .map { it.newOrder(it.order - 1) }\n                .let { pendingInsertList += it }\n        }\n        insertAll(pendingInsertList)\n    }\n\n    private fun FreadContent.toEntity(): FreadContentEntity {\n        return FreadContentEntity(\n            id = this.id,\n            content = this,\n        )\n    }\n\n}"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/daynight/DayNightHelper.kt",
    "content": "package com.zhangke.fread.common.daynight\n\nimport androidx.compose.runtime.staticCompositionLocalOf\nimport com.zhangke.fread.common.config.LocalConfigManager\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.runBlocking\n\nclass DayNightHelper (\n    private val localConfigManager: LocalConfigManager,\n) {\n\n    companion object {\n        private const val DAY_NIGHT_SETTING = \"day_night_setting\"\n        private const val AMOLED_MODE = \"amoled_mode\"\n    }\n\n    private val _dayNightModeFlow: MutableStateFlow<DayNightMode>\n    val dayNightModeFlow: StateFlow<DayNightMode>\n\n    private val _amoledModeFlow: MutableStateFlow<Boolean>\n    val amoledModeFlow: StateFlow<Boolean>\n\n    private val dayNightPlatformHelper = DayNightPlatformHelper()\n\n    init {\n        val (model, amoledEnabled) = runBlocking {\n            getDayNightModeSetting() to getAmoledModeSetting()\n        }\n        dayNightPlatformHelper.setDefaultMode(model)\n\n        _dayNightModeFlow = MutableStateFlow(model)\n        dayNightModeFlow = _dayNightModeFlow.asStateFlow()\n\n        _amoledModeFlow = MutableStateFlow(amoledEnabled)\n        amoledModeFlow = _amoledModeFlow.asStateFlow()\n    }\n\n    fun setDefaultMode() {\n        dayNightPlatformHelper.setDefaultMode(dayNightModeFlow.value)\n    }\n\n    // FIXME: use ActivityDayNightHelper.setMode before https://github.com/adrielcafe/voyager/issues/489 fix\n    suspend fun setMode(mode: DayNightMode) {\n        _dayNightModeFlow.value = mode\n        localConfigManager.putInt(DAY_NIGHT_SETTING, mode.localKey)\n        dayNightPlatformHelper.setMode(mode)\n    }\n\n    suspend fun setAmoledMode(enabled: Boolean) {\n        _amoledModeFlow.value = enabled\n        localConfigManager.putBoolean(AMOLED_MODE, enabled)\n        dayNightPlatformHelper.setAmoledMode(enabled)\n    }\n\n    private suspend fun getDayNightModeSetting(): DayNightMode {\n        return localConfigManager.getInt(DAY_NIGHT_SETTING)\n            ?.let { DayNightMode.fromLocalKey(it) } ?: DayNightMode.FOLLOW_SYSTEM\n    }\n\n    private suspend fun getAmoledModeSetting(): Boolean {\n        return localConfigManager.getBoolean(AMOLED_MODE) ?: false\n    }\n\n}\n\nexpect class DayNightPlatformHelper() {\n\n    fun setDefaultMode(modeValue: DayNightMode)\n\n    fun setMode(mode: DayNightMode)\n\n    fun setAmoledMode(enabled: Boolean)\n}\n\nval LocalActivityDayNightHelper =\n    staticCompositionLocalOf<DayNightHelper> { error(\"No ActivityDayNightHelper provided\") }"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/daynight/DayNightMode.kt",
    "content": "package com.zhangke.fread.common.daynight\n\nimport androidx.compose.foundation.isSystemInDarkTheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.ReadOnlyComposable\n\nenum class DayNightMode(val localKey: Int) {\n    DAY(1),\n    NIGHT(2),\n    FOLLOW_SYSTEM(-1),\n    ;\n\n    val isNight: Boolean\n        @ReadOnlyComposable\n        @Composable\n        get() {\n            return when (this) {\n                DAY -> false\n                NIGHT -> true\n                FOLLOW_SYSTEM -> isSystemInDarkTheme()\n            }\n        }\n\n    companion object {\n\n        fun fromLocalKey(key: Int): DayNightMode {\n            return when (key) {\n                DAY.localKey -> DAY\n                NIGHT.localKey -> NIGHT\n                FOLLOW_SYSTEM.localKey -> FOLLOW_SYSTEM\n                else -> FOLLOW_SYSTEM\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/db/ContentConfigDatabases.kt",
    "content": "package com.zhangke.fread.common.db\n\nimport androidx.room.ConstructedBy\nimport androidx.room.Dao\nimport androidx.room.Database\nimport androidx.room.Delete\nimport androidx.room.Entity\nimport androidx.room.Insert\nimport androidx.room.OnConflictStrategy\nimport androidx.room.PrimaryKey\nimport androidx.room.Query\nimport androidx.room.RoomDatabase\nimport androidx.room.RoomDatabaseConstructor\nimport androidx.room.TypeConverters\nimport com.zhangke.framework.network.FormalBaseUrl\nimport com.zhangke.fread.common.db.converts.ContentTabConverter\nimport com.zhangke.fread.common.db.converts.ContentTypeConverter\nimport com.zhangke.fread.common.db.converts.FormalBaseUrlConverter\nimport com.zhangke.fread.common.db.converts.FormalUriConverter\nimport com.zhangke.fread.common.db.converts.StatusProviderUriListConverter\nimport com.zhangke.fread.status.model.ContentConfig\nimport com.zhangke.fread.status.model.ContentType\nimport com.zhangke.fread.status.uri.FormalUri\nimport kotlinx.coroutines.flow.Flow\n\nprivate const val DB_VERSION = 1\nprivate const val TABLE_NAME = \"content_configs\"\n\n@TypeConverters(\n    ContentTabConverter::class,\n)\n@Entity(tableName = TABLE_NAME)\ndata class ContentConfigEntity(\n    @PrimaryKey(autoGenerate = true) val id: Long,\n    val order: Int,\n    val name: String,\n    val type: ContentType,\n    val sourceUriList: List<FormalUri>?,\n    val baseUrl: FormalBaseUrl?,\n    val showingTabList: List<ContentConfig.ActivityPubContent.ContentTab>,\n    val hiddenTabList: List<ContentConfig.ActivityPubContent.ContentTab>,\n)\n\n@Dao\ninterface ContentConfigDao {\n\n    @Query(\"SELECT * FROM $TABLE_NAME ORDER BY `order` ASC\")\n    suspend fun queryAllContentConfig(): List<ContentConfigEntity>\n\n    @Query(\"SELECT * FROM $TABLE_NAME ORDER BY `order` ASC\")\n    fun queryAllContentConfigFlow(): Flow<List<ContentConfigEntity>>\n\n    @Query(\"SELECT * FROM $TABLE_NAME WHERE id=:id\")\n    fun getContentConfigFlow(id: Long): Flow<ContentConfigEntity?>\n\n    @Query(\"SELECT * FROM $TABLE_NAME WHERE name=:name\")\n    suspend fun queryByName(name: String): ContentConfigEntity?\n\n    @Query(\"UPDATE $TABLE_NAME SET sourceUriList=:sourceUriList WHERE id=:id\")\n    suspend fun updateSourceList(id: Long, sourceUriList: List<FormalUri>)\n\n    @Query(\"UPDATE $TABLE_NAME SET name=:name WHERE id=:id\")\n    suspend fun updateName(id: Long, name: String)\n\n    @Query(\"SELECT * FROM $TABLE_NAME WHERE id=:id\")\n    suspend fun queryById(id: Long): ContentConfigEntity?\n\n    @Query(\"SELECT * FROM $TABLE_NAME WHERE id=:id\")\n    fun queryFlowById(id: Long): Flow<ContentConfigEntity?>\n\n    @Query(\"SELECT MAX(`order`) FROM $TABLE_NAME\")\n    suspend fun queryMaxOrder(): Int?\n\n    @Insert(onConflict = OnConflictStrategy.REPLACE)\n    suspend fun insert(entity: ContentConfigEntity)\n\n    @Insert(onConflict = OnConflictStrategy.REPLACE)\n    suspend fun insertList(entities: List<ContentConfigEntity>)\n\n    @Delete\n    suspend fun delete(entity: ContentConfigEntity)\n\n    @Query(\"DELETE FROM $TABLE_NAME\")\n    suspend fun deleteTable()\n\n    @Query(\"DELETE FROM $TABLE_NAME WHERE id=:id\")\n    suspend fun deleteById(id: Long)\n}\n\n@TypeConverters(\n    ContentTabConverter::class,\n    ContentTypeConverter::class,\n    FormalUriConverter::class,\n    StatusProviderUriListConverter::class,\n    FormalBaseUrlConverter::class,\n)\n@Database(\n    entities = [ContentConfigEntity::class],\n    version = DB_VERSION,\n    exportSchema = false,\n)\n@ConstructedBy(ContentConfigDatabasesConstructor::class)\nabstract class ContentConfigDatabases : RoomDatabase() {\n\n    abstract fun getContentConfigDao(): ContentConfigDao\n\n    companion object {\n        const val DB_NAME = \"ContentConfig.db\"\n    }\n}\n\n// The Room compiler generates the `actual` implementations.\n@Suppress(\"NO_ACTUAL_FOR_EXPECT\")\nexpect object ContentConfigDatabasesConstructor : RoomDatabaseConstructor<ContentConfigDatabases> {\n    override fun initialize(): ContentConfigDatabases\n}\n"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/db/FreadContentDatabase.kt",
    "content": "package com.zhangke.fread.common.db\n\nimport androidx.room.ConstructedBy\nimport androidx.room.Dao\nimport androidx.room.Database\nimport androidx.room.Entity\nimport androidx.room.Insert\nimport androidx.room.OnConflictStrategy\nimport androidx.room.PrimaryKey\nimport androidx.room.Query\nimport androidx.room.RoomDatabase\nimport androidx.room.RoomDatabaseConstructor\nimport androidx.room.TypeConverters\nimport com.zhangke.fread.common.db.converts.FreadContentConverter\nimport com.zhangke.fread.status.model.FreadContent\nimport kotlinx.coroutines.flow.Flow\n\nprivate const val DB_VERSION = 1\nprivate const val TABLE_NAME = \"fread_content\"\n\n@Entity(tableName = TABLE_NAME)\ndata class FreadContentEntity(\n    @PrimaryKey val id: String,\n    val content: FreadContent,\n)\n\n@Dao\ninterface FreadContentDao {\n\n    @Query(\"SELECT * FROM $TABLE_NAME\")\n    fun queryAllFlow(): Flow<List<FreadContentEntity>>\n\n    @Query(\"SELECT * FROM $TABLE_NAME WHERE id = :id\")\n    fun queryFlow(id: String): Flow<FreadContentEntity?>\n\n    @Query(\"SELECT * FROM $TABLE_NAME\")\n    suspend fun queryAll(): List<FreadContentEntity>\n\n    @Query(\"SELECT * FROM $TABLE_NAME WHERE id = :id\")\n    suspend fun query(id: String): FreadContentEntity?\n\n    @Insert(onConflict = OnConflictStrategy.REPLACE)\n    suspend fun insert(content: FreadContentEntity)\n\n    @Insert(onConflict = OnConflictStrategy.REPLACE)\n    suspend fun insertAll(list: List<FreadContentEntity>)\n\n    @Query(\"DELETE FROM $TABLE_NAME WHERE id = :id\")\n    suspend fun delete(id: String)\n}\n\n@TypeConverters(\n    FreadContentConverter::class\n)\n@Database(\n    entities = [FreadContentEntity::class],\n    version = DB_VERSION,\n    exportSchema = false,\n)\n@ConstructedBy(FreadContentDatabaseConstructor::class)\nabstract class FreadContentDatabase : RoomDatabase() {\n\n    abstract fun contentDao(): FreadContentDao\n\n    companion object {\n        const val DB_NAME = \"fread_content_1.db\"\n    }\n}\n\n// The Room compiler generates the `actual` implementations.\n@Suppress(\"NO_ACTUAL_FOR_EXPECT\")\nexpect object FreadContentDatabaseConstructor : RoomDatabaseConstructor<FreadContentDatabase> {\n    override fun initialize(): FreadContentDatabase\n}\n"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/db/MixedStatusDatabases.kt",
    "content": "package com.zhangke.fread.common.db\n\nimport androidx.room.ConstructedBy\nimport androidx.room.Dao\nimport androidx.room.Database\nimport androidx.room.Entity\nimport androidx.room.Insert\nimport androidx.room.OnConflictStrategy\nimport androidx.room.Query\nimport androidx.room.RoomDatabase\nimport androidx.room.RoomDatabaseConstructor\nimport androidx.room.TypeConverters\nimport androidx.room.migration.Migration\nimport androidx.sqlite.SQLiteConnection\nimport androidx.sqlite.execSQL\nimport com.zhangke.fread.common.db.converts.FormalUriConverter\nimport com.zhangke.fread.common.db.converts.StatusConverter\nimport com.zhangke.fread.common.db.converts.StatusUiStateConverter\nimport com.zhangke.fread.status.model.StatusUiState\nimport com.zhangke.fread.status.uri.FormalUri\nimport kotlinx.coroutines.flow.Flow\n\nprivate const val DB_VERSION = 2\nprivate const val TABLE_NAME = \"mixed_status\"\n\n@Entity(tableName = TABLE_NAME, primaryKeys = [\"sourceUri\", \"statusId\"])\ndata class MixedStatusEntity(\n    val statusId: String,\n    val sourceUri: FormalUri,\n    val status: StatusUiState,\n    val createAt: Long,\n    val cursor: String?,\n)\n\n@Dao\ninterface MixedStatusDao {\n\n    @Query(\"SELECT * FROM $TABLE_NAME WHERE sourceUri IN (:sourceUris) ORDER BY createAt DESC\")\n    fun queryFlow(sourceUris: List<FormalUri>): Flow<List<MixedStatusEntity>>\n\n    @Query(\"SELECT * FROM $TABLE_NAME WHERE statusId = :statusId\")\n    suspend fun queryByStatusId(statusId: String): List<MixedStatusEntity>\n\n    @Query(\"SELECT * FROM $TABLE_NAME WHERE sourceUri IN (:sourceUris) ORDER BY createAt DESC\")\n    suspend fun queryAll(sourceUris: List<FormalUri>): List<MixedStatusEntity>\n\n    @Query(\"SELECT * FROM $TABLE_NAME WHERE sourceUri = :sourceUri ORDER BY createAt DESC\")\n    suspend fun query(sourceUri: FormalUri): List<MixedStatusEntity>\n\n    @Insert(onConflict = OnConflictStrategy.REPLACE)\n    suspend fun insertStatus(status: List<MixedStatusEntity>)\n\n    @Query(\"DELETE FROM $TABLE_NAME WHERE sourceUri IN (:sourceUris)\")\n    suspend fun deleteStatusOfSources(sourceUris: List<FormalUri>)\n\n    @Query(\"DELETE FROM $TABLE_NAME WHERE sourceUri = :sourceUri\")\n    suspend fun deleteStatusOfSource(sourceUri: FormalUri)\n}\n\n@TypeConverters(\n    StatusConverter::class,\n    StatusUiStateConverter::class,\n    FormalUriConverter::class,\n)\n@Database(\n    entities = [MixedStatusEntity::class],\n    version = DB_VERSION,\n    exportSchema = false,\n)\n@ConstructedBy(MixedStatusDatabasesConstructor::class)\nabstract class MixedStatusDatabases : RoomDatabase() {\n\n    abstract fun mixedStatusDao(): MixedStatusDao\n\n    companion object {\n\n        const val DB_NAME = \"mixed_status_1.db\"\n\n        val MIGRATION_1_2 = object : Migration(1, 2) {\n\n            override fun migrate(connection: SQLiteConnection) {\n                connection.execSQL(\"DELETE FROM $TABLE_NAME\")\n            }\n        }\n    }\n}\n\n// The Room compiler generates the `actual` implementations.\n@Suppress(\"NO_ACTUAL_FOR_EXPECT\")\nexpect object MixedStatusDatabasesConstructor : RoomDatabaseConstructor<MixedStatusDatabases> {\n    override fun initialize(): MixedStatusDatabases\n}\n"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/db/converts/BlogMediaConverterHelper.kt",
    "content": "package com.zhangke.fread.common.db.converts\n\nimport androidx.room.TypeConverter\nimport com.zhangke.framework.architect.json.globalJson\nimport com.zhangke.fread.status.blog.BlogMedia\nimport com.zhangke.fread.status.blog.BlogMediaMeta\nimport com.zhangke.fread.status.blog.BlogMediaType\nimport kotlinx.serialization.encodeToString\nimport kotlinx.serialization.json.JsonObject\nimport kotlinx.serialization.json.JsonPrimitive\nimport kotlinx.serialization.json.contentOrNull\nimport kotlinx.serialization.json.jsonObject\nimport kotlinx.serialization.json.jsonPrimitive\n\nclass BlogMediaConverterHelper {\n\n    fun fromJsonObject(jsonObject: JsonObject?): BlogMedia? {\n        if (jsonObject == null) return null\n        return BlogMedia(\n            id = jsonObject.getString(\"id\"),\n            url = jsonObject.getString(\"url\"),\n            type = jsonObject.getString(\"type\").let(BlogMediaType::valueOf),\n            previewUrl = jsonObject.getStringOrNull(\"previewUrl\"),\n            remoteUrl = jsonObject.getStringOrNull(\"remoteUrl\"),\n            description = jsonObject.getStringOrNull(\"description\"),\n            blurhash = jsonObject.getStringOrNull(\"blurhash\"),\n            meta = jsonObject.get(\"meta\")?.jsonObject?.let(::convertJsonObjectToMeta),\n        )\n    }\n\n    fun toJsonObject(media: BlogMedia?): JsonObject? {\n        if (media == null) return null\n        val map = buildMap {\n            put(\"id\", JsonPrimitive(media.id))\n            put(\"url\", JsonPrimitive(media.url))\n            put(\"type\", JsonPrimitive(media.type.name))\n            if (media.previewUrl != null) put(\"previewUrl\", JsonPrimitive(media.previewUrl))\n            if (media.remoteUrl != null) put(\"remoteUrl\", JsonPrimitive(media.remoteUrl))\n            if (media.description != null) put(\"description\", JsonPrimitive(media.description))\n            if (media.blurhash != null) put(\"blurhash\", JsonPrimitive(media.blurhash))\n            media.meta\n                ?.let { convertMetaToJsonObject(it) }\n                ?.let { put(\"meta\", it) }\n        }\n        return JsonObject(map)\n    }\n\n    private fun convertJsonObjectToMeta(jsonObject: JsonObject): BlogMediaMeta {\n        return when (jsonObject.getStringOrNull(\"type\")?.let(::toType)) {\n            BlogMediaType.IMAGE -> {\n                globalJson.decodeFromString<BlogMediaMeta.ImageMeta>(\n                    jsonObject.getStringOrNull(\"data\").orEmpty()\n                )\n            }\n\n            BlogMediaType.GIFV -> {\n                globalJson.decodeFromString<BlogMediaMeta.GifvMeta>(\n                    jsonObject.getStringOrNull(\"data\").orEmpty()\n                )\n            }\n\n            BlogMediaType.VIDEO -> {\n                globalJson.decodeFromString<BlogMediaMeta.VideoMeta>(\n                    jsonObject.getStringOrNull(\"data\").orEmpty()\n                )\n            }\n\n            BlogMediaType.AUDIO -> {\n                globalJson.decodeFromString<BlogMediaMeta.AudioMeta>(\n                    jsonObject.getStringOrNull(\"data\").orEmpty()\n                )\n            }\n\n            else -> throw IllegalArgumentException(\"Unknown type!\")\n        }\n    }\n\n    @TypeConverter\n    private fun convertMetaToJsonObject(meta: BlogMediaMeta): JsonObject {\n        val type = when (meta) {\n            is BlogMediaMeta.ImageMeta -> BlogMediaType.IMAGE\n            is BlogMediaMeta.GifvMeta -> BlogMediaType.GIFV\n            is BlogMediaMeta.VideoMeta -> BlogMediaType.VIDEO\n            is BlogMediaMeta.AudioMeta -> BlogMediaType.AUDIO\n        }\n        val map = buildMap {\n            put(\"type\", JsonPrimitive(type.name))\n            put(\"data\", JsonPrimitive(globalJson.encodeToString(meta)))\n        }\n        return JsonObject(map)\n    }\n\n    private fun toType(typeName: String): BlogMediaType {\n        return BlogMediaType.valueOf(typeName)\n    }\n\n    private fun JsonObject.getString(key: String): String {\n        return get(key)?.jsonPrimitive?.contentOrNull.orEmpty()\n    }\n\n    private fun JsonObject.getStringOrNull(key: String): String? {\n        return get(key)?.jsonPrimitive?.contentOrNull\n    }\n}"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/db/converts/BlogMediaListConverter.kt",
    "content": "package com.zhangke.fread.common.db.converts\n\nimport androidx.room.TypeConverter\nimport com.zhangke.framework.architect.json.globalJson\nimport com.zhangke.fread.status.blog.BlogMedia\nimport kotlinx.serialization.json.JsonArray\nimport kotlinx.serialization.json.jsonObject\n\nclass BlogMediaListConverter {\n\n    private val mediaConvertHelper = BlogMediaConverterHelper()\n\n    @TypeConverter\n    fun fromStringList(text: String): List<BlogMedia> {\n        val jsonArray: JsonArray = globalJson.decodeFromString(text)\n        val list = mutableListOf<BlogMedia>()\n        jsonArray.forEach { element ->\n            mediaConvertHelper.fromJsonObject(element.jsonObject)?.let { list += it }\n        }\n        return list\n    }\n\n    @TypeConverter\n    fun toStringList(list: List<BlogMedia>): String {\n        return JsonArray(\n            buildList {\n                list.forEach { media ->\n                    mediaConvertHelper.toJsonObject(media)?.let { add(it) }\n                }\n            },\n        ).toString()\n    }\n}\n"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/db/converts/BlogPollConverter.kt",
    "content": "package com.zhangke.fread.common.db.converts\n\nimport androidx.room.TypeConverter\nimport com.zhangke.framework.architect.json.globalJson\nimport com.zhangke.fread.status.blog.BlogPoll\nimport kotlinx.serialization.encodeToString\n\nclass BlogPollConverter {\n\n    @TypeConverter\n    fun fromStringList(text: String?): BlogPoll? {\n        if (text.isNullOrEmpty()) return null\n        return globalJson.decodeFromString(text)\n    }\n\n    @TypeConverter\n    fun toStringList(poll: BlogPoll?): String? {\n        if (poll == null) return null\n        return globalJson.encodeToString(poll)\n    }\n}\n"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/db/converts/ContentTabConverter.kt",
    "content": "package com.zhangke.fread.common.db.converts\n\nimport androidx.room.TypeConverter\nimport com.zhangke.framework.architect.json.globalJson\nimport com.zhangke.fread.status.model.ContentConfig\nimport kotlinx.serialization.encodeToString\n\nclass ContentTabConverter {\n\n    @TypeConverter\n    fun toJsonString(tabList: List<ContentConfig.ActivityPubContent.ContentTab>): String {\n        if (tabList.isEmpty()) return \"\"\n        val stringList = tabList.map {\n            globalJson.encodeToString(ContentConfig.ActivityPubContent.ContentTab.serializer(), it)\n        }\n        return globalJson.encodeToString(stringList)\n    }\n\n    @TypeConverter\n    fun toTabList(jsonText: String): List<ContentConfig.ActivityPubContent.ContentTab> {\n        if (jsonText.isEmpty()) return emptyList()\n        return globalJson.decodeFromString<List<String>>(jsonText).map {\n            globalJson.decodeFromString(\n                deserializer = ContentConfig.ActivityPubContent.ContentTab.serializer(),\n                string = it,\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/db/converts/ContentTypeConverter.kt",
    "content": "package com.zhangke.fread.common.db.converts\n\nimport androidx.room.TypeConverter\nimport com.zhangke.fread.status.model.ContentType\n\nclass ContentTypeConverter {\n\n    @TypeConverter\n    fun fromString(text: String): ContentType {\n        return ContentType.valueOf(text)\n    }\n\n    @TypeConverter\n    fun toString(type: ContentType): String {\n        return type.name\n    }\n}\n"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/db/converts/FormalBaseUrlConverter.kt",
    "content": "package com.zhangke.fread.common.db.converts\n\nimport androidx.room.TypeConverter\nimport com.zhangke.framework.network.FormalBaseUrl\n\nclass FormalBaseUrlConverter {\n\n    @TypeConverter\n    fun fromString(text: String?): FormalBaseUrl? {\n        text ?: return null\n        return FormalBaseUrl.parse(text)!!\n    }\n\n    @TypeConverter\n    fun toString(baseUrl: FormalBaseUrl?): String? {\n        baseUrl ?: return null\n        return baseUrl.toString()\n    }\n}\n"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/db/converts/FormalUriConverter.kt",
    "content": "package com.zhangke.fread.common.db.converts\n\nimport androidx.room.TypeConverter\nimport com.zhangke.fread.status.uri.FormalUri\n\nclass FormalUriConverter {\n\n    @TypeConverter\n    fun convertToString(uri: FormalUri): String {\n        return uri.toString()\n    }\n\n    @TypeConverter\n    fun convertToUri(uri: String): FormalUri {\n        return FormalUri.from(uri)!!\n    }\n}\n"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/db/converts/FreadContentConverter.kt",
    "content": "package com.zhangke.fread.common.db.converts\n\nimport androidx.room.TypeConverter\nimport com.zhangke.framework.architect.json.fromJson\nimport com.zhangke.framework.architect.json.globalJson\nimport com.zhangke.fread.status.model.FreadContent\nimport kotlinx.serialization.encodeToString\n\nclass FreadContentConverter {\n\n    @TypeConverter\n    fun fromJsonText(text: String): FreadContent {\n        return globalJson.fromJson(text)\n    }\n\n    @TypeConverter\n    fun toJsonText(content: FreadContent): String {\n        return globalJson.encodeToString(content)\n    }\n}\n"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/db/converts/PlatformLocatorConverter.kt",
    "content": "package com.zhangke.fread.common.db.converts\n\nimport androidx.room.TypeConverter\nimport com.zhangke.fread.status.model.PlatformLocator\nimport kotlinx.serialization.json.Json\n\nclass PlatformLocatorConverter {\n\n    @TypeConverter\n    fun fromString(string: String): PlatformLocator {\n        return Json.decodeFromString(PlatformLocator.serializer(), string)\n    }\n\n    @TypeConverter\n    fun toString(locator: PlatformLocator): String {\n        return Json.encodeToString(PlatformLocator.serializer(), locator)\n    }\n}\n"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/db/converts/StatusConverter.kt",
    "content": "package com.zhangke.fread.common.db.converts\n\nimport androidx.room.TypeConverter\nimport com.zhangke.fread.status.status.model.Status\nimport kotlinx.serialization.json.Json\n\nclass StatusConverter {\n\n    @TypeConverter\n    fun fromString(string: String?): Status? {\n        string ?: return null\n        return Json.decodeFromString(Status.serializer(), string)\n    }\n\n    @TypeConverter\n    fun toString(status: Status?): String? {\n        status ?: return null\n        return Json.encodeToString(Status.serializer(), status)\n    }\n}\n\nclass NonNullStatusConverter {\n\n    @TypeConverter\n    fun fromString(string: String): Status {\n        return Json.decodeFromString(Status.serializer(), string)\n    }\n\n    @TypeConverter\n    fun toString(status: Status): String {\n        return Json.encodeToString(Status.serializer(), status)\n    }\n}\n"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/db/converts/StatusNotificationConverter.kt",
    "content": "package com.zhangke.fread.common.db.converts\n\nimport androidx.room.TypeConverter\nimport com.zhangke.framework.architect.json.globalJson\nimport com.zhangke.fread.status.notification.StatusNotification\nimport kotlinx.serialization.serializer\n\nclass StatusNotificationConverter {\n\n    @TypeConverter\n    fun convertToNotification(jsonString: String): StatusNotification {\n        return globalJson.decodeFromString(serializer(), jsonString)\n    }\n\n    @TypeConverter\n    fun convertToString(notification: StatusNotification): String {\n        return globalJson.encodeToString(serializer(), notification)\n    }\n}"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/db/converts/StatusProviderUriListConverter.kt",
    "content": "package com.zhangke.fread.common.db.converts\n\nimport androidx.room.TypeConverter\nimport com.zhangke.framework.architect.json.globalJson\nimport com.zhangke.fread.status.uri.FormalUri\nimport kotlinx.serialization.encodeToString\n\nclass StatusProviderUriListConverter {\n\n    @TypeConverter\n    fun fromString(text: String?): List<FormalUri>? {\n        text ?: return null\n        val stringList: List<String> = globalJson.decodeFromString(text)\n        return stringList.map(::stringToUri)\n    }\n\n    @TypeConverter\n    fun toString(uriList: List<FormalUri>?): String? {\n        uriList ?: return null\n        val stringList = uriList.map(::uriToString)\n        return globalJson.encodeToString(stringList)\n    }\n\n    private fun stringToUri(string: String): FormalUri {\n        return FormalUri.from(string)!!\n    }\n\n    private fun uriToString(uri: FormalUri): String {\n        return uri.toString()\n    }\n}\n"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/db/converts/StatusUiStateConverter.kt",
    "content": "package com.zhangke.fread.common.db.converts\n\nimport androidx.room.TypeConverter\nimport com.zhangke.fread.status.model.StatusUiState\nimport kotlinx.serialization.json.Json\n\nclass StatusUiStateConverter {\n\n    @TypeConverter\n    fun convertToText(status: StatusUiState): String {\n        return Json.encodeToString(StatusUiState.serializer(), status)\n    }\n\n    @TypeConverter\n    fun convertToStatus(text: String): StatusUiState {\n        return Json.decodeFromString(StatusUiState.serializer(), text)\n    }\n}\n"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/db/old/OldFreadContentDatabase.kt",
    "content": "package com.zhangke.fread.common.db.old\n\nimport androidx.room.ConstructedBy\nimport androidx.room.Dao\nimport androidx.room.Database\nimport androidx.room.Entity\nimport androidx.room.Insert\nimport androidx.room.OnConflictStrategy\nimport androidx.room.PrimaryKey\nimport androidx.room.Query\nimport androidx.room.RoomDatabase\nimport androidx.room.RoomDatabaseConstructor\nimport androidx.room.TypeConverters\nimport com.zhangke.fread.common.db.converts.FreadContentConverter\nimport com.zhangke.fread.status.model.FreadContent\nimport kotlinx.coroutines.flow.Flow\n\nprivate const val DB_VERSION = 1\nprivate const val TABLE_NAME = \"fread_content\"\n\n@Entity(tableName = TABLE_NAME)\ndata class OldFreadContentEntity(\n    @PrimaryKey val id: String,\n    val content: FreadContent,\n)\n\n@Dao\ninterface OldFreadContentDao {\n\n    @Query(\"SELECT * FROM $TABLE_NAME\")\n    fun queryAllFlow(): Flow<List<OldFreadContentEntity>>\n\n    @Query(\"SELECT * FROM $TABLE_NAME WHERE id = :id\")\n    fun queryFlow(id: String): Flow<OldFreadContentEntity?>\n\n    @Query(\"SELECT * FROM $TABLE_NAME\")\n    suspend fun queryAll(): List<OldFreadContentEntity>\n\n    @Query(\"SELECT * FROM $TABLE_NAME WHERE id = :id\")\n    suspend fun query(id: String): OldFreadContentEntity?\n\n    @Insert(onConflict = OnConflictStrategy.REPLACE)\n    suspend fun insert(content: OldFreadContentEntity)\n\n    @Insert(onConflict = OnConflictStrategy.REPLACE)\n    suspend fun insertAll(list: List<OldFreadContentEntity>)\n\n    @Query(\"DELETE FROM $TABLE_NAME WHERE id = :id\")\n    suspend fun delete(id: String)\n}\n\n@TypeConverters(\n    FreadContentConverter::class\n)\n@Database(\n    entities = [OldFreadContentEntity::class],\n    version = DB_VERSION,\n    exportSchema = false,\n)\n@ConstructedBy(OldFreadContentDatabaseConstructor::class)\nabstract class OldFreadContentDatabase : RoomDatabase() {\n\n    abstract fun contentDao(): OldFreadContentDao\n\n    companion object {\n        const val DB_NAME = \"fread_content.db\"\n    }\n}\n\n// The Room compiler generates the `actual` implementations.\n@Suppress(\"NO_ACTUAL_FOR_EXPECT\")\nexpect object OldFreadContentDatabaseConstructor : RoomDatabaseConstructor<OldFreadContentDatabase> {\n    override fun initialize(): OldFreadContentDatabase\n}\n"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/deeplink/ExternalInputHandler.kt",
    "content": "package com.zhangke.fread.common.deeplink\n\nimport com.zhangke.framework.network.SimpleUri\nimport com.zhangke.fread.common.action.ComposableActions\nimport com.zhangke.fread.common.action.RouteAction\nimport com.zhangke.fread.common.utils.GlobalScreenNavigation\nimport com.zhangke.krouter.KRouter\nimport kotlinx.coroutines.delay\n\nobject ExternalInputHandler {\n\n    suspend fun handle(text: String) {\n        delay(500) // delay for waiting page resumed\n        val fixedText = ExternalInputParser.parseExternalText(text)\n        val uri = SimpleUri.parse(fixedText)\n        if (uri == null || uri.host.isNullOrEmpty()) {\n            // goto publish screen\n            GlobalScreenNavigation.navigate(SelectAccountForPublishScreenKey(fixedText))\n        } else {\n            val action = KRouter.route<RouteAction>(fixedText)\n            if (action?.execute() == true) return\n            ComposableActions.post(fixedText)\n        }\n    }\n}\n"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/deeplink/ExternalInputParser.kt",
    "content": "package com.zhangke.fread.common.deeplink\n\nimport com.eygraber.uri.Uri\n\nobject ExternalInputParser {\n\n    /**\n     * Parsing text for external input, such as from Chrome.\n     * “Copied Text”\n     * http://example.com/#:~:text=Copied%20Text\n     */\n    fun parseExternalText(text: String): String {\n        if (text.isEmpty() || text.isBlank()) return text\n        if (!text.contains(\"http\")) return text.trim()\n        if (!text.startsWith('\"')) return text.trim()\n        val endIndex = text.indexOf('\"', 1)\n        if (endIndex == -1) return text.trim()\n        val extractedText = text.substring(1, endIndex).trim()\n        if (endIndex == text.lastIndex) return extractedText\n        val urlText = text.substring(endIndex + 1, text.length).trim()\n        val url = Uri.parseOrNull(urlText) ?: return text.trim()\n        if (url.fragment?.contains(extractedText) == true) {\n            return extractedText\n        }\n        return text.trim()\n    }\n}\n"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/deeplink/SelectAccountScreen.kt",
    "content": "package com.zhangke.fread.common.deeplink\n\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material3.CircularProgressIndicator\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.dp\nimport androidx.lifecycle.ViewModel\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.composable.ConsumeFlow\nimport com.zhangke.framework.composable.LoadableState\nimport com.zhangke.framework.composable.currentOrThrow\nimport com.zhangke.framework.composable.requireSuccessData\nimport com.zhangke.framework.ktx.launchInViewModel\nimport com.zhangke.framework.nav.LocalNavBackStack\nimport com.zhangke.framework.nav.popIfNotRoot\nimport com.zhangke.fread.common.composable.SelectableAccount\nimport com.zhangke.fread.common.utils.GlobalScreenNavigation\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.StatusProvider\nimport com.zhangke.fread.status.account.LoggedAccount\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asSharedFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.serialization.Serializable\nimport org.jetbrains.compose.resources.stringResource\n\n@Serializable\ndata class SelectAccountForPublishScreenKey(val text: String) : NavKey\n\n@Composable\nfun SelectAccountForPublishScreen(viewModel: SelectAccountForPublishViewModel) {\n    val loadingAccountList by viewModel.loggedAccounts.collectAsState()\n    val navigator = LocalNavBackStack.currentOrThrow\n    ConsumeFlow(viewModel.openScreenFlow) {\n        navigator.popIfNotRoot()\n        GlobalScreenNavigation.navigate(it)\n    }\n    ConsumeFlow(viewModel.finishScreenFlow) {\n        navigator.popIfNotRoot()\n    }\n    Box(\n        modifier = Modifier,\n        contentAlignment = Alignment.Center,\n    ) {\n        when (loadingAccountList) {\n            is LoadableState.Loading -> {\n                Surface(\n                    modifier = Modifier,\n                    shape = RoundedCornerShape(16.dp),\n                ) {\n                    CircularProgressIndicator(\n                        modifier = Modifier\n                            .padding(vertical = 24.dp, horizontal = 64.dp)\n                            .size(80.dp)\n                    )\n                }\n            }\n\n            is LoadableState.Success -> {\n                val accountList = loadingAccountList.requireSuccessData()\n                Surface(\n                    shape = RoundedCornerShape(16.dp),\n                    shadowElevation = 6.dp,\n                ) {\n                    Column(\n                        modifier = Modifier.fillMaxWidth()\n                            .padding(16.dp)\n                            .verticalScroll(rememberScrollState()),\n                    ) {\n                        Text(\n                            modifier = Modifier,\n                            text = stringResource(LocalizedString.statusUiSwitchAccountDialogTitle),\n                            style = MaterialTheme.typography.titleMedium\n                                .copy(fontWeight = FontWeight.SemiBold),\n                        )\n                        for (account in accountList) {\n                            Spacer(modifier = Modifier.height(16.dp))\n                            SelectableAccount(\n                                account = account,\n                                onClick = { viewModel.onAccountSelected(it) },\n                            )\n                        }\n                    }\n                }\n            }\n\n            else -> {\n                navigator.popIfNotRoot()\n            }\n        }\n    }\n}\n\nclass SelectAccountForPublishViewModel(\n    private val statusProvider: StatusProvider,\n    private val text: String,\n) : ViewModel() {\n\n    private val _loggedAccounts =\n        MutableStateFlow<LoadableState<List<LoggedAccount>>>(LoadableState.idle())\n    val loggedAccounts = _loggedAccounts.asStateFlow()\n\n    private val _openScreenFlow = MutableSharedFlow<NavKey>()\n    val openScreenFlow = _openScreenFlow.asSharedFlow()\n\n    private val _finishScreenFlow = MutableSharedFlow<Unit>()\n    val finishScreenFlow = _finishScreenFlow.asSharedFlow()\n\n    init {\n        launchInViewModel {\n            _loggedAccounts.emit(LoadableState.loading())\n            val accounts = statusProvider.accountManager.getAllLoggedAccount()\n            if (accounts.isEmpty()) {\n                _finishScreenFlow.emit(Unit)\n            } else if (accounts.size == 1) {\n                onAccountSelected(accounts.first())\n            } else {\n                _loggedAccounts.emit(LoadableState.success(accounts))\n            }\n        }\n    }\n\n    fun onAccountSelected(account: LoggedAccount) {\n        statusProvider.screenProvider\n            .getPublishScreen(account, text)\n            ?.let { launchInViewModel { _openScreenFlow.emit(it) } }\n    }\n}\n"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/deeplink/SelectedContentSwitcher.kt",
    "content": "package com.zhangke.fread.common.deeplink\n\nimport com.zhangke.fread.status.model.FreadContent\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.asSharedFlow\n\nclass SelectedContentSwitcher () {\n\n    private val _selectedContentFlow = MutableSharedFlow<FreadContent>(1)\n    val selectedContentFlow get() = _selectedContentFlow.asSharedFlow()\n\n    suspend fun switchToContent(content: FreadContent) {\n        _selectedContentFlow.emit(content)\n    }\n\n    fun resetReplayCache() {\n        _selectedContentFlow.resetReplayCache()\n    }\n}"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/di/ApplicationCoroutineScope.kt",
    "content": "package com.zhangke.fread.common.di\n\ntypealias ApplicationCoroutineScope = kotlinx.coroutines.CoroutineScope\n"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/feeds/model/RefreshResult.kt",
    "content": "package com.zhangke.fread.common.feeds.model\n\nimport com.zhangke.fread.status.model.StatusUiState\n\ndata class RefreshResult(\n    val newStatus: List<StatusUiState>,\n    val deletedStatus: List<StatusUiState>,\n    val useOldData: Boolean = true,\n) {\n\n    companion object {\n        val EMPTY = RefreshResult(emptyList(), emptyList())\n    }\n}\n"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/handler/TextHandler.kt",
    "content": "package com.zhangke.fread.common.handler\n\nimport androidx.compose.runtime.staticCompositionLocalOf\n\nexpect class TextHandler {\n\n    val packageName: String\n\n    val versionName: String\n\n    val versionCode: String\n\n    fun copyText(text: String)\n\n    fun shareUrl(url: String, text: String)\n\n    fun openSendEmail()\n\n    fun openAppMarket()\n}\n\nval LocalTextHandler =\n    staticCompositionLocalOf<TextHandler> { error(\"No TextHandler provided\") }\n"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/language/LanguageHelper.kt",
    "content": "package com.zhangke.fread.common.language\n\nimport androidx.compose.runtime.staticCompositionLocalOf\n\ninternal const val LOCAL_KEY_LANGUAGE = \"app_language_code\"\n\n@Deprecated(\"Use LanguageCode directly\")\nenum class LanguageSettingType(val value: Int) {\n    CN(1),\n    EN(2),\n    SYSTEM(3),\n    ;\n}\n\nexpect class ActivityLanguageHelper {\n\n    val currentLanguage: LanguageSettingItem\n\n    fun initialize()\n\n    fun setLanguage(item: LanguageSettingItem)\n}\n\nval LocalActivityLanguageHelper =\n    staticCompositionLocalOf<ActivityLanguageHelper> { error(\"No ActivityLanguageHelper provided\") }\n"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/language/LanguageSettingItem.kt",
    "content": "package com.zhangke.fread.common.language\n\nimport androidx.compose.runtime.Composable\nimport com.zhangke.fread.localization.LanguageCode\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.localization.displayName\nimport org.jetbrains.compose.resources.stringResource\n\nsealed interface LanguageSettingItem {\n\n    companion object {\n\n        val items: List<LanguageSettingItem>\n            get() = buildList {\n                add(FollowSystem)\n                LanguageCode.entries.forEach {\n                    add(Language(it))\n                }\n            }\n\n        fun fromLocalId(id: String): LanguageSettingItem? {\n            return if (id == FollowSystem.LOCAL_ID) {\n                FollowSystem\n            } else {\n                LanguageCode.fromCode(id)?.let { Language(it) }\n            }\n        }\n    }\n\n    val localId: String\n\n    @Composable\n    fun getDisplayName(): String\n\n    data object FollowSystem : LanguageSettingItem {\n\n        const val LOCAL_ID = \"FOLLOW_SYSTEM\"\n\n        override val localId: String = LOCAL_ID\n\n        @Composable\n        override fun getDisplayName(): String {\n            return stringResource(LocalizedString.profileSettingLanguageSystem)\n        }\n    }\n\n    data class Language(val code: LanguageCode) : LanguageSettingItem {\n\n        override val localId: String = code.code\n\n        @Composable\n        override fun getDisplayName(): String {\n            return code.displayName\n        }\n    }\n}"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/mixed/MixedStatusRepo.kt",
    "content": "package com.zhangke.fread.common.mixed\n\nimport com.zhangke.fread.common.db.MixedStatusDatabases\nimport com.zhangke.fread.common.db.MixedStatusEntity\nimport com.zhangke.fread.common.status.StatusConfigurationDefault\nimport com.zhangke.fread.status.StatusProvider\nimport com.zhangke.fread.status.content.MixedContent\nimport com.zhangke.fread.status.model.PagedData\nimport com.zhangke.fread.status.model.StatusUiState\nimport com.zhangke.fread.status.uri.FormalUri\nimport kotlinx.coroutines.async\nimport kotlinx.coroutines.awaitAll\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.flow\nimport kotlinx.coroutines.supervisorScope\n\nclass MixedStatusRepo (\n    private val statusProvider: StatusProvider,\n    mixedStatusDatabases: MixedStatusDatabases,\n) {\n\n    private val mixedStatusDao = mixedStatusDatabases.mixedStatusDao()\n\n    fun getLocalStatusFlow(content: MixedContent): Flow<List<StatusUiState>> {\n        return flow {\n            mixedStatusDao.queryFlow(content.sourceUriList)\n                .collect { emit(calculateDisplayList(it)) }\n        }\n    }\n\n    private fun calculateDisplayList(statusList: List<MixedStatusEntity>): List<StatusUiState> {\n        val endingList = getAllCrackEndingEntity(statusList)\n        if (endingList.isEmpty()) return statusList.map { it.status }\n        return statusList.subListOrNot(0, statusList.indexOf(endingList.first()) + 1)\n            .map { it.status }\n    }\n\n    suspend fun refresh(content: MixedContent): Result<Unit> {\n        if (content.sourceUriList.isEmpty()) return Result.success(Unit)\n        val statusResolver = statusProvider.statusResolver\n        val allResult = supervisorScope {\n            content.sourceUriList.map { sourceUri ->\n                async {\n                    sourceUri to statusResolver.getStatusList(\n                        uri = sourceUri,\n                        limit = StatusConfigurationDefault.config.loadFromServerLimit,\n                    )\n                }\n            }\n        }.awaitAll()\n        if (allResult.all { it.second.isFailure }) {\n            return Result.failure(allResult.first().second.exceptionOrNull()!!)\n        }\n        allResult.mapNotNull { (sourceUri, result) ->\n            result.getOrNull()?.toEntityList(sourceUri)\n        }.flatten().let {\n            mixedStatusDao.deleteStatusOfSources(content.sourceUriList)\n            mixedStatusDao.insertStatus(it)\n        }\n        return Result.success(Unit)\n    }\n\n    suspend fun loadMoreStatus(\n        content: MixedContent,\n    ): Result<Unit> {\n        val statusList = mixedStatusDao.queryAll(content.sourceUriList)\n        if (statusList.isEmpty()) return Result.success(Unit)\n        val endingList = getAllCrackEndingEntity(statusList)\n        if (endingList.isEmpty()) return Result.success(Unit)\n        val allResult = supervisorScope {\n            endingList.take(3).map { entity ->\n                async {\n                    entity.sourceUri to statusProvider.statusResolver.getStatusList(\n                        uri = entity.sourceUri,\n                        limit = StatusConfigurationDefault.config.loadFromServerLimit,\n                        maxId = entity.cursor,\n                    )\n                }\n            }\n        }.awaitAll()\n        if (allResult.all { it.second.isFailure }) {\n            return Result.failure(allResult.first().second.exceptionOrNull()!!)\n        }\n        allResult.mapNotNull { (sourceUri, result) ->\n            result.getOrNull()?.toEntityList(sourceUri)\n        }.flatten().let { mixedStatusDao.insertStatus(it) }\n        return Result.success(Unit)\n    }\n\n    suspend fun updateStatus(status: StatusUiState) {\n        mixedStatusDao.queryByStatusId(status.status.id)\n            .map { it.copy(status = status) }\n            .let { mixedStatusDao.insertStatus(it) }\n    }\n\n    suspend fun deleteStatus(statusId: String) {\n        val entityList = mixedStatusDao.queryByStatusId(statusId)\n        if (entityList.isEmpty()) return\n        val newStatusList = mutableListOf<MixedStatusEntity>()\n        entityList.groupBy { it.sourceUri }\n            .filter { it.value.isNotEmpty() }\n            .map { it.value.sortedByDescending { item -> item.createAt } }\n            .forEach { statusList ->\n                val lastEntity = statusList.last()\n                if (lastEntity.statusId == statusId) {\n                    var list = statusList.subList(0, statusList.lastIndex)\n                    if (!lastEntity.cursor.isNullOrEmpty()) {\n                        list = list.mapIndexed { index, entity ->\n                            if (index == list.lastIndex) {\n                                entity.copy(cursor = lastEntity.cursor)\n                            } else {\n                                entity\n                            }\n                        }\n                    }\n                    newStatusList.addAll(list)\n                } else {\n                    newStatusList.addAll(statusList)\n                }\n            }\n        mixedStatusDao.insertStatus(newStatusList)\n    }\n\n    /**\n     * 获取每个 Source 下对应的帖子列表中最早的那个帖子，如果这个帖子可以加载更多（包含 cursor）。\n     */\n    private fun getAllCrackEndingEntity(list: List<MixedStatusEntity>): List<MixedStatusEntity> {\n        val endingList = mutableListOf<MixedStatusEntity>()\n        val groupedList = list.groupBy { it.sourceUri }\n        groupedList.forEach { (_, entities) ->\n            entities.minByOrNull { it.createAt }\n                ?.takeIf { !it.cursor.isNullOrEmpty() }\n                ?.let { endingList += it }\n        }\n        return endingList.sortedByDescending { it.createAt }\n    }\n\n    private fun PagedData<StatusUiState>.toEntityList(sourceUri: FormalUri): List<MixedStatusEntity> {\n        return this.list.sortedByDescending { it.status.createAt.epochMillis }\n            .mapIndexed { index, item ->\n                MixedStatusEntity(\n                    statusId = item.status.id,\n                    status = item,\n                    createAt = item.status.createAt.epochMillis,\n                    sourceUri = sourceUri,\n                    cursor = if (index == this.list.lastIndex) this.cursor else null,\n                )\n            }\n    }\n\n    private fun <T> List<T>.subListOrNot(fromIndex: Int, toIndex: Int): List<T> {\n        if (fromIndex < 0 || fromIndex > toIndex || fromIndex > size) return this\n        if (toIndex < 0 || toIndex > size) return this\n        return subList(fromIndex, toIndex)\n    }\n}"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/onboarding/OnboardingComponent.kt",
    "content": "package com.zhangke.fread.common.onboarding\n\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.asSharedFlow\n\nclass OnboardingComponent () {\n\n    private val _onboardingFinishedFlow = MutableSharedFlow<Unit>(1)\n    val onboardingFinishedFlow = _onboardingFinishedFlow.asSharedFlow()\n\n    suspend fun onboardingSuccess() {\n        _onboardingFinishedFlow.emit(Unit)\n    }\n\n    fun clearState() {\n        _onboardingFinishedFlow.resetReplayCache()\n    }\n}"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/page/BasePagerTabHookManager.kt",
    "content": "package com.zhangke.fread.common.page\n\nimport androidx.compose.runtime.Composable\n\nobject BasePagerTabHookManager {\n\n    private val _hookList = mutableListOf<BasePagerTabHook>()\n    val hookList: List<BasePagerTabHook> get() = _hookList\n\n    init {\n        _hookList.addAll(findBasePagerTabImplementers())\n    }\n}\n\ninterface BasePagerTabHook {\n\n    @Composable\n    fun HookContent()\n}\n\ninternal expect fun findBasePagerTabImplementers(): List<BasePagerTabHook>"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/publish/PublishPostManager.kt",
    "content": "package com.zhangke.fread.common.publish\n\nclass PublishPostManager () {\n\n    fun publish(){\n\n    }\n}"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/push/IPushManager.kt",
    "content": "package com.zhangke.fread.common.push\n\nimport kotlinx.serialization.json.JsonObject\n\ninterface IPushManager {\n\n    fun getEndpointUrl(encodedAccountId: String, deviceId: String): String\n\n    suspend fun registerToRelay(accountId: String, deviceId: String): Result<Unit>\n\n    suspend fun unregisterToRelay(accountId: String, deviceId: String): Result<JsonObject>\n\n}\n\ninterface PushMessageReceiver {\n\n    fun onReceiveNewMessage(message: PushMessage)\n}\n\ndata class PushMessage(\n    val encodedAccountId: String,\n    val messageData: String,\n    val cryptoKey: String,\n    val contentEncoding: String,\n    val encryption: String,\n)\n"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/repo/LinkPreviewCardRepo.kt",
    "content": "package com.zhangke.fread.common.repo\n\nimport com.zhangke.framework.architect.http.sharedHttpClient\nimport com.zhangke.framework.utils.LinkPreviewInfo\nimport com.zhangke.framework.utils.LinkPreviewUtils\nimport io.ktor.client.call.body\nimport io.ktor.client.request.get\nimport io.ktor.http.takeFrom\n\nclass LinkPreviewCardRepo {\n\n    private val urlToInfoMap = mutableMapOf<String, LinkPreviewInfo>()\n\n    suspend fun fetchPreviewInfo(url: String): Result<LinkPreviewInfo> {\n        urlToInfoMap[url]?.let { return Result.success(it) }\n        val html = sharedHttpClient.get { url { takeFrom(url) } }.body<String>()\n        val info = LinkPreviewUtils.fetchPreviewInfo(url, html) ?: return Result.failure(\n            IllegalStateException(\"Failed to fetch link preview info\")\n        )\n        urlToInfoMap[url] = info\n        return Result.success(info)\n    }\n}\n"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/resources/ProtocolsSymbol.kt",
    "content": "package com.zhangke.fread.common.resources\n\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.RssFeed\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport com.zhangke.fread.common.daynight.LocalActivityDayNightHelper\nimport com.zhangke.fread.commonbiz.Res\nimport com.zhangke.fread.commonbiz.bluesky_logo\nimport com.zhangke.fread.commonbiz.mastodon_black_text\nimport com.zhangke.fread.commonbiz.mastodon_logo\nimport com.zhangke.fread.commonbiz.mastodon_white_text\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.model.StatusProviderProtocol\nimport com.zhangke.fread.status.model.isActivityPub\nimport com.zhangke.fread.status.model.isBluesky\nimport com.zhangke.fread.status.model.isRss\nimport org.jetbrains.compose.resources.stringResource\nimport org.jetbrains.compose.resources.vectorResource\n\nval StatusProviderProtocol.logo: ImageVector\n    @Composable get() {\n        return when {\n            isActivityPub -> mastodonLogo()\n            isRss -> rssLogo()\n            isBluesky -> blueskyLogo()\n            else -> rssLogo()\n        }\n    }\n\n@Composable\nfun PlatformLogo(\n    modifier: Modifier,\n    protocol: StatusProviderProtocol,\n) {\n    Box(\n        modifier = modifier,\n    ) {\n        val logo = when {\n            protocol.isBluesky -> blueskyLogo()\n            protocol.isActivityPub -> mastodonLogo()\n            else -> null\n        }\n        if (logo != null) {\n            Image(\n                modifier = Modifier.fillMaxSize(),\n                imageVector = logo,\n                contentDescription = null,\n            )\n        }\n    }\n}\n\n@Composable\nfun mastodonName(): String {\n    return stringResource(LocalizedString.mastodonName)\n}\n\n@Composable\nfun mastodonDescription(): String {\n    return stringResource(LocalizedString.mastodonDescription)\n}\n\n@Composable\nfun mastodonLogo(): ImageVector {\n    return vectorResource(Res.drawable.mastodon_logo)\n}\n\n@Composable\nfun mastodonHorizontalLogo(): ImageVector {\n    val night = LocalActivityDayNightHelper.current.dayNightModeFlow.value.isNight\n    return if (night) {\n        vectorResource(Res.drawable.mastodon_white_text)\n    } else {\n        vectorResource(Res.drawable.mastodon_black_text)\n    }\n}\n\n@Composable\nfun blueskyName(): String {\n    return stringResource(LocalizedString.blueskyName)\n}\n\n@Composable\nfun blueskyDescription(): String {\n    return stringResource(LocalizedString.blueskyDescription)\n}\n\n@Composable\nfun blueskyLogo(): ImageVector {\n    return vectorResource(Res.drawable.bluesky_logo)\n}\n\n@Composable\nfun rssLogo(): ImageVector {\n    return Icons.Default.RssFeed\n}\n\n@Composable\nfun mixedName(): String {\n    return stringResource(LocalizedString.mixedContentName)\n}\n\n@Composable\nfun mixedDescription(): String {\n    return stringResource(LocalizedString.mixedContentDescription)\n}\n"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/review/DefaultAppStoreReviewer.kt",
    "content": "package com.zhangke.fread.common.review\n\ninterface DefaultAppStoreReviewer {\n\n    fun showAppStoreReviewPopup(\n        onReviewSuccess: () -> Unit,\n        onReviewCancel: () -> Unit,\n    )\n}\n"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/review/FreadReviewManager.kt",
    "content": "package com.zhangke.fread.common.review\n\nimport androidx.compose.runtime.staticCompositionLocalOf\nimport com.zhangke.fread.common.config.LocalConfigManager\nimport com.zhangke.fread.common.di.ApplicationCoroutineScope\nimport com.zhangke.fread.common.utils.getCurrentTimeMillis\nimport com.zhangke.krouter.KRouter\nimport kotlinx.coroutines.launch\nimport kotlin.time.Duration\nimport kotlin.time.Duration.Companion.days\nimport kotlin.time.Duration.Companion.milliseconds\n\nclass FreadReviewManager (\n    private val localConfigManager: LocalConfigManager,\n    private val applicationCoroutineScope: ApplicationCoroutineScope,\n\n    ) {\n\n    companion object {\n        private const val LOCAL_KEY_LATEST_SHOW_TIME = \"latestShowPlayReviewTime\"\n        private const val LOCAL_KEY_REVIEWED = \"playReviewed\"\n        private const val LOCAL_KET_REVIEW_POP_COUNT = \"playReviewPopCount\"\n        private val maxInternal = 90.days\n    }\n\n    fun trigger(forceShow: Boolean = false) {\n        if (forceShow) {\n            showPlayReviewPopup()\n        } else {\n            applicationCoroutineScope.launch {\n                maybeShowPlayReviewPopup()\n            }\n        }\n    }\n\n    private suspend fun maybeShowPlayReviewPopup() {\n        val reviewed = getReviewed()\n        if (reviewed) return\n        val count = getPlayReviewPopCount()\n        val latestShowTime = getLatestShowPlayReviewTime()\n        if (count == 0 && latestShowTime == 0) {\n            // mark first launching app time\n            setLatestShowPlayReviewTime()\n            return\n        }\n        val duration = getCurrentTimeMillis().milliseconds - latestShowTime.milliseconds\n        if (isDurationOvertime(duration, count)) {\n            showPlayReviewPopup()\n        }\n    }\n\n    private fun showPlayReviewPopup() {\n        KRouter.getServices<DefaultAppStoreReviewer>().firstOrNull()?.showAppStoreReviewPopup(\n            onReviewSuccess = ::onReviewSuccess,\n            onReviewCancel = ::onReviewCancel,\n        )\n    }\n\n    private fun isDurationOvertime(duration: Duration, count: Int): Boolean {\n        if (duration > maxInternal) return true\n        if (count == 0) return duration >= 3.days\n        if (count == 1) return duration > 10.days\n        if (count == 2) return duration > 30.days\n        return false\n    }\n\n    internal fun onReviewSuccess() {\n        applicationCoroutineScope.launch {\n            setReviewed()\n        }\n    }\n\n    internal fun onReviewCancel() {\n        applicationCoroutineScope.launch {\n            increasePlayReviewPopCount()\n            setLatestShowPlayReviewTime()\n        }\n    }\n\n    private suspend fun setReviewed() {\n        localConfigManager.putBoolean(LOCAL_KEY_REVIEWED, true)\n    }\n\n    private suspend fun getReviewed(): Boolean {\n        return localConfigManager.getBoolean(LOCAL_KEY_REVIEWED) ?: false\n    }\n\n    private suspend fun increasePlayReviewPopCount() {\n        val count = getPlayReviewPopCount() + 1\n        localConfigManager.putInt(LOCAL_KET_REVIEW_POP_COUNT, count)\n    }\n\n    private suspend fun getPlayReviewPopCount(): Int {\n        return localConfigManager.getInt(LOCAL_KET_REVIEW_POP_COUNT) ?: 0\n    }\n\n    private suspend fun setLatestShowPlayReviewTime() {\n        val time = (getCurrentTimeMillis() / 1000).toInt()\n        localConfigManager.putInt(LOCAL_KEY_LATEST_SHOW_TIME, time)\n    }\n\n    private suspend fun getLatestShowPlayReviewTime(): Int {\n        return localConfigManager.getInt(LOCAL_KEY_LATEST_SHOW_TIME) ?: 0\n    }\n}\n\nval LocalFreadReviewManager =\n    staticCompositionLocalOf<FreadReviewManager> { error(\"No FreadReviewManager provided\") }"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/startup/FeedsRepoModuleStartup.kt",
    "content": "package com.zhangke.fread.common.startup\n\nimport com.zhangke.framework.module.ModuleStartup\n\nclass FeedsRepoModuleStartup (\n) : ModuleStartup {\n\n    override fun onAppCreate() {\n    }\n}"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/startup/FreadConfigModuleStartup.kt",
    "content": "package com.zhangke.fread.common.startup\n\nimport com.zhangke.framework.module.ModuleStartup\nimport com.zhangke.fread.common.config.FreadConfigManager\nimport com.zhangke.fread.common.di.ApplicationCoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.IO\nimport kotlinx.coroutines.launch\n\nclass FreadConfigModuleStartup(\n    private val applicationCoroutineScope: ApplicationCoroutineScope,\n    private val freadConfigManager: FreadConfigManager,\n) : ModuleStartup {\n    override fun onAppCreate() {\n        applicationCoroutineScope.launch(Dispatchers.IO) {\n            freadConfigManager.initConfig()\n        }\n    }\n}\n"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/startup/StartupManager.kt",
    "content": "package com.zhangke.fread.common.startup\n\nimport com.zhangke.framework.module.ModuleStartup\nclass StartupManager (\n    private val startupList: Set<ModuleStartup>,\n) {\n    fun initialize() {\n        startupList.forEach {\n            it.onAppCreate()\n        }\n    }\n}"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/status/StatusConfiguration.kt",
    "content": "package com.zhangke.fread.common.status\n\nimport kotlin.time.Duration\nimport kotlin.time.Duration.Companion.minutes\n\ndata class StatusConfiguration(\n    val loadFromServerLimit: Int,\n    val loadFromLocalLimit: Int,\n    val loadFromLocalRedundancies: Int,\n    val autoFetchNewerFeedsInterval: Duration,\n)\n\nobject StatusConfigurationDefault {\n\n    val config = StatusConfiguration(\n        loadFromServerLimit = 60,\n        loadFromLocalLimit = 100,\n        loadFromLocalRedundancies = 3,\n        autoFetchNewerFeedsInterval = 2.minutes,\n    )\n}\n"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/status/StatusIdGenerator.kt",
    "content": "package com.zhangke.fread.common.status\n\nimport com.zhangke.fread.status.status.model.Status\nimport com.zhangke.fread.status.uri.FormalUri\n\nclass StatusIdGenerator () {\n\n    fun generate(sourceUri: FormalUri, status: Status): String {\n        return \"${sourceUri.host}_${sourceUri.path}_${status.id}\"\n    }\n}"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/status/StatusUpdater.kt",
    "content": "package com.zhangke.fread.common.status\n\nimport com.zhangke.fread.status.model.StatusUiState\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.SharedFlow\nimport kotlinx.coroutines.flow.asSharedFlow\n\nclass StatusUpdater () {\n\n    private val _statusUpdateFlow = MutableSharedFlow<StatusUiState>()\n    val statusUpdateFlow: SharedFlow<StatusUiState> get() = _statusUpdateFlow.asSharedFlow()\n\n    suspend fun update(status: StatusUiState) {\n        _statusUpdateFlow.emit(status)\n    }\n}"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/status/adapter/ContentConfigAdapter.kt",
    "content": "package com.zhangke.fread.common.status.adapter\n\nimport com.zhangke.fread.common.db.ContentConfigEntity\nimport com.zhangke.fread.status.model.ContentConfig\nimport com.zhangke.fread.status.model.ContentType\n\nclass ContentConfigAdapter () {\n\n    fun toContentConfig(entity: ContentConfigEntity): ContentConfig {\n        return when (entity.type) {\n            ContentType.MIXED -> ContentConfig.MixedContent(\n                id = entity.id,\n                order = entity.order,\n                name = entity.name,\n                sourceUriList = entity.sourceUriList!!,\n            )\n\n            ContentType.ACTIVITY_PUB -> ContentConfig.ActivityPubContent(\n                id = entity.id,\n                order = entity.order,\n                name = entity.name,\n                baseUrl = entity.baseUrl!!,\n                showingTabList = entity.showingTabList,\n                hiddenTabList = entity.hiddenTabList,\n            )\n\n            ContentType.BLUESKY -> ContentConfig.BlueskyContent(\n                id = entity.id,\n                order = entity.order,\n                name = entity.name,\n                baseUrl = entity.baseUrl!!,\n                tabList = emptyList(),\n            )\n        }\n    }\n\n    fun toEntity(config: ContentConfig): ContentConfigEntity {\n        return when (config) {\n            is ContentConfig.MixedContent -> ContentConfigEntity(\n                id = config.id,\n                order = config.order,\n                name = config.name,\n                type = ContentType.MIXED,\n                sourceUriList = config.sourceUriList,\n                baseUrl = null,\n                showingTabList = emptyList(),\n                hiddenTabList = emptyList()\n            )\n\n            is ContentConfig.ActivityPubContent -> ContentConfigEntity(\n                id = config.id,\n                name = config.name,\n                order = config.order,\n                type = ContentType.ACTIVITY_PUB,\n                sourceUriList = null,\n                baseUrl = config.baseUrl,\n                showingTabList = config.showingTabList,\n                hiddenTabList = config.hiddenTabList,\n            )\n\n            is ContentConfig.BlueskyContent -> ContentConfigEntity(\n                id = config.id,\n                name = config.name,\n                order = config.order,\n                type = ContentType.BLUESKY,\n                sourceUriList = null,\n                baseUrl = config.baseUrl,\n                showingTabList = emptyList(),\n                hiddenTabList = emptyList(),\n            )\n        }\n    }\n}"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/status/model/SearchResultUiState.kt",
    "content": "package com.zhangke.fread.common.status.model\n\nimport com.zhangke.fread.status.author.BlogAuthor\nimport com.zhangke.fread.status.model.Hashtag\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.model.StatusUiState\nimport com.zhangke.fread.status.platform.BlogPlatform\n\nsealed interface SearchResultUiState {\n\n    data class Author(val locator: PlatformLocator, val author: BlogAuthor) : SearchResultUiState\n\n    data class Platform(val locator: PlatformLocator, val platform: BlogPlatform) :\n        SearchResultUiState\n\n    data class SearchedStatus(val status: StatusUiState) : SearchResultUiState\n\n    data class SearchedHashtag(val locator: PlatformLocator, val hashtag: Hashtag) :\n        SearchResultUiState\n}\n"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/status/usecase/FormatStatusDisplayTimeUseCase.kt",
    "content": "package com.zhangke.fread.common.status.usecase\n\nimport com.zhangke.fread.status.utils.DateTimeFormatter\nimport com.zhangke.fread.status.utils.DatetimeFormatConfig\nimport com.zhangke.fread.status.utils.defaultFormatConfig\n\nclass FormatStatusDisplayTimeUseCase (\n) {\n\n    suspend operator fun invoke(\n        datetime: Long,\n    ): String {\n        return invoke(\n            datetime,\n            config = defaultFormatConfig(),\n        )\n    }\n\n    operator fun invoke(\n        datetime: Long,\n        config: DatetimeFormatConfig,\n    ): String {\n        return DateTimeFormatter.format(datetime, config)\n    }\n}"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/theme/ThemeType.kt",
    "content": "package com.zhangke.fread.common.theme\n\nenum class ThemeType {\n\n    DEFAULT,\n    SYSTEM_DYNAMIC,\n}\n"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/update/AppPlatformUpdater.kt",
    "content": "package com.zhangke.fread.common.update\n\nexpect class AppPlatformUpdater() {\n\n    val platformName: String\n\n    val signingForFDroid: Boolean\n\n    fun getAppVersionCode(): Long\n\n    fun triggerUpdate(releaseInfo: AppReleaseInfo)\n}\n"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/update/AppReleaseInfo.kt",
    "content": "package com.zhangke.fread.common.update\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class AppReleaseInfo(\n    val versionCode: Long,\n    val versionName: String,\n    val releaseNote: String,\n    val downloadUrl: String,\n)\n"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/update/AppUpdateManager.kt",
    "content": "package com.zhangke.fread.common.update\n\nimport com.zhangke.framework.architect.http.sharedHttpClient\nimport com.zhangke.framework.utils.exceptionOrThrow\nimport com.zhangke.fread.common.config.FreadConfigManager\nimport io.ktor.client.call.body\nimport io.ktor.client.request.get\nimport io.ktor.client.request.parameter\nimport io.ktor.http.takeFrom\n\nclass AppUpdateManager (\n    private val configManager: FreadConfigManager,\n) {\n\n    companion object {\n\n        private const val API_RELEASE = \"https://api.fread.xyz/app/release\"\n        private const val QUERY_PLATFORM = \"platform\"\n    }\n\n    private val platformUpdater = AppPlatformUpdater()\n\n    val enableAutoCheckUpdate: Boolean get() = !platformUpdater.signingForFDroid\n\n    suspend fun checkForUpdate(checkIgnoreVersion: Boolean = true): Result<Pair<Boolean, AppReleaseInfo>> {\n        val releaseInfoResult = getReleaseInfo()\n        if (releaseInfoResult.isFailure) return Result.failure(releaseInfoResult.exceptionOrThrow())\n        val releaseInfo = releaseInfoResult.getOrThrow()\n        val currentVersion = platformUpdater.getAppVersionCode()\n        if (releaseInfo.versionCode <= currentVersion) {\n            return Result.success(false to releaseInfo)\n        }\n        if (checkIgnoreVersion) {\n            val ignoreUpdateVersion = configManager.getIgnoreUpdateVersion() ?: -1\n            if (ignoreUpdateVersion >= releaseInfo.versionCode) {\n                // ignore this version\n                return Result.success(false to releaseInfo)\n            }\n        }\n        return Result.success(true to releaseInfo)\n    }\n\n    suspend fun updateApp(releaseInfo: AppReleaseInfo) {\n        platformUpdater.triggerUpdate(releaseInfo)\n    }\n\n    suspend fun ignoreVersion(releaseInfo: AppReleaseInfo) {\n        configManager.updateIgnoreUpdateVersion(releaseInfo.versionCode)\n    }\n\n    private suspend fun getReleaseInfo(): Result<AppReleaseInfo> {\n        return runCatching {\n            sharedHttpClient.get {\n                url {\n                    takeFrom(API_RELEASE)\n                    parameter(QUERY_PLATFORM, platformUpdater.platformName)\n                }\n            }.body<AppReleaseInfo>()\n        }\n    }\n}"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/utils/GlobalScreenNavigation.kt",
    "content": "package com.zhangke.fread.common.utils\n\nimport androidx.navigation3.runtime.NavKey\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.SharedFlow\nimport kotlinx.coroutines.flow.asSharedFlow\n\nobject GlobalScreenNavigation {\n\n    private val _openScreenFlow = MutableSharedFlow<NavKey>()\n    val openScreenFlow: SharedFlow<NavKey> get() = _openScreenFlow.asSharedFlow()\n\n    suspend fun navigate(screen: NavKey) {\n        _openScreenFlow.emit(screen)\n    }\n}\n"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/utils/HashtagTextUtils.kt",
    "content": "package com.zhangke.fread.common.utils\n\nimport androidx.compose.ui.text.TextRange\n\nobject HashtagTextUtils {\n\n    private const val MARK_START = Int.MIN_VALUE\n    private const val MARK_END = Int.MAX_VALUE\n\n    private val BLUESKY_EXCLUDED_CHARS =\n        setOf('\\u00AD', '\\u2060', '\\u200A', '\\u200B', '\\u200C', '\\u200D', '\\u20E2')\n\n    private val Int.isStartMark: Boolean get() = this == MARK_START\n    private val Int.isEndMark: Boolean get() = this == MARK_END\n\n    /**\n     * Returns `true` if this character is a Unicode mark.\n     *\n     * Equivalent to testing if the char would be matched by the regular expression\n     * `\\p{M}` or `\\p{Mark}` (with Unicode enabled).\n     *\n     * Specifically, a character is a Unicode mark if its [category] is one of\n     * [CharCategory.NON_SPACING_MARK], [CharCategory.COMBINING_SPACING_MARK], and\n     * [CharCategory.ENCLOSING_MARK].\n     */\n    private fun Char.isMark(): Boolean {\n        return when (this.category) {\n            CharCategory.NON_SPACING_MARK,\n            CharCategory.COMBINING_SPACING_MARK,\n            CharCategory.ENCLOSING_MARK\n                -> true\n            else -> false\n        }\n    }\n\n    /**\n     * Returns `true` if this character is a Unicode number.\n     *\n     * Equivalent to testing if the char would be matched by the regular expression\n     * `\\p{N}` or `\\p{Number}` (with Unicode enabled).\n     *\n     * Specifically, a character is a Unicode number if its [category] is one of\n     * [CharCategory.DECIMAL_DIGIT_NUMBER], [CharCategory.LETTER_NUMBER], and\n     * [CharCategory.OTHER_NUMBER].\n     */\n    private fun Char.isNumber(): Boolean {\n        return when (this.category) {\n            CharCategory.DECIMAL_DIGIT_NUMBER,\n            CharCategory.LETTER_NUMBER,\n            CharCategory.OTHER_NUMBER\n                -> true\n            else -> false\n        }\n    }\n\n    /**\n     * Returns `true` if this character is a Unicode symbol.\n     *\n     * Equivalent to testing if the char would be matched by the regular expression\n     * `\\p{S}` or `\\p{Symbol}` (with Unicode enabled).\n     *\n     * Specifically, a character is a Unicode symbol if its [category] is one of\n     * [CharCategory.MATH_SYMBOL], [CharCategory.CURRENCY_SYMBOL],\n     * [CharCategory.MODIFIER_SYMBOL], and [CharCategory.OTHER_SYMBOL].\n     */\n    private fun Char.isSymbol(): Boolean {\n        return when (this.category) {\n            CharCategory.MATH_SYMBOL,\n            CharCategory.CURRENCY_SYMBOL,\n            CharCategory.MODIFIER_SYMBOL,\n            CharCategory.OTHER_SYMBOL\n                -> true\n            else -> false\n        }\n    }\n\n    /**\n     * @param text The text to search for hashtags\n     *\n     * @param allowHashtagInHashtag If `false`, use a Mastodon-like method to parse hashtags. If\n     * `true`, use a Bluesky-style parsing method.\n     *\n     * @see \"commonbiz/common/src/commonTest/kotlin/com/zhangke/fread/common/utils/HashtagTextUtilsTest.kt\"\n     * for examples of differences between Mastodon and Bluesky-style hashtag processing\n     */\n    fun findHashtags(text: String, allowHashtagInHashtag: Boolean = false): List<TextRange> {\n        if (text.isEmpty()) return emptyList()\n        return if (allowHashtagInHashtag) {\n            findHashtagsBlueskyStyle(text)\n        } else {\n            findHashtagsMastodonStyle(text)\n        }\n    }\n\n    private fun findHashtagsMastodonStyle(text: String): List<TextRange> {\n        val list = mutableListOf<TextRange>()\n        val chars = text.toCharArray()\n        var index = 0\n        var start = MARK_START\n        var end = MARK_END\n        var prevIsSep = true\n        var hasAlpha = false\n        while (index < text.length) {\n            val char = chars[index]\n            when {\n                char == '#' -> {\n                    if (prevIsSep) {\n                        start = index\n                        hasAlpha = false\n                    } else if (!start.isStartMark) {\n                        end = index\n                    }\n                    prevIsSep = true\n                }\n\n                char.isWhitespace() -> {\n                    if (!start.isStartMark && end.isEndMark) {\n                        end = index\n                    }\n                    prevIsSep = true\n                }\n\n                char.isLetter() || char.isMark() -> {\n                    prevIsSep = false\n                    hasAlpha = true\n                }\n\n                char.isNumber() || char.category == CharCategory.CONNECTOR_PUNCTUATION -> {\n                    prevIsSep = false\n                }\n\n                char != '\\u00b7' && char != '\\u200c' -> {\n                    if (!start.isStartMark && end.isEndMark) {\n                        end = index\n                    }\n\n                    prevIsSep = (char != '/' && char != ')')\n                }\n            }\n            if (!start.isStartMark && !end.isEndMark) {\n                if (hasAlpha) {\n                    list += TextRange(start = start, end = end)\n                }\n                start = MARK_START\n                end = MARK_END\n            }\n            index++\n        }\n\n        if (index == text.length && !start.isStartMark && end.isEndMark && hasAlpha) {\n            list += TextRange(start = start, end = index)\n        }\n        return list\n    }\n\n    private fun findHashtagsBlueskyStyle(text: String): List<TextRange> {\n        val list = mutableListOf<TextRange>()\n        val chars = text.toCharArray()\n        var index = 0\n        var start = MARK_START\n        var end = MARK_END\n        var lastEnd = MARK_END\n        var prevIsSep = true\n        var hasAlpha = false\n        while (index < text.length) {\n            val char = chars[index]\n            when {\n                char == '#' -> {\n                    if (prevIsSep) {\n                        start = index\n                        hasAlpha = false\n                        lastEnd = MARK_END\n                    } else if (!start.isStartMark) {\n                        hasAlpha = false\n                    }\n                    prevIsSep = false\n                }\n\n                char.isWhitespace() -> {\n                    if (!start.isStartMark && end.isEndMark) {\n                        end = if (hasAlpha) index else lastEnd\n                        if (end.isEndMark) {\n                            start = MARK_START\n                        }\n                    }\n                    prevIsSep = true\n                }\n\n                BLUESKY_EXCLUDED_CHARS.contains(char) -> {\n                    if (!start.isStartMark && end.isEndMark) {\n                        end = if (hasAlpha) index else lastEnd\n                        if (end.isEndMark) {\n                            start = MARK_START\n                        }\n                    }\n                    prevIsSep = false\n                }\n\n                char.isLetter() || char.isMark() || char.isSymbol() -> {\n                    prevIsSep = false\n                    hasAlpha = true\n                    lastEnd = index + 1\n                }\n\n                else -> {\n                    prevIsSep = false\n                    hasAlpha = false\n                }\n            }\n            if (!start.isStartMark && !end.isEndMark) {\n                list += TextRange(start = start, end = end)\n                start = MARK_START\n                end = MARK_END\n            }\n            index++\n        }\n\n        if (index == text.length && !start.isStartMark && end.isEndMark) {\n            end = if (hasAlpha) index else lastEnd\n            if (!end.isEndMark) {\n                list += TextRange(start = start, end = end)\n            }\n        }\n        return list\n    }\n}\n"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/utils/InstantExt.kt",
    "content": "@file:OptIn(ExperimentalTime::class)\n\npackage com.zhangke.fread.common.utils\n\nimport com.zhangke.framework.date.InstantFormater\nimport kotlinx.datetime.Instant\nimport kotlin.time.Clock\nimport kotlin.time.ExperimentalTime\n\nfun getCurrentInstant(): Instant {\n    return Instant.fromEpochMilliseconds(Clock.System.now().toEpochMilliseconds())\n}\n\nfun getCurrentTimeMillis(): Long {\n    return Clock.System.now().toEpochMilliseconds()\n}\n\nfun com.zhangke.framework.datetime.Instant.formatDefault(): String {\n    return this.instant.formatDefault()\n}\n\nfun com.zhangke.framework.datetime.Instant.formatDate(): String {\n    return this.instant.formatDate()\n}\n\nfun Instant.formatDefault(): String {\n    return InstantFormater().formatToMediumDate(this)\n}\n\nfun Instant.formatDate(): String {\n    return InstantFormater().formatToMediumDateWithoutTime(this)\n}\n"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/utils/LinkTextUtils.kt",
    "content": "package com.zhangke.fread.common.utils\n\nimport androidx.compose.ui.text.TextRange\n\nobject LinkTextUtils {\n\n    private val urlRegex = \"\"\"\n        (?:https?://)?(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\\.)+[A-Za-z]{2,63}(?!\\.[A-Za-z0-9-])(?=$|[:/?#]|[^\\w-])(?::\\d{2,5})?(?:[/?#][^\\s'\"]*)?\n    \"\"\".trimIndent().toRegex()\n\n    fun findLinks(text: String): List<TextRange> {\n        if (text.isEmpty()) return emptyList()\n        val list = mutableListOf<TextRange>()\n        urlRegex.findAll(text)\n            .forEach {\n                list += TextRange(start = it.range.start, end = it.range.endInclusive + 1)\n            }\n        return list\n    }\n}\n"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/utils/ListStringConverter.kt",
    "content": "package com.zhangke.fread.common.utils\n\nimport androidx.room.TypeConverter\nimport com.zhangke.framework.architect.json.globalJson\nimport kotlinx.serialization.encodeToString\n\nclass ListStringConverter {\n\n    @TypeConverter\n    fun fromStringList(text: String): List<String> {\n        return globalJson.decodeFromString(text)\n    }\n\n    @TypeConverter\n    fun toStringList(list: List<String>): String {\n        return globalJson.encodeToString(list)\n    }\n}"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/utils/MediaFileHelper.kt",
    "content": "package com.zhangke.fread.common.utils\n\nimport androidx.compose.runtime.staticCompositionLocalOf\n\nexpect class MediaFileHelper {\n    fun saveImageToGallery(url: String)\n\n    fun saveVideoToGallery(url: String)\n}\n\nval LocalMediaFileHelper =\n    staticCompositionLocalOf<MediaFileHelper> { error(\"No MediaFileHelper provided\") }\n"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/utils/MentionTextUtil.kt",
    "content": "package com.zhangke.fread.common.utils\n\nimport androidx.compose.ui.text.TextRange\nimport androidx.compose.ui.text.input.TextFieldValue\nimport androidx.compose.ui.text.substring\n\nobject MentionTextUtil {\n\n    fun findTypingMentionName(text: TextFieldValue): String? {\n        val range = findTypingMentionRange(text) ?: return null\n        return text.text.substring(range)\n    }\n\n    fun insertMention(text: TextFieldValue, insertText: String): TextFieldValue {\n        if (insertText.isEmpty()) return text\n        val fixedInsertText = \"@$insertText\"\n        val range = findTypingMentionRange(text)\n        if (range == null) {\n            val newText = \"${text.text} $fixedInsertText \"\n            return TextFieldValue(text = newText, TextRange(newText.length))\n        } else {\n            val finalInsertText = \" $fixedInsertText \"\n            val newText = text.text.replaceRange(\n                startIndex = range.start,\n                endIndex = range.end,\n                replacement = finalInsertText,\n            )\n            return TextFieldValue(text = newText, TextRange(range.start + finalInsertText.length))\n        }\n    }\n\n    fun findMentionList(text: String): List<TextRange> {\n        if (text.isEmpty()) return emptyList()\n        return TextMentionMatcher().findMentionRanges(text.toCharArray())\n    }\n\n    private fun findTypingMentionRange(text: TextFieldValue): TextRange? {\n        if (text.selection.length != 0) return null\n        if (text.selection.start <= 0) return null\n        val chars = text.text.toCharArray()\n        return TypingMentionMatcher().findTypingMentionRange(\n            chars = chars,\n            startIndex = text.selection.start - 1,\n        )\n    }\n}\n\nclass TypingMentionMatcher {\n\n    companion object {\n\n        private const val STATE_INIT = 0\n\n        /**\n         * scanning @AtomZ@m.cmx.im or @AtomX suffix\n         */\n        private const val STATE_SCANNING_NAME_OR_HOST = STATE_INIT + 1\n\n        /**\n         * scanning @AtomZ@m.cmx.im name part\n         */\n        private const val STATE_SCANNING_NAME = STATE_SCANNING_NAME_OR_HOST + 1\n    }\n\n    fun findTypingMentionRange(chars: CharArray, startIndex: Int): TextRange? {\n        if (startIndex <= 0 || startIndex >= chars.size) return null\n        var state = STATE_INIT\n        var index = startIndex\n        var firstMentionFlagIndex = -1\n        while (index >= 0) {\n            when (state) {\n                STATE_INIT -> {\n                    state = STATE_SCANNING_NAME_OR_HOST\n                }\n\n                STATE_SCANNING_NAME_OR_HOST -> {\n                    if (chars[index].validateWebFinerSymbol) {\n                        index--\n                    } else if (chars[index].isMentionFlag) {\n                        firstMentionFlagIndex = index\n                        index--\n                        state = STATE_SCANNING_NAME\n                    } else {\n                        return null\n                    }\n                }\n\n                STATE_SCANNING_NAME -> {\n                    if (chars[index].validateWebFinerSymbol) {\n                        index--\n                    } else if (chars[index].isMentionFlag) {\n                        if (firstMentionFlagIndex - index == 1) break\n                        return TextRange(index, startIndex + 1)\n                    } else {\n                        break\n                    }\n                }\n            }\n        }\n        if (state == STATE_SCANNING_NAME) {\n            return TextRange(firstMentionFlagIndex, startIndex + 1)\n        }\n        return null\n    }\n}\n\nclass TextMentionMatcher {\n\n    companion object {\n\n        private const val STATE_SCANNING_FIRST_MENTION_FLAG = 0\n        private const val STATE_SCANNING_NAME = STATE_SCANNING_FIRST_MENTION_FLAG + 1\n        private const val STATE_SCANNING_HOST = STATE_SCANNING_NAME + 1\n    }\n\n    fun findMentionRanges(chars: CharArray): List<TextRange> {\n        if (chars.isEmpty()) return emptyList()\n        val list = mutableListOf<TextRange>()\n        var state = STATE_SCANNING_FIRST_MENTION_FLAG\n        var index = 0\n        var nameMentionFlagIndex = -1\n        while (index < chars.size) {\n            val char = chars[index]\n            when (state) {\n                STATE_SCANNING_FIRST_MENTION_FLAG -> {\n                    if (char.isMentionFlag) {\n                        nameMentionFlagIndex = index\n                        state = STATE_SCANNING_NAME\n                    }\n                    index++\n                }\n\n                STATE_SCANNING_NAME -> {\n                    if (char.isMentionFlag) {\n                        state = STATE_SCANNING_HOST\n                    } else if (!char.validateWebFinerSymbol) {\n                        list += TextRange(nameMentionFlagIndex, index)\n                        nameMentionFlagIndex = -1\n                        state = STATE_SCANNING_FIRST_MENTION_FLAG\n                    }\n                    index++\n                }\n\n                STATE_SCANNING_HOST -> {\n                    if (!char.validateWebFinerSymbol) {\n                        if (index - nameMentionFlagIndex > 1) {\n                            list += TextRange(nameMentionFlagIndex, index)\n                        }\n                        state = STATE_SCANNING_FIRST_MENTION_FLAG\n                    }\n                    index++\n                }\n            }\n        }\n\n        if (state == STATE_SCANNING_HOST && index - nameMentionFlagIndex > 1) {\n            list += TextRange(nameMentionFlagIndex, index)\n        }\n        if (state == STATE_SCANNING_NAME){\n            list += TextRange(nameMentionFlagIndex, index)\n        }\n        return list\n    }\n}\n\nprivate val Char.isMentionFlag: Boolean get() = this == '@'\n\nprivate val validateWebFingerSymbols = arrayOf(\n    '-', '.', '_', '+', '%', '!', '=', '~', '*'\n)\n\nprivate val Char.validateWebFinerSymbol: Boolean\n    get() {\n        return this.isLetterOrDigit() || this in validateWebFingerSymbols\n    }\n"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/utils/PlatformUriHelper.kt",
    "content": "package com.zhangke.fread.common.utils\n\nimport androidx.compose.runtime.staticCompositionLocalOf\nimport com.zhangke.framework.utils.ContentProviderFile\nimport com.zhangke.framework.utils.PlatformUri\n\nexpect class PlatformUriHelper {\n\n    suspend fun read(uri: PlatformUri): ContentProviderFile?\n\n    suspend fun readBytes(uri: PlatformUri): ByteArray?\n\n    fun queryFileName(uri: PlatformUri): String?\n}\n\nval LocalPlatformUriHelper = staticCompositionLocalOf<PlatformUriHelper> {\n    error(\"No PlatformUriHelper provided\")\n}"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/utils/RandomIdGenerator.kt",
    "content": "package com.zhangke.fread.common.utils\n\nexpect class RandomIdGenerator() {\n\n    fun generateId(): String\n}\n"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/utils/StorageHelper.kt",
    "content": "package com.zhangke.fread.common.utils\n\nimport okio.Path\n\nexpect class StorageHelper {\n    val cacheDir: Path\n}\n"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/utils/ToastHelper.kt",
    "content": "package com.zhangke.fread.common.utils\n\nimport androidx.compose.runtime.staticCompositionLocalOf\n\nexpect class ToastHelper {\n    fun showToast(content: String)\n}\n\nval LocalToastHelper = staticCompositionLocalOf<ToastHelper> {\n    error(\"No ToastHelper provided\")\n}\n"
  },
  {
    "path": "commonbiz/common/src/commonMain/kotlin/com/zhangke/fread/common/utils/WebFingerConverter.kt",
    "content": "package com.zhangke.fread.common.utils\n\nimport androidx.room.TypeConverter\nimport com.zhangke.framework.utils.WebFinger\n\nclass WebFingerConverter {\n\n    @TypeConverter\n    fun fromWebFinger(webFinger: WebFinger): String {\n        return webFinger.toString()\n    }\n\n    @TypeConverter\n    fun toWebFinger(text: String): WebFinger {\n        return WebFinger.create(text)!!\n    }\n}"
  },
  {
    "path": "commonbiz/common/src/commonTest/kotlin/com/zhangke/fread/common/status/utils/createStatus.kt",
    "content": "package com.zhangke.fread.common.status.utils\n\nimport com.zhangke.framework.network.FormalBaseUrl\nimport com.zhangke.framework.utils.WebFinger\nimport com.zhangke.fread.common.utils.createActivityPubUserUri\nimport com.zhangke.fread.status.author.BlogAuthor\nimport com.zhangke.fread.status.blog.Blog\nimport com.zhangke.fread.status.blog.BlogMedia\nimport com.zhangke.fread.status.blog.BlogPoll\nimport com.zhangke.fread.status.blog.PostingApplication\nimport com.zhangke.fread.status.model.StatusProviderProtocol\nimport com.zhangke.fread.status.model.StatusVisibility\nimport com.zhangke.fread.status.platform.BlogPlatform\nimport com.zhangke.fread.status.status.model.Status\nimport com.zhangke.fread.status.uri.FormalUri\nimport kotlinx.datetime.Clock\nimport kotlinx.datetime.Instant\n\nfun createStatus(\n    id: String = \"1\",\n    title: String = \"title\",\n    author: BlogAuthor = createBlogAuthor(),\n    content: String = \"content\",\n    date: Instant = Clock.System.now(),\n    forwardCount: Int? = null,\n    likeCount: Int? = null,\n    repliesCount: Int? = null,\n    sensitive: Boolean = false,\n    spoilerText: String = \"\",\n    mediaList: List<BlogMedia> = emptyList(),\n    pinned: Boolean = false,\n    poll: BlogPoll? = null,\n    visibility: StatusVisibility = StatusVisibility.PUBLIC,\n    card: PreviewCard? = null,\n    isSelf: Boolean = false,\n    supportTranslate: Boolean = false,\n    editedAt: Instant? = null,\n    application: PostingApplication? = null,\n): Status {\n    return Status.NewBlog(\n        blog = Blog(\n            id = id,\n            author = author,\n            description = null,\n            content = content,\n            title = title,\n            createAt = com.zhangke.framework.datetime.Instant(date),\n            emojis = emptyList(),\n            forwardCount = forwardCount,\n            likeCount = likeCount,\n            repliesCount = repliesCount,\n            url = \"\",\n            sensitive = sensitive,\n            spoilerText = spoilerText,\n            mediaList = mediaList,\n            poll = poll,\n            platform = createBlogPlatform(),\n            mentions = emptyList(),\n            tags = emptyList(),\n            card = null,\n            supportTranslate = false,\n            pinned = pinned,\n            visibility = visibility,\n            isSelf = isSelf,\n            editedAt = null,\n            application = application,\n        ),\n        supportInteraction = emptyList()\n    )\n}\n\nfun createBlogPlatform(\n    uri: String = \"https://example.com\",\n    name: String = \"example\",\n    description: String = \"description\",\n    baseUrl: FormalBaseUrl = FormalBaseUrl.build(\"https\", \"example.com\"),\n    protocol: StatusProviderProtocol = mockStatusProviderProtocol(),\n    thumbnail: String? = null,\n): BlogPlatform {\n    return BlogPlatform(\n        uri = uri,\n        name = name,\n        description = description,\n        baseUrl = baseUrl,\n        protocol = protocol,\n        thumbnail = thumbnail\n    )\n}\n\nfun mockStatusProviderProtocol(): StatusProviderProtocol {\n    return StatusProviderProtocol(\n        id = \"id\",\n        name = \"example\",\n    )\n}\n\nfun createBlogAuthor(\n    uri: FormalUri = createActivityPubUserUri(),\n    webFinger: WebFinger = WebFinger.create(\"@AtomZ@m.cmx.im\")!!,\n    name: String = \"Atom\",\n    description: String = \"mock desc\",\n    avatar: String? = null,\n) = BlogAuthor(\n    uri = uri,\n    webFinger = webFinger,\n    name = name,\n    description = description,\n    avatar = avatar,\n    emojis = emptyList(),\n)\n"
  },
  {
    "path": "commonbiz/common/src/commonTest/kotlin/com/zhangke/fread/common/utils/DateTimeFormatterTest.kt",
    "content": "package com.zhangke.fread.common.utils\n\nimport com.zhangke.fread.status.utils.DateTimeFormatter\nimport com.zhangke.fread.status.utils.DatetimeFormatConfig\nimport kotlinx.datetime.Clock\nimport kotlin.test.BeforeTest\nimport kotlin.test.Test\nimport kotlin.test.assertEquals\nimport kotlin.time.Duration.Companion.days\nimport kotlin.time.Duration.Companion.hours\nimport kotlin.time.Duration.Companion.minutes\nimport kotlin.time.Duration.Companion.seconds\n\nclass DateTimeFormatterTest {\n\n    private lateinit var config: DatetimeFormatConfig\n\n    @BeforeTest\n    fun initConfig() {\n        config = DatetimeFormatConfig(\n            agoPrefix = \"\",\n            agoSuffix = \"前\",\n            day = \"天\",\n            hour = \"小时\",\n            minutes = \"分钟\",\n            second = \"秒\",\n        )\n    }\n\n    @Test\n    fun testFormat() {\n        val datetime = Clock.System.now()\n        assertEquals(\"10 秒前\", DateTimeFormatter.format(datetime.minus(10.seconds).toEpochMilliseconds(), config))\n        assertEquals(\"10 分钟前\", DateTimeFormatter.format(datetime.minus(10.minutes).toEpochMilliseconds(), config))\n        assertEquals(\"10 小时前\", DateTimeFormatter.format(datetime.minus(10.hours).toEpochMilliseconds(), config))\n        assertEquals(\"1 天前\", DateTimeFormatter.format(datetime.minus(1.days).toEpochMilliseconds(), config))\n        assertEquals(\"2 天前\", DateTimeFormatter.format(datetime.minus(2.days).toEpochMilliseconds(), config))\n        assertEquals(\"2 天前\", DateTimeFormatter.format(datetime.minus(2.9.days).toEpochMilliseconds(), config))\n        assertEquals(\"2024-09-13\", DateTimeFormatter.format(1726201813038, config))\n    }\n\n    @Test\n    fun testFormatWithPrefix() {\n        val datetime = Clock.System.now()\n        val config = DatetimeFormatConfig(\n            agoPrefix = \"Hace \",\n            agoSuffix = \"\",\n            day = \"día\",\n            hour = \"hora\",\n            minutes = \"min\",\n            second = \"seg\",\n        )\n        assertEquals(\"Hace 10 seg\", DateTimeFormatter.format(datetime.minus(10.seconds).toEpochMilliseconds(), config))\n        assertEquals(\"Hace 10 min\", DateTimeFormatter.format(datetime.minus(10.minutes).toEpochMilliseconds(), config))\n    }\n}\n"
  },
  {
    "path": "commonbiz/common/src/commonTest/kotlin/com/zhangke/fread/common/utils/FormalUriTest.kt",
    "content": "package com.zhangke.fread.common.utils\n\nimport com.zhangke.fread.status.uri.FormalUri\n\nconst val ACTIVITY_PUB_HOST = \"activitypub.com\"\n\nprivate fun createActivityPubUri(path: String, queries: Map<String, String>): FormalUri {\n    return FormalUri.create(\n        host = ACTIVITY_PUB_HOST,\n        path = path,\n        queries = queries,\n    )\n}\n\nfun createActivityPubUserUri(\n    userId: String = \"1\",\n    finger: String = \"@AtomZ@m.cmx.im\"\n): FormalUri = createActivityPubUri(\n    path = \"/user\",\n    queries = mapOf(\n        \"userId\" to userId,\n        \"finger\" to finger,\n    ),\n)\n"
  },
  {
    "path": "commonbiz/common/src/commonTest/kotlin/com/zhangke/fread/common/utils/HashtagTextUtilsTest.kt",
    "content": "package com.zhangke.fread.common.utils\n\nimport kotlin.test.Test\nimport kotlin.test.assertContentEquals\nimport androidx.compose.ui.text.TextRange\n\nclass HashtagTextUtilsTest {\n    @Test\n    fun testMastodonHashtags() {\n        // for Mastodon hashtag behavior see:\n        // https://github.com/mastodon/mastodon/blob/e89acc2302df49cbd7815b031e9c2939632bd204/app/javascript/mastodon/utils/hashtags.ts\n        assertContentEquals(\n            listOf(),\n            HashtagTextUtils.findHashtags(\"abc\", false)\n        )\n        assertContentEquals(\n            listOf(\n                TextRange(0, 4),\n            ),\n            HashtagTextUtils.findHashtags(\"#abc\", false)\n        )\n        assertContentEquals(\n            listOf(\n                TextRange(0, 4),\n            ),\n            HashtagTextUtils.findHashtags(\"#abc#def\", false)\n        )\n        assertContentEquals(\n            listOf(\n                TextRange(0, 4),\n                TextRange(5, 9),\n            ),\n            HashtagTextUtils.findHashtags(\"#abc #def\", false)\n        )\n        assertContentEquals(\n            listOf(\n                TextRange(1, 5),\n            ),\n            HashtagTextUtils.findHashtags(\"##abc\", false)\n        )\n        assertContentEquals(\n            listOf(\n                TextRange(0, 2),\n                TextRange(3, 7),\n            ),\n            HashtagTextUtils.findHashtags(\"#a##abc\", false)\n        )\n        assertContentEquals(\n            listOf(\n                TextRange(0, 2),\n                TextRange(3, 7),\n                TextRange(9, 11),\n            ),\n            HashtagTextUtils.findHashtags(\"#a!#b_c!##d\", false)\n        )\n        assertContentEquals(\n            listOf(),\n            HashtagTextUtils.findHashtags(\"foo #\", false)\n        )\n        assertContentEquals(\n            listOf(),\n            HashtagTextUtils.findHashtags(\"foo #_\", false)\n        )\n        assertContentEquals(\n            listOf(),\n            HashtagTextUtils.findHashtags(\"# a\", false)\n        )\n        assertContentEquals(\n            listOf(\n                TextRange(1, 3),\n            ),\n            HashtagTextUtils.findHashtags(\"(#a)\", false)\n        )\n        assertContentEquals(\n            listOf(),\n            HashtagTextUtils.findHashtags(\"()#a\", false)\n        )\n        assertContentEquals(\n            listOf(\n                TextRange(2, 4),\n            ),\n            HashtagTextUtils.findHashtags(\")(#a\", false)\n        )\n        assertContentEquals(\n            listOf(),\n            HashtagTextUtils.findHashtags(\"/#a\", false)\n        )\n        /*\n        // this will fail. the Mastodon client will process this as a hashtag, but this seems\n        // like fairly obscure or even unintended behavior\n        assertContentEquals(\n            listOf(\n                TextRange(0, 4),\n            ),\n            HashtagTextUtils.findHashtags(\"#___\", false)\n        )*/\n    }\n\n    @Test\n    fun testBlueskyHashtags() {\n        // for Bluesky hashtag behavior see:\n        // https://github.com/bluesky-social/atproto/blob/3cf5b31a2d8194dcfbfb8c3cc8e61282e48c9a82/packages/api/src/rich-text/util.ts#L10-L12\n        assertContentEquals(\n            listOf(),\n            HashtagTextUtils.findHashtags(\"abc\", true),\n        )\n        assertContentEquals(\n            listOf(\n                TextRange(0, 4),\n            ),\n            HashtagTextUtils.findHashtags(\"#abc\", true),\n        )\n        assertContentEquals(\n            listOf(\n                TextRange(0, 8),\n            ),\n            HashtagTextUtils.findHashtags(\"#abc#def\", true)\n        )\n        assertContentEquals(\n            listOf(\n                TextRange(0, 4),\n                TextRange(5, 9),\n            ),\n            HashtagTextUtils.findHashtags(\"#abc #def\", true)\n        )\n        assertContentEquals(\n            listOf(\n                TextRange(0, 5),\n            ),\n            HashtagTextUtils.findHashtags(\"##abc\", true)\n        )\n        assertContentEquals(\n            listOf(\n                TextRange(0, 7),\n            ),\n            HashtagTextUtils.findHashtags(\"#a##abc\", true)\n        )\n        assertContentEquals(\n            listOf(\n                TextRange(0, 11),\n            ),\n            HashtagTextUtils.findHashtags(\"#a!#b_c!##d\", true)\n        )\n        assertContentEquals(\n            listOf(),\n            HashtagTextUtils.findHashtags(\"foo #\", true)\n        )\n        assertContentEquals(\n            listOf(),\n            HashtagTextUtils.findHashtags(\"foo #_\", true)\n        )\n        assertContentEquals(\n            listOf(),\n            HashtagTextUtils.findHashtags(\"# a\", true)\n        )\n        assertContentEquals(\n            listOf(),\n            HashtagTextUtils.findHashtags(\"(#a)\", true)\n        )\n        assertContentEquals(\n            listOf(),\n            HashtagTextUtils.findHashtags(\"()#a\", true)\n        )\n        assertContentEquals(\n            listOf(),\n            HashtagTextUtils.findHashtags(\")(#a\", true)\n        )\n        assertContentEquals(\n            listOf(),\n            HashtagTextUtils.findHashtags(\"/#a\", true)\n        )\n        assertContentEquals(\n            listOf(\n                TextRange(0, 2),\n            ),\n            HashtagTextUtils.findHashtags(\"#a! b\", true)\n        )\n        assertContentEquals(\n            listOf(\n                TextRange(0, 2),\n            ),\n            HashtagTextUtils.findHashtags(\"#a!\", true)\n        )\n        assertContentEquals(\n            listOf(\n                TextRange(0, 4),\n            ),\n            HashtagTextUtils.findHashtags(\"#a!b\", true)\n        )\n        assertContentEquals(\n            listOf(\n                TextRange(0, 6),\n            ),\n            HashtagTextUtils.findHashtags(\"#a!b!c\", true)\n        )\n        assertContentEquals(\n            listOf(\n                TextRange(0, 6),\n            ),\n            HashtagTextUtils.findHashtags(\"#a!b!c!\", true)\n        )\n        assertContentEquals(\n            listOf(\n                TextRange(0, 5),\n            ),\n            HashtagTextUtils.findHashtags(\"#a!!b\", true)\n        )\n        assertContentEquals(\n            listOf(),\n            HashtagTextUtils.findHashtags(\"#!! \", true)\n        )\n        assertContentEquals(\n            listOf(\n                TextRange(0, 4),\n            ),\n            HashtagTextUtils.findHashtags(\"#!!a\", true)\n        )\n        assertContentEquals(\n            listOf(),\n            HashtagTextUtils.findHashtags(\"#\", true)\n        )\n        assertContentEquals(\n            listOf(\n                TextRange(0, 4)\n            ),\n            HashtagTextUtils.findHashtags(\"#a=b\", true)\n        )\n        assertContentEquals(\n            listOf(\n                TextRange(0, 2)\n            ),\n            HashtagTextUtils.findHashtags(\"#=\", true)\n        )\n        assertContentEquals(\n            listOf(),\n            HashtagTextUtils.findHashtags(\"a#b\", true)\n        )\n        assertContentEquals(\n            listOf(),\n            HashtagTextUtils.findHashtags(\"a#b c\", true)\n        )\n    }\n}\n"
  },
  {
    "path": "commonbiz/common/src/iosMain/kotlin/com/zhangke/fread/common/CommonIosModule.kt",
    "content": "package com.zhangke.fread.common\n\nimport androidx.room.Room\nimport androidx.sqlite.driver.bundled.BundledSQLiteDriver\nimport com.russhwolf.settings.NSUserDefaultsSettings\nimport com.russhwolf.settings.coroutines.FlowSettings\nimport com.russhwolf.settings.coroutines.toFlowSettings\nimport com.zhangke.fread.common.browser.IosSystemBrowserLauncher\nimport com.zhangke.fread.common.browser.SystemBrowserLauncher\nimport com.zhangke.fread.common.db.ContentConfigDatabases\nimport com.zhangke.fread.common.db.FreadContentDatabase\nimport com.zhangke.fread.common.db.MixedStatusDatabases\nimport com.zhangke.fread.common.db.old.OldFreadContentDatabase\nimport com.zhangke.fread.common.handler.TextHandler\nimport com.zhangke.fread.common.language.ActivityLanguageHelper\nimport com.zhangke.fread.common.utils.MediaFileHelper\nimport com.zhangke.fread.common.utils.PlatformUriHelper\nimport com.zhangke.fread.common.utils.StorageHelper\nimport com.zhangke.fread.common.utils.ToastHelper\nimport kotlinx.cinterop.ExperimentalForeignApi\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.IO\nimport org.koin.core.module.Module\nimport org.koin.core.module.dsl.singleOf\nimport org.koin.dsl.bind\nimport platform.Foundation.NSDocumentDirectory\nimport platform.Foundation.NSFileManager\nimport platform.Foundation.NSUserDefaults\nimport platform.Foundation.NSUserDomainMask\n\nactual fun Module.createPlatformModule() {\n    single<ContentConfigDatabases> {\n        val dbFilePath = getDBFilePath(ContentConfigDatabases.DB_NAME)\n        Room.databaseBuilder<ContentConfigDatabases>(\n            name = dbFilePath,\n        ).setDriver(BundledSQLiteDriver())\n            .setQueryCoroutineContext(Dispatchers.IO)\n            .build()\n    }\n    single<FreadContentDatabase> {\n        val dbFilePath = getDBFilePath(FreadContentDatabase.DB_NAME)\n        Room.databaseBuilder<FreadContentDatabase>(\n            name = dbFilePath,\n        ).setDriver(BundledSQLiteDriver())\n            .setQueryCoroutineContext(Dispatchers.IO)\n            .build()\n    }\n    single<OldFreadContentDatabase> {\n        val dbFilePath = getDBFilePath(OldFreadContentDatabase.DB_NAME)\n        Room.databaseBuilder<OldFreadContentDatabase>(\n            name = dbFilePath,\n        ).setDriver(BundledSQLiteDriver())\n            .setQueryCoroutineContext(Dispatchers.IO)\n            .build()\n    }\n    single<MixedStatusDatabases> {\n        val dbFilePath = getDBFilePath(MixedStatusDatabases.DB_NAME)\n        Room.databaseBuilder<MixedStatusDatabases>(\n            name = dbFilePath,\n        ).setDriver(BundledSQLiteDriver())\n            .setQueryCoroutineContext(Dispatchers.IO)\n            .build()\n    }\n\n    singleOf(::MediaFileHelper)\n    singleOf(::PlatformUriHelper)\n    singleOf(::StorageHelper)\n    singleOf(::ToastHelper)\n    singleOf(::ActivityLanguageHelper)\n    singleOf(::TextHandler)\n    singleOf(::IosSystemBrowserLauncher) bind SystemBrowserLauncher::class\n    single<FlowSettings> {\n        NSUserDefaultsSettings(NSUserDefaults.standardUserDefaults).toFlowSettings()\n    }.bind(FlowSettings::class)\n}\n\nprivate fun getDBFilePath(dbName: String): String {\n    return documentDirectory() + \"/$dbName\"\n}\n\n@OptIn(ExperimentalForeignApi::class)\nfun documentDirectory(): String {\n    val documentDirectory = NSFileManager.defaultManager.URLForDirectory(\n        directory = NSDocumentDirectory,\n        inDomain = NSUserDomainMask,\n        appropriateForURL = null,\n        create = false,\n        error = null,\n    )\n    return requireNotNull(documentDirectory?.path)\n}\n"
  },
  {
    "path": "commonbiz/common/src/iosMain/kotlin/com/zhangke/fread/common/browser/IosSystemBrowserLauncher.kt",
    "content": "package com.zhangke.fread.common.browser\n\nimport com.eygraber.uri.toNSURL\nimport com.zhangke.framework.utils.PlatformUri\nimport platform.SafariServices.SFSafariViewController\nimport platform.UIKit.UIApplication\nimport platform.UIKit.UIViewController\n\nclass IosSystemBrowserLauncher(\n    private val viewController: Lazy<UIViewController>,\n    private val application: UIApplication,\n) : SystemBrowserLauncher {\n\n    override fun launchBySystemBrowser(uri: PlatformUri) {\n        application.openURL(uri.toNSURL()!!)\n    }\n\n    override fun launchWebTabInApp(uri: PlatformUri) {\n        try {\n            val safari = SFSafariViewController(uri.toNSURL()!!)\n            // safari.modalPresentationStyle = UIModalPresentationPageSheet\n            viewController.value.presentViewController(safari, animated = true, completion = null)\n        } catch (e: Exception) {\n            e.printStackTrace()\n            launchBySystemBrowser(uri)\n        }\n    }\n}\n"
  },
  {
    "path": "commonbiz/common/src/iosMain/kotlin/com/zhangke/fread/common/browser/OAuthLauncher.ios.kt",
    "content": "package com.zhangke.fread.common.browser\n\nimport kotlinx.coroutines.suspendCancellableCoroutine\nimport platform.AuthenticationServices.ASWebAuthenticationPresentationContextProvidingProtocol\nimport platform.AuthenticationServices.ASWebAuthenticationSession\nimport platform.Foundation.NSError\nimport platform.Foundation.NSURL\nimport platform.Foundation.NSURLComponents\nimport platform.Foundation.NSURLQueryItem\nimport platform.UIKit.UIApplication\nimport platform.UIKit.UIWindow\nimport platform.darwin.NSObject\nimport kotlin.coroutines.resume\nimport kotlin.coroutines.resumeWithException\n\nactual class OAuthHandler (\n    private val application: UIApplication,\n) {\n    @Suppress(\"UNCHECKED_CAST\")\n    actual suspend fun startOAuth(url: String): String = suspendCancellableCoroutine { continuation ->\n        val webAuthSession = ASWebAuthenticationSession(\n            uRL = NSURL(string = url),\n            callbackURLScheme = \"freadapp\"\n        ) { callbackURL: NSURL?, error: NSError? ->\n            when {\n                error != null -> {\n                    when (error.code) {\n                        1L -> {\n                            continuation.cancel()\n                        }\n                        else -> {\n                            continuation.resumeWithException(RuntimeException(error.localizedDescription))\n                        }\n                    }\n                }\n                callbackURL != null -> {\n                    val components = NSURLComponents(uRL = callbackURL, resolvingAgainstBaseURL = false)\n                    val queryItems = components.queryItems as? List<NSURLQueryItem>\n                    val code = queryItems?.firstOrNull {\n                        it.name == \"code\"\n                    }?.value\n\n                    if (code != null) {\n                        continuation.resume(code)\n                    } else {\n                        continuation.resumeWithException(RuntimeException(\"No code received\"))\n                    }\n                }\n                else -> {\n                    continuation.cancel()\n                }\n            }\n        }\n\n        val contextProvider =\n            object : NSObject(), ASWebAuthenticationPresentationContextProvidingProtocol {\n                override fun presentationAnchorForWebAuthenticationSession(\n                    session: ASWebAuthenticationSession\n                ): UIWindow {\n                    return application.keyWindow!!\n                }\n\n                override fun description(): String {\n                    return \"ASWebAuthenticationPresentationContextProvider\"\n                }\n\n                override fun hash(): ULong {\n                    return hashCode().toULong()\n                }\n\n                override fun isEqual(`object`: Any?): Boolean {\n                    return this === `object`\n                }\n            }\n\n        webAuthSession.presentationContextProvider = contextProvider\n        webAuthSession.prefersEphemeralWebBrowserSession = false\n        webAuthSession.start()\n    }\n}"
  },
  {
    "path": "commonbiz/common/src/iosMain/kotlin/com/zhangke/fread/common/daynight/DayNightPlatformHelper.ios.kt",
    "content": "package com.zhangke.fread.common.daynight\n\nactual class DayNightPlatformHelper {\n\n\n    actual fun setDefaultMode(modeValue: DayNightMode) {\n\n    }\n\n    actual fun setMode(mode: DayNightMode) {\n\n    }\n\n    actual fun setAmoledMode(enabled: Boolean) {\n\n    }\n}\n"
  },
  {
    "path": "commonbiz/common/src/iosMain/kotlin/com/zhangke/fread/common/handler/TextHandler.ios.kt",
    "content": "package com.zhangke.fread.common.handler\n\nimport com.zhangke.fread.common.utils.SystemUtils\nimport platform.Foundation.NSBundle\nimport platform.Foundation.NSURL\nimport platform.UIKit.UIActivityViewController\nimport platform.UIKit.UIApplication\nimport platform.UIKit.UIPasteboard\n\nactual class TextHandler () {\n    actual val packageName: String\n        get() = NSBundle.mainBundle().bundleIdentifier().orEmpty()\n\n    actual val versionName: String\n        get() = NSBundle.mainBundle().infoDictionary()?.get(\"CFBundleShortVersionString\") as? String\n            ?: \"\"\n\n    actual val versionCode: String\n        get() = NSBundle.mainBundle().infoDictionary()?.get(\"CFBundleVersion\") as? String ?: \"\"\n\n    actual fun copyText(text: String) {\n        val pasteboard = UIPasteboard.generalPasteboard()\n        pasteboard.string = text\n    }\n\n    actual fun shareUrl(url: String, text: String) {\n        val items = listOf(url, text)\n        val activityViewController = UIActivityViewController(items, null)\n        val rootViewController = UIApplication.sharedApplication.keyWindow?.rootViewController\n        rootViewController?.presentViewController(\n            activityViewController,\n            animated = true,\n            completion = null\n        )\n    }\n\n    actual fun openSendEmail() {\n        val mailtoUrl = \"mailto:\"\n        val url = NSURL.URLWithString(mailtoUrl)\n        if (url != null && UIApplication.sharedApplication.canOpenURL(url)) {\n            UIApplication.sharedApplication.openURL(url)\n        }\n    }\n\n    actual fun openAppMarket() {\n        SystemUtils.openAppStore(packageName)\n    }\n}"
  },
  {
    "path": "commonbiz/common/src/iosMain/kotlin/com/zhangke/fread/common/language/LanguageHelper.ios.kt",
    "content": "package com.zhangke.fread.common.language\n\nimport com.zhangke.framework.architect.coroutines.ApplicationScope\nimport com.zhangke.fread.common.config.LocalConfigManager\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.runBlocking\n\nclass LanguageHelper (\n    private val localConfigManager: LocalConfigManager,\n) {\n\n    var currentLanguage = readLocalFromStorage()\n        private set\n\n    fun initialize() {\n\n    }\n\n    private fun readLocalFromStorage(): LanguageSettingItem {\n        return runBlocking {\n            localConfigManager.getString(LOCAL_KEY_LANGUAGE)\n                ?.let { LanguageSettingItem.fromLocalId(it) }\n                ?: LanguageSettingItem.FollowSystem\n        }\n    }\n\n    private fun saveLocalToStorage(item: LanguageSettingItem) {\n        ApplicationScope.launch {\n            localConfigManager.putString(LOCAL_KEY_LANGUAGE, item.localId)\n        }\n    }\n\n    fun setLanguage(item: LanguageSettingItem) {\n        currentLanguage = item\n        saveLocalToStorage(item)\n    }\n}\n\nactual class ActivityLanguageHelper(\n    private val languageHelper: LanguageHelper,\n) {\n\n    actual val currentLanguage get() = languageHelper.currentLanguage\n\n    actual fun initialize() {\n        languageHelper.initialize()\n    }\n\n    actual fun setLanguage(item: LanguageSettingItem) {\n        languageHelper.setLanguage(item)\n    }\n}"
  },
  {
    "path": "commonbiz/common/src/iosMain/kotlin/com/zhangke/fread/common/page/BasePagerTabHookManager.ios.kt",
    "content": "package com.zhangke.fread.common.page\n\ninternal actual fun findBasePagerTabImplementers(): List<BasePagerTabHook> {\n    // TODO: Not yet implemented\n    return emptyList()\n}"
  },
  {
    "path": "commonbiz/common/src/iosMain/kotlin/com/zhangke/fread/common/update/AppPlatformUpdater.ios.kt",
    "content": "package com.zhangke.fread.common.update\n\nimport com.zhangke.fread.common.utils.SystemUtils\nimport platform.Foundation.NSBundle\n\nactual class AppPlatformUpdater {\n\n    actual val platformName: String = \"ios\"\n\n    actual val signingForFDroid: Boolean = false\n\n    actual fun getAppVersionCode(): Long {\n        val versionCode =\n            NSBundle.mainBundle().infoDictionary()?.get(\"CFBundleVersion\") as? String ?: \"\"\n        return versionCode.toLongOrNull() ?: 0L\n    }\n\n    actual fun triggerUpdate(releaseInfo: AppReleaseInfo) {\n        SystemUtils.openAppStore(NSBundle.mainBundle().bundleIdentifier().orEmpty())\n    }\n}\n"
  },
  {
    "path": "commonbiz/common/src/iosMain/kotlin/com/zhangke/fread/common/utils/MediaFileHelper.ios.kt",
    "content": "package com.zhangke.fread.common.utils\n\n\nactual class MediaFileHelper () {\n    actual fun saveImageToGallery(url: String) {\n        TODO(\"Not yet implemented\")\n    }\n\n    actual fun saveVideoToGallery(url: String) {\n        TODO(\"Not yet implemented\")\n    }\n}"
  },
  {
    "path": "commonbiz/common/src/iosMain/kotlin/com/zhangke/fread/common/utils/PlatformUriHelper.ios.kt",
    "content": "package com.zhangke.fread.common.utils\n\nimport com.zhangke.framework.utils.ContentProviderFile\nimport com.zhangke.framework.utils.PlatformUri\nactual class PlatformUriHelper () {\n    actual suspend fun read(uri: PlatformUri): ContentProviderFile? {\n        TODO(\"Not yet implemented\")\n    }\n\n    actual suspend fun readBytes(uri: PlatformUri): ByteArray? {\n        TODO(\"Not yet implemented\")\n    }\n\n    actual fun queryFileName(uri: PlatformUri): String? {\n        TODO(\"Not yet implemented\")\n    }\n}"
  },
  {
    "path": "commonbiz/common/src/iosMain/kotlin/com/zhangke/fread/common/utils/RandomIdGenerator.ios.kt",
    "content": "package com.zhangke.fread.common.utils\n\nimport kotlinx.datetime.Clock\n\nactual class RandomIdGenerator {\n\n    actual fun generateId(): String {\n        // TODO get device info for generate id\n        return Clock.System.now().toString()\n    }\n}\n"
  },
  {
    "path": "commonbiz/common/src/iosMain/kotlin/com/zhangke/fread/common/utils/StorageHelper.ios.kt",
    "content": "package com.zhangke.fread.common.utils\n\nimport okio.Path\nimport okio.Path.Companion.toPath\nimport platform.Foundation.NSCachesDirectory\nimport platform.Foundation.NSSearchPathForDirectoriesInDomains\nimport platform.Foundation.NSUserDomainMask\n\nactual class StorageHelper () {\n    actual val cacheDir: Path\n        get() = getCacheDir().toPath()\n}\n\nprivate fun getCacheDir(): String {\n    return NSSearchPathForDirectoriesInDomains(\n        NSCachesDirectory,\n        NSUserDomainMask,\n        true,\n    ).first() as String\n}"
  },
  {
    "path": "commonbiz/common/src/iosMain/kotlin/com/zhangke/fread/common/utils/SystemUtils.kt",
    "content": "package com.zhangke.fread.common.utils\n\nimport platform.Foundation.NSURL\nimport platform.UIKit.UIApplication\n\nobject SystemUtils {\n\n    fun openAppStore(packageName: String) {\n        val appStoreUrl = \"https://apps.apple.com/cn/app/id${packageName}\"\n        val url = NSURL.URLWithString(appStoreUrl)\n        if (url != null && UIApplication.sharedApplication.canOpenURL(url)) {\n            UIApplication.sharedApplication.openURL(url)\n        }\n    }\n}\n"
  },
  {
    "path": "commonbiz/common/src/iosMain/kotlin/com/zhangke/fread/common/utils/ToastHelper.ios.kt",
    "content": "package com.zhangke.fread.common.utils\n\nactual class ToastHelper () {\n    actual fun showToast(content: String) {\n        TODO(\"Not yet implemented\")\n    }\n}"
  },
  {
    "path": "commonbiz/sharedscreen/.gitignore",
    "content": "/build"
  },
  {
    "path": "commonbiz/sharedscreen/build.gradle.kts",
    "content": "plugins {\n    id(\"fread.project.framework.kmp\")\n    id(\"com.google.devtools.ksp\")\n    id(\"kotlin-parcelize\")\n    alias(libs.plugins.room)\n}\n\nandroid {\n    namespace = \"com.zhangke.fread.commonbiz.shared.screen\"\n}\n\nkotlin {\n    sourceSets {\n        commonMain {\n            dependencies {\n                implementation(project(path = \":framework\"))\n                implementation(project(path = \":commonbiz:common\"))\n                implementation(project(path = \":bizframework:status-provider\"))\n                implementation(project(path = \":commonbiz:status-ui\"))\n                implementation(project(\":commonbiz:analytics\"))\n\n                implementation(compose.components.resources)\n\n                implementation(libs.androidx.room)\n                implementation(libs.compose.jb.backhandler)\n\n                implementation(libs.kotlinx.serialization.core)\n                implementation(libs.kotlinx.serialization.json)\n\n                implementation(libs.jetbrains.lifecycle.viewmodel)\n\n                implementation(libs.imageLoader)\n\n                implementation(libs.krouter.runtime)\n\n                implementation(libs.androidx.paging.common)\n            }\n        }\n        commonTest {\n            dependencies {\n                implementation(kotlin(\"test\"))\n            }\n        }\n        androidMain {\n            dependencies {\n                implementation(libs.androidx.core.ktx)\n                implementation(libs.androidx.appcompat)\n                implementation(libs.androidx.annotation)\n                implementation(libs.bundles.androidx.fragment)\n                implementation(libs.bundles.androidx.activity)\n                implementation(libs.bundles.androidx.preference)\n                implementation(libs.bundles.androidx.datastore)\n                implementation(libs.bundles.androidx.collection)\n                implementation(libs.androidx.browser)\n\n                implementation(libs.auto.service.annotations)\n            }\n        }\n    }\n}\n\ndependencies {\n    kspAll(libs.androidx.room.compiler)\n    add(\"kspAndroid\", libs.auto.service.ksp)\n    kspAll(libs.krouter.collecting.compiler)\n}\n\ncompose {\n    resources {\n        publicResClass = true\n        packageOfResClass = \"com.zhangke.fread.commonbiz.shared.screen\"\n        generateResClass = always\n    }\n}\n\nroom {\n    schemaDirectory(\"$projectDir/schemas\")\n}"
  },
  {
    "path": "commonbiz/sharedscreen/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguardFiles setting in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n\n# Uncomment this to preserve the line number information for\n# debugging stack traces.\n#-keepattributes SourceFile,LineNumberTable\n\n# If you keep the line number information, uncomment this to\n# hide the original source file name.\n#-renamesourcefileattribute SourceFile"
  },
  {
    "path": "commonbiz/sharedscreen/src/androidMain/kotlin/com/zhangke/fread/commonbiz/shared/SharedScreenAndroidEntryProvider.kt",
    "content": "package com.zhangke.fread.commonbiz.shared\n\nimport androidx.navigation3.runtime.EntryProviderScope\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.nav.NavEntryProvider\nimport kotlinx.serialization.modules.PolymorphicModuleBuilder\n\nclass SharedScreenAndroidEntryProvider : NavEntryProvider {\n\n    override fun EntryProviderScope<NavKey>.build() {\n    }\n\n    override fun PolymorphicModuleBuilder<NavKey>.polymorph() {\n    }\n}\n"
  },
  {
    "path": "commonbiz/sharedscreen/src/androidMain/kotlin/com/zhangke/fread/commonbiz/shared/SharedScreenAndroidModule.kt",
    "content": "package com.zhangke.fread.commonbiz.shared\n\nimport androidx.room.Room\nimport com.zhangke.framework.nav.NavEntryProvider\nimport com.zhangke.fread.commonbiz.shared.db.SelectedAccountPublishingDatabase\nimport org.koin.android.ext.koin.androidContext\nimport org.koin.core.module.Module\nimport org.koin.core.module.dsl.factoryOf\nimport org.koin.dsl.bind\n\nactual fun Module.createPlatformModule() {\n    single<SelectedAccountPublishingDatabase> {\n        Room.databaseBuilder(\n            androidContext(),\n            SelectedAccountPublishingDatabase::class.java,\n            SelectedAccountPublishingDatabase.DB_NAME,\n        ).build()\n    }\n    factoryOf(::SharedScreenAndroidEntryProvider) bind NavEntryProvider::class\n}\n"
  },
  {
    "path": "commonbiz/sharedscreen/src/androidMain/kotlin/com/zhangke/fread/commonbiz/shared/composable/WebViewPreviewer.android.kt",
    "content": "package com.zhangke.fread.commonbiz.shared.composable\n\nimport android.annotation.SuppressLint\nimport android.webkit.WebResourceRequest\nimport android.webkit.WebView\nimport android.webkit.WebViewClient\nimport androidx.compose.material3.LocalContentColor\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.toArgb\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.viewinterop.AndroidView\nimport com.zhangke.framework.utils.dpToPx\nimport com.zhangke.framework.utils.toPlatformUri\nimport com.zhangke.fread.common.browser.LocalActivityBrowserLauncher\nimport com.zhangke.fread.common.browser.launchWebTabInApp\n\n\n@Composable\nactual fun WebViewPreviewer(\n    html: String,\n    modifier: Modifier,\n) {\n    val browserLauncher = LocalActivityBrowserLauncher.current\n    val density = LocalDensity.current\n    val fontColor = LocalContentColor.current.toArgb()\n    val coroutineScope = rememberCoroutineScope()\n    AndroidView(\n        modifier = modifier,\n        factory = {\n            WebView(it).apply {\n                this.setBackgroundColor(Color.Transparent.toArgb())\n                settings.defaultFontSize = 16.dp.dpToPx(density).toInt()\n                @SuppressLint(\"SetJavaScriptEnabled\")\n                settings.javaScriptEnabled = true\n                webViewClient = object : WebViewClient() {\n                    override fun shouldOverrideUrlLoading(\n                        view: WebView?,\n                        request: WebResourceRequest,\n                    ): Boolean {\n                        browserLauncher.launchWebTabInApp(coroutineScope, request.url.toPlatformUri())\n                        return true\n                    }\n                }\n                settings.loadWithOverviewMode = true\n                settings.useWideViewPort = true\n            }\n        },\n        update = {\n            val finalHtml = warpBlogContentHtml(html, fontColor)\n            it.loadDataWithBaseURL(\"\", finalHtml, \"text/html\", \"UTF-8\", null)\n        },\n    )\n}\n\nprivate fun warpBlogContentHtml(\n    html: String,\n    fontColor: Int,\n): String {\n    val colorString = String.format(\"#%06X\", 0xFFFFFF and fontColor)\n    return \"\"\"\n        <html>\n        <head>\n        <style>\n        body {\n            color: ${colorString};\n        }\n        </style>\n        </head>\n        <body>\n        $html\n        </body>\n        </html>\n    \"\"\".trimIndent()\n}"
  },
  {
    "path": "commonbiz/sharedscreen/src/androidMain/kotlin/com/zhangke/fread/commonbiz/shared/screen/ImageViewerScreen.android.kt",
    "content": "package com.zhangke.fread.commonbiz.shared.screen\n\nimport com.seiko.imageloader.model.ImageResult\nimport com.zhangke.framework.utils.aspectRatio\n\ninternal actual fun ImageResult.aspectRatio(): Float? {\n    return when (this) {\n        is ImageResult.OfBitmap -> bitmap.width.toFloat() / bitmap.height\n        is ImageResult.OfImage -> image.drawable.aspectRatio()\n        else -> null\n    }\n}"
  },
  {
    "path": "commonbiz/sharedscreen/src/commonMain/kotlin/com/zhangke/fread/commonbiz/shared/ModuleScreenVisitor.kt",
    "content": "package com.zhangke.fread.commonbiz.shared\n\nimport androidx.compose.runtime.ProvidableCompositionLocal\nimport androidx.compose.runtime.compositionLocalOf\nimport androidx.navigation3.runtime.NavKey\n\nclass ModuleScreenVisitor(\n    val feedsScreenVisitor: IFeedsScreenVisitor,\n    val profileScreenVisitor: IProfileScreenVisitor,\n)\n\ninterface IFeedsScreenVisitor {\n\n    fun getAddContentScreen(): NavKey\n}\n\ninterface IProfileScreenVisitor {\n\n    fun getDonateScreen(): NavKey\n}\n\nval LocalModuleScreenVisitor: ProvidableCompositionLocal<ModuleScreenVisitor> =\n    compositionLocalOf { error(\"ModuleScreenVisitor not init!\") }\n"
  },
  {
    "path": "commonbiz/sharedscreen/src/commonMain/kotlin/com/zhangke/fread/commonbiz/shared/SharedScreenModule.kt",
    "content": "package com.zhangke.fread.commonbiz.shared\n\nimport com.zhangke.framework.nav.NavEntryProvider\nimport com.zhangke.fread.commonbiz.shared.blog.detail.RssBlogDetailViewModel\nimport com.zhangke.fread.commonbiz.shared.repo.SelectedAccountPublishingRepo\nimport com.zhangke.fread.commonbiz.shared.screen.publish.multi.MultiAccountPublishingViewModel\nimport com.zhangke.fread.commonbiz.shared.screen.status.account.SelectAccountOpenStatusViewModel\nimport com.zhangke.fread.commonbiz.shared.screen.status.context.StatusContextViewModel\nimport com.zhangke.fread.commonbiz.shared.usecase.PublishPostOnMultiAccountUseCase\nimport com.zhangke.fread.commonbiz.shared.usecase.RefactorToNewBlogUseCase\nimport com.zhangke.fread.commonbiz.shared.usecase.RefactorToNewStatusUseCase\nimport org.koin.core.module.Module\nimport org.koin.core.module.dsl.factoryOf\nimport org.koin.core.module.dsl.singleOf\nimport org.koin.core.module.dsl.viewModelOf\nimport org.koin.dsl.bind\nimport org.koin.dsl.module\n\nval sharedScreenModule = module {\n\n    createPlatformModule()\n\n    factoryOf(::SharedScreenNavEntryProvider) bind NavEntryProvider::class\n\n    factoryOf(::SelectedAccountPublishingRepo)\n    factoryOf(::RefactorToNewBlogUseCase)\n    factoryOf(::RefactorToNewStatusUseCase)\n    factoryOf(::PublishPostOnMultiAccountUseCase)\n\n    viewModelOf(::StatusContextViewModel)\n    viewModelOf(::RssBlogDetailViewModel)\n    viewModelOf(::MultiAccountPublishingViewModel)\n    viewModelOf(::SelectAccountOpenStatusViewModel)\n\n    singleOf(::ModuleScreenVisitor)\n}\n\nexpect fun Module.createPlatformModule()\n"
  },
  {
    "path": "commonbiz/sharedscreen/src/commonMain/kotlin/com/zhangke/fread/commonbiz/shared/SharedScreenNavEntryProvider.kt",
    "content": "package com.zhangke.fread.commonbiz.shared\n\nimport androidx.navigation3.runtime.EntryProviderScope\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.architect.json.globalJson\nimport com.zhangke.framework.nav.NavEntryProvider\nimport com.zhangke.fread.commonbiz.shared.blog.detail.RssBlogDetailScreen\nimport com.zhangke.fread.commonbiz.shared.blog.detail.RssBlogDetailScreenNavKey\nimport com.zhangke.fread.commonbiz.shared.screen.ImageViewerScreen\nimport com.zhangke.fread.commonbiz.shared.screen.ImageViewerScreenNavKey\nimport com.zhangke.fread.commonbiz.shared.screen.SelectLanguageScreen\nimport com.zhangke.fread.commonbiz.shared.screen.SelectLanguageScreenNavKey\nimport com.zhangke.fread.commonbiz.shared.screen.publish.PublishBlogScreen\nimport com.zhangke.fread.commonbiz.shared.screen.publish.PublishBlogScreenNavKey\nimport com.zhangke.fread.commonbiz.shared.screen.publish.multi.MultiAccountPublishingScreen\nimport com.zhangke.fread.commonbiz.shared.screen.publish.multi.MultiAccountPublishingScreenKey\nimport com.zhangke.fread.commonbiz.shared.screen.status.context.StatusContextScreen\nimport com.zhangke.fread.commonbiz.shared.screen.status.context.StatusContextScreenNavKey\nimport com.zhangke.fread.commonbiz.shared.screen.video.FullVideoScreen\nimport com.zhangke.fread.commonbiz.shared.screen.video.FullVideoScreenNavKey\nimport kotlinx.serialization.modules.PolymorphicModuleBuilder\nimport kotlinx.serialization.modules.subclass\nimport org.koin.compose.viewmodel.koinViewModel\nimport org.koin.core.parameter.parametersOf\n\nclass SharedScreenNavEntryProvider : NavEntryProvider {\n\n    override fun EntryProviderScope<NavKey>.build() {\n        entry<ImageViewerScreenNavKey> {\n            ImageViewerScreen(\n                selectedIndex = it.selectedIndex,\n                imageList = it.imageList,\n            )\n        }\n        entry<SelectLanguageScreenNavKey> {\n            SelectLanguageScreen(\n                selectedLanguages = it.selectedLanguages,\n                maxSelectCount = it.maxSelectCount,\n            )\n        }\n        entry<PublishBlogScreenNavKey> {\n            PublishBlogScreen()\n        }\n        entry<MultiAccountPublishingScreenKey> {\n            val list = globalJson.decodeFromString<List<String>>(it.userUrisJson)\n            MultiAccountPublishingScreen(\n                viewModel = koinViewModel { parametersOf(list) }\n            )\n        }\n        entry<StatusContextScreenNavKey> {\n            StatusContextScreen(\n                locator = it.locator,\n                serializedStatus = it.serializedStatus,\n                serializedBlog = it.serializedBlog,\n                blogId = it.blogId,\n                platform = it.platform,\n                blogTranslationUiState = it.blogTranslationUiState,\n                containerViewModel = koinViewModel(),\n            )\n        }\n        entry<RssBlogDetailScreenNavKey> {\n            RssBlogDetailScreen(\n                serializedBlog = it.serializedBlog,\n                viewModel = koinViewModel(),\n            )\n        }\n        entry<FullVideoScreenNavKey> {\n            FullVideoScreen(it.uri)\n        }\n    }\n\n    override fun PolymorphicModuleBuilder<NavKey>.polymorph() {\n        subclass(ImageViewerScreenNavKey::class)\n        subclass(SelectLanguageScreenNavKey::class)\n        subclass(PublishBlogScreenNavKey::class)\n        subclass(MultiAccountPublishingScreenKey::class)\n        subclass(StatusContextScreenNavKey::class)\n        subclass(RssBlogDetailScreenNavKey::class)\n        subclass(FullVideoScreenNavKey::class)\n    }\n}\n"
  },
  {
    "path": "commonbiz/sharedscreen/src/commonMain/kotlin/com/zhangke/fread/commonbiz/shared/blog/detail/RssBlogDetailScreen.kt",
    "content": "@file:OptIn(ExperimentalTime::class)\n\npackage com.zhangke.fread.commonbiz.shared.blog.detail\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.OpenInBrowser\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.produceState\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.architect.json.globalJson\nimport com.zhangke.framework.composable.ConsumeOpenScreenFlow\nimport com.zhangke.framework.composable.SimpleIconButton\nimport com.zhangke.framework.composable.Toolbar\nimport com.zhangke.framework.composable.currentOrThrow\nimport com.zhangke.framework.ktx.ifNullOrEmpty\nimport com.zhangke.framework.nav.LocalNavBackStack\nimport com.zhangke.fread.common.browser.LocalActivityBrowserLauncher\nimport com.zhangke.fread.commonbiz.shared.composable.WebViewPreviewer\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.blog.Blog\nimport com.zhangke.fread.status.model.BlogTranslationUiState\nimport com.zhangke.fread.status.ui.StatusInfoLine\nimport com.zhangke.fread.status.ui.style.StatusStyles\nimport com.zhangke.fread.status.utils.DateTimeFormatter\nimport kotlinx.coroutines.launch\nimport kotlinx.serialization.Serializable\nimport org.jetbrains.compose.resources.stringResource\nimport kotlin.time.ExperimentalTime\n\n@Serializable\ndata class RssBlogDetailScreenNavKey(val serializedBlog: String) : NavKey\n\n@Composable\nfun RssBlogDetailScreen(\n    serializedBlog: String,\n    viewModel: RssBlogDetailViewModel,\n) {\n    val blog: Blog = remember { globalJson.decodeFromString(serializedBlog) }\n    val navigator = LocalNavBackStack.currentOrThrow\n    val browserLauncher = LocalActivityBrowserLauncher.current\n    val coroutineScope = rememberCoroutineScope()\n    ConsumeOpenScreenFlow(viewModel.openScreenFlow)\n    Scaffold(\n        topBar = {\n            Toolbar(\n                title = blog.title.ifNullOrEmpty {\n                    stringResource(LocalizedString.sharedStatusContextScreenTitle)\n                },\n                onBackClick = navigator::removeLastOrNull,\n                actions = {\n                    SimpleIconButton(\n                        onClick = {\n                            coroutineScope.launch {\n                                browserLauncher.launchWebTabInApp(\n                                    blog.url,\n                                    checkAppSupportPage = false\n                                )\n                            }\n                        },\n                        imageVector = Icons.Default.OpenInBrowser,\n                        contentDescription = \"Open In Browser\",\n                    )\n                }\n            )\n        }\n    ) { innerPaddings ->\n        Column(\n            modifier = Modifier\n                .padding(innerPaddings)\n                .fillMaxSize()\n                .verticalScroll(rememberScrollState())\n                .background(MaterialTheme.colorScheme.surface),\n        ) {\n            Spacer(modifier = Modifier.height(16.dp))\n            val displayTime by produceState(\"\", blog.createAt) {\n                value =\n                    DateTimeFormatter.format(blog.createAt.instant.toEpochMilliseconds())\n            }\n            StatusInfoLine(\n                modifier = Modifier.fillMaxWidth(),\n                blog = blog,\n                isOwner = false,\n                visibility = blog.visibility,\n                displayTime = displayTime,\n                style = StatusStyles.medium(),\n                onInteractive = { _, _ -> },\n                onUserInfoClick = viewModel::onUserInfoClick,\n                onUrlClick = {\n                    coroutineScope.launch {\n                        browserLauncher.launchWebTabInApp(it)\n                    }\n                },\n                blogTranslationState = BlogTranslationUiState(support = false),\n                editedAt = blog.editedAt?.instant,\n                showOpenBlogWithOtherAccountBtn = false,\n                allowToShowFollowButton = false,\n                onTranslateClick = {},\n            )\n            WebViewPreviewer(\n                modifier = Modifier\n                    .padding(16.dp)\n                    .fillMaxSize(),\n                html = blog.content,\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "commonbiz/sharedscreen/src/commonMain/kotlin/com/zhangke/fread/commonbiz/shared/blog/detail/RssBlogDetailViewModel.kt",
    "content": "package com.zhangke.fread.commonbiz.shared.blog.detail\n\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.fread.status.StatusProvider\nimport com.zhangke.fread.status.author.BlogAuthor\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.asSharedFlow\nimport kotlinx.coroutines.launch\n\nclass RssBlogDetailViewModel (\n    private val statusProvider: StatusProvider,\n) : ViewModel() {\n\n    private val _openScreenFlow = MutableSharedFlow<NavKey>()\n    val openScreenFlow = _openScreenFlow.asSharedFlow()\n\n    fun onUserInfoClick(author: BlogAuthor) {\n        viewModelScope.launch {\n            statusProvider.screenProvider\n                .getUserDetailScreenWithoutAccount(author.uri)\n                ?.let { _openScreenFlow.emit(it) }\n        }\n    }\n}\n"
  },
  {
    "path": "commonbiz/sharedscreen/src/commonMain/kotlin/com/zhangke/fread/commonbiz/shared/composable/BlogUi.kt",
    "content": "package com.zhangke.fread.commonbiz.shared.composable\n\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport com.zhangke.framework.composable.currentOrThrow\nimport com.zhangke.framework.nav.LocalNavBackStack\nimport com.zhangke.fread.common.browser.LocalActivityBrowserLauncher\nimport com.zhangke.fread.status.blog.Blog\nimport com.zhangke.fread.status.model.BlogTranslationUiState\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.model.StatusVisibility\nimport com.zhangke.fread.status.ui.BlogUi\nimport com.zhangke.fread.status.ui.ComposedStatusInteraction\nimport com.zhangke.fread.status.ui.getStatusTopLabel\nimport com.zhangke.fread.status.ui.style.StatusStyle\nimport com.zhangke.fread.status.ui.threads.ThreadsType\nimport kotlinx.coroutines.launch\n\n@Composable\nfun BlogUi(\n    modifier: Modifier,\n    blog: Blog,\n    locator: PlatformLocator,\n    indexInList: Int,\n    sharedElementId: String? = null,\n    style: StatusStyle,\n    showBottomPanel: Boolean,\n    showMoreOperationIcon: Boolean,\n    composedStatusInteraction: ComposedStatusInteraction,\n) {\n    val browserLauncher = LocalActivityBrowserLauncher.current\n    val backState = LocalNavBackStack.currentOrThrow\n    val fixedThreadType = if (blog.isReply) {\n        ThreadsType.CONTINUED_THREAD\n    } else {\n        ThreadsType.NONE\n    }\n    var continueThreadHeight: Int? by remember { mutableStateOf(null) }\n    val coroutineScope = rememberCoroutineScope()\n    Box(modifier = modifier) {\n        BlogUi(\n            modifier = Modifier,\n            blog = blog,\n            logged = null,\n            isOwner = null,\n            blogTranslationState = BlogTranslationUiState.DEFAULT,\n            indexInList = indexInList,\n            sharedElementId = sharedElementId,\n            showMoreOperationIcon = showMoreOperationIcon,\n            style = style,\n            threadsType = fixedThreadType,\n            continueThreadLabelHeight = continueThreadHeight,\n            topLabels = getStatusTopLabel(\n                isReblog = false,\n                pinned = blog.pinned,\n                isReply = blog.isReply,\n                author = blog.author,\n                mentionOnly = blog.visibility == StatusVisibility.DIRECT,\n                style = style,\n                threadsType = fixedThreadType,\n                onUserInfoClick = {\n                    composedStatusInteraction.onUserInfoClick(locator, it)\n                },\n                onContinueThreadHeightChanged = { continueThreadHeight = it }\n            ),\n            onInteractive = { type, _ -> },\n            onUserInfoClick = {\n                composedStatusInteraction.onUserInfoClick(locator, it)\n            },\n            onMediaClick = { event ->\n                onStatusMediaClick(\n                    navigator = backState,\n                    event = event,\n                )\n            },\n            showDivider = false,\n            showBottomPanel = showBottomPanel,\n            onVoted = {},\n            onHashtagInStatusClick = {\n                composedStatusInteraction.onHashtagInStatusClick(locator, it)\n            },\n            onMentionClick = {\n                composedStatusInteraction.onMentionClick(locator, it)\n            },\n            onMentionDidClick = {\n                composedStatusInteraction.onMentionClick(\n                    locator = locator,\n                    did = it,\n                    protocol = blog.platform.protocol,\n                )\n            },\n            onUrlClick = {\n                coroutineScope.launch {\n                    browserLauncher.launchWebTabInApp(it, locator)\n                }\n            },\n            onShowOriginalClick = {},\n            onTranslateClick = {},\n            onBlogClick = {\n                composedStatusInteraction.onBlockClick(locator, it)\n            },\n            onMaybeHashtagClick = {\n                composedStatusInteraction.onMaybeHashtagClick(\n                    locator = locator,\n                    protocol = blog.platform.protocol,\n                    hashtag = it,\n                )\n            },\n        )\n    }\n}\n"
  },
  {
    "path": "commonbiz/sharedscreen/src/commonMain/kotlin/com/zhangke/fread/commonbiz/shared/composable/FeedsContent.kt",
    "content": "package com.zhangke.fread.commonbiz.shared.composable\n\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.MutableState\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.input.nestedscroll.NestedScrollConnection\nimport androidx.compose.ui.unit.dp\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.composable.ConsumeFlow\nimport com.zhangke.framework.composable.ConsumeOpenScreenFlow\nimport com.zhangke.framework.composable.LocalContentPadding\nimport com.zhangke.framework.composable.TextString\nimport com.zhangke.framework.composable.applyNestedScrollConnection\nimport com.zhangke.framework.composable.textString\nimport com.zhangke.framework.loadable.lazycolumn.LoadableInlineVideoLazyColumn\nimport com.zhangke.framework.loadable.lazycolumn.rememberLoadableInlineVideoLazyColumnState\nimport com.zhangke.framework.utils.LoadState\nimport com.zhangke.fread.common.composable.EmptyContent\nimport com.zhangke.fread.common.composable.EmptyContentType\nimport com.zhangke.fread.common.composable.ErrorContent\nimport com.zhangke.fread.common.composable.ErrorType\nimport com.zhangke.fread.commonbiz.shared.feeds.CommonFeedsUiState\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.account.isAuthenticationFailure\nimport com.zhangke.fread.status.model.StatusUiState\nimport com.zhangke.fread.status.ui.ComposedStatusInteraction\nimport com.zhangke.fread.status.ui.StatusListPlaceholder\nimport com.zhangke.fread.status.ui.common.LocalNestedTabConnection\nimport com.zhangke.fread.status.ui.common.NewStatusNotifyBar\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.flow.SharedFlow\nimport kotlinx.coroutines.launch\nimport org.jetbrains.compose.resources.stringResource\nimport kotlin.time.Duration.Companion.seconds\n\n@Composable\nfun FeedsContent(\n    uiState: CommonFeedsUiState,\n    openScreenFlow: SharedFlow<NavKey>,\n    newStatusNotifyFlow: SharedFlow<Unit>?,\n    onRefresh: () -> Unit,\n    onLoadMore: () -> Unit,\n    composedStatusInteraction: ComposedStatusInteraction,\n    nestedScrollConnection: NestedScrollConnection? = null,\n    observeScrollToTopEvent: Boolean = false,\n    onScrollToTopConsumed: (() -> Unit)? = null,\n    contentCanScrollBackward: MutableState<Boolean>? = null,\n    onImmersiveEvent: ((immersive: Boolean) -> Unit)? = null,\n    onScrollInProgress: ((Boolean) -> Unit)? = null,\n    onLoginClick: (() -> Unit)? = null,\n) {\n    ConsumeOpenScreenFlow(openScreenFlow)\n    FeedsContent(\n        feeds = uiState.feeds,\n        refreshing = uiState.refreshing,\n        loadMoreState = uiState.loadMoreState,\n        showPagingLoadingPlaceholder = uiState.showPagingLoadingPlaceholder,\n        pageErrorContent = uiState.pageErrorContent,\n        newStatusNotifyFlow = newStatusNotifyFlow,\n        onRefresh = onRefresh,\n        onLoadMore = onLoadMore,\n        composedStatusInteraction = composedStatusInteraction,\n        nestedScrollConnection = nestedScrollConnection,\n        observeScrollToTopEvent = observeScrollToTopEvent,\n        onScrollToTopConsumed = onScrollToTopConsumed,\n        contentCanScrollBackward = contentCanScrollBackward,\n        onImmersiveEvent = onImmersiveEvent,\n        onScrollInProgress = onScrollInProgress,\n        onLoginClick = onLoginClick,\n    )\n}\n\n@Composable\nfun FeedsContent(\n    feeds: List<StatusUiState>,\n    refreshing: Boolean,\n    loadMoreState: LoadState,\n    showPagingLoadingPlaceholder: Boolean,\n    pageErrorContent: Throwable?,\n    newStatusNotifyFlow: SharedFlow<Unit>?,\n    onRefresh: () -> Unit,\n    onLoadMore: () -> Unit,\n    composedStatusInteraction: ComposedStatusInteraction,\n    nestedScrollConnection: NestedScrollConnection? = null,\n    observeScrollToTopEvent: Boolean = false,\n    onScrollToTopConsumed: (() -> Unit)? = null,\n    contentCanScrollBackward: MutableState<Boolean>? = null,\n    onImmersiveEvent: ((immersive: Boolean) -> Unit)? = null,\n    onScrollInProgress: ((Boolean) -> Unit)? = null,\n    onLoginClick: (() -> Unit)? = null,\n) {\n    if (feeds.isEmpty()) {\n        if (showPagingLoadingPlaceholder) {\n            StatusListPlaceholder()\n        } else if (pageErrorContent != null) {\n            InitErrorContent(\n                error = pageErrorContent,\n                onLoginClick = onLoginClick,\n                onRetryClick = onRefresh,\n            )\n        } else {\n            EmptyListContent()\n        }\n    } else {\n        Box(modifier = Modifier.fillMaxSize()) {\n            val state = rememberLoadableInlineVideoLazyColumnState(\n                onRefresh = onRefresh,\n                onLoadMore = onLoadMore,\n            )\n            val lazyListState = state.lazyListState\n            if (contentCanScrollBackward != null) {\n                val canScrollBackward by remember {\n                    derivedStateOf {\n                        lazyListState.firstVisibleItemIndex != 0 || lazyListState.firstVisibleItemScrollOffset != 0\n                    }\n                }\n                contentCanScrollBackward.value = canScrollBackward\n            }\n            if (onImmersiveEvent != null) {\n                ObserveForImmersive(\n                    listState = lazyListState,\n                    onImmersiveEvent = onImmersiveEvent,\n                )\n            }\n            if (onScrollInProgress != null) {\n                LaunchedEffect(lazyListState.isScrollInProgress) {\n                    onScrollInProgress(lazyListState.isScrollInProgress)\n                }\n            }\n            val feedsConnection = LocalNestedTabConnection.current\n            if (observeScrollToTopEvent) {\n                LaunchedEffect(feedsConnection, lazyListState) {\n                    feedsConnection.scrollToTopFlow.collect {\n                        if (lazyListState.layoutInfo.totalItemsCount > 0) {\n                            lazyListState.scrollToItem(0)\n                        }\n                        onScrollToTopConsumed?.invoke()\n                    }\n                }\n            }\n            LoadableInlineVideoLazyColumn(\n                modifier = Modifier\n                    .fillMaxSize()\n                    .applyNestedScrollConnection(nestedScrollConnection),\n                state = state,\n                refreshing = refreshing,\n                loadState = loadMoreState,\n            ) {\n                itemsIndexed(feeds) { index, item ->\n                    FeedsStatusNode(\n                        modifier = Modifier.fillMaxWidth(),\n                        status = item,\n                        composedStatusInteraction = composedStatusInteraction,\n                        indexInList = index,\n                    )\n                }\n            }\n            var showNewStatusNotifyBar by remember {\n                mutableStateOf(false)\n            }\n            if (newStatusNotifyFlow != null) {\n                ConsumeFlow(newStatusNotifyFlow) {\n                    delay(1000)\n                    if (state.lazyListState.firstVisibleItemIndex > 0) {\n                        showNewStatusNotifyBar = true\n                    }\n                    delay(20.seconds)\n                }\n            }\n            val coroutineScope = rememberCoroutineScope()\n            AnimatedVisibility(\n                modifier = Modifier\n                    .padding(LocalContentPadding.current)\n                    .padding(top = 32.dp)\n                    .align(Alignment.TopCenter),\n                visible = showNewStatusNotifyBar,\n            ) {\n                NewStatusNotifyBar(\n                    modifier = Modifier,\n                    onClick = {\n                        showNewStatusNotifyBar = false\n                        coroutineScope.launch {\n                            if (feeds.isNotEmpty()) {\n                                state.lazyListState.animateScrollToItem(0)\n                                onScrollToTopConsumed?.invoke()\n                            }\n                        }\n                    },\n                )\n            }\n        }\n    }\n}\n\n@Composable\nfun InitErrorContent(\n    errorMessage: TextString,\n    onRetryClick: (() -> Unit)? = null,\n) {\n    InitErrorContent(\n        errorMessage = textString(text = errorMessage),\n        onRetryClick = onRetryClick,\n    )\n}\n\n@Composable\nfun InitErrorContent(\n    errorMessage: String,\n    onRetryClick: (() -> Unit)? = null,\n) {\n    ErrorContent(\n        modifier = Modifier.padding(LocalContentPadding.current).fillMaxSize(),\n        type = ErrorType.Network,\n        errorMessage = errorMessage,\n        onRetryClick = onRetryClick ?: {},\n    )\n}\n\n@Composable\nfun InitErrorContent(\n    error: Throwable,\n    onLoginClick: (() -> Unit)? = null,\n    onRetryClick: (() -> Unit)? = null,\n) {\n    if (onLoginClick != null && error.isAuthenticationFailure) {\n        NotLoginPageError(\n            modifier = Modifier.padding(LocalContentPadding.current),\n            message = error.message,\n            onLoginClick = onLoginClick,\n        )\n    } else {\n        InitErrorContent(\n            errorMessage = error.message.orEmpty(),\n            onRetryClick = onRetryClick,\n        )\n    }\n}\n\n@Composable\nfun NotLoginPageError(\n    modifier: Modifier,\n    message: String?,\n    onLoginClick: () -> Unit,\n) {\n    EmptyContent(\n        modifier = modifier.fillMaxSize(),\n        type = EmptyContentType.Account,\n        contentTitle = stringResource(LocalizedString.profileAccountNotLogin),\n        subtitle = message,\n        onClick = onLoginClick,\n    )\n}\n\n@Composable\nfun EmptyListContent() {\n    EmptyContent(\n        modifier = Modifier.fillMaxSize().padding(LocalContentPadding.current),\n        type = EmptyContentType.Content,\n        contentTitle = stringResource(LocalizedString.listContentEmptyPlaceholder),\n        subtitle = null,\n        onClick = null,\n    )\n}\n"
  },
  {
    "path": "commonbiz/sharedscreen/src/commonMain/kotlin/com/zhangke/fread/commonbiz/shared/composable/FeedsStatusNode.kt",
    "content": "package com.zhangke.fread.commonbiz.shared.composable\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport com.zhangke.framework.composable.currentOrThrow\nimport com.zhangke.framework.nav.LocalNavBackStack\nimport com.zhangke.fread.commonbiz.shared.screen.status.account.SelectAccountOpenStatusBottomSheet\nimport com.zhangke.fread.commonbiz.shared.screen.status.account.rememberSelectAccountOpenStatusSheetState\nimport com.zhangke.fread.status.model.StatusUiState\nimport com.zhangke.fread.status.ui.ComposedStatusInteraction\nimport com.zhangke.fread.status.ui.StatusUi\nimport com.zhangke.fread.status.ui.style.LocalStatusUiConfig\nimport com.zhangke.fread.status.ui.style.StatusStyle\n\n@Composable\nfun FeedsStatusNode(\n    modifier: Modifier = Modifier,\n    status: StatusUiState,\n    indexInList: Int,\n    composedStatusInteraction: ComposedStatusInteraction,\n    showDivider: Boolean = true,\n    sharedElementId: String? = null,\n    style: StatusStyle = LocalStatusUiConfig.current.contentStyle,\n) {\n    val backStack = LocalNavBackStack.currentOrThrow\n    val selectAccountOpenStatusBottomSheetState = rememberSelectAccountOpenStatusSheetState()\n    StatusUi(\n        modifier = modifier.clickable {\n            composedStatusInteraction.onStatusClick(status)\n        },\n        status = status,\n        indexInList = indexInList,\n        sharedElementId = sharedElementId,\n        style = style,\n        showDivider = showDivider,\n        composedStatusInteraction = composedStatusInteraction,\n        onMediaClick = { event ->\n            onStatusMediaClick(\n                navigator = backStack,\n                event = event,\n            )\n        },\n        onOpenBlogWithOtherAccountClick = {\n            selectAccountOpenStatusBottomSheetState.show(it)\n        },\n    )\n    SelectAccountOpenStatusBottomSheet(selectAccountOpenStatusBottomSheetState)\n}\n"
  },
  {
    "path": "commonbiz/sharedscreen/src/commonMain/kotlin/com/zhangke/fread/commonbiz/shared/composable/ObserveForFeedsConnection.kt",
    "content": "package com.zhangke.fread.commonbiz.shared.composable\n\nimport androidx.compose.foundation.lazy.LazyListState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.rememberCoroutineScope\nimport com.zhangke.framework.composable.ScrollDirection\nimport com.zhangke.framework.composable.rememberDirectionalLazyListState\nimport com.zhangke.fread.status.ui.common.LocalNestedTabConnection\nimport kotlinx.coroutines.launch\n\n@Composable\nfun ObserveForFeedsConnection(\n    listState: LazyListState,\n    onRefresh: () -> Unit,\n) {\n    val mainTabConnection = LocalNestedTabConnection.current\n    val coroutineScope = rememberCoroutineScope()\n    LaunchedEffect(listState, mainTabConnection.scrollToTopFlow) {\n        mainTabConnection.scrollToTopFlow\n            .collect {\n                if (listState.layoutInfo.totalItemsCount > 0) {\n                    coroutineScope.launch {\n                        listState.animateScrollToItem(0)\n                    }\n                }\n            }\n    }\n    LaunchedEffect(listState.isScrollInProgress) {\n        mainTabConnection.updateContentScrollInProgress(listState.isScrollInProgress)\n    }\n    ObserveForImmersive(\n        listState = listState,\n        onImmersiveEvent = {\n            if (it) {\n                mainTabConnection.openImmersiveMode(coroutineScope)\n            } else {\n                mainTabConnection.closeImmersiveMode(coroutineScope)\n            }\n        }\n    )\n    LaunchedEffect(mainTabConnection.refreshFlow) {\n        mainTabConnection.refreshFlow.collect {\n            onRefresh()\n        }\n    }\n}\n\n@Composable\nfun ObserveForImmersive(\n    listState: LazyListState,\n    onImmersiveEvent: (immersive: Boolean) -> Unit,\n) {\n    val directional = rememberDirectionalLazyListState(listState).scrollDirection\n    LaunchedEffect(directional) {\n        if (directional == ScrollDirection.Down) {\n            onImmersiveEvent(true)\n        } else if (directional == ScrollDirection.Up) {\n            onImmersiveEvent(false)\n        }\n    }\n}\n"
  },
  {
    "path": "commonbiz/sharedscreen/src/commonMain/kotlin/com/zhangke/fread/commonbiz/shared/composable/OnBlogMediaClick.kt",
    "content": "package com.zhangke.fread.commonbiz.shared.composable\n\nimport androidx.navigation3.runtime.NavBackStack\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.fread.commonbiz.shared.screen.ImageViewerImage\nimport com.zhangke.fread.commonbiz.shared.screen.ImageViewerScreenNavKey\nimport com.zhangke.fread.commonbiz.shared.screen.video.FullVideoScreenNavKey\nimport com.zhangke.fread.status.blog.BlogMediaType\nimport com.zhangke.fread.status.blog.asImageMetaOrNull\nimport com.zhangke.fread.status.ui.image.BlogMediaClickEvent\nimport com.zhangke.fread.status.ui.image.ClickedBlogMedia\n\nfun onStatusMediaClick(\n    navigator: NavBackStack<NavKey>,\n    event: BlogMediaClickEvent,\n) {\n    when (event) {\n        is BlogMediaClickEvent.BlogImageClickEvent -> {\n            if (event.mediaList[event.index].media.type == BlogMediaType.GIFV) {\n                navigator.add(FullVideoScreenNavKey(event.mediaList[event.index].media.url))\n                return\n            }\n            navigator.add(\n                ImageViewerScreenNavKey(\n                    imageList = event.mediaList.toImages(),\n                    selectedIndex = event.index,\n                )\n            )\n        }\n\n        is BlogMediaClickEvent.BlogVideoClickEvent -> {\n            navigator.add(FullVideoScreenNavKey(event.media.url))\n        }\n    }\n}\n\nfun ClickedBlogMedia.toImage(): ImageViewerImage {\n    val media = this.media\n    return ImageViewerImage(\n        url = media.url,\n        previewUrl = media.previewUrl,\n        description = media.description,\n        blurhash = media.blurhash,\n        aspect = media.meta?.asImageMetaOrNull()?.original?.aspect,\n        sharedElementKey = this.sharedElementKey,\n    )\n}\n\nfun List<ClickedBlogMedia>.toImages(): List<ImageViewerImage> {\n    return this.map { it.toImage() }\n}\n"
  },
  {
    "path": "commonbiz/sharedscreen/src/commonMain/kotlin/com/zhangke/fread/commonbiz/shared/composable/SearchResultUi.kt",
    "content": "package com.zhangke.fread.commonbiz.shared.composable\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.ui.Modifier\nimport com.zhangke.fread.common.browser.LocalActivityBrowserLauncher\nimport com.zhangke.fread.common.browser.launchWebTabInApp\nimport com.zhangke.fread.common.status.model.SearchResultUiState\nimport com.zhangke.fread.status.ui.BlogAuthorUi\nimport com.zhangke.fread.status.ui.ComposedStatusInteraction\nimport com.zhangke.fread.status.ui.hashtag.HashtagUi\nimport com.zhangke.fread.status.ui.source.BlogPlatformUi\n\n@Composable\nfun SearchResultUi(\n    searchResult: SearchResultUiState,\n    modifier: Modifier = Modifier,\n    indexInList: Int,\n    composedStatusInteraction: ComposedStatusInteraction,\n) {\n    val browserLauncher = LocalActivityBrowserLauncher.current\n    val coroutineScope = rememberCoroutineScope()\n    when (searchResult) {\n        is SearchResultUiState.Platform -> {\n            BlogPlatformUi(\n                modifier = modifier,\n                platform = searchResult.platform,\n            )\n        }\n\n        is SearchResultUiState.SearchedStatus -> {\n            FeedsStatusNode(\n                modifier = modifier,\n                status = searchResult.status,\n                indexInList = indexInList,\n                composedStatusInteraction = composedStatusInteraction,\n            )\n        }\n\n        is SearchResultUiState.SearchedHashtag -> {\n            HashtagUi(\n                modifier = modifier,\n                tag = searchResult.hashtag,\n                onClick = {\n                    composedStatusInteraction.onHashtagClick(searchResult.locator, it)\n                },\n            )\n        }\n\n        is SearchResultUiState.Author -> {\n            BlogAuthorUi(\n                modifier = modifier,\n                author = searchResult.author,\n                onClick = {\n                    composedStatusInteraction.onUserInfoClick(searchResult.locator, it)\n                },\n                onUrlClick = {\n                    browserLauncher.launchWebTabInApp(coroutineScope, it, searchResult.locator)\n                }\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "commonbiz/sharedscreen/src/commonMain/kotlin/com/zhangke/fread/commonbiz/shared/composable/UserInfoCard.kt",
    "content": "package com.zhangke.fread.commonbiz.shared.composable\n\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.aspectRatio\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.material3.Card\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport com.seiko.imageloader.ui.AutoSizeImage\nimport com.zhangke.framework.composable.VerticalIndentLayout\nimport com.zhangke.framework.utils.HighlightTextBuildUtil\nimport com.zhangke.framework.utils.formatToHumanReadable\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.author.BlogAuthor\nimport com.zhangke.fread.status.richtext.RichText\nimport com.zhangke.fread.status.ui.BlogAuthorAvatar\nimport com.zhangke.fread.status.ui.common.RelationshipStateButton\nimport com.zhangke.fread.status.ui.richtext.SelectableRichText\nimport com.zhangke.fread.status.ui.user.UserHandleLine\nimport com.zhangke.fread.statusui.Res\nimport com.zhangke.fread.statusui.img_banner_background\nimport org.jetbrains.compose.resources.painterResource\nimport org.jetbrains.compose.resources.stringResource\n\nprivate const val BANNER_ASPECT = 3F\n\n@Composable\nfun UserInfoCard(\n    modifier: Modifier,\n    user: BlogAuthor,\n    onUserClick: (BlogAuthor) -> Unit,\n    onFollowAccountClick: (BlogAuthor) -> Unit,\n    onUnfollowAccountClick: (BlogAuthor) -> Unit,\n    onUnblockClick: (BlogAuthor) -> Unit,\n    onCancelFollowRequestClick: (BlogAuthor) -> Unit,\n) {\n    BasicProfileCard(\n        modifier = modifier,\n        banner = user.banner.orEmpty(),\n        avatar = user.avatar.orEmpty(),\n        nickName = user.humanizedName,\n        prettyHandle = user.prettyHandle,\n        bot = user.bot,\n        showActiveState = false,\n        description = user.humanizedDescription,\n        followersCount = user.followersCount,\n        followingCount = user.followingCount,\n        statusesCount = user.statusesCount,\n        actionButton = if (user.relationships != null) {\n            {\n                RelationshipStateButton(\n                    modifier = Modifier,\n                    relationship = user.relationships!!,\n                    onFollowClick = { onFollowAccountClick(user) },\n                    onUnfollowClick = { onUnfollowAccountClick(user) },\n                    onUnblockClick = { onUnblockClick(user) },\n                    onCancelFollowRequestClick = { onCancelFollowRequestClick(user) },\n                )\n            }\n        } else {\n            null\n        },\n        onProfileClick = { onUserClick(user) },\n    )\n}\n\n@Composable\nfun UserInfoCard(\n    modifier: Modifier,\n    user: BlogAuthor,\n    showActiveState: Boolean,\n    onUserClick: (BlogAuthor) -> Unit,\n    actionButton: (@Composable () -> Unit)?,\n    bottomPanel: (@Composable () -> Unit)? = null,\n) {\n    BasicProfileCard(\n        modifier = modifier,\n        banner = user.banner.orEmpty(),\n        avatar = user.avatar.orEmpty(),\n        nickName = user.humanizedName,\n        prettyHandle = user.prettyHandle,\n        bot = user.bot,\n        showActiveState = showActiveState,\n        description = user.humanizedDescription,\n        followersCount = user.followersCount,\n        followingCount = user.followingCount,\n        statusesCount = user.statusesCount,\n        actionButton = actionButton,\n        onProfileClick = { onUserClick(user) },\n        bottomPanel = bottomPanel,\n    )\n}\n\n@Composable\nfun BasicProfileCard(\n    modifier: Modifier,\n    banner: String,\n    avatar: String,\n    nickName: RichText,\n    prettyHandle: String,\n    bot: Boolean,\n    description: RichText,\n    followersCount: Long?,\n    followingCount: Long?,\n    statusesCount: Long?,\n    actionButton: (@Composable () -> Unit)?,\n    showActiveState: Boolean,\n    onProfileClick: () -> Unit,\n    bottomPanel: (@Composable () -> Unit)? = null,\n) {\n    Box(modifier = modifier.clickable { onProfileClick() }) {\n        Card(\n            modifier = Modifier.fillMaxWidth(),\n        ) {\n            val avatarContainerHeight = 74.dp\n            val overLapHeight = 22.dp\n            val avatarSize = 68.dp\n            VerticalIndentLayout(\n                modifier = Modifier.fillMaxWidth(),\n                indentHeight = overLapHeight,\n                headerContent = {\n                    Box(\n                        modifier = Modifier.fillMaxWidth().aspectRatio(BANNER_ASPECT),\n                    ) {\n                        Image(\n                            modifier = Modifier.fillMaxSize(),\n                            painter = painterResource(Res.drawable.img_banner_background),\n                            contentDescription = null,\n                            contentScale = ContentScale.Crop,\n                        )\n                        AutoSizeImage(\n                            modifier = Modifier.fillMaxSize(),\n                            url = banner,\n                            contentScale = ContentScale.Crop,\n                            contentDescription = null,\n                            placeholderPainter = {\n                                painterResource(Res.drawable.img_banner_background)\n                            },\n                            errorPainter = {\n                                painterResource(Res.drawable.img_banner_background)\n                            },\n                        )\n                    }\n                },\n                indentContent = {\n                    Column(\n                        modifier = Modifier.fillMaxWidth()\n                            .padding(start = 8.dp, end = 8.dp, bottom = 8.dp),\n                    ) {\n                        Box(\n                            modifier = Modifier.fillMaxWidth()\n                                .height(avatarContainerHeight)\n                                .padding(start = 8.dp),\n                        ) {\n                            BlogAuthorAvatar(\n                                modifier = Modifier\n                                    .clip(CircleShape)\n                                    .border(\n                                        width = 2.dp,\n                                        color = MaterialTheme.colorScheme.surfaceContainerHighest,\n                                        shape = CircleShape,\n                                    )\n                                    .size(avatarSize),\n                                imageUrl = avatar,\n                            )\n                            Column(\n                                modifier = Modifier.padding(start = avatarSize, top = overLapHeight)\n                                    .padding(start = 8.dp, top = 1.dp)\n                            ) {\n                                Row(\n                                    modifier = Modifier,\n                                    verticalAlignment = Alignment.CenterVertically,\n                                ) {\n                                    SelectableRichText(\n                                        modifier = Modifier,\n                                        richText = nickName,\n                                        maxLines = 1,\n                                        overflow = TextOverflow.Ellipsis,\n                                        fontSize = 18.sp,\n                                        onUrlClick = {},\n                                    )\n                                    if (showActiveState) {\n                                        Box(\n                                            modifier = Modifier.padding(start = 4.dp)\n                                                .size(6.dp)\n                                                .clip(CircleShape)\n                                                .background(Color(0xFF22C55E))\n                                        )\n                                    }\n                                }\n                                UserHandleLine(\n                                    modifier = Modifier.padding(top = 1.dp),\n                                    handle = prettyHandle,\n                                    bot = bot,\n                                    followedBy = false,\n                                )\n                            }\n                        }\n\n                        SelectableRichText(\n                            modifier = Modifier\n                                .padding(\n                                    start = 16.dp,\n                                    end = 16.dp,\n                                )\n                                .fillMaxWidth(),\n                            richText = description,\n                            onUrlClick = {},\n                            fontSize = 14.sp,\n                            onMaybeHashtagClick = {},\n                        )\n\n                        Row(\n                            modifier = Modifier.fillMaxWidth().padding(start = 16.dp),\n                            verticalAlignment = Alignment.CenterVertically,\n                        ) {\n                            if (followingCount != null && followersCount != null) {\n                                UserFollowInfo(\n                                    modifier = Modifier.weight(1F),\n                                    followersCount = followersCount,\n                                    followingCount = followingCount,\n                                    statusesCount = statusesCount,\n                                )\n                            } else {\n                                Spacer(modifier = Modifier.weight(1F))\n                            }\n                            if (actionButton != null) {\n                                actionButton()\n                            } else {\n                                Box(modifier = Modifier.size(height = 28.dp, width = 8.dp))\n                            }\n                        }\n                        if (bottomPanel != null) {\n                            bottomPanel()\n                        }\n                    }\n                }\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun UserFollowInfo(\n    modifier: Modifier,\n    followersCount: Long?,\n    followingCount: Long?,\n    statusesCount: Long?,\n) {\n    val followerText = stringResource(LocalizedString.statusUiUserDetailFollowerInfo)\n    val followingText = stringResource(LocalizedString.statusUiUserDetailFollowingInfo)\n    val statusText = stringResource(LocalizedString.statusUiUserDetailPosts)\n    val infoText = remember(followingCount, followersCount, statusesCount) {\n        buildString {\n            if (followersCount != null) {\n                append(HighlightTextBuildUtil.HIGHLIGHT_START_SYMBOL)\n                append(followersCount.formatToHumanReadable())\n                append(HighlightTextBuildUtil.HIGHLIGHT_END_SYMBOL)\n                append(' ')\n                append(followerText)\n            }\n            if (followingCount != null) {\n                if (followersCount != null) {\n                    append(\" · \")\n                }\n                append(HighlightTextBuildUtil.HIGHLIGHT_START_SYMBOL)\n                append(followingCount.formatToHumanReadable())\n                append(HighlightTextBuildUtil.HIGHLIGHT_END_SYMBOL)\n                append(' ')\n                append(followingText)\n            }\n            if (statusesCount != null) {\n                if (followersCount != null || followingCount != null) {\n                    append(\" · \")\n                }\n                append(HighlightTextBuildUtil.HIGHLIGHT_START_SYMBOL)\n                append(statusesCount.formatToHumanReadable())\n                append(HighlightTextBuildUtil.HIGHLIGHT_END_SYMBOL)\n                append(' ')\n                append(statusText)\n            }\n        }.let {\n            HighlightTextBuildUtil.buildHighlightText(\n                text = it,\n                fontWeight = FontWeight.Bold,\n            )\n        }\n    }\n    Text(\n        text = infoText,\n        modifier = modifier,\n        style = MaterialTheme.typography.bodySmall,\n        maxLines = 1,\n        overflow = TextOverflow.Ellipsis,\n    )\n}\n"
  },
  {
    "path": "commonbiz/sharedscreen/src/commonMain/kotlin/com/zhangke/fread/commonbiz/shared/composable/WebViewPreviewer.kt",
    "content": "package com.zhangke.fread.commonbiz.shared.composable\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\n\n@Composable\nexpect fun WebViewPreviewer(\n    html: String,\n    modifier: Modifier = Modifier,\n)\n"
  },
  {
    "path": "commonbiz/sharedscreen/src/commonMain/kotlin/com/zhangke/fread/commonbiz/shared/db/SelectedAccountPublishingDatabase.kt",
    "content": "package com.zhangke.fread.commonbiz.shared.db\n\nimport androidx.room.ConstructedBy\nimport androidx.room.Dao\nimport androidx.room.Database\nimport androidx.room.Entity\nimport androidx.room.Insert\nimport androidx.room.PrimaryKey\nimport androidx.room.Query\nimport androidx.room.RoomDatabase\nimport androidx.room.RoomDatabaseConstructor\n\nprivate const val TABLE_NAME = \"selected_account_publishing\"\n\n@Entity(tableName = TABLE_NAME)\ndata class SelectedAccountPublishing(\n    @PrimaryKey val accountUri: String\n)\n\n@Dao\ninterface SelectedAccountPublishingDao {\n\n    @Query(\"SELECT * FROM $TABLE_NAME\")\n    suspend fun getAll(): List<SelectedAccountPublishing>\n\n    @Insert\n    suspend fun insert(items: List<SelectedAccountPublishing>)\n\n    @Query(\"DELETE FROM $TABLE_NAME\")\n    suspend fun deleteTable()\n\n}\n\n@Database(\n    entities = [SelectedAccountPublishing::class],\n    version = 1,\n    exportSchema = false,\n)\n@ConstructedBy(SelectedAccountPublishingDatabaseConstructor::class)\nabstract class SelectedAccountPublishingDatabase : RoomDatabase() {\n\n    abstract fun dao(): SelectedAccountPublishingDao\n\n    companion object {\n\n        const val DB_NAME = \"SelectedAccountPublishing.db\"\n    }\n}\n\n// The Room compiler generates the `actual` implementations.\n@Suppress(\"NO_ACTUAL_FOR_EXPECT\")\nexpect object SelectedAccountPublishingDatabaseConstructor :\n    RoomDatabaseConstructor<SelectedAccountPublishingDatabase> {\n    override fun initialize(): SelectedAccountPublishingDatabase\n}\n\n"
  },
  {
    "path": "commonbiz/sharedscreen/src/commonMain/kotlin/com/zhangke/fread/commonbiz/shared/feeds/CommonFeedsUiState.kt",
    "content": "package com.zhangke.fread.commonbiz.shared.feeds\n\nimport com.zhangke.framework.utils.LoadState\nimport com.zhangke.fread.status.model.StatusUiState\n\ndata class CommonFeedsUiState(\n    val feeds: List<StatusUiState>,\n    val showPagingLoadingPlaceholder: Boolean,\n    val pageErrorContent: Throwable?,\n    val refreshing: Boolean,\n    val loadMoreState: LoadState,\n)\n"
  },
  {
    "path": "commonbiz/sharedscreen/src/commonMain/kotlin/com/zhangke/fread/commonbiz/shared/feeds/FeedsViewModelController.kt",
    "content": "package com.zhangke.fread.commonbiz.shared.feeds\n\nimport com.zhangke.framework.collections.container\nimport com.zhangke.framework.composable.emitTextMessageFromThrowable\nimport com.zhangke.framework.composable.toTextStringOrNull\nimport com.zhangke.framework.utils.LoadState\nimport com.zhangke.fread.common.adapter.StatusUiStateAdapter\nimport com.zhangke.fread.common.feeds.model.RefreshResult\nimport com.zhangke.fread.common.status.StatusConfigurationDefault\nimport com.zhangke.fread.common.status.StatusUpdater\nimport com.zhangke.fread.commonbiz.shared.usecase.RefactorToNewStatusUseCase\nimport com.zhangke.fread.status.StatusProvider\nimport com.zhangke.fread.status.author.BlogAuthor\nimport com.zhangke.fread.status.blog.Blog\nimport com.zhangke.fread.status.blog.BlogPoll\nimport com.zhangke.fread.status.model.Hashtag\nimport com.zhangke.fread.status.model.HashtagInStatus\nimport com.zhangke.fread.status.model.Mention\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.model.StatusActionType\nimport com.zhangke.fread.status.model.StatusProviderProtocol\nimport com.zhangke.fread.status.model.StatusUiState\nimport com.zhangke.fread.status.model.updateStatus\nimport com.zhangke.fread.status.platform.BlogPlatform\nimport com.zhangke.fread.status.richtext.preParse\nimport com.zhangke.fread.status.status.model.Status\nimport com.zhangke.fread.status.ui.ComposedStatusInteraction\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.update\nimport kotlinx.coroutines.launch\n\nclass FeedsViewModelController(\n    statusProvider: StatusProvider,\n    private val statusUiStateAdapter: StatusUiStateAdapter,\n    statusUpdater: StatusUpdater,\n    refactorToNewStatus: RefactorToNewStatusUseCase,\n) : IFeedsViewModelController {\n\n    private lateinit var coroutineScope: CoroutineScope\n    private lateinit var locatorResolver: (Status) -> PlatformLocator\n    private lateinit var loadFirstPageLocalFeeds: suspend () -> Result<List<StatusUiState>>\n    private lateinit var loadNewFromServerFunction: suspend () -> Result<RefreshResult>\n    private lateinit var loadMoreFunction: suspend (maxId: String) -> Result<List<StatusUiState>>\n    private lateinit var onStatusUpdate: suspend (Status) -> Unit\n\n    private val interactiveHandler = InteractiveHandler(\n        statusProvider = statusProvider,\n        statusUpdater = statusUpdater,\n        statusUiStateAdapter = statusUiStateAdapter,\n        refactorToNewStatus = refactorToNewStatus,\n    )\n\n    override val mutableUiState = MutableStateFlow(\n        CommonFeedsUiState(\n            feeds = emptyList(),\n            showPagingLoadingPlaceholder = false,\n            pageErrorContent = null,\n            refreshing = false,\n            loadMoreState = LoadState.Idle,\n        )\n    )\n\n    override val mutableNewStatusNotifyFlow = MutableSharedFlow<Unit>()\n    override val mutableErrorMessageFlow = interactiveHandler.mutableErrorMessageFlow\n    override val errorMessageFlow = interactiveHandler.errorMessageFlow\n    override val mutableOpenScreenFlow = interactiveHandler.mutableOpenScreenFlow\n\n    override val composedStatusInteraction: ComposedStatusInteraction\n        get() = interactiveHandler.composedStatusInteraction\n\n    private var initFeedsJob: Job? = null\n    private var refreshJob: Job? = null\n    private var loadMoreJob: Job? = null\n    private var autoFetchNewerFeedsJob: Job? = null\n\n    override fun initController(\n        coroutineScope: CoroutineScope,\n        locatorResolver: (Status) -> PlatformLocator,\n        loadFirstPageLocalFeeds: suspend () -> Result<List<StatusUiState>>,\n        loadNewFromServerFunction: suspend () -> Result<RefreshResult>,\n        loadMoreFunction: suspend (maxId: String) -> Result<List<StatusUiState>>,\n        onStatusUpdate: suspend (Status) -> Unit\n    ) {\n        this.coroutineScope = coroutineScope\n        this.locatorResolver = locatorResolver\n        this.loadFirstPageLocalFeeds = loadFirstPageLocalFeeds\n        this.loadNewFromServerFunction = loadNewFromServerFunction\n        this.loadMoreFunction = loadMoreFunction\n        this.onStatusUpdate = onStatusUpdate\n        interactiveHandler.initInteractiveHandler(\n            coroutineScope = coroutineScope,\n            onInteractiveHandleResult = {\n                it.handleResult()\n            },\n        )\n    }\n\n    override fun initFeeds(needLocalData: Boolean) {\n        initFeedsJob?.cancel()\n        initFeedsJob = coroutineScope.launch {\n            mutableUiState.update {\n                it.copy(\n                    showPagingLoadingPlaceholder = true,\n                    pageErrorContent = null,\n                    feeds = emptyList(),\n                )\n            }\n            if (needLocalData) {\n                loadFirstPageLocalFeeds()\n                    .map { list ->\n                        list.preParse()\n                        list\n                    }\n                    .onSuccess { localStatus ->\n                        if (localStatus.isNotEmpty()) {\n                            mutableUiState.update { state ->\n                                state.copy(\n                                    feeds = localStatus,\n                                    showPagingLoadingPlaceholder = false,\n                                )\n                            }\n                        }\n                    }\n            }\n            loadNewFromServerFunction()\n                .map { result ->\n                    val newStatus = result.newStatus\n                    newStatus.preParse()\n                    newStatus\n                }\n                .onFailure {\n                    mutableUiState.update { state ->\n                        state.copy(\n                            showPagingLoadingPlaceholder = false,\n                            pageErrorContent = if (state.feeds.isEmpty()) {\n                                it\n                            } else {\n                                null\n                            },\n                        )\n                    }\n                    if (mutableUiState.value.feeds.isNotEmpty()) {\n                        mutableErrorMessageFlow.emitTextMessageFromThrowable(it)\n                    }\n                }.onSuccess {\n                    mutableUiState.update { state ->\n                        state.copy(\n                            feeds = it,\n                            showPagingLoadingPlaceholder = false,\n                        )\n                    }\n                }\n        }\n    }\n\n    override fun startAutoFetchNewerFeeds() {\n        if (autoFetchNewerFeedsJob != null) return\n        autoFetchNewerFeedsJob = coroutineScope.launch {\n            while (true) {\n                delay(StatusConfigurationDefault.config.autoFetchNewerFeedsInterval)\n                autoFetchNewerFeeds()\n            }\n        }\n    }\n\n    private suspend fun autoFetchNewerFeeds() {\n        loadNewFromServerFunction()\n            .map {\n                it.newStatus.preParse()\n                it\n            }\n            .onSuccess {\n                val oldFirstId = mutableUiState.value.feeds.firstOrNull()?.status?.id\n                val newFirstId = it.newStatus.firstOrNull()?.status?.id\n                mutableUiState.update { state ->\n                    state.copy(\n                        feeds = state.feeds.applyRefreshResult(it),\n                    )\n                }\n\n                if (it.newStatus.isNotEmpty() && oldFirstId != newFirstId) {\n                    mutableNewStatusNotifyFlow.emit(Unit)\n                }\n            }\n    }\n\n    override fun initInteractiveHandler(\n        coroutineScope: CoroutineScope,\n        onInteractiveHandleResult: suspend (InteractiveHandleResult) -> Unit\n    ) {\n        interactiveHandler.initInteractiveHandler(\n            coroutineScope = coroutineScope,\n            onInteractiveHandleResult = onInteractiveHandleResult,\n        )\n    }\n\n    override fun onStatusInteractive(\n        status: StatusUiState,\n        type: StatusActionType\n    ) {\n        interactiveHandler.onStatusInteractive(status, type)\n    }\n\n    override fun onUserInfoClick(locator: PlatformLocator, blogAuthor: BlogAuthor) {\n        interactiveHandler.onUserInfoClick(locator, blogAuthor)\n    }\n\n    override fun onStatusClick(status: StatusUiState) {\n        interactiveHandler.onStatusClick(status)\n    }\n\n    override fun onBlogClick(locator: PlatformLocator, blog: Blog) {\n        interactiveHandler.onBlogClick(locator, blog)\n    }\n\n    override fun onBlogIdClick(locator: PlatformLocator, platform: BlogPlatform, blogId: String) {\n        interactiveHandler.onBlogIdClick(locator, platform, blogId)\n    }\n\n    override fun onVoted(status: StatusUiState, votedOption: List<BlogPoll.Option>) {\n        interactiveHandler.onVoted(status, votedOption)\n    }\n\n    override fun onFollowClick(locator: PlatformLocator, target: BlogAuthor) {\n        interactiveHandler.onFollowClick(locator, target)\n    }\n\n    override fun onUnfollowClick(locator: PlatformLocator, target: BlogAuthor) {\n        interactiveHandler.onUnfollowClick(locator, target)\n    }\n\n    override fun onMentionClick(locator: PlatformLocator, mention: Mention) {\n        interactiveHandler.onMentionClick(locator, mention)\n    }\n\n    override fun onMentionClick(\n        locator: PlatformLocator,\n        did: String,\n        protocol: StatusProviderProtocol\n    ) {\n        interactiveHandler.onMentionClick(locator, did, protocol)\n    }\n\n    override fun onHashtagClick(locator: PlatformLocator, tag: HashtagInStatus) {\n        interactiveHandler.onHashtagClick(locator, tag)\n    }\n\n    override fun onHashtagClick(locator: PlatformLocator, tag: Hashtag) {\n        interactiveHandler.onHashtagClick(locator, tag)\n    }\n\n    override fun onMaybeHashtagClick(\n        locator: PlatformLocator,\n        protocol: StatusProviderProtocol,\n        tag: String,\n    ) {\n        interactiveHandler.onMaybeHashtagClick(locator, protocol, tag)\n    }\n\n    override fun onRefresh() {\n        val uiState = mutableUiState.value\n        if (uiState.showPagingLoadingPlaceholder || uiState.refreshing || uiState.loadMoreState.loading) return\n        refreshJob?.cancel()\n        refreshJob = coroutineScope.launch {\n            mutableUiState.update { it.copy(refreshing = true) }\n            loadNewFromServerFunction()\n                .onSuccess { refreshResult ->\n                    mutableUiState.update {\n                        it.copy(\n                            refreshing = false,\n                            feeds = it.feeds.applyRefreshResult(refreshResult),\n                        )\n                    }\n                }.onFailure { e ->\n                    mutableErrorMessageFlow.emitTextMessageFromThrowable(e)\n                    mutableUiState.update {\n                        it.copy(refreshing = false)\n                    }\n                }\n        }\n    }\n\n    override fun onLoadMore() {\n        val uiState = mutableUiState.value\n        if (uiState.showPagingLoadingPlaceholder || uiState.refreshing || uiState.loadMoreState.loading) return\n        val feeds = uiState.feeds\n        if (feeds.isEmpty()) return\n        loadMoreJob?.cancel()\n        loadMoreJob = coroutineScope.launch {\n            mutableUiState.update { it.copy(loadMoreState = LoadState.Loading) }\n            loadMoreFunction(feeds.last().status.id)\n                .onFailure { e ->\n                    mutableUiState.update {\n                        it.copy(\n                            loadMoreState = LoadState.Failed(e.toTextStringOrNull()),\n                        )\n                    }\n                }.onSuccess { list ->\n                    mutableUiState.update {\n                        val newList = it.feeds.toMutableList()\n                        newList.addAllIgnoreDuplicate(list)\n                        it.copy(\n                            loadMoreState = LoadState.Idle,\n                            feeds = newList,\n                        )\n                    }\n                }\n        }\n    }\n\n    private fun List<StatusUiState>.applyRefreshResult(\n        refreshResult: RefreshResult,\n    ): List<StatusUiState> {\n        if (refreshResult.useOldData) {\n            val deletedIdsSet = refreshResult.deletedStatus\n                .map { it.status.id }\n                .toSet()\n            val oldList = this.filter { !deletedIdsSet.contains(it.status.id) }\n            val addedNewList = refreshResult.newStatus\n                .toMutableList()\n            addedNewList.addAllIgnoreDuplicate(oldList)\n            return addedNewList\n        } else {\n            return refreshResult.newStatus\n        }\n    }\n\n    private fun MutableList<StatusUiState>.addAllIgnoreDuplicate(\n        newItems: List<StatusUiState>,\n    ) {\n        newItems.forEach { this.addIfNotExist(it) }\n    }\n\n    private fun MutableList<StatusUiState>.addIfNotExist(newItemUiState: StatusUiState) {\n        if (this.container { it.status.id == newItemUiState.status.id }) return\n        this += newItemUiState\n    }\n\n    private suspend fun InteractiveHandleResult.handleResult() {\n        this.handle(\n            uiStatusUpdater = { newUiState ->\n                onStatusUpdate(newUiState.status)\n                mutableUiState.update { currentUiState ->\n                    currentUiState.copy(feeds = currentUiState.feeds.updateStatus(newUiState))\n                }\n            },\n            deleteStatus = { deletedStatusId ->\n                mutableUiState.update { currentUiState ->\n                    currentUiState.copy(\n                        feeds = currentUiState.feeds.filter { it.status.id != deletedStatusId }\n                    )\n                }\n            },\n            followStateUpdater = { _, _ ->\n\n            }\n        )\n    }\n}\n"
  },
  {
    "path": "commonbiz/sharedscreen/src/commonMain/kotlin/com/zhangke/fread/commonbiz/shared/feeds/IFeedsViewModelController.kt",
    "content": "package com.zhangke.fread.commonbiz.shared.feeds\n\nimport com.zhangke.fread.common.feeds.model.RefreshResult\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.model.StatusUiState\nimport com.zhangke.fread.status.status.model.Status\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.SharedFlow\nimport kotlinx.coroutines.flow.StateFlow\n\ninterface IFeedsViewModelController : IInteractiveHandler {\n\n    val mutableUiState: MutableStateFlow<CommonFeedsUiState>\n    val uiState: StateFlow<CommonFeedsUiState> get() = mutableUiState\n    val mutableNewStatusNotifyFlow: MutableSharedFlow<Unit>\n    val newStatusNotifyFlow: SharedFlow<Unit> get() = mutableNewStatusNotifyFlow\n\n    fun initController(\n        coroutineScope: CoroutineScope,\n        locatorResolver: (Status) -> PlatformLocator,\n        loadFirstPageLocalFeeds: suspend () -> Result<List<StatusUiState>>,\n        loadNewFromServerFunction: suspend () -> Result<RefreshResult>,\n        loadMoreFunction: suspend (maxId: String) -> Result<List<StatusUiState>>,\n        onStatusUpdate: suspend (Status) -> Unit,\n    )\n\n    fun initFeeds(needLocalData: Boolean)\n\n    fun startAutoFetchNewerFeeds()\n\n    fun onRefresh()\n\n    fun onLoadMore()\n}\n"
  },
  {
    "path": "commonbiz/sharedscreen/src/commonMain/kotlin/com/zhangke/fread/commonbiz/shared/feeds/IInteractiveHandler.kt",
    "content": "package com.zhangke.fread.commonbiz.shared.feeds\n\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.composable.TextString\nimport com.zhangke.fread.status.author.BlogAuthor\nimport com.zhangke.fread.status.blog.Blog\nimport com.zhangke.fread.status.blog.BlogPoll\nimport com.zhangke.fread.status.model.Hashtag\nimport com.zhangke.fread.status.model.HashtagInStatus\nimport com.zhangke.fread.status.model.Mention\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.model.StatusActionType\nimport com.zhangke.fread.status.model.StatusProviderProtocol\nimport com.zhangke.fread.status.model.StatusUiState\nimport com.zhangke.fread.status.platform.BlogPlatform\nimport com.zhangke.fread.status.ui.ComposedStatusInteraction\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.SharedFlow\n\ninterface IInteractiveHandler {\n\n    val mutableErrorMessageFlow: MutableSharedFlow<TextString>\n    val errorMessageFlow: SharedFlow<TextString> get() = mutableErrorMessageFlow\n\n    val mutableOpenScreenFlow: MutableSharedFlow<NavKey>\n    val openScreenFlow: SharedFlow<NavKey> get() = mutableOpenScreenFlow\n\n    val composedStatusInteraction: ComposedStatusInteraction\n\n    fun initInteractiveHandler(\n        coroutineScope: CoroutineScope,\n        onInteractiveHandleResult: suspend (InteractiveHandleResult) -> Unit,\n    )\n\n    fun onStatusInteractive(status: StatusUiState, type: StatusActionType)\n\n    fun onUserInfoClick(locator: PlatformLocator, blogAuthor: BlogAuthor)\n\n    fun onStatusClick(status: StatusUiState)\n\n    fun onBlogClick(locator: PlatformLocator, blog: Blog)\n    fun onBlogIdClick(locator: PlatformLocator, platform: BlogPlatform, blogId: String)\n\n    fun onVoted(status: StatusUiState, votedOption: List<BlogPoll.Option>)\n\n    fun onFollowClick(locator: PlatformLocator, target: BlogAuthor)\n\n    fun onUnfollowClick(locator: PlatformLocator, target: BlogAuthor)\n\n    fun onMentionClick(locator: PlatformLocator, mention: Mention)\n\n    fun onMentionClick(locator: PlatformLocator, did: String, protocol: StatusProviderProtocol)\n\n    fun onHashtagClick(locator: PlatformLocator, tag: HashtagInStatus)\n\n    fun onHashtagClick(locator: PlatformLocator, tag: Hashtag)\n\n    fun onMaybeHashtagClick(locator: PlatformLocator, protocol: StatusProviderProtocol, tag: String)\n}\n"
  },
  {
    "path": "commonbiz/sharedscreen/src/commonMain/kotlin/com/zhangke/fread/commonbiz/shared/feeds/InteractiveHandleResult.kt",
    "content": "package com.zhangke.fread.commonbiz.shared.feeds\n\nimport com.zhangke.framework.controller.CommonLoadableUiState\nimport com.zhangke.fread.status.model.StatusUiState\nimport com.zhangke.fread.status.model.updateStatus\nimport com.zhangke.fread.status.uri.FormalUri\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.update\n\nsealed interface InteractiveHandleResult {\n\n    data class UpdateStatus(val status: StatusUiState) : InteractiveHandleResult\n\n    data class DeleteStatus(val statusId: String) : InteractiveHandleResult\n\n    data class UpdateFollowState(\n        val userUri: FormalUri,\n        val following: Boolean,\n    ) : InteractiveHandleResult\n}\n\nsuspend fun InteractiveHandleResult.handle(\n    uiStatusUpdater: suspend (StatusUiState) -> Unit,\n    deleteStatus: (statusId: String) -> Unit,\n    followStateUpdater: suspend (FormalUri, Boolean) -> Unit,\n) {\n    when (this) {\n        is InteractiveHandleResult.UpdateStatus -> {\n            uiStatusUpdater(this.status)\n        }\n\n        is InteractiveHandleResult.UpdateFollowState -> {\n            val (userUri, following) = this\n            followStateUpdater(userUri, following)\n        }\n\n        is InteractiveHandleResult.DeleteStatus -> {\n            deleteStatus(this.statusId)\n        }\n    }\n}\n\nsuspend fun InteractiveHandleResult.handle(\n    mutableUiState: MutableStateFlow<CommonLoadableUiState<StatusUiState>>,\n    followStateUpdater: suspend (FormalUri, Boolean) -> Unit,\n) {\n    handle(\n        uiStatusUpdater = { newStatus ->\n            mutableUiState.update {\n                it.copyObject(\n                    dataList = it.dataList.updateStatus(newStatus)\n                )\n            }\n        },\n        deleteStatus = { deletedStatusId ->\n            mutableUiState.update { uiState ->\n                uiState.copyObject(\n                    dataList = uiState.dataList\n                        .filter { status -> status.status.id != deletedStatusId }\n                )\n            }\n        },\n        followStateUpdater = followStateUpdater,\n    )\n}\n"
  },
  {
    "path": "commonbiz/sharedscreen/src/commonMain/kotlin/com/zhangke/fread/commonbiz/shared/feeds/InteractiveHandler.kt",
    "content": "package com.zhangke.fread.commonbiz.shared.feeds\n\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.architect.json.globalJson\nimport com.zhangke.framework.composable.TextString\nimport com.zhangke.framework.composable.emitTextMessageFromThrowable\nimport com.zhangke.framework.utils.exceptionOrThrow\nimport com.zhangke.framework.utils.getDefaultLocale\nimport com.zhangke.framework.utils.languageCode\nimport com.zhangke.fread.common.adapter.StatusUiStateAdapter\nimport com.zhangke.fread.common.status.StatusUpdater\nimport com.zhangke.fread.commonbiz.shared.blog.detail.RssBlogDetailScreenNavKey\nimport com.zhangke.fread.commonbiz.shared.screen.status.context.StatusContextScreenNavKey\nimport com.zhangke.fread.commonbiz.shared.usecase.RefactorToNewStatusUseCase\nimport com.zhangke.fread.status.StatusProvider\nimport com.zhangke.fread.status.author.BlogAuthor\nimport com.zhangke.fread.status.blog.Blog\nimport com.zhangke.fread.status.blog.BlogPoll\nimport com.zhangke.fread.status.blog.BlogTranslation\nimport com.zhangke.fread.status.model.Hashtag\nimport com.zhangke.fread.status.model.HashtagInStatus\nimport com.zhangke.fread.status.model.Mention\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.model.StatusActionType\nimport com.zhangke.fread.status.model.StatusProviderProtocol\nimport com.zhangke.fread.status.model.StatusUiState\nimport com.zhangke.fread.status.model.isRss\nimport com.zhangke.fread.status.platform.BlogPlatform\nimport com.zhangke.fread.status.ui.ComposedStatusInteraction\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.launch\nimport kotlinx.serialization.serializer\n\nclass InteractiveHandler(\n    private val statusProvider: StatusProvider,\n    private val statusUpdater: StatusUpdater,\n    private val statusUiStateAdapter: StatusUiStateAdapter,\n    private val refactorToNewStatus: RefactorToNewStatusUseCase,\n) : IInteractiveHandler {\n\n    override val mutableErrorMessageFlow = MutableSharedFlow<TextString>()\n    override val mutableOpenScreenFlow = MutableSharedFlow<NavKey>()\n\n    private lateinit var onInteractiveHandleResult: suspend (InteractiveHandleResult) -> Unit\n\n    private lateinit var coroutineScope: CoroutineScope\n\n    private val screenProvider = statusProvider.screenProvider\n\n    override val composedStatusInteraction = object : ComposedStatusInteraction {\n\n        override fun onStatusInteractive(status: StatusUiState, type: StatusActionType) {\n            this@InteractiveHandler.onStatusInteractive(status, type)\n        }\n\n        override fun onUserInfoClick(locator: PlatformLocator, blogAuthor: BlogAuthor) {\n            this@InteractiveHandler.onUserInfoClick(locator, blogAuthor)\n        }\n\n        override fun onVoted(status: StatusUiState, blogPollOptions: List<BlogPoll.Option>) {\n            this@InteractiveHandler.onVoted(status, blogPollOptions)\n        }\n\n        override fun onHashtagInStatusClick(\n            locator: PlatformLocator,\n            hashtagInStatus: HashtagInStatus\n        ) {\n            this@InteractiveHandler.onHashtagClick(locator, hashtagInStatus)\n        }\n\n        override fun onMaybeHashtagClick(\n            locator: PlatformLocator,\n            protocol: StatusProviderProtocol,\n            hashtag: String,\n        ) {\n            this@InteractiveHandler.onMaybeHashtagClick(locator, protocol, hashtag)\n        }\n\n        override fun onMentionClick(locator: PlatformLocator, mention: Mention) {\n            this@InteractiveHandler.onMentionClick(locator, mention)\n        }\n\n        override fun onMentionClick(\n            locator: PlatformLocator,\n            did: String,\n            protocol: StatusProviderProtocol\n        ) {\n            this@InteractiveHandler.onMentionClick(locator, did, protocol)\n        }\n\n        override fun onStatusClick(status: StatusUiState) {\n            this@InteractiveHandler.onStatusClick(status)\n        }\n\n        override fun onBlogClick(locator: PlatformLocator, blog: Blog) {\n            this@InteractiveHandler.onBlogClick(locator, blog)\n        }\n\n        override fun onBlogIdClick(\n            locator: PlatformLocator,\n            platform: BlogPlatform,\n            blogId: String\n        ) {\n            this@InteractiveHandler.onBlogIdClick(locator, platform, blogId)\n        }\n\n        override fun onBlockClick(locator: PlatformLocator, blog: Blog) {\n            this@InteractiveHandler.onBlogClick(locator, blog)\n        }\n\n        override fun onFollowClick(locator: PlatformLocator, target: BlogAuthor) {\n            this@InteractiveHandler.onFollowClick(locator, target)\n        }\n\n        override fun onUnfollowClick(locator: PlatformLocator, target: BlogAuthor) {\n            this@InteractiveHandler.onUnfollowClick(locator, target)\n        }\n\n        override fun onHashtagClick(locator: PlatformLocator, tag: Hashtag) {\n            this@InteractiveHandler.onHashtagClick(locator, tag)\n        }\n\n        override fun onBoostedClick(locator: PlatformLocator, status: StatusUiState) {\n            this@InteractiveHandler.onBoostedClick(locator, status)\n        }\n\n        override fun onFavouritedClick(locator: PlatformLocator, status: StatusUiState) {\n            this@InteractiveHandler.onFavouritedClick(locator, status)\n        }\n\n        override fun onTranslateClick(locator: PlatformLocator, status: StatusUiState) {\n            this@InteractiveHandler.onTranslateClick(locator, status)\n        }\n\n        override fun onShowOriginalClick(status: StatusUiState) {\n            this@InteractiveHandler.onShowOriginalClick(status)\n        }\n    }\n\n    override fun initInteractiveHandler(\n        coroutineScope: CoroutineScope,\n        onInteractiveHandleResult: suspend (InteractiveHandleResult) -> Unit,\n    ) {\n        this.coroutineScope = coroutineScope\n        this.onInteractiveHandleResult = onInteractiveHandleResult\n        coroutineScope.launch {\n            statusUpdater.statusUpdateFlow.collect {\n                onInteractiveHandleResult(InteractiveHandleResult.UpdateStatus(it))\n            }\n        }\n    }\n\n    override fun onStatusInteractive(status: StatusUiState, type: StatusActionType) {\n        if (type == StatusActionType.REPLY) {\n            screenProvider.getReplyBlogScreen(status.locator, status.status.intrinsicBlog)\n                ?.let(::openScreen)\n            return\n        }\n        if (type == StatusActionType.EDIT) {\n            screenProvider.getEditBlogScreen(status.locator, status.status.intrinsicBlog)\n                ?.let(::openScreen)\n            return\n        }\n        if (type == StatusActionType.QUOTE) {\n            screenProvider.getQuoteBlogScreen(status.locator, status.status.intrinsicBlog)\n                ?.let(::openScreen)\n            return\n        }\n        coroutineScope.launch {\n            val result = statusProvider.statusResolver\n                .interactive(status.locator, status.status, type)\n                .map { s -> s?.let { statusUiStateAdapter.toStatusUiState(status, it) } }\n            if (result.isFailure) {\n                mutableErrorMessageFlow.emitTextMessageFromThrowable(result.exceptionOrThrow())\n                return@launch\n            }\n            val statusUiState = result.getOrNull()\n            if (statusUiState == null) {\n                onInteractiveHandleResult(InteractiveHandleResult.DeleteStatus(status.status.id))\n            } else {\n                val interactiveResult = InteractiveHandleResult.UpdateStatus(statusUiState)\n                statusUpdater.update(statusUiState)\n                onInteractiveHandleResult(interactiveResult)\n            }\n        }\n    }\n\n    override fun onUserInfoClick(locator: PlatformLocator, blogAuthor: BlogAuthor) {\n        coroutineScope.launch {\n            screenProvider.getUserDetailScreen(locator, blogAuthor.uri, blogAuthor.userId)\n                ?.let { mutableOpenScreenFlow.emit(it) }\n        }\n    }\n\n    override fun onStatusClick(status: StatusUiState) {\n        coroutineScope.launch {\n            val screen = if (status.status.intrinsicBlog.platform.protocol.isRss) {\n                RssBlogDetailScreenNavKey(serializedBlog = globalJson.encodeToString(status.status.intrinsicBlog))\n            } else {\n                StatusContextScreenNavKey.create(refactorToNewStatus(status))\n            }\n            mutableOpenScreenFlow.emit(screen)\n        }\n    }\n\n    override fun onBlogClick(locator: PlatformLocator, blog: Blog) {\n        coroutineScope.launch {\n            val screen = if (blog.platform.protocol.isRss) {\n                RssBlogDetailScreenNavKey(\n                    serializedBlog = globalJson.encodeToString(\n                        serializer(),\n                        blog\n                    ),\n                )\n            } else {\n                StatusContextScreenNavKey.create(\n                    locator = locator,\n                    blog = blog,\n                )\n            }\n            mutableOpenScreenFlow.emit(screen)\n        }\n    }\n\n    override fun onBlogIdClick(\n        locator: PlatformLocator,\n        platform: BlogPlatform,\n        blogId: String,\n    ) {\n        coroutineScope.launch {\n            StatusContextScreenNavKey.create(\n                locator = locator,\n                blogId = blogId,\n                platform = platform,\n            )\n        }\n    }\n\n    override fun onVoted(status: StatusUiState, votedOption: List<BlogPoll.Option>) {\n        coroutineScope.launch {\n            val result = statusProvider.statusResolver\n                .votePoll(status.locator, status.status.intrinsicBlog, votedOption)\n                .map { statusUiStateAdapter.toStatusUiState(status, it) }\n            if (result.isFailure) {\n                mutableErrorMessageFlow.emitTextMessageFromThrowable(result.exceptionOrThrow())\n                return@launch\n            }\n            val interactiveHandleResult = InteractiveHandleResult.UpdateStatus(result.getOrThrow())\n            onInteractiveHandleResult(interactiveHandleResult)\n        }\n    }\n\n    override fun onFollowClick(locator: PlatformLocator, target: BlogAuthor) {\n        coroutineScope.launch {\n            statusProvider.statusResolver\n                .follow(locator, target)\n                .onSuccess {\n                    onInteractiveHandleResult(\n                        InteractiveHandleResult.UpdateFollowState(\n                            target.uri,\n                            true\n                        )\n                    )\n                }.onFailure {\n                    mutableErrorMessageFlow.emitTextMessageFromThrowable(it)\n                }\n        }\n    }\n\n    override fun onUnfollowClick(locator: PlatformLocator, target: BlogAuthor) {\n        coroutineScope.launch {\n            statusProvider.statusResolver\n                .unfollow(locator, target)\n                .onSuccess {\n                    onInteractiveHandleResult(\n                        InteractiveHandleResult.UpdateFollowState(\n                            target.uri,\n                            false\n                        )\n                    )\n                }.onFailure {\n                    mutableErrorMessageFlow.emitTextMessageFromThrowable(it)\n                }\n        }\n    }\n\n    override fun onMentionClick(locator: PlatformLocator, mention: Mention) {\n        coroutineScope.launch {\n            screenProvider.getUserDetailScreen(\n                locator = locator,\n                webFinger = mention.webFinger,\n                protocol = mention.protocol,\n            )?.let { mutableOpenScreenFlow.emit(it) }\n        }\n    }\n\n    override fun onMentionClick(\n        locator: PlatformLocator,\n        did: String,\n        protocol: StatusProviderProtocol\n    ) {\n        coroutineScope.launch {\n            screenProvider.getUserDetailScreen(\n                locator = locator,\n                did = did,\n                protocol = protocol,\n            )?.let { mutableOpenScreenFlow.emit(it) }\n        }\n    }\n\n    override fun onHashtagClick(locator: PlatformLocator, tag: HashtagInStatus) {\n        openHashtagTimelineScreen(\n            locator = locator,\n            tag = tag.name,\n            protocol = tag.protocol,\n        )\n    }\n\n    override fun onHashtagClick(locator: PlatformLocator, tag: Hashtag) {\n        openHashtagTimelineScreen(\n            locator = locator,\n            tag = tag.name,\n            protocol = tag.protocol,\n        )\n    }\n\n    override fun onMaybeHashtagClick(\n        locator: PlatformLocator,\n        protocol: StatusProviderProtocol,\n        tag: String,\n    ) {\n        openHashtagTimelineScreen(locator = locator, tag = tag, protocol = protocol)\n    }\n\n    private fun openHashtagTimelineScreen(\n        locator: PlatformLocator,\n        tag: String,\n        protocol: StatusProviderProtocol,\n    ) {\n        screenProvider.getTagTimelineScreen(\n            locator = locator,\n            tag = tag,\n            protocol = protocol\n        )?.let(::openScreen)\n    }\n\n    private fun onBoostedClick(locator: PlatformLocator, status: StatusUiState) {\n        screenProvider.getBlogBoostedScreen(\n            locator = locator,\n            blog = status.status.intrinsicBlog,\n            protocol = status.status.intrinsicBlog.platform.protocol,\n        )?.let(::openScreen)\n    }\n\n    private fun onFavouritedClick(locator: PlatformLocator, status: StatusUiState) {\n        screenProvider.getBlogFavouritedScreen(\n            locator = locator,\n            blog = status.status.intrinsicBlog,\n            protocol = status.status.intrinsicBlog.platform.protocol,\n        )?.let(::openScreen)\n    }\n\n    private fun onTranslateClick(locator: PlatformLocator, status: StatusUiState) {\n        coroutineScope.launch {\n            onInteractiveHandleResult(InteractiveHandleResult.UpdateStatus(status.translating()))\n            statusProvider.statusResolver\n                .translate(locator, status.status, getDefaultLocale().languageCode)\n                .onFailure {\n                    mutableErrorMessageFlow.emitTextMessageFromThrowable(it)\n                    onInteractiveHandleResult(InteractiveHandleResult.UpdateStatus(status.translateFinish()))\n                }.onSuccess {\n                    onInteractiveHandleResult(\n                        InteractiveHandleResult.UpdateStatus(status.translated(it))\n                    )\n                }\n        }\n    }\n\n    private fun onShowOriginalClick(status: StatusUiState) {\n        coroutineScope.launch {\n            val showOriginalBlog = status.copy(\n                blogTranslationState = status.blogTranslationState.copy(\n                    showingTranslation = false,\n                )\n            )\n            onInteractiveHandleResult(InteractiveHandleResult.UpdateStatus(showOriginalBlog))\n        }\n    }\n\n    private fun StatusUiState.translating(): StatusUiState {\n        return this.copy(\n            blogTranslationState = this.blogTranslationState.copy(\n                translating = true,\n            )\n        )\n    }\n\n    private fun StatusUiState.translateFinish(): StatusUiState {\n        return this.copy(\n            blogTranslationState = this.blogTranslationState.copy(\n                translating = false,\n            )\n        )\n    }\n\n    private fun StatusUiState.translated(translation: BlogTranslation): StatusUiState {\n        return this.copy(\n            blogTranslationState = this.blogTranslationState.copy(\n                translating = false,\n                blogTranslation = translation,\n                showingTranslation = true,\n            )\n        )\n    }\n\n    private fun openScreen(screen: NavKey) {\n        coroutineScope.launch {\n            mutableOpenScreenFlow.emit(screen)\n        }\n    }\n}\n"
  },
  {
    "path": "commonbiz/sharedscreen/src/commonMain/kotlin/com/zhangke/fread/commonbiz/shared/notification/FollowNotification.kt",
    "content": "package com.zhangke.fread.commonbiz.shared.notification\n\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.PersonAdd\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport com.zhangke.fread.commonbiz.shared.composable.UserInfoCard\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.author.BlogAuthor\nimport com.zhangke.fread.status.notification.StatusNotification\nimport org.jetbrains.compose.resources.stringResource\n\n@Composable\nfun FollowNotification(\n    notification: StatusNotification.Follow,\n    style: NotificationStyle,\n    onUserInfoClick: (BlogAuthor) -> Unit,\n    onFollowAccountClick: (BlogAuthor) -> Unit,\n    onUnfollowAccountClick: (BlogAuthor) -> Unit,\n    onUnblockClick: (BlogAuthor) -> Unit,\n    onCancelFollowRequestClick: (BlogAuthor) -> Unit,\n) {\n    Column(\n        modifier = Modifier.fillMaxWidth()\n            .padding(style.containerPaddings),\n    ) {\n        NotificationHeadLine(\n            modifier = Modifier.fillMaxWidth(),\n            icon = Icons.Default.PersonAdd,\n            avatar = notification.author.avatar,\n            createAt = notification.formattingDisplayTime,\n            accountName = notification.author.humanizedName,\n            interactionDesc = stringResource(LocalizedString.sharedNotificationFollowDesc),\n            style = style,\n        )\n        UserInfoCard(\n            modifier = Modifier.padding(top = style.headLineToContentPadding)\n                .fillMaxWidth(),\n            user = notification.author,\n            onUserClick = onUserInfoClick,\n            onFollowAccountClick = onFollowAccountClick,\n            onUnfollowAccountClick = onUnfollowAccountClick,\n            onUnblockClick = onUnblockClick,\n            onCancelFollowRequestClick = onCancelFollowRequestClick,\n        )\n    }\n}\n"
  },
  {
    "path": "commonbiz/sharedscreen/src/commonMain/kotlin/com/zhangke/fread/commonbiz/shared/notification/FollowRequestNotification.kt",
    "content": "package com.zhangke.fread.commonbiz.shared.notification\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Check\nimport androidx.compose.material.icons.filled.Clear\nimport androidx.compose.material.icons.filled.PersonAddAlt1\nimport androidx.compose.material3.IconButtonDefaults\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport com.zhangke.framework.composable.SimpleIconButton\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.author.BlogAuthor\nimport com.zhangke.fread.status.notification.StatusNotification\nimport com.zhangke.fread.status.ui.BlogAuthorAvatar\nimport com.zhangke.fread.status.ui.richtext.FreadRichText\nimport org.jetbrains.compose.resources.stringResource\n\n@Composable\nfun FollowRequestNotification(\n    notification: StatusNotification.FollowRequest,\n    style: NotificationStyle,\n    onUserInfoClick: (BlogAuthor) -> Unit,\n    onRejectClick: (BlogAuthor) -> Unit,\n    onAcceptClick: (BlogAuthor) -> Unit,\n) {\n    Column(\n        modifier = Modifier.fillMaxWidth()\n            .padding(style.containerPaddings)\n            .padding(bottom = 16.dp)\n    ) {\n        NotificationHeadLine(\n            modifier = Modifier.clickable {\n                onUserInfoClick(notification.author)\n            },\n            icon = Icons.Default.PersonAddAlt1,\n            createAt = notification.formattingDisplayTime,\n            avatar = notification.author.avatar,\n            accountName = null,\n            interactionDesc = stringResource(LocalizedString.sharedNotificationFollowRequest),\n            style = style,\n        )\n\n        Row(\n            modifier = Modifier.fillMaxWidth()\n                .padding(top = style.headLineToContentPadding),\n            verticalAlignment = Alignment.CenterVertically,\n        ) {\n            BlogAuthorAvatar(\n                modifier = Modifier\n                    .size(style.statusStyle.infoLineStyle.avatarSize),\n                imageUrl = notification.author.avatar,\n            )\n\n            Column(\n                modifier = Modifier.weight(1F).padding(start = 6.dp, end = 6.dp),\n            ) {\n                FreadRichText(\n                    modifier = Modifier,\n                    richText = notification.author.humanizedName,\n                    maxLines = 1,\n                    overflow = TextOverflow.Ellipsis,\n                )\n\n                Text(\n                    modifier = Modifier.padding(top = 2.dp),\n                    textAlign = TextAlign.Left,\n                    text = notification.author.webFinger.toString(),\n                    maxLines = 1,\n                    overflow = TextOverflow.Ellipsis,\n                )\n            }\n\n            SimpleIconButton(\n                modifier = Modifier\n                    .size(32.dp),\n                colors = IconButtonDefaults.iconButtonColors(\n                    containerColor = MaterialTheme.colorScheme.primaryContainer,\n                ),\n                onClick = {\n                    onRejectClick(notification.author)\n                },\n                imageVector = Icons.Default.Clear,\n                contentDescription = \"Reject\",\n            )\n\n            Spacer(Modifier.width(8.dp))\n            SimpleIconButton(\n                modifier = Modifier.size(32.dp),\n                colors = IconButtonDefaults.iconButtonColors(\n                    containerColor = MaterialTheme.colorScheme.primaryContainer,\n                ),\n                onClick = {\n                    onAcceptClick(notification.author)\n                },\n                imageVector = Icons.Default.Check,\n                contentDescription = \"Accept\",\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "commonbiz/sharedscreen/src/commonMain/kotlin/com/zhangke/fread/commonbiz/shared/notification/NotificationHeadLine.kt",
    "content": "package com.zhangke.fread.commonbiz.shared.notification\n\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.LocalContentColor\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport com.zhangke.framework.composable.TwoTextsInRow\nimport com.zhangke.fread.status.model.FormattingTime\nimport com.zhangke.fread.status.richtext.RichText\nimport com.zhangke.fread.status.ui.BlogAuthorAvatar\nimport com.zhangke.fread.status.ui.richtext.FreadRichText\n\n@Composable\nfun NotificationHeadLine(\n    modifier: Modifier,\n    icon: ImageVector,\n    avatar: String?,\n    accountName: RichText?,\n    interactionDesc: String,\n    style: NotificationStyle,\n    createAt: FormattingTime,\n    iconTint: Color = LocalContentColor.current,\n) {\n    Row(\n        modifier = modifier.fillMaxWidth(),\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        Icon(\n            modifier = Modifier.size(style.typeLogoSize),\n            imageVector = icon,\n            contentDescription = null,\n            tint = iconTint,\n        )\n\n        if (!avatar.isNullOrEmpty()) {\n            BlogAuthorAvatar(\n                modifier = Modifier\n                    .padding(start = 6.dp)\n                    .size(style.triggerAccountAvatarSize),\n                imageUrl = avatar,\n            )\n        }\n\n        TwoTextsInRow(\n            modifier = Modifier.weight(1F),\n            firstText = {\n                FreadRichText(\n                    modifier = Modifier.padding(start = 6.dp),\n                    richText = accountName ?: RichText.empty,\n                    maxLines = 1,\n                    overflow = TextOverflow.Ellipsis,\n                    fontSize = 12.sp,\n                )\n            },\n            secondText = {\n                Text(\n                    modifier = Modifier,\n                    text = interactionDesc,\n                    maxLines = 1,\n                    overflow = TextOverflow.Ellipsis,\n                    style = MaterialTheme.typography.labelMedium,\n                )\n            },\n            spacing = 2.dp,\n        )\n\n        Text(\n            modifier = Modifier.padding(start = 4.dp),\n            text = createAt.formattedTime(),\n            maxLines = 1,\n            color = MaterialTheme.colorScheme.onSurfaceVariant,\n            style = MaterialTheme.typography.labelMedium,\n        )\n    }\n}\n"
  },
  {
    "path": "commonbiz/sharedscreen/src/commonMain/kotlin/com/zhangke/fread/commonbiz/shared/notification/NotificationWithWholeStatus.kt",
    "content": "package com.zhangke.fread.commonbiz.shared.notification\n\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.LocalContentColor\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.unit.dp\nimport com.zhangke.fread.commonbiz.shared.composable.BlogUi\nimport com.zhangke.fread.status.author.BlogAuthor\nimport com.zhangke.fread.status.blog.Blog\nimport com.zhangke.fread.status.model.FormattingTime\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.ui.ComposedStatusInteraction\n\n@Composable\nfun NotificationWithWholeStatus(\n    blog: Blog,\n    locator: PlatformLocator,\n    author: BlogAuthor?,\n    createAt: FormattingTime,\n    indexInList: Int,\n    sharedElementId: String,\n    icon: ImageVector,\n    interactionDesc: String,\n    style: NotificationStyle,\n    composedStatusInteraction: ComposedStatusInteraction,\n    iconTint: Color = LocalContentColor.current,\n) {\n    Column(\n        modifier = Modifier\n            .clickable { composedStatusInteraction.onBlogClick(locator, blog) }\n            .fillMaxWidth()\n            .padding(style.containerPaddings)\n    ) {\n        NotificationHeadLine(\n            modifier = Modifier.clickable(enabled = author != null) {\n                composedStatusInteraction.onUserInfoClick(locator, author!!)\n            },\n            icon = icon,\n            avatar = author?.avatar,\n            iconTint = iconTint,\n            createAt = createAt,\n            accountName = author?.humanizedName,\n            interactionDesc = interactionDesc,\n            style = style,\n        )\n\n        BlogUi(\n            modifier = Modifier.padding(top = style.headLineToContentPadding)\n                .statusBorder()\n                .padding(style.internalBlogPadding),\n            blog = blog,\n            locator = locator,\n            indexInList = indexInList,\n            sharedElementId = sharedElementId,\n            style = style.statusStyle.copy(\n                containerStartPadding = 0.dp,\n                containerEndPadding = 0.dp,\n                containerTopPadding = 0.dp,\n                containerBottomPadding = 0.dp,\n            ),\n            showBottomPanel = false,\n            showMoreOperationIcon = false,\n            composedStatusInteraction = composedStatusInteraction,\n        )\n    }\n}\n\n@Composable\nfun Modifier.statusBorder(show: Boolean = true): Modifier {\n    return if (show) {\n        this.border(0.5.dp, MaterialTheme.colorScheme.outlineVariant, RoundedCornerShape(4.dp))\n    } else {\n        this\n    }\n}\n"
  },
  {
    "path": "commonbiz/sharedscreen/src/commonMain/kotlin/com/zhangke/fread/commonbiz/shared/notification/SeveredRelationshipsNotification.kt",
    "content": "package com.zhangke.fread.commonbiz.shared.notification\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.WarningAmber\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.text.style.TextOverflow\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.author.BlogAuthor\nimport com.zhangke.fread.status.notification.StatusNotification\nimport org.jetbrains.compose.resources.stringResource\n\n@Composable\nfun SeveredRelationshipsNotification(\n    notification: StatusNotification.SeveredRelationships,\n    style: NotificationStyle,\n    onUserInfoClick: (BlogAuthor) -> Unit,\n) {\n    Column(\n        modifier = Modifier\n            .fillMaxWidth()\n            .clickable { onUserInfoClick(notification.author) }\n            .padding(style.containerPaddings),\n    ) {\n        NotificationHeadLine(\n            modifier = Modifier,\n            icon = Icons.Default.WarningAmber,\n            avatar = notification.author.avatar,\n            createAt = notification.formattingDisplayTime,\n            accountName = notification.author.humanizedName,\n            interactionDesc = stringResource(LocalizedString.sharedNotificationSeveredDesc),\n            style = style,\n        )\n\n        Text(\n            modifier = Modifier\n                .padding(top = style.headLineToContentPadding)\n                .align(Alignment.CenterHorizontally),\n            text = notification.reason,\n            maxLines = 1,\n            overflow = TextOverflow.Ellipsis,\n        )\n    }\n}\n"
  },
  {
    "path": "commonbiz/sharedscreen/src/commonMain/kotlin/com/zhangke/fread/commonbiz/shared/notification/StatusNotificationUi.kt",
    "content": "package com.zhangke.fread.commonbiz.shared.notification\n\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Edit\nimport androidx.compose.material.icons.filled.Favorite\nimport androidx.compose.material.icons.filled.NotificationsNone\nimport androidx.compose.material.icons.filled.Poll\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport com.zhangke.fread.commonbiz.shared.composable.FeedsStatusNode\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.author.BlogAuthor\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.notification.StatusNotification\nimport com.zhangke.fread.status.ui.ComposedStatusInteraction\nimport com.zhangke.fread.status.ui.action.quoteIcon\nimport com.zhangke.fread.status.ui.action.quoteInLeftIcon\nimport com.zhangke.fread.status.ui.style.LocalStatusUiConfig\nimport com.zhangke.fread.status.ui.style.StatusStyle\nimport com.zhangke.fread.statusui.ic_status_forward\nimport org.jetbrains.compose.resources.stringResource\nimport org.jetbrains.compose.resources.vectorResource\n\n@Composable\nfun StatusNotificationUi(\n    modifier: Modifier,\n    notification: StatusNotification,\n    indexInList: Int,\n    style: NotificationStyle = defaultNotificationStyle(),\n    onRejectClick: (BlogAuthor) -> Unit,\n    onAcceptClick: (BlogAuthor) -> Unit,\n    onUnblockClick: (PlatformLocator, BlogAuthor) -> Unit,\n    onCancelFollowRequestClick: (PlatformLocator, BlogAuthor) -> Unit,\n    composedStatusInteraction: ComposedStatusInteraction,\n) {\n    Column(modifier = modifier) {\n        Box(modifier = Modifier) {\n            when (notification) {\n                is StatusNotification.Like -> {\n                    NotificationWithWholeStatus(\n                        blog = notification.blog,\n                        locator = notification.locator,\n                        author = notification.author,\n                        createAt = notification.formattingDisplayTime,\n                        indexInList = indexInList,\n                        sharedElementId = notification.id,\n                        icon = Icons.Default.Favorite,\n                        iconTint = MaterialTheme.colorScheme.tertiary,\n                        interactionDesc = stringResource(LocalizedString.sharedNotificationFavouritedDesc),\n                        style = style,\n                        composedStatusInteraction = composedStatusInteraction,\n                    )\n                }\n\n                is StatusNotification.Mention -> {\n                    FeedsStatusNode(\n                        modifier = Modifier.fillMaxWidth(),\n                        status = notification.status,\n                        indexInList = indexInList,\n                        sharedElementId = notification.id,\n                        style = style.statusStyle,\n                        showDivider = false,\n                        composedStatusInteraction = composedStatusInteraction,\n                    )\n                }\n\n                is StatusNotification.Reply -> {\n                    FeedsStatusNode(\n                        modifier = Modifier.fillMaxWidth(),\n                        status = notification.status,\n                        indexInList = indexInList,\n                        sharedElementId = notification.id,\n                        style = style.statusStyle,\n                        showDivider = false,\n                        composedStatusInteraction = composedStatusInteraction,\n                    )\n                }\n\n                is StatusNotification.Repost -> {\n                    NotificationWithWholeStatus(\n                        blog = notification.blog,\n                        locator = notification.locator,\n                        author = notification.author,\n                        createAt = notification.formattingDisplayTime,\n                        indexInList = indexInList,\n                        sharedElementId = notification.id,\n                        icon = vectorResource(com.zhangke.fread.statusui.Res.drawable.ic_status_forward),\n                        interactionDesc = stringResource(LocalizedString.sharedNotificationReblogDesc),\n                        style = style,\n                        composedStatusInteraction = composedStatusInteraction,\n                    )\n                }\n\n                is StatusNotification.Poll -> {\n                    NotificationWithWholeStatus(\n                        blog = notification.blog,\n                        locator = notification.locator,\n                        createAt = notification.formattingDisplayTime,\n                        author = null,\n                        indexInList = indexInList,\n                        sharedElementId = notification.id,\n                        icon = Icons.Default.Poll,\n                        interactionDesc = stringResource(LocalizedString.sharedNotificationPollDesc),\n                        style = style,\n                        composedStatusInteraction = composedStatusInteraction,\n                    )\n                }\n\n                is StatusNotification.Follow -> {\n                    FollowNotification(\n                        notification = notification,\n                        style = style,\n                        onUserInfoClick = {\n                            composedStatusInteraction.onUserInfoClick(notification.locator, it)\n                        },\n                        onFollowAccountClick = {\n                            composedStatusInteraction.onFollowClick(notification.locator, it)\n                        },\n                        onUnblockClick = { onUnblockClick(notification.locator, it) },\n                        onUnfollowAccountClick = {\n                            composedStatusInteraction.onUnfollowClick(notification.locator, it)\n                        },\n                        onCancelFollowRequestClick = {\n                            onCancelFollowRequestClick(notification.locator, it)\n                        },\n                    )\n                }\n\n                is StatusNotification.Update -> {\n                    NotificationWithWholeStatus(\n                        blog = notification.status.status.intrinsicBlog,\n                        locator = notification.locator,\n                        author = notification.status.status.triggerAuthor,\n                        createAt = notification.formattingDisplayTime,\n                        indexInList = indexInList,\n                        sharedElementId = notification.id,\n                        icon = Icons.Default.Edit,\n                        interactionDesc = stringResource(LocalizedString.sharedNotificationUpdateDesc),\n                        style = style,\n                        composedStatusInteraction = composedStatusInteraction,\n                    )\n                }\n\n                is StatusNotification.FollowRequest -> {\n                    FollowRequestNotification(\n                        notification = notification,\n                        style = style,\n                        onUserInfoClick = {\n                            composedStatusInteraction.onUserInfoClick(notification.locator, it)\n                        },\n                        onRejectClick = onRejectClick,\n                        onAcceptClick = onAcceptClick,\n                    )\n                }\n\n                is StatusNotification.NewStatus -> {\n                    NotificationWithWholeStatus(\n                        blog = notification.status.status.intrinsicBlog,\n                        locator = notification.locator,\n                        author = notification.status.status.triggerAuthor,\n                        createAt = notification.formattingDisplayTime,\n                        indexInList = indexInList,\n                        sharedElementId = notification.id,\n                        icon = Icons.Default.NotificationsNone,\n                        interactionDesc = stringResource(LocalizedString.sharedNotificationNewStatusDesc),\n                        style = style,\n                        composedStatusInteraction = composedStatusInteraction,\n                    )\n                }\n\n                is StatusNotification.Quote -> {\n                    NotificationWithWholeStatus(\n                        blog = notification.status.status.intrinsicBlog,\n                        locator = notification.locator,\n                        author = notification.status.status.triggerAuthor,\n                        createAt = notification.formattingDisplayTime,\n                        indexInList = indexInList,\n                        sharedElementId = notification.id,\n                        icon = quoteInLeftIcon(),\n                        interactionDesc = stringResource(LocalizedString.sharedNotificationQuoteDesc),\n                        style = style,\n                        composedStatusInteraction = composedStatusInteraction,\n                    )\n                }\n\n                is StatusNotification.QuoteUpdate -> {\n                    NotificationWithWholeStatus(\n                        blog = notification.status.status.intrinsicBlog,\n                        locator = notification.locator,\n                        author = notification.author,\n                        createAt = notification.formattingDisplayTime,\n                        indexInList = indexInList,\n                        sharedElementId = notification.id,\n                        icon = Icons.Default.Edit,\n                        interactionDesc = stringResource(LocalizedString.sharedNotificationUpdateDesc),\n                        style = style,\n                        composedStatusInteraction = composedStatusInteraction,\n                    )\n                }\n\n                is StatusNotification.SeveredRelationships -> {\n                    SeveredRelationshipsNotification(\n                        notification = notification,\n                        style = style,\n                        onUserInfoClick = {\n                            composedStatusInteraction.onUserInfoClick(notification.locator, it)\n                        },\n                    )\n                }\n\n                is StatusNotification.Unknown -> {\n                    UnknownNotification(notification = notification)\n                }\n            }\n        }\n    }\n}\n\ndata class NotificationStyle(\n    val nameMaxLength: Int,\n    /**\n     * 通知UI整体的外部边距\n     */\n    val containerPaddings: PaddingValues,\n    /**\n     * 通知内部的博文正文和边框之间的边距\n     */\n    val internalBlogPadding: PaddingValues,\n\n    /**\n     * 通知标题到博客正文之间的边距\n     */\n    val headLineToContentPadding: Dp,\n\n    val statusStyle: StatusStyle,\n\n    /**\n     * 通知触发者的头像大小\n     */\n    val triggerAccountAvatarSize: Dp,\n\n    val typeLogoSize: Dp,\n)\n\nobject NotificationStyleDefaults {\n\n    const val nameMaxLength = 10\n\n    val containerStartPadding = 16.dp\n    val containerTopPadding = 8.dp\n    val containerEndPadding = 16.dp\n    val containerBottomPadding = 8.dp\n\n    val internalBlogStartPadding = 8.dp\n    val internalBlogTopPadding = 8.dp\n    val internalBlogEndPadding = 8.dp\n    val internalBlogBottomPadding = 8.dp\n\n    val headLineToContentPadding = 6.dp\n\n    val triggerAccountAvatarSize = 26.dp\n\n    val typeLogoSize = 20.dp\n}\n\n@Composable\nfun defaultNotificationStyle(\n    nameMaxLength: Int = NotificationStyleDefaults.nameMaxLength,\n    containerPaddings: PaddingValues = PaddingValues(\n        start = NotificationStyleDefaults.containerStartPadding,\n        top = NotificationStyleDefaults.containerTopPadding,\n        end = NotificationStyleDefaults.containerEndPadding,\n        bottom = NotificationStyleDefaults.containerBottomPadding,\n    ),\n    internalBlogPadding: PaddingValues = PaddingValues(\n        start = NotificationStyleDefaults.internalBlogStartPadding,\n        top = NotificationStyleDefaults.internalBlogTopPadding,\n        end = NotificationStyleDefaults.internalBlogEndPadding,\n        bottom = NotificationStyleDefaults.internalBlogBottomPadding,\n    ),\n    headLineToContentPadding: Dp = NotificationStyleDefaults.headLineToContentPadding,\n    statusStyle: StatusStyle = LocalStatusUiConfig.current.contentStyle,\n    triggerAccountAvatarSize: Dp = NotificationStyleDefaults.triggerAccountAvatarSize,\n    typeLogoSize: Dp = NotificationStyleDefaults.typeLogoSize,\n) = NotificationStyle(\n    nameMaxLength = nameMaxLength,\n    containerPaddings = containerPaddings,\n    internalBlogPadding = internalBlogPadding,\n    headLineToContentPadding = headLineToContentPadding,\n    statusStyle = statusStyle,\n    triggerAccountAvatarSize = triggerAccountAvatarSize,\n    typeLogoSize = typeLogoSize,\n)\n"
  },
  {
    "path": "commonbiz/sharedscreen/src/commonMain/kotlin/com/zhangke/fread/commonbiz/shared/notification/UnknownNotification.kt",
    "content": "package com.zhangke.fread.commonbiz.shared.notification\n\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.notification.StatusNotification\nimport org.jetbrains.compose.resources.stringResource\n\n@Composable\nfun UnknownNotification(\n    notification: StatusNotification.Unknown,\n) {\n    Column(\n        modifier = Modifier\n            .fillMaxWidth(),\n        horizontalAlignment = Alignment.CenterHorizontally,\n    ) {\n        Text(\n            modifier = Modifier\n                .fillMaxWidth()\n                .padding(horizontal = 16.dp),\n            text = stringResource(LocalizedString.sharedNotificationUnknownDesc),\n        )\n        Text(\n            modifier = Modifier\n                .fillMaxWidth()\n                .padding(horizontal = 16.dp),\n            text = notification.message,\n            maxLines = 3,\n            overflow = TextOverflow.Ellipsis,\n        )\n    }\n}\n"
  },
  {
    "path": "commonbiz/sharedscreen/src/commonMain/kotlin/com/zhangke/fread/commonbiz/shared/repo/SelectedAccountPublishingRepo.kt",
    "content": "package com.zhangke.fread.commonbiz.shared.repo\n\nimport com.zhangke.fread.commonbiz.shared.db.SelectedAccountPublishing\nimport com.zhangke.fread.commonbiz.shared.db.SelectedAccountPublishingDatabase\n\nclass SelectedAccountPublishingRepo (\n    private val database: SelectedAccountPublishingDatabase,\n) {\n\n    suspend fun getAll(): List<String> {\n        return database.dao().getAll().map { it.accountUri }\n    }\n\n    suspend fun replace(items: List<String>) {\n        database.dao().let {\n            it.deleteTable()\n            it.insert(items.map { SelectedAccountPublishing(it) })\n        }\n    }\n\n    suspend fun deleteTable() {\n        database.dao().deleteTable()\n    }\n}"
  },
  {
    "path": "commonbiz/sharedscreen/src/commonMain/kotlin/com/zhangke/fread/commonbiz/shared/screen/ImageViewerScreen.kt",
    "content": "package com.zhangke.fread.commonbiz.shared.screen\n\nimport androidx.compose.animation.core.Animatable\nimport androidx.compose.animation.core.FastOutSlowInEasing\nimport androidx.compose.animation.core.tween\nimport androidx.compose.foundation.Canvas\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.BoxScope\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.statusBarsPadding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.layout.wrapContentHeight\nimport androidx.compose.foundation.pager.HorizontalPager\nimport androidx.compose.foundation.pager.rememberPagerState\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Info\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.ModalBottomSheet\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableFloatStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.geometry.Size\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.unit.dp\nimport androidx.navigation3.runtime.NavKey\nimport com.seiko.imageloader.LocalImageLoader\nimport com.seiko.imageloader.model.ImageRequest\nimport com.seiko.imageloader.model.ImageResult\nimport com.seiko.imageloader.option.SizeResolver\nimport com.seiko.imageloader.rememberImagePainter\nimport com.zhangke.framework.blurhash.blurhash\nimport com.zhangke.framework.composable.HorizontalPageIndicator\nimport com.zhangke.framework.composable.SimpleIconButton\nimport com.zhangke.framework.composable.Toolbar\nimport com.zhangke.framework.composable.currentOrThrow\nimport com.zhangke.framework.composable.image.viewer.ImageViewer\nimport com.zhangke.framework.composable.image.viewer.ImageViewerDefault\nimport com.zhangke.framework.composable.image.viewer.rememberImageViewerState\nimport com.zhangke.framework.composable.rememberTransientModalBottomSheetState\nimport com.zhangke.framework.imageloader.executeSafety\nimport com.zhangke.framework.nav.LocalNavBackStack\nimport com.zhangke.framework.nav.popIfNotRoot\nimport com.zhangke.framework.nav.sharedElement\nimport com.zhangke.framework.permission.RequireLocalStoragePermission\nimport com.zhangke.framework.utils.PlatformSerializable\nimport com.zhangke.fread.common.utils.LocalMediaFileHelper\nimport kotlinx.coroutines.delay\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class ImageViewerScreenNavKey(\n    val selectedIndex: Int,\n    val imageList: List<ImageViewerImage>,\n) : NavKey\n\n@Composable\nfun ImageViewerScreen(\n    selectedIndex: Int,\n    imageList: List<ImageViewerImage>,\n) {\n    val backStack = LocalNavBackStack.currentOrThrow\n    val backgroundCommonAlpha = 0.95F\n    if (imageList.isEmpty()) {\n        LaunchedEffect(imageList) { backStack.popIfNotRoot() }\n        return\n    }\n    var backgroundColorAlpha by remember {\n        mutableFloatStateOf(0F)\n    }\n    var showIndicator by remember {\n        mutableStateOf(false)\n    }\n    LaunchedEffect(Unit) {\n        Animatable(0F).animateTo(\n            targetValue = backgroundCommonAlpha,\n            animationSpec = tween(\n                ImageViewerDefault.ANIMATION_DURATION,\n                easing = FastOutSlowInEasing\n            ),\n        ) {\n            backgroundColorAlpha = value\n        }\n    }\n    Box(\n        modifier = Modifier.fillMaxSize(),\n    ) {\n        Canvas(modifier = Modifier.fillMaxSize()) {\n            drawRect(\n                color = Color.Black,\n                alpha = backgroundColorAlpha,\n            )\n        }\n        val pagerState = rememberPagerState(\n            initialPage = selectedIndex.coerceAtLeast(0),\n            pageCount = imageList::size,\n        )\n\n        Box(modifier = Modifier.fillMaxSize()) {\n            HorizontalPager(\n                modifier = Modifier\n                    .fillMaxSize(),\n                state = pagerState,\n            ) { pageIndex ->\n                val currentMedia = imageList[pageIndex]\n                ImagePageContent(\n                    image = currentMedia,\n                    onDismissRequest = backStack::popIfNotRoot,\n                )\n            }\n\n            ImageTopBar(imageList[pagerState.currentPage])\n\n            LaunchedEffect(Unit) {\n                delay(300)\n                showIndicator = true\n            }\n            if (showIndicator && pagerState.pageCount > 1) {\n                HorizontalPageIndicator(\n                    currentIndex = pagerState.currentPage,\n                    pageCount = pagerState.pageCount,\n                    modifier = Modifier\n                        .padding(bottom = 64.dp)\n                        .align(Alignment.BottomCenter)\n                        .fillMaxWidth(),\n                )\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun ImagePageContent(\n    image: ImageViewerImage,\n    onDismissRequest: () -> Unit,\n) {\n    val imageLoader = LocalImageLoader.current\n    var aspectRatio: Float? by remember { mutableStateOf(image.aspect) }\n    if (aspectRatio == null) {\n        LaunchedEffect(image) {\n            val request = ImageRequest {\n                data(image.url)\n                size(SizeResolver(Size(50f, 50f)))\n            }\n            aspectRatio = imageLoader.executeSafety(request).aspectRatio() ?: 1F\n        }\n    }\n    if (aspectRatio != null) {\n        val viewerState = rememberImageViewerState(\n            aspectRatio = aspectRatio!!,\n            onDismissRequest = onDismissRequest,\n        )\n        ImageViewer(\n            state = viewerState,\n            modifier = Modifier.fillMaxSize(),\n        ) {\n            val request = remember(image.url) {\n                ImageRequest(image.url)\n            }\n            Image(\n                painter = rememberImagePainter(request = request),\n                modifier = Modifier\n                    .fillMaxSize()\n                    .blurhash(image.blurhash)\n                    .let {\n                        if (image.sharedElementKey.isNullOrEmpty()) {\n                            it\n                        } else {\n                            it.sharedElement(image.sharedElementKey)\n                        }\n                    },\n                contentScale = ContentScale.FillBounds,\n                contentDescription = image.description,\n            )\n        }\n    }\n}\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nprivate fun BoxScope.ImageTopBar(image: ImageViewerImage) {\n    var showBottomSheet by remember { mutableStateOf(false) }\n    val mediaFileHelper = LocalMediaFileHelper.current\n    Row(\n        modifier = Modifier\n            .align(Alignment.TopEnd)\n            .statusBarsPadding()\n            .padding(top = 8.dp, end = 8.dp),\n        horizontalArrangement = Arrangement.Center,\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        var needSaveImage by remember { mutableStateOf(false) }\n        if (needSaveImage) {\n            RequireLocalStoragePermission(\n                onPermissionGranted = {\n                    mediaFileHelper.saveImageToGallery(image.url)\n                    needSaveImage = false\n                },\n                onPermissionDenied = {\n                    needSaveImage = false\n                },\n            )\n        }\n        Toolbar.DownloadButton(\n            onClick = {\n                needSaveImage = true\n            },\n            tint = Color.White.copy(alpha = 0.7F),\n        )\n        if (!image.description.isNullOrEmpty()) {\n            Spacer(modifier = Modifier.width(16.dp))\n            SimpleIconButton(\n                onClick = { showBottomSheet = true },\n                tint = Color.White.copy(alpha = 0.7F),\n                imageVector = Icons.Default.Info,\n                contentDescription = \"Image description\",\n            )\n            if (showBottomSheet) {\n                val sheetState = rememberTransientModalBottomSheetState()\n                ModalBottomSheet(\n                    sheetState = sheetState,\n                    onDismissRequest = { showBottomSheet = false },\n                ) {\n                    Box(\n                        modifier = Modifier\n                            .fillMaxWidth()\n                            .padding(start = 16.dp, top = 8.dp, end = 16.dp, bottom = 28.dp)\n                            .wrapContentHeight()\n                    ) {\n                        Text(text = image.description)\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Serializable\ndata class ImageViewerImage(\n    val url: String,\n    val previewUrl: String? = null,\n    val description: String? = null,\n    val blurhash: String? = null,\n    val aspect: Float? = null,\n    val sharedElementKey: String? = null,\n) : PlatformSerializable\n\ninternal expect fun ImageResult.aspectRatio(): Float?\n"
  },
  {
    "path": "commonbiz/sharedscreen/src/commonMain/kotlin/com/zhangke/fread/commonbiz/shared/screen/SelectLanguageScreen.kt",
    "content": "package com.zhangke.fread.commonbiz.shared.screen\n\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.fadeIn\nimport androidx.compose.animation.fadeOut\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.imePadding\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.statusBars\nimport androidx.compose.foundation.layout.systemBarsPadding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.LazyRow\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Check\nimport androidx.compose.material.icons.filled.Clear\nimport androidx.compose.material.icons.filled.Search\nimport androidx.compose.material3.Card\nimport androidx.compose.material3.CardDefaults\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.SearchBar\nimport androidx.compose.material3.SearchBarDefaults\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateListOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.composable.Toolbar\nimport com.zhangke.framework.composable.currentOrThrow\nimport com.zhangke.framework.nav.LocalNavBackStack\nimport com.zhangke.framework.nav.ScreenEventFlow\nimport com.zhangke.framework.utils.LanguageUtils\nimport com.zhangke.framework.utils.Locale\nimport com.zhangke.framework.utils.getDisplayName\nimport com.zhangke.framework.utils.languageCode\nimport com.zhangke.fread.localization.LocalizedString\nimport kotlinx.coroutines.launch\nimport kotlinx.serialization.Serializable\nimport org.jetbrains.compose.resources.stringResource\n\n@Serializable\ndata class SelectLanguageScreenNavKey(\n    val selectedLanguages: List<String> = emptyList(),\n    val maxSelectCount: Int = 1,\n) : NavKey {\n\n    companion object {\n\n        val selectedFlow = ScreenEventFlow<List<String>>()\n    }\n}\n\n@Composable\nfun SelectLanguageScreen(\n    selectedLanguages: List<String> = emptyList(),\n    maxSelectCount: Int = 1,\n) {\n    val backStack = LocalNavBackStack.currentOrThrow\n    val multipleSelection = maxSelectCount > 1\n    val languageList = remember {\n        val list = LanguageUtils.getAllLanguages().map { language ->\n            val selected = selectedLanguages.any { language.languageCode == it }\n            LanguageUiState(language, selected)\n        }\n        mutableStateListOf(*list.toTypedArray())\n    }\n    val coroutineScope = rememberCoroutineScope()\n    fun onLanguageSelected(languageUiState: LanguageUiState) {\n        if (!multipleSelection) {\n            coroutineScope.launch {\n                SelectLanguageScreenNavKey.selectedFlow.emit(listOf(languageUiState.local.languageCode))\n                backStack.removeLastOrNull()\n            }\n            return\n        }\n        if (languageUiState.selected) {\n            languageList[languageList.indexOf(languageUiState)] =\n                languageUiState.copy(selected = false)\n        } else {\n            if (languageList.count { it.selected } < maxSelectCount) {\n                languageList[languageList.indexOf(languageUiState)] =\n                    languageUiState.copy(selected = true)\n            }\n        }\n    }\n    Scaffold(\n        topBar = {\n            var toolbarVisible by remember { mutableStateOf(true) }\n            AnimatedVisibility(\n                visible = toolbarVisible,\n                enter = fadeIn(),\n                exit = fadeOut(),\n            ) {\n                Toolbar(\n                    title = stringResource(LocalizedString.sharedSelectLanguageTitle),\n                    onBackClick = backStack::removeLastOrNull,\n                    actions = {\n                        IconButton(\n                            onClick = { toolbarVisible = false },\n                        ) {\n                            Icon(\n                                imageVector = Icons.Default.Search,\n                                contentDescription = stringResource(LocalizedString.search),\n                            )\n                        }\n                        if (multipleSelection) {\n                            IconButton(\n                                onClick = {\n                                    coroutineScope.launch {\n                                        SelectLanguageScreenNavKey.selectedFlow\n                                            .emit(languageList.filter { it.selected }\n                                                .map { it.local.languageCode })\n                                        backStack.removeLastOrNull()\n                                    }\n                                },\n                            ) {\n                                Icon(\n                                    imageVector = Icons.Default.Check,\n                                    contentDescription = stringResource(LocalizedString.ok),\n                                )\n                            }\n                        }\n                    }\n                )\n            }\n            var query by rememberSaveable { mutableStateOf(\"\") }\n            AnimatedVisibility(\n                visible = !toolbarVisible,\n                enter = fadeIn(),\n                exit = fadeOut(),\n            ) {\n                SearchLanguageBar(\n                    query = query,\n                    onQueryChanged = { query = it },\n                    list = languageList,\n                    onClose = { toolbarVisible = true },\n                    onLanguageClicked = { onLanguageSelected(it) },\n                )\n            }\n        },\n    ) { paddingValues ->\n        LazyColumn(\n            modifier = Modifier\n                .padding(paddingValues)\n                .fillMaxWidth(),\n        ) {\n            if (languageList.count { it.selected } > 0) {\n                stickyHeader {\n                    LazyRow(\n                        verticalAlignment = Alignment.CenterVertically,\n                        contentPadding = PaddingValues(horizontal = 16.dp, vertical = 6.dp),\n                    ) {\n                        languageList.filter { it.selected }\n                            .reversed()\n                            .forEach { languageUiState ->\n                                item {\n                                    SelectedLanguageItem(\n                                        languageUiState = languageUiState,\n                                        onRemoveClick = {\n                                            languageList[languageList.indexOf(languageUiState)] =\n                                                languageUiState.copy(selected = false)\n                                        }\n                                    )\n                                }\n                                item {\n                                    Spacer(modifier = Modifier.width(16.dp))\n                                }\n                            }\n                    }\n                }\n            }\n            items(languageList) { languageUiState ->\n                LanguageItem(\n                    languageUiState = languageUiState,\n                    onLanguageClicked = { onLanguageSelected(it) },\n                )\n            }\n        }\n    }\n}\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nprivate fun SearchLanguageBar(\n    query: String,\n    onQueryChanged: (String) -> Unit,\n    onClose: () -> Unit,\n    list: List<LanguageUiState>,\n    onLanguageClicked: (LanguageUiState) -> Unit,\n) {\n    val currentLanguageList = remember {\n        mutableStateListOf(*list.filterByQuery(query).toTypedArray())\n    }\n    SearchBar(\n        modifier = Modifier.fillMaxWidth().systemBarsPadding(),\n        expanded = true,\n        windowInsets = WindowInsets.statusBars,\n        onExpandedChange = { onClose() },\n        inputField = {\n            SearchBarDefaults.InputField(\n                query = query,\n                onQueryChange = { q ->\n                    onQueryChanged(q)\n                    currentLanguageList.clear()\n                    if (q.isNotEmpty()) {\n                        currentLanguageList += list.filterByQuery(q)\n                    }\n                },\n                onSearch = {},\n                expanded = true,\n                onExpandedChange = {\n                    if (!it) {\n                        onClose()\n                    }\n                },\n                leadingIcon = {\n                    Toolbar.BackButton(onBackClick = onClose)\n                },\n            )\n        },\n    ) {\n        LazyColumn(modifier = Modifier.fillMaxSize().imePadding()) {\n            items(currentLanguageList) { language ->\n                LanguageItem(\n                    languageUiState = language,\n                    onLanguageClicked = {\n                        onLanguageClicked(it)\n                        onClose()\n                    },\n                )\n            }\n        }\n    }\n}\n\nprivate fun List<LanguageUiState>.filterByQuery(query: String): List<LanguageUiState> {\n    if (query.isEmpty()) return emptyList()\n    return this.filter { languageUiState ->\n        val displayName = languageUiState.local.getDisplayName(languageUiState.local).lowercase()\n        val lowercaseQuery = query.lowercase()\n        displayName.contains(lowercaseQuery) || lowercaseQuery.contains(displayName)\n    }\n}\n\n@Composable\nprivate fun LanguageItem(\n    languageUiState: LanguageUiState,\n    onLanguageClicked: (LanguageUiState) -> Unit,\n) {\n    Box(\n        modifier = Modifier\n            .fillMaxWidth()\n            .height(60.dp)\n            .clickable { onLanguageClicked(languageUiState) },\n    ) {\n        Box(\n            modifier = Modifier.fillMaxSize().padding(start = 16.dp, end = 16.dp),\n        ) {\n            Text(\n                modifier = Modifier\n                    .align(Alignment.CenterStart),\n                text = languageUiState.local.getDisplayName(languageUiState.local),\n            )\n            if (languageUiState.selected) {\n                Icon(\n                    modifier = Modifier\n                        .align(Alignment.CenterEnd),\n                    imageVector = Icons.Default.Check,\n                    contentDescription = \"Checked\",\n                    tint = MaterialTheme.colorScheme.primary,\n                )\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun SelectedLanguageItem(languageUiState: LanguageUiState, onRemoveClick: () -> Unit) {\n    Box(\n        modifier = Modifier,\n    ) {\n        Card(\n            elevation = CardDefaults.cardElevation(defaultElevation = 2.dp),\n            colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.secondaryContainer),\n            onClick = onRemoveClick,\n            shape = RoundedCornerShape(50),\n        ) {\n            Row(\n                modifier = Modifier.padding(horizontal = 16.dp, vertical = 6.dp),\n                verticalAlignment = Alignment.CenterVertically,\n            ) {\n                Text(\n                    modifier = Modifier,\n                    text = languageUiState.local.getDisplayName(languageUiState.local),\n                    style = MaterialTheme.typography.labelMedium,\n                )\n                Spacer(modifier = Modifier.size(2.dp))\n                Icon(\n                    modifier = Modifier.size(14.dp),\n                    imageVector = Icons.Default.Clear,\n                    contentDescription = \"Remove\",\n                )\n            }\n        }\n    }\n}\n\ndata class LanguageUiState(\n    val local: Locale,\n    val selected: Boolean,\n)\n"
  },
  {
    "path": "commonbiz/sharedscreen/src/commonMain/kotlin/com/zhangke/fread/commonbiz/shared/screen/publish/PublishBlogScreen.kt",
    "content": "package com.zhangke.fread.commonbiz.shared.screen.publish\n\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.SnackbarHost\nimport androidx.compose.material3.SnackbarHostState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.text.input.TextFieldValue\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.composable.Toolbar\nimport com.zhangke.framework.composable.rememberSnackbarHostState\nimport com.zhangke.fread.localization.LocalizedString\nimport kotlinx.serialization.Serializable\nimport org.jetbrains.compose.resources.stringResource\n\n@Serializable\nobject PublishBlogScreenNavKey : NavKey\n\n@Composable\nfun PublishBlogScreen() {\n    val snackbarHostState = rememberSnackbarHostState()\n//        PublishBlogContent(\n//            snackbarHostState = snackbarHostState,\n//            onBackClick = navigator::pop,\n//        )\n}\n\n@Composable\nprivate fun PublishBlogContent(\n    uiState: PublishBlogUiState,\n    onContentChanged: (TextFieldValue) -> Unit,\n    snackbarHostState: SnackbarHostState,\n    onBackClick: () -> Unit,\n) {\n    Scaffold(\n        topBar = {\n            Toolbar(\n                title = stringResource(LocalizedString.sharedPublishBlogTitle),\n                onBackClick = onBackClick,\n            )\n        },\n        snackbarHost = {\n            SnackbarHost(snackbarHostState)\n        },\n    ) { innerPadding ->\n        Column(\n            modifier = Modifier.fillMaxSize().padding(innerPadding),\n        ) {\n//                InputBlogTextField(\n//                    modifier = Modifier\n//                        .fillMaxWidth()\n//                        .padding(horizontal = 8.dp),\n//                    textFieldValue = uiState.content,\n//                    onContentChanged = onContentChanged,\n//                )\n        }\n    }\n}\n"
  },
  {
    "path": "commonbiz/sharedscreen/src/commonMain/kotlin/com/zhangke/fread/commonbiz/shared/screen/publish/PublishBlogUiState.kt",
    "content": "package com.zhangke.fread.commonbiz.shared.screen.publish\n\nimport androidx.compose.ui.text.input.TextFieldValue\n\ndata class PublishBlogUiState(\n    val content: TextFieldValue,\n)\n"
  },
  {
    "path": "commonbiz/sharedscreen/src/commonMain/kotlin/com/zhangke/fread/commonbiz/shared/screen/publish/PublishPostBottomPanel.kt",
    "content": "package com.zhangke.fread.commonbiz.shared.screen.publish\n\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.RowScope\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.ime\nimport androidx.compose.foundation.layout.navigationBarsPadding\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Image\nimport androidx.compose.material.icons.filled.Language\nimport androidx.compose.material.icons.filled.SmartDisplay\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.unit.dp\nimport com.zhangke.framework.composable.currentOrThrow\nimport com.zhangke.framework.composable.ConsumeFlow\nimport com.zhangke.framework.composable.SimpleIconButton\nimport com.zhangke.framework.composable.pick.PickVisualMediaLauncherContainer\nimport com.zhangke.framework.nav.LocalNavBackStack\nimport com.zhangke.framework.utils.PlatformUri\nimport com.zhangke.framework.utils.getDisplayName\nimport com.zhangke.framework.utils.initLocale\nimport com.zhangke.fread.commonbiz.shared.screen.SelectLanguageScreenNavKey\nimport com.zhangke.fread.status.ui.common.RemainingTextStatus\nimport com.zhangke.fread.statusui.ic_post_status_spoiler\nimport org.jetbrains.compose.resources.painterResource\n\n@Composable\nfun Modifier.bottomPaddingAsBottomBar(): Modifier {\n    val bottomPaddingByIme = WindowInsets.ime.asPaddingValues().calculateBottomPadding()\n    return if (bottomPaddingByIme > 0.dp) {\n        this.padding(bottom = bottomPaddingByIme)\n    } else {\n        this.navigationBarsPadding()\n    }\n}\n\n@Composable\nfun PublishPostFeaturesPanel(\n    modifier: Modifier,\n    contentLength: Int,\n    maxContentLimit: Int,\n    mediaAvailableCount: Int,\n    onMediaSelected: (List<PlatformUri>) -> Unit,\n    selectedLanguages: List<String>,\n    maxLanguageCount: Int,\n    onLanguageSelected: (List<String>) -> Unit,\n    mediaSelectEnabled: Boolean = true,\n    containerColor: Color = MaterialTheme.colorScheme.surface,\n    actions: @Composable RowScope.() -> Unit = {},\n    floatingBar: @Composable () -> Unit = {},\n) {\n    Surface(\n        modifier = modifier.fillMaxWidth(),\n        color = containerColor,\n    ) {\n        Column(modifier = Modifier.fillMaxWidth()) {\n            floatingBar.invoke()\n            Row(\n                modifier = Modifier.fillMaxWidth()\n                    .padding(start = 8.dp, end = 16.dp),\n                verticalAlignment = Alignment.CenterVertically,\n            ) {\n                PickVisualMediaLauncherContainer(\n                    onResult = onMediaSelected,\n                    maxItems = mediaAvailableCount,\n                ) {\n                    val enabled = mediaSelectEnabled && mediaAvailableCount > 0\n                    SimpleIconButton(\n                        modifier = Modifier,\n                        onClick = { launchImage() },\n                        onLongClick = { launchImageFile() },\n                        enabled = enabled,\n                        imageVector = Icons.Default.Image,\n                        contentDescription = \"Add Image\",\n                    )\n                }\n                PickVisualMediaLauncherContainer(\n                    onResult = onMediaSelected,\n                    maxItems = 1,\n                ) {\n                    val enabled = mediaSelectEnabled && mediaAvailableCount > 0\n                    SimpleIconButton(\n                        modifier = Modifier,\n                        onClick = { launchVideo() },\n                        onLongClick = { launchVideoFile() },\n                        enabled = enabled,\n                        imageVector = Icons.Default.SmartDisplay,\n                        contentDescription = \"Add Video\",\n                    )\n                }\n                actions()\n                Spacer(modifier = Modifier.weight(1F))\n                SelectLanguageIconButton(\n                    modifier = Modifier,\n                    onLanguageSelected = onLanguageSelected,\n                    selectedLanguages = selectedLanguages,\n                    maxLanguageCount = maxLanguageCount,\n                )\n                RemainingTextStatus(\n                    modifier = Modifier,\n                    maxCount = maxContentLimit,\n                    contentLength = contentLength,\n                )\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun SelectLanguageIconButton(\n    modifier: Modifier,\n    selectedLanguages: List<String>,\n    maxLanguageCount: Int,\n    onLanguageSelected: (List<String>) -> Unit,\n) {\n    val backStack = LocalNavBackStack.currentOrThrow\n    ConsumeFlow(SelectLanguageScreenNavKey.selectedFlow.flow) {\n        onLanguageSelected(it)\n    }\n    fun selectLanguage() {\n        backStack.add(\n            SelectLanguageScreenNavKey(\n                selectedLanguages = selectedLanguages,\n                maxSelectCount = maxLanguageCount,\n            )\n        )\n    }\n    Box(modifier = modifier) {\n        if (selectedLanguages.isEmpty()) {\n            SimpleIconButton(\n                onClick = { selectLanguage() },\n                imageVector = Icons.Default.Language,\n                contentDescription = \"Choose language\",\n            )\n        } else {\n            TextButton(\n                onClick = { selectLanguage() },\n            ) {\n                val languages = remember(selectedLanguages) {\n                    selectedLanguages.map { initLocale(it) }\n                        .joinToString { it.getDisplayName(it) }\n                }\n                Text(\n                    text = languages,\n                    style = MaterialTheme.typography.labelMedium,\n                    color = MaterialTheme.colorScheme.primary,\n                )\n            }\n        }\n    }\n}\n\n@Composable\nfun SensitiveIconButton(onSensitiveClick: () -> Unit) {\n    IconButton(\n        onClick = onSensitiveClick,\n        modifier = Modifier.padding(start = 4.dp),\n    ) {\n        Box(modifier = Modifier.size(29.dp)) {\n            Icon(\n                modifier = Modifier.size(24.dp).align(Alignment.TopCenter),\n                painter = painterResource(com.zhangke.fread.statusui.Res.drawable.ic_post_status_spoiler),\n                contentDescription = \"Sensitive content\",\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "commonbiz/sharedscreen/src/commonMain/kotlin/com/zhangke/fread/commonbiz/shared/screen/publish/PublishPostMediaAttachment.kt",
    "content": "package com.zhangke.fread.commonbiz.shared.screen.publish\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.aspectRatio\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Add\nimport androidx.compose.material.icons.filled.Check\nimport androidx.compose.material.icons.filled.Close\nimport androidx.compose.material.icons.filled.PlayArrow\nimport androidx.compose.material3.Button\nimport androidx.compose.material3.Card\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.ModalBottomSheet\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextField\nimport androidx.compose.material3.TextFieldDefaults\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.dp\nimport com.seiko.imageloader.ui.AutoSizeImage\nimport com.zhangke.framework.composable.Grid\nimport com.zhangke.framework.composable.noRippleClick\nimport com.zhangke.framework.composable.rememberTransientModalBottomSheetState\nimport com.zhangke.framework.utils.transparentIndicatorColors\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.ui.common.RemainingTextStatus\nimport org.jetbrains.compose.resources.stringResource\n\n@Composable\nfun PublishPostMediaAttachment(\n    modifier: Modifier,\n    medias: List<PublishPostMedia>,\n    mediaAltMaxCharacters: Int,\n    onAltChanged: (PublishPostMedia, String) -> Unit,\n    onDeleteClick: (PublishPostMedia) -> Unit,\n) {\n    Grid(\n        modifier = modifier,\n        columnCount = 2,\n        verticalSpacing = 16.dp,\n        horizontalSpacing = 16.dp,\n    ) {\n        medias.forEach { media ->\n            PublishPostMediaAttachmentImage(\n                modifier = Modifier.fillMaxWidth().aspectRatio(1F),\n                image = media,\n                isVideo = media.isVideo,\n                mediaAltMaxCharacters = mediaAltMaxCharacters,\n                onAltChanged = onAltChanged,\n                onDeleteClick = onDeleteClick,\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun PublishPostMediaAttachmentImage(\n    modifier: Modifier,\n    image: PublishPostMedia,\n    isVideo: Boolean,\n    mediaAltMaxCharacters: Int,\n    onAltChanged: (PublishPostMedia, String) -> Unit,\n    onDeleteClick: (PublishPostMedia) -> Unit,\n) {\n    val shadowColor = Color.Black.copy(alpha = 0.7F)\n    val fontColor = Color.White\n    var showAltDialog by remember { mutableStateOf(false) }\n    Box(modifier = modifier) {\n        AutoSizeImage(\n            url = image.uri,\n            modifier = Modifier.fillMaxSize()\n                .clip(RoundedCornerShape(6.dp))\n                .border(\n                    width = 1.dp,\n                    color = MaterialTheme.colorScheme.outlineVariant,\n                    shape = RoundedCornerShape(6.dp),\n                ),\n            contentDescription = image.alt,\n            contentScale = ContentScale.Crop,\n        )\n        Row(\n            modifier = Modifier.padding(start = 8.dp, top = 8.dp)\n                .background(\n                    color = shadowColor,\n                    shape = RoundedCornerShape(2.dp),\n                ).padding(start = 2.dp, top = 2.dp, bottom = 2.dp, end = 2.dp)\n                .noRippleClick { showAltDialog = true },\n            verticalAlignment = Alignment.CenterVertically,\n        ) {\n            val iconVector = if (image.alt.isNullOrEmpty()) {\n                Icons.Default.Add\n            } else {\n                Icons.Default.Check\n            }\n            Icon(\n                modifier = Modifier.size(14.dp),\n                imageVector = iconVector,\n                contentDescription = if (image.alt.isNullOrEmpty()) {\n                    \"Add ALT\"\n                } else {\n                    \"ALT Added\"\n                },\n                tint = fontColor,\n            )\n            Spacer(modifier = Modifier.width(1.dp))\n            Text(\n                text = stringResource(LocalizedString.sharedAltLabel),\n                color = fontColor,\n                style = MaterialTheme.typography.labelSmall,\n            )\n        }\n        Box(\n            modifier = Modifier.noRippleClick { onDeleteClick(image) }\n                .align(Alignment.TopEnd)\n                .padding(top = 8.dp, end = 8.dp)\n                .background(\n                    color = shadowColor,\n                    shape = CircleShape,\n                ).padding(4.dp),\n        ) {\n            Icon(\n                modifier = Modifier.size(14.dp),\n                imageVector = Icons.Default.Close,\n                contentDescription = \"Delete\",\n                tint = fontColor,\n            )\n        }\n        if (isVideo) {\n            Box(\n                modifier = Modifier.align(Alignment.Center)\n                    .background(\n                        color = shadowColor,\n                        shape = CircleShape,\n                    ).padding(1.dp),\n            ) {\n                Icon(\n                    modifier = Modifier.size(24.dp),\n                    imageVector = Icons.Default.PlayArrow,\n                    contentDescription = null,\n                    tint = fontColor,\n                )\n            }\n        }\n    }\n    if (showAltDialog) {\n        PublishPostImageAltDialog(\n            imageUri = image.uri,\n            onDismissRequest = { showAltDialog = false },\n            alt = image.alt.orEmpty(),\n            maxCharacters = mediaAltMaxCharacters,\n            onAltChanged = { onAltChanged(image, it) },\n        )\n    }\n}\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nprivate fun PublishPostImageAltDialog(\n    imageUri: String,\n    onDismissRequest: () -> Unit,\n    alt: String,\n    maxCharacters: Int,\n    onAltChanged: (String) -> Unit,\n) {\n    val sheetState = rememberTransientModalBottomSheetState(skipPartiallyExpanded = true)\n    ModalBottomSheet(\n        onDismissRequest = onDismissRequest,\n        sheetState = sheetState,\n    ) {\n        Column(\n            modifier = Modifier.fillMaxWidth().padding(start = 16.dp, end = 16.dp, bottom = 24.dp),\n        ) {\n            Text(\n                text = stringResource(LocalizedString.sharedPublishMediaAltDialogTitle),\n                style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold),\n            )\n\n            Card(\n                modifier = Modifier.padding(top = 6.dp)\n                    .fillMaxWidth()\n                    .aspectRatio(1.7F),\n            ) {\n                Box(\n                    modifier = Modifier.fillMaxSize(),\n                ) {\n                    AutoSizeImage(\n                        url = imageUri,\n                        modifier = Modifier.align(Alignment.Center),\n                        contentDescription = null,\n                    )\n                }\n            }\n\n            Text(\n                modifier = Modifier.padding(top = 16.dp),\n                text = stringResource(LocalizedString.sharedPublishMediaAltDialogInputTip),\n                style = MaterialTheme.typography.labelMedium,\n            )\n\n            var inputtedValue by remember(alt) { mutableStateOf(alt) }\n            TextField(\n                modifier = Modifier.padding(top = 8.dp)\n                    .fillMaxWidth()\n                    .border(\n                        width = 1.dp,\n                        color = MaterialTheme.colorScheme.primary,\n                        shape = RoundedCornerShape(16.dp),\n                    ),\n                value = inputtedValue,\n                onValueChange = { inputtedValue = it },\n                minLines = 1,\n                placeholder = { Text(text = stringResource(LocalizedString.sharedPublishMediaAltDialogInputHint)) },\n                colors = TextFieldDefaults.transparentIndicatorColors.copy(\n                    focusedContainerColor = Color.Transparent,\n                    unfocusedContainerColor = Color.Transparent,\n                ),\n            )\n            Row(\n                modifier = Modifier.fillMaxWidth()\n                    .padding(top = 16.dp),\n                verticalAlignment = Alignment.CenterVertically,\n            ) {\n                RemainingTextStatus(\n                    modifier = Modifier.padding(start = 16.dp),\n                    maxCount = maxCharacters,\n                    contentLength = inputtedValue.length,\n                )\n                Button(\n                    modifier = Modifier.padding(start = 16.dp).weight(1F),\n                    onClick = {\n                        onAltChanged(inputtedValue)\n                        onDismissRequest()\n                    },\n                ) {\n                    Text(text = stringResource(LocalizedString.save))\n                }\n            }\n        }\n    }\n}\n\ninterface PublishPostMedia {\n\n    val uri: String\n\n    val alt: String?\n\n    val isVideo: Boolean\n}\n"
  },
  {
    "path": "commonbiz/sharedscreen/src/commonMain/kotlin/com/zhangke/fread/commonbiz/shared/screen/publish/PublishPostScaffold.kt",
    "content": "package com.zhangke.fread.commonbiz.shared.screen.publish\n\nimport androidx.compose.foundation.gestures.animateScrollBy\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.heightIn\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Group\nimport androidx.compose.material.icons.filled.PersonAdd\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.SnackbarHost\nimport androidx.compose.material3.SnackbarHostState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.layout.onSizeChanged\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.platform.LocalFocusManager\nimport androidx.compose.ui.text.buildAnnotatedString\nimport androidx.compose.ui.text.input.TextFieldValue\nimport androidx.compose.ui.unit.dp\nimport com.seiko.imageloader.ui.AutoSizeImage\nimport com.zhangke.framework.composable.SimpleIconButton\nimport com.zhangke.framework.composable.keyboardAsState\nimport com.zhangke.framework.utils.HighlightTextBuildUtil\nimport com.zhangke.framework.utils.pxToDp\nimport com.zhangke.framework.utils.toPx\nimport com.zhangke.fread.commonbiz.shared.screen.publish.composable.InputBlogTextField\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.account.LoggedAccount\nimport com.zhangke.fread.status.blog.Blog\nimport com.zhangke.fread.status.richtext.buildRichText\nimport com.zhangke.fread.status.ui.embed.BlogInEmbedding\nimport com.zhangke.fread.status.ui.publish.NameAndAccountInfo\nimport com.zhangke.fread.status.ui.publish.PublishBlogStyle\nimport com.zhangke.fread.status.ui.publish.PublishBlogStyleDefault\nimport com.zhangke.fread.status.ui.threads.ThreadsType\nimport com.zhangke.fread.status.ui.threads.blogBeReplyThreads\nimport com.zhangke.fread.status.ui.threads.blogInReplyingThreads\nimport kotlinx.coroutines.delay\nimport org.jetbrains.compose.resources.stringResource\n\n@Composable\nfun PublishPostScaffold(\n    account: LoggedAccount?,\n    snackBarHostState: SnackbarHostState,\n    content: TextFieldValue,\n    showSwitchAccountIcon: Boolean,\n    showAddAccountIcon: Boolean,\n    publishEnabled: Boolean,\n    publishing: Boolean,\n    replyingBlog: Blog? = null,\n    onContentChanged: (TextFieldValue) -> Unit,\n    onPublishClick: () -> Unit,\n    onBackClick: () -> Unit,\n    onSwitchAccountClick: () -> Unit = {},\n    onAddAccountClick: () -> Unit = {},\n    contentWarning: @Composable () -> Unit = {},\n    postSettingLabel: @Composable () -> Unit,\n    bottomPanel: @Composable () -> Unit,\n    attachment: @Composable (PublishBlogStyle) -> Unit,\n    allowHashtagInHashtag: Boolean = false,\n) {\n    val density = LocalDensity.current\n    val style = PublishBlogStyleDefault.defaultStyle()\n    val focusManager = LocalFocusManager.current\n    val keyboardState by keyboardAsState()\n    LaunchedEffect(keyboardState) {\n        if (!keyboardState) {\n            focusManager.clearFocus()\n        }\n    }\n    Scaffold(\n        topBar = {\n            PublishTopBar(\n                publishing = publishing,\n                onBackClick = onBackClick,\n                publishEnabled = publishEnabled,\n                onPublishClick = onPublishClick,\n            )\n        },\n        snackbarHost = { SnackbarHost(snackBarHostState) },\n        bottomBar = { bottomPanel() },\n    ) { innerPadding ->\n        var contentHeight: Float? by remember { mutableStateOf(null) }\n        var replyingHeight: Float? by remember { mutableStateOf(null) }\n        val scrollState = rememberScrollState()\n        if (replyingBlog != null && replyingHeight != null) {\n            LaunchedEffect(replyingHeight) {\n                delay(300)\n                scrollState.animateScrollBy(replyingHeight!!)\n            }\n        }\n        Column(\n            modifier = Modifier.fillMaxSize()\n                .onSizeChanged {\n                    if (contentHeight === null || contentHeight == 0F) {\n                        contentHeight = it.height.toFloat()\n                    }\n                }\n                .padding(innerPadding)\n                .verticalScroll(scrollState)\n                .let {\n                    if (contentHeight != null && replyingHeight != null) {\n                        it.heightIn(min = (contentHeight!! + replyingHeight!!).pxToDp(density))\n                    } else {\n                        it\n                    }\n                },\n        ) {\n            if (replyingBlog != null) {\n                BlogInEmbedding(\n                    modifier = Modifier.fillMaxWidth()\n                        .onSizeChanged {\n                            if (replyingHeight == null || replyingHeight == 0F) {\n                                replyingHeight = it.height.toFloat()\n                            }\n                        }\n                        .blogBeReplyThreads(\n                            threadsType = ThreadsType.ANCESTOR,\n                            publishBlogStyle = style,\n                        )\n                        .padding(\n                            start = style.startPadding,\n                            end = style.endPadding,\n                        ),\n                    blog = replyingBlog,\n                    style = style.statusStyle,\n                )\n            }\n            Row(\n                modifier = Modifier.fillMaxWidth()\n                    .let {\n                        if (replyingBlog != null) {\n                            it.blogInReplyingThreads(\n                                threadsType = ThreadsType.ANCHOR,\n                                infoToTopSpacing = style.topPadding.toPx(),\n                                publishBlogStyle = style,\n                            ).padding(top = style.topPadding)\n                        } else {\n                            it\n                        }\n                    },\n            ) {\n                AutoSizeImage(\n                    url = account?.avatar.orEmpty(),\n                    modifier = Modifier.padding(start = 16.dp)\n                        .clip(CircleShape)\n                        .size(style.statusStyle.infoLineStyle.avatarSize),\n                    contentDescription = null,\n                )\n                Column(\n                    modifier = Modifier.weight(1F).padding(horizontal = 8.dp),\n                ) {\n                    NameAndAccountInfo(\n                        modifier = Modifier.fillMaxWidth(),\n                        humanizedName = buildRichText(document = account?.userName.orEmpty()),\n                        handle = account?.prettyHandle.orEmpty(),\n                        style = style.statusStyle,\n                    )\n                    Spacer(modifier = Modifier.height(1.dp))\n                    postSettingLabel()\n                }\n                if (showSwitchAccountIcon) {\n                    SimpleIconButton(\n                        modifier = Modifier,\n                        onClick = onSwitchAccountClick,\n                        imageVector = Icons.Default.Group,\n                        iconSize = 36.dp,\n                        contentDescription = \"Switch Account\",\n                    )\n                }\n                if (showAddAccountIcon) {\n                    Spacer(modifier = Modifier.width(2.dp))\n                    SimpleIconButton(\n                        modifier = Modifier,\n                        onClick = onAddAccountClick,\n                        imageVector = Icons.Default.PersonAdd,\n                        iconSize = 36.dp,\n                        contentDescription = \"Multi Account\",\n                    )\n                    Spacer(modifier = Modifier.width(8.dp))\n                }\n            }\n            contentWarning()\n            InputBlogTextField(\n                modifier = Modifier.fillMaxWidth(),\n                textFieldValue = content,\n                onContentChanged = onContentChanged,\n                placeholder = if (replyingBlog != null) {\n                    HighlightTextBuildUtil.buildHighlightText(\n                        text = stringResource(\n                            LocalizedString.sharedPublishReplyInputHint,\n                            replyingBlog.author.prettyHandle,\n                        ),\n                        highLightColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.7F)\n                    )\n                } else {\n                    buildAnnotatedString {\n                        append(stringResource(LocalizedString.sharedPublishBlogTextHint))\n                    }\n                },\n                allowHashtagInHashtag = allowHashtagInHashtag,\n            )\n\n            Spacer(modifier = Modifier.height(8.dp))\n\n            attachment(style)\n        }\n    }\n}\n"
  },
  {
    "path": "commonbiz/sharedscreen/src/commonMain/kotlin/com/zhangke/fread/commonbiz/shared/screen/publish/PublishSettingLabel.kt",
    "content": "package com.zhangke.fread.commonbiz.shared.screen.publish\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.unit.dp\n\n@Composable\nfun PublishSettingLabel(\n    modifier: Modifier,\n    label: String,\n    icon: ImageVector,\n    tail: (@Composable () -> Unit)? = null,\n) {\n    Row(\n        modifier = modifier\n            .background(\n                color = MaterialTheme.colorScheme.surfaceContainer,\n                shape = RoundedCornerShape(6.dp),\n            ).padding(horizontal = 6.dp, vertical = 1.dp),\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        Icon(\n            modifier = Modifier.size(14.dp),\n            imageVector = icon,\n            contentDescription = null,\n            tint = MaterialTheme.colorScheme.onSurfaceVariant,\n        )\n        Text(\n            modifier = Modifier.padding(start = 2.dp),\n            color = MaterialTheme.colorScheme.onSurfaceVariant,\n            text = label,\n            style = MaterialTheme.typography.labelSmall,\n        )\n        if (tail != null) {\n            Spacer(modifier = Modifier.width(2.dp))\n            tail()\n        }\n    }\n}\n"
  },
  {
    "path": "commonbiz/sharedscreen/src/commonMain/kotlin/com/zhangke/fread/commonbiz/shared/screen/publish/PublishTopBar.kt",
    "content": "package com.zhangke.fread.commonbiz.shared.screen.publish\n\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.filled.Send\nimport androidx.compose.material3.CircularProgressIndicator\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport com.zhangke.framework.composable.SimpleIconButton\nimport com.zhangke.framework.composable.Toolbar\nimport com.zhangke.fread.localization.LocalizedString\nimport org.jetbrains.compose.resources.stringResource\n\n@Composable\nfun PublishTopBar(\n    publishing: Boolean,\n    publishEnabled: Boolean,\n    onBackClick: () -> Unit,\n    onPublishClick: () -> Unit,\n) {\n    Toolbar(\n        title = stringResource(LocalizedString.sharedPublishBlogTitle),\n        onBackClick = onBackClick,\n        actions = {\n            if (publishing) {\n                CircularProgressIndicator(\n                    modifier = Modifier\n                        .padding(end = 8.dp)\n                        .size(24.dp),\n                    color = MaterialTheme.colorScheme.primary,\n                )\n            } else {\n                SimpleIconButton(\n                    onClick = onPublishClick,\n                    enabled = publishEnabled,\n                    tint = MaterialTheme.colorScheme.primary,\n                    imageVector = Icons.AutoMirrored.Filled.Send,\n                    contentDescription = \"Publish\",\n                )\n            }\n        },\n    )\n}\n"
  },
  {
    "path": "commonbiz/sharedscreen/src/commonMain/kotlin/com/zhangke/fread/commonbiz/shared/screen/publish/composable/AvatarsHorizontalStack.kt",
    "content": "package com.zhangke.fread.commonbiz.shared.screen.publish.composable\n\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport com.zhangke.fread.status.ui.BlogAuthorAvatar\n\n@Composable\nfun AvatarsHorizontalStack(\n    modifier: Modifier,\n    avatars: List<String?>,\n    style: AvatarStackStyle = AvatarStackStyle.default(),\n) {\n    Box(\n        modifier = modifier,\n    ) {\n        avatars.take(3).forEachIndexed { index, url ->\n            AvatarWithBorder(\n                modifier = Modifier.padding(start = 8.dp * index),\n                url = url,\n                style = style,\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun AvatarWithBorder(modifier: Modifier, url: String?, style: AvatarStackStyle) {\n    BlogAuthorAvatar(\n        modifier = modifier.size(style.avatarSize)\n            .border(width = 1.dp, color = style.borderColor, shape = CircleShape),\n        imageUrl = url,\n    )\n}\n\ndata class AvatarStackStyle(\n    val avatarSize: Dp,\n    val borderColor: Color,\n) {\n\n    companion object {\n\n        @Composable\n        fun default(): AvatarStackStyle {\n            return AvatarStackStyle(\n                avatarSize = 42.dp,\n                borderColor = Color.White,\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "commonbiz/sharedscreen/src/commonMain/kotlin/com/zhangke/fread/commonbiz/shared/screen/publish/composable/BlogMediaAttachment.kt",
    "content": "package com.zhangke.fread.commonbiz.shared.screen.publish.composable\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport com.zhangke.fread.commonbiz.shared.screen.publish.model.PublishBlogMediaAttachment\n\n@Composable\nfun BlogMediaAttachment(\n    modifier: Modifier,\n    attachment: PublishBlogMediaAttachment,\n) {\n\n}\n"
  },
  {
    "path": "commonbiz/sharedscreen/src/commonMain/kotlin/com/zhangke/fread/commonbiz/shared/screen/publish/composable/InputBlogTextField.kt",
    "content": "package com.zhangke.fread.commonbiz.shared.screen.publish.composable\n\nimport androidx.compose.foundation.shape.GenericShape\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextField\nimport androidx.compose.material3.TextFieldDefaults\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.text.AnnotatedString\nimport androidx.compose.ui.text.input.TextFieldValue\nimport com.zhangke.framework.utils.transparentIndicatorColors\nimport com.zhangke.fread.status.ui.common.PostStatusTextVisualTransformation\nimport kotlinx.coroutines.delay\n\n@Composable\nfun InputBlogTextField(\n    modifier: Modifier,\n    textFieldValue: TextFieldValue,\n    placeholder: AnnotatedString,\n    onContentChanged: (TextFieldValue) -> Unit,\n    mentionHighlightEnabled: Boolean = true,\n    allowHashtagInHashtag: Boolean = false,\n) {\n    val focusRequester = remember { FocusRequester() }\n    LaunchedEffect(Unit) {\n        delay(500)\n        focusRequester.requestFocus()\n    }\n    TextField(\n        modifier = modifier.focusRequester(focusRequester),\n        shape = GenericShape { _, _ -> },\n        placeholder = {\n            Text(\n                text = placeholder,\n                style = MaterialTheme.typography.bodyLarge,\n            )\n        },\n        minLines = 3,\n        visualTransformation = PostStatusTextVisualTransformation(\n            highLightColor = MaterialTheme.colorScheme.primary,\n            enableMentions = mentionHighlightEnabled,\n            allowHashtagInHashtag = allowHashtagInHashtag,\n        ),\n        value = textFieldValue,\n        colors = TextFieldDefaults.transparentIndicatorColors,\n        textStyle = MaterialTheme.typography.bodyLarge,\n        onValueChange = {\n            onContentChanged(it)\n        },\n    )\n}\n"
  },
  {
    "path": "commonbiz/sharedscreen/src/commonMain/kotlin/com/zhangke/fread/commonbiz/shared/screen/publish/composable/PostInteractionSettingLabel.kt",
    "content": "package com.zhangke.fread.commonbiz.shared.screen.publish.composable\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Check\nimport androidx.compose.material.icons.filled.Public\nimport androidx.compose.material.icons.outlined.Group\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.HorizontalDivider\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.ModalBottomSheet\nimport androidx.compose.material3.Switch\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.dp\nimport com.zhangke.framework.composable.noRippleClick\nimport com.zhangke.framework.composable.rememberTransientModalBottomSheetState\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.model.PostInteractionSetting\nimport com.zhangke.fread.status.model.ReplySetting\nimport com.zhangke.fread.status.model.StatusList\nimport com.zhangke.fread.commonbiz.shared.screen.publish.PublishSettingLabel\nimport org.jetbrains.compose.resources.stringResource\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun PostInteractionSettingLabel(\n    modifier: Modifier,\n    setting: PostInteractionSetting,\n    lists: List<StatusList>,\n    onSettingSelected: (PostInteractionSetting) -> Unit,\n) {\n    PostInteractionSettingLabel(\n        modifier = modifier,\n        setting = setting,\n        lists = lists,\n        onQuoteChange = { onSettingSelected(setting.copy(allowQuote = it)) },\n        onSettingSelected = {\n            onSettingSelected(setting.copy(replySetting = it))\n        },\n        onSettingOptionsSelected = { option ->\n            val options = setting.replySetting.let { it as? ReplySetting.Combined }\n                ?.options?.toMutableList() ?: mutableListOf()\n            if (option in options) {\n                options.remove(option)\n            } else {\n                options.add(option)\n            }\n            onSettingSelected(setting.copy(replySetting = ReplySetting.Combined(options)))\n        },\n    )\n}\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun PostInteractionSettingLabel(\n    modifier: Modifier,\n    setting: PostInteractionSetting,\n    lists: List<StatusList>,\n    onQuoteChange: (Boolean) -> Unit,\n    onSettingSelected: (ReplySetting) -> Unit,\n    onSettingOptionsSelected: (ReplySetting.CombineOption) -> Unit,\n) {\n    var showSelector by remember { mutableStateOf(false) }\n    PublishSettingLabel(\n        modifier = modifier.noRippleClick { showSelector = true },\n        label = setting.label,\n        icon = setting.labelIcon,\n    )\n    val state = rememberTransientModalBottomSheetState(skipPartiallyExpanded = true)\n    if (showSelector) {\n        ModalBottomSheet(\n            onDismissRequest = { showSelector = false },\n            sheetState = state,\n        ) {\n            Column(\n                modifier = Modifier.fillMaxWidth()\n                    .padding(start = 16.dp, top = 16.dp, end = 16.dp, bottom = 32.dp)\n                    .verticalScroll(rememberScrollState()),\n            ) {\n                Text(\n                    text = stringResource(LocalizedString.sharedPublishInteractionDialogTitle),\n                    style = MaterialTheme.typography.titleLarge.copy(\n                        fontWeight = FontWeight.SemiBold,\n                    ),\n                )\n                Text(\n                    text = stringResource(LocalizedString.sharedPublishInteractionDialogSubtitle),\n                    style = MaterialTheme.typography.bodyMedium,\n                )\n                HorizontalDivider(modifier = Modifier.padding(top = 16.dp).fillMaxWidth())\n                Text(\n                    modifier = Modifier.padding(top = 26.dp),\n                    text = stringResource(LocalizedString.sharedPublishInteractionDialogQuoteTitle),\n                    style = MaterialTheme.typography.titleMedium.copy(\n                        fontWeight = FontWeight.SemiBold,\n                    ),\n                )\n                Row(\n                    modifier = Modifier.fillMaxWidth(),\n                    verticalAlignment = Alignment.CenterVertically,\n                ) {\n                    Text(\n                        text = stringResource(LocalizedString.sharedPublishInteractionDialogQuoteAllow),\n                        style = MaterialTheme.typography.bodySmall,\n                    )\n                    Spacer(modifier = Modifier.weight(1F))\n                    Switch(\n                        checked = setting.allowQuote,\n                        onCheckedChange = onQuoteChange,\n                    )\n                }\n                HorizontalDivider(modifier = Modifier.padding(top = 16.dp).fillMaxWidth())\n                Text(\n                    modifier = Modifier.padding(top = 26.dp),\n                    text = stringResource(LocalizedString.sharedPublishInteractionDialogReplyTitle),\n                    style = MaterialTheme.typography.titleMedium.copy(\n                        fontWeight = FontWeight.SemiBold,\n                    ),\n                )\n                Text(\n                    modifier = Modifier.padding(top = 16.dp),\n                    text = stringResource(LocalizedString.sharedPublishInteractionDialogReplySubtitle),\n                    style = MaterialTheme.typography.bodySmall,\n                )\n                Row(\n                    modifier = Modifier.fillMaxWidth().padding(top = 16.dp),\n                    verticalAlignment = Alignment.CenterVertically,\n                ) {\n                    InteractionOption(\n                        modifier = Modifier.weight(1F)\n                            .noRippleClick { onSettingSelected(ReplySetting.Everybody) },\n                        text = stringResource(LocalizedString.sharedPublishInteractionDialogReplyAll),\n                        selected = setting.replySetting is ReplySetting.Everybody,\n                    )\n                    Spacer(modifier = Modifier.width(16.dp))\n                    InteractionOption(\n                        modifier = Modifier.weight(1F)\n                            .noRippleClick { onSettingSelected(ReplySetting.Nobody) },\n                        text = stringResource(LocalizedString.sharedPublishInteractionDialogReplyNobody),\n                        selected = setting.replySetting is ReplySetting.Nobody,\n                    )\n                }\n                if (setting.replySetting !is ReplySetting.Nobody) {\n                    Text(\n                        modifier = Modifier.padding(top = 26.dp),\n                        text = stringResource(LocalizedString.sharedPublishInteractionDialogReplyCombineTitle),\n                        style = MaterialTheme.typography.bodySmall,\n                    )\n\n                    InteractionOption(\n                        modifier = Modifier.padding(top = 16.dp).fillMaxWidth()\n                            .noRippleClick { onSettingOptionsSelected(ReplySetting.CombineOption.Mentioned) },\n                        text = stringResource(LocalizedString.sharedPublishInteractionDialogMentioned),\n                        selected = setting.replySetting.combinedMentions,\n                    )\n\n                    InteractionOption(\n                        modifier = Modifier.padding(top = 16.dp).fillMaxWidth()\n                            .noRippleClick { onSettingOptionsSelected(ReplySetting.CombineOption.Following) },\n                        text = stringResource(LocalizedString.sharedPublishInteractionDialogFollowing),\n                        selected = setting.replySetting.combinedFollowing,\n                    )\n\n                    InteractionOption(\n                        modifier = Modifier.padding(top = 16.dp).fillMaxWidth()\n                            .noRippleClick { onSettingOptionsSelected(ReplySetting.CombineOption.Followers) },\n                        text = stringResource(LocalizedString.sharedPublishInteractionDialogFollower),\n                        selected = setting.replySetting.combinedFollowers,\n                    )\n\n                    for (listView in lists) {\n                        val selected = if (setting.replySetting is ReplySetting.Combined) {\n                            (setting.replySetting as ReplySetting.Combined).options\n                                .filterIsInstance<ReplySetting.CombineOption.UserInList>()\n                                .any { it.listView.cid == listView.cid }\n                        } else {\n                            false\n                        }\n                        InteractionOption(\n                            modifier = Modifier.padding(top = 16.dp).fillMaxWidth()\n                                .noRippleClick {\n                                    onSettingOptionsSelected(\n                                        ReplySetting.CombineOption.UserInList(listView)\n                                    )\n                                },\n                            text = listView.name,\n                            selected = selected,\n                        )\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nfun InteractionOption(\n    modifier: Modifier,\n    text: String,\n    selected: Boolean,\n) {\n    val backgroundColor = if (selected) {\n        MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3F)\n    } else {\n        MaterialTheme.colorScheme.surfaceContainer\n    }\n    Row(\n        modifier = modifier.background(\n            color = backgroundColor,\n            shape = RoundedCornerShape(6.dp),\n        ).padding(vertical = 16.dp),\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        val fontStyle = if (selected) {\n            MaterialTheme.typography.bodyMedium.copy(\n                fontWeight = FontWeight.SemiBold,\n            )\n        } else {\n            MaterialTheme.typography.bodyMedium\n        }\n        Text(\n            modifier = Modifier.padding(start = 16.dp),\n            text = text,\n            style = fontStyle,\n        )\n        if (selected) {\n            Spacer(modifier = Modifier.weight(1F))\n            Icon(\n                imageVector = Icons.Default.Check,\n                contentDescription = null,\n                tint = MaterialTheme.colorScheme.primary,\n                modifier = Modifier.padding(end = 16.dp),\n            )\n        }\n    }\n}\n\ninternal val PostInteractionSetting.label: String\n    @Composable get() {\n        return if (this.replySetting is ReplySetting.Everybody) {\n            stringResource(LocalizedString.sharedPublishInteractionNoLimit)\n        } else {\n            stringResource(LocalizedString.sharedPublishInteractionLimited)\n        }\n    }\n\ninternal val PostInteractionSetting.labelIcon: ImageVector\n    @Composable get() {\n        return if (this.replySetting is ReplySetting.Everybody) {\n            Icons.Default.Public\n        } else {\n            Icons.Outlined.Group\n        }\n    }\n"
  },
  {
    "path": "commonbiz/sharedscreen/src/commonMain/kotlin/com/zhangke/fread/commonbiz/shared/screen/publish/composable/PostStatusVisibilityUi.kt",
    "content": "package com.zhangke.fread.commonbiz.shared.screen.publish.composable\n\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Public\nimport androidx.compose.material3.DropdownMenuItem\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport com.zhangke.framework.composable.PopupMenu\nimport com.zhangke.framework.composable.noRippleClick\nimport com.zhangke.fread.commonbiz.shared.screen.publish.PublishSettingLabel\nimport com.zhangke.fread.commonbiz.shared.screen.publish.multi.describeStringId\nimport com.zhangke.fread.status.model.StatusVisibility\nimport org.jetbrains.compose.resources.stringResource\n\n@Composable\nfun PostStatusVisibilityUi(\n    modifier: Modifier,\n    visibility: StatusVisibility,\n    changeable: Boolean,\n    onVisibilitySelect: (StatusVisibility) -> Unit,\n) {\n    var showSelector by remember {\n        mutableStateOf(false)\n    }\n    Box(modifier = modifier) {\n        PublishSettingLabel(\n            modifier = modifier.noRippleClick(enabled = changeable) { showSelector = true },\n            label = stringResource(visibility.describeStringId),\n            icon = Icons.Default.Public,\n        )\n        PopupMenu(\n            expanded = showSelector,\n            onDismissRequest = { showSelector = false },\n        ) {\n            StatusVisibility.entries.forEach {\n                DropdownMenuItem(\n                    text = {\n                        Text(text = stringResource(it.describeStringId))\n                    },\n                    onClick = {\n                        showSelector = false\n                        onVisibilitySelect(it)\n                    },\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "commonbiz/sharedscreen/src/commonMain/kotlin/com/zhangke/fread/commonbiz/shared/screen/publish/composable/PostStatusWarning.kt",
    "content": "package com.zhangke.fread.commonbiz.shared.screen.publish.composable\n\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextField\nimport androidx.compose.material3.TextFieldDefaults\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.text.input.TextFieldValue\nimport androidx.compose.ui.unit.dp\nimport com.zhangke.framework.architect.theme.inverseOnSurfaceDark\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.ui.drawSpoilerBackground\nimport org.jetbrains.compose.resources.stringResource\n\n@Composable\nfun PostStatusWarning(\n    modifier: Modifier,\n    warning: TextFieldValue,\n    onValueChanged: (TextFieldValue) -> Unit,\n) {\n    Box(\n        modifier = modifier.drawSpoilerBackground()\n    ) {\n        TextField(\n            modifier = Modifier\n                .padding(top = 8.dp)\n                .fillMaxSize(),\n            value = warning,\n            onValueChange = onValueChanged,\n            placeholder = {\n                Text(\n                    text = stringResource(LocalizedString.postStatusContentWarning),\n                    style = MaterialTheme.typography.bodyMedium,\n                    color = inverseOnSurfaceDark,\n                )\n            },\n            colors = TextFieldDefaults.colors(\n                focusedTextColor = inverseOnSurfaceDark,\n                unfocusedTextColor = inverseOnSurfaceDark,\n                focusedIndicatorColor = Color.Transparent,\n                unfocusedIndicatorColor = Color.Transparent,\n                disabledIndicatorColor = Color.Transparent,\n                errorIndicatorColor = Color.Transparent,\n                disabledContainerColor = Color.Transparent,\n                focusedContainerColor = Color.Transparent,\n                errorContainerColor = Color.Transparent,\n                unfocusedContainerColor = Color.Transparent,\n            ),\n            textStyle = MaterialTheme.typography.bodyMedium,\n        )\n    }\n}\n"
  },
  {
    "path": "commonbiz/sharedscreen/src/commonMain/kotlin/com/zhangke/fread/commonbiz/shared/screen/publish/model/PublishBlogMediaAttachment.kt",
    "content": "package com.zhangke.fread.commonbiz.shared.screen.publish.model\n\nsealed interface PublishBlogMediaAttachment {\n\n    data class Image(val files: List<PublishBlogMediaAttachmentFile>) : PublishBlogMediaAttachment\n\n    data class Video(val file: PublishBlogMediaAttachmentFile) : PublishBlogMediaAttachment\n}\n\ndata class PublishBlogMediaAttachmentFile(\n    val uri: String,\n    val description: String?,\n)\n"
  },
  {
    "path": "commonbiz/sharedscreen/src/commonMain/kotlin/com/zhangke/fread/commonbiz/shared/screen/publish/multi/MultiAccountPublishingScreen.kt",
    "content": "package com.zhangke.fread.commonbiz.shared.screen.publish.multi\n\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.SnackbarHost\nimport androidx.compose.material3.SnackbarHostState\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.ExperimentalComposeUiApi\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.text.buildAnnotatedString\nimport androidx.compose.ui.text.input.TextFieldValue\nimport androidx.compose.ui.unit.dp\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.architect.json.globalJson\nimport com.zhangke.framework.composable.BackHandler\nimport com.zhangke.framework.composable.ConsumeFlow\nimport com.zhangke.framework.composable.ConsumeSnackbarFlow\nimport com.zhangke.framework.composable.FreadDialog\nimport com.zhangke.framework.composable.currentOrThrow\nimport com.zhangke.framework.composable.rememberSnackbarHostState\nimport com.zhangke.framework.nav.LocalNavBackStack\nimport com.zhangke.framework.nav.popIfNotRoot\nimport com.zhangke.framework.toast.toast\nimport com.zhangke.framework.utils.PlatformUri\nimport com.zhangke.framework.utils.languageCode\nimport com.zhangke.fread.commonbiz.shared.screen.publish.PublishPostFeaturesPanel\nimport com.zhangke.fread.commonbiz.shared.screen.publish.PublishPostMedia\nimport com.zhangke.fread.commonbiz.shared.screen.publish.PublishPostMediaAttachment\nimport com.zhangke.fread.commonbiz.shared.screen.publish.PublishTopBar\nimport com.zhangke.fread.commonbiz.shared.screen.publish.SensitiveIconButton\nimport com.zhangke.fread.commonbiz.shared.screen.publish.bottomPaddingAsBottomBar\nimport com.zhangke.fread.commonbiz.shared.screen.publish.composable.InputBlogTextField\nimport com.zhangke.fread.commonbiz.shared.screen.publish.composable.PostInteractionSettingLabel\nimport com.zhangke.fread.commonbiz.shared.screen.publish.composable.PostStatusVisibilityUi\nimport com.zhangke.fread.commonbiz.shared.screen.publish.composable.PostStatusWarning\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.account.LoggedAccount\nimport com.zhangke.fread.status.model.PostInteractionSetting\nimport com.zhangke.fread.status.model.StatusVisibility\nimport kotlinx.serialization.Serializable\nimport org.jetbrains.compose.resources.stringResource\n\n@Serializable\ndata class MultiAccountPublishingScreenKey(val userUrisJson: String) : NavKey {\n\n    companion object {\n\n        fun create(accounts: List<LoggedAccount>): MultiAccountPublishingScreenKey {\n            return MultiAccountPublishingScreenKey(\n                userUrisJson = globalJson.encodeToString(accounts.map { it.uri.toString() }),\n            )\n        }\n    }\n}\n\n@OptIn(ExperimentalComposeUiApi::class)\n@Composable\nfun MultiAccountPublishingScreen(\n    viewModel: MultiAccountPublishingViewModel,\n) {\n    val backStack = LocalNavBackStack.currentOrThrow\n    val snackBarHostState = rememberSnackbarHostState()\n    val uiState by viewModel.uiState.collectAsState()\n    var showExitDialog by remember { mutableStateOf(false) }\n    fun onBack() {\n        if (uiState.hasInputtedData) {\n            showExitDialog = true\n            return\n        }\n        backStack.popIfNotRoot()\n    }\n    MultiAccountPublishingContent(\n        uiState = uiState,\n        snackBarHostState = snackBarHostState,\n        onBackClick = backStack::popIfNotRoot,\n        onPublishClick = viewModel::onPublishClick,\n        onMediaSelected = viewModel::onMediaSelected,\n        onLanguageSelected = viewModel::onLanguageSelected,\n        onRemoveAccountClick = viewModel::onRemoveAccountClick,\n        onContentChanged = viewModel::onContentChanged,\n        onSensitiveClick = viewModel::onSensitiveClick,\n        onWarningContentChanged = viewModel::onWarningContentChanged,\n        onMediaAltChanged = viewModel::onMediaAltChanged,\n        onDeleteMediaClick = viewModel::onDeleteMediaClick,\n        onVisibilitySelect = viewModel::onVisibilitySelect,\n        onSettingSelected = viewModel::onSettingSelected,\n        onAddAccountClick = viewModel::onAddAccount,\n    )\n    val successMessage = stringResource(LocalizedString.postStatusSuccess)\n    ConsumeFlow(viewModel.publishSuccessFlow) {\n        toast(successMessage)\n        backStack.popIfNotRoot()\n    }\n    BackHandler(true) { onBack() }\n    if (showExitDialog) {\n        FreadDialog(\n            onDismissRequest = { showExitDialog = false },\n            content = {\n                Text(text = stringResource(LocalizedString.postStatusExitDialogContent))\n            },\n            onNegativeClick = {\n                showExitDialog = false\n            },\n            onPositiveClick = {\n                showExitDialog = false\n                backStack.popIfNotRoot()\n            },\n        )\n    }\n    ConsumeSnackbarFlow(snackBarHostState, viewModel.snackMessage)\n}\n\n@Composable\nprivate fun MultiAccountPublishingContent(\n    uiState: MultiAccountPublishingUiState,\n    snackBarHostState: SnackbarHostState,\n    onBackClick: () -> Unit,\n    onPublishClick: () -> Unit,\n    onMediaSelected: (List<PlatformUri>) -> Unit,\n    onLanguageSelected: (String) -> Unit,\n    onRemoveAccountClick: (LoggedAccount) -> Unit,\n    onContentChanged: (TextFieldValue) -> Unit,\n    onSensitiveClick: () -> Unit,\n    onWarningContentChanged: (TextFieldValue) -> Unit,\n    onMediaAltChanged: (PublishPostMedia, String) -> Unit,\n    onDeleteMediaClick: (PublishPostMedia) -> Unit,\n    onVisibilitySelect: (StatusVisibility) -> Unit,\n    onSettingSelected: (PostInteractionSetting) -> Unit,\n    onAddAccountClick: (MultiPublishingAccountWithRules) -> Unit,\n) {\n    Scaffold(\n        topBar = {\n            PublishTopBar(\n                publishing = uiState.publishing,\n                onBackClick = onBackClick,\n                publishEnabled = uiState.publishEnabled,\n                onPublishClick = onPublishClick,\n            )\n        },\n        snackbarHost = { SnackbarHost(snackBarHostState) },\n        bottomBar = {\n            PublishPostFeaturesPanel(\n                modifier = Modifier.fillMaxWidth().bottomPaddingAsBottomBar(),\n                contentLength = uiState.content.text.length,\n                maxContentLimit = uiState.globalRules.maxCharacters,\n                mediaAvailableCount = uiState.mediaAvailableCount,\n                onMediaSelected = onMediaSelected,\n                containerColor = MaterialTheme.colorScheme.background,\n                selectedLanguages = listOf(uiState.selectedLanguage.languageCode),\n                maxLanguageCount = uiState.globalRules.maxLanguageCount,\n                onLanguageSelected = {\n                    it.firstOrNull()?.let { lan -> onLanguageSelected(lan) }\n                },\n                actions = {\n                    SensitiveIconButton(onSensitiveClick = onSensitiveClick)\n                },\n            )\n        },\n    ) { innerPadding ->\n        Column(\n            modifier = Modifier.fillMaxSize()\n                .padding(innerPadding)\n                .verticalScroll(rememberScrollState()),\n        ) {\n            PublishingAccounts(\n                modifier = Modifier.fillMaxWidth(),\n                uiState = uiState,\n                onRemoveAccountClick = onRemoveAccountClick,\n                onAddAccountClick = onAddAccountClick,\n            )\n            if (uiState.showInteractionSetting || uiState.showPostVisibilitySetting) {\n                Row(\n                    modifier = Modifier.fillMaxWidth()\n                        .padding(start = 16.dp, top = 16.dp, end = 16.dp),\n                    verticalAlignment = Alignment.CenterVertically,\n                ) {\n                    if (uiState.showPostVisibilitySetting) {\n                        PostStatusVisibilityUi(\n                            modifier = Modifier,\n                            changeable = true,\n                            visibility = uiState.postVisibility,\n                            onVisibilitySelect = onVisibilitySelect,\n                        )\n                        Spacer(modifier = Modifier.width(8.dp))\n                    }\n                    if (uiState.showInteractionSetting) {\n                        PostInteractionSettingLabel(\n                            modifier = Modifier,\n                            setting = uiState.interactionSetting,\n                            lists = emptyList(),\n                            onSettingSelected = onSettingSelected,\n                        )\n                    }\n                }\n            }\n            if (uiState.sensitive) {\n                PostStatusWarning(\n                    modifier = Modifier.fillMaxWidth()\n                        .padding(start = 16.dp, top = 16.dp, end = 16.dp),\n                    warning = uiState.warningContent,\n                    onValueChanged = onWarningContentChanged,\n                )\n            }\n            InputBlogTextField(\n                modifier = Modifier.fillMaxWidth(),\n                textFieldValue = uiState.content,\n                onContentChanged = onContentChanged,\n                mentionHighlightEnabled = false,\n                placeholder = buildAnnotatedString {\n                    append(stringResource(LocalizedString.sharedPublishBlogTextHint))\n                },\n            )\n            PublishPostMediaAttachment(\n                modifier = Modifier.padding(start = 16.dp, top = 16.dp, end = 16.dp)\n                    .fillMaxWidth(),\n                medias = uiState.medias,\n                mediaAltMaxCharacters = uiState.globalRules.mediaAltMaxCharacters,\n                onAltChanged = onMediaAltChanged,\n                onDeleteClick = onDeleteMediaClick,\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "commonbiz/sharedscreen/src/commonMain/kotlin/com/zhangke/fread/commonbiz/shared/screen/publish/multi/MultiAccountPublishingUiState.kt",
    "content": "package com.zhangke.fread.commonbiz.shared.screen.publish.multi\n\nimport androidx.compose.ui.text.input.TextFieldValue\nimport com.zhangke.framework.utils.ContentProviderFile\nimport com.zhangke.framework.utils.Locale\nimport com.zhangke.framework.utils.getDefaultLocale\nimport com.zhangke.fread.commonbiz.shared.screen.publish.PublishPostMedia\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.account.LoggedAccount\nimport com.zhangke.fread.status.model.PostInteractionSetting\nimport com.zhangke.fread.status.model.PublishBlogRules\nimport com.zhangke.fread.status.model.StatusVisibility\nimport com.zhangke.fread.status.model.isActivityPub\nimport com.zhangke.fread.status.model.isBluesky\nimport org.jetbrains.compose.resources.StringResource\n\ndata class MultiAccountPublishingUiState(\n    val addedAccounts: List<MultiPublishingAccountUiState>,\n    val allAccounts: List<MultiPublishingAccountWithRules>,\n    val publishing: Boolean,\n    val content: TextFieldValue,\n    val globalRules: PublishBlogRules,\n    val medias: List<PublishPostMediaAttachmentFile>,\n    val selectedLanguage: Locale,\n    val postVisibility: StatusVisibility,\n    val interactionSetting: PostInteractionSetting,\n    val sensitive: Boolean,\n    val warningContent: TextFieldValue,\n) {\n\n    val publishEnabled: Boolean\n        get() {\n            if (publishing) return false\n            if (content.text.isEmpty() && medias.isEmpty()) return false\n            if (addedAccounts.isEmpty()) return false\n            return true\n        }\n\n    val mediaAvailableCount: Int\n        get() = globalRules.maxMediaCount - medias.size\n\n    val showPostVisibilitySetting: Boolean\n        get() = allAccounts.any { it.account.platform.protocol.isActivityPub }\n\n    val showInteractionSetting: Boolean\n        get() = allAccounts.any { it.account.platform.protocol.isBluesky }\n\n    val hasInputtedData: Boolean\n        get() = content.text.isNotEmpty() || medias.isNotEmpty() || warningContent.text.isNotEmpty()\n\n    companion object {\n\n        fun default(): MultiAccountPublishingUiState {\n            return MultiAccountPublishingUiState(\n                addedAccounts = emptyList(),\n                allAccounts = emptyList(),\n                publishing = false,\n                content = TextFieldValue(\"\"),\n                globalRules = defaultRules(),\n                medias = emptyList(),\n                selectedLanguage = getDefaultLocale(),\n                postVisibility = StatusVisibility.PUBLIC,\n                interactionSetting = PostInteractionSetting.default(),\n                sensitive = false,\n                warningContent = TextFieldValue(\"\"),\n            )\n        }\n\n        fun defaultRules(): PublishBlogRules {\n            return PublishBlogRules(\n                maxCharacters = 120,\n                maxMediaCount = 4,\n                maxPollOptions = 0,\n                supportPoll = false,\n                supportSpoiler = false,\n                maxLanguageCount = 1,\n                mediaAltMaxCharacters = 1500,\n            )\n        }\n    }\n}\n\ndata class MultiPublishingAccountUiState(\n    val account: LoggedAccount,\n    val rules: PublishBlogRules,\n)\n\ndata class MultiPublishingAccountWithRules(\n    val account: LoggedAccount,\n    val rules: PublishBlogRules?,\n)\n\nval StatusVisibility.describeStringId: StringResource\n    get() = when (this) {\n        StatusVisibility.PUBLIC -> LocalizedString.postStatusScopePublic\n        StatusVisibility.UNLISTED -> LocalizedString.postStatusScopeUnlisted\n        StatusVisibility.PRIVATE -> LocalizedString.postStatusScopeFollowerOnly\n        StatusVisibility.DIRECT -> LocalizedString.postStatusScopeMentionedOnly\n    }\n\ndata class PublishPostMediaAttachmentFile(\n    val file: ContentProviderFile,\n    override val isVideo: Boolean,\n    override val alt: String?,\n) : PublishPostMedia {\n\n    override val uri: String\n        get() = file.uri.toString()\n\n}\n"
  },
  {
    "path": "commonbiz/sharedscreen/src/commonMain/kotlin/com/zhangke/fread/commonbiz/shared/screen/publish/multi/MultiAccountPublishingViewModel.kt",
    "content": "package com.zhangke.fread.commonbiz.shared.screen.publish.multi\n\nimport androidx.compose.ui.text.input.TextFieldValue\nimport androidx.lifecycle.ViewModel\nimport com.zhangke.framework.composable.TextString\nimport com.zhangke.framework.composable.emitInViewModel\nimport com.zhangke.framework.composable.textOf\nimport com.zhangke.framework.ktx.ifNullOrEmpty\nimport com.zhangke.framework.ktx.launchInViewModel\nimport com.zhangke.framework.utils.PlatformUri\nimport com.zhangke.framework.utils.initLocale\nimport com.zhangke.framework.utils.languageCode\nimport com.zhangke.fread.common.utils.PlatformUriHelper\nimport com.zhangke.fread.commonbiz.shared.repo.SelectedAccountPublishingRepo\nimport com.zhangke.fread.commonbiz.shared.screen.publish.PublishPostMedia\nimport com.zhangke.fread.commonbiz.shared.usecase.PublishPostOnMultiAccountUseCase\nimport com.zhangke.fread.commonbiz.shared.usecase.PublishingPartFailed\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.StatusProvider\nimport com.zhangke.fread.status.account.LoggedAccount\nimport com.zhangke.fread.status.model.PostInteractionSetting\nimport com.zhangke.fread.status.model.PublishBlogRules\nimport com.zhangke.fread.status.model.StatusVisibility\nimport com.zhangke.fread.status.publish.PublishingMedia\nimport com.zhangke.fread.status.publish.PublishingPost\nimport kotlinx.coroutines.async\nimport kotlinx.coroutines.awaitAll\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.SharedFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.update\n\nclass MultiAccountPublishingViewModel (\n    private val statusProvider: StatusProvider,\n    private val platformUriHelper: PlatformUriHelper,\n    private val publishPostOnMultiAccount: PublishPostOnMultiAccountUseCase,\n    private val selectedAccountPublishingRepo: SelectedAccountPublishingRepo,\n    private val defaultAddAccountList: List<String>,\n) : ViewModel() {\n\n    private val _uiState = MutableStateFlow(MultiAccountPublishingUiState.default())\n    val uiState = _uiState.asStateFlow()\n\n    private val _snackMessage = MutableSharedFlow<TextString>()\n    val snackMessage: SharedFlow<TextString> get() = _snackMessage\n\n    private val _publishSuccessFlow = MutableSharedFlow<Unit>()\n    val publishSuccessFlow: SharedFlow<Unit> get() = _publishSuccessFlow\n\n    init {\n        launchInViewModel {\n            val allAccounts = statusProvider.accountManager.getAllLoggedAccount()\n            val addedAccounts = getInitialAccount(allAccounts)\n            _uiState.update {\n                it.copy(\n                    addedAccounts = addedAccounts.map { it.toDefaultUiState() },\n                    allAccounts = allAccounts.map { MultiPublishingAccountWithRules(it, null) },\n                )\n            }\n            val addedAccountUiState = addedAccounts.map { account ->\n                val rules = loadRules(account) ?: MultiAccountPublishingUiState.defaultRules()\n                MultiPublishingAccountUiState(account, rules)\n            }\n            _uiState.update { state ->\n                state.copy(\n                    addedAccounts = addedAccountUiState,\n                    allAccounts = state.allAccounts.map { (account, _) ->\n                        val rules =\n                            addedAccountUiState.firstOrNull { it.account.uri == account.uri }?.rules\n                        MultiPublishingAccountWithRules(account, rules)\n                    },\n                )\n            }\n            updateGlobalRules()\n        }\n    }\n\n    private suspend fun getInitialAccount(allLoggedAccounts: List<LoggedAccount>): List<LoggedAccount> {\n        val pendingAddAccounts = selectedAccountPublishingRepo.getAll() + defaultAddAccountList\n        return allLoggedAccounts.filter { account ->\n            pendingAddAccounts.contains(account.uri.toString())\n        }\n    }\n\n    fun onAddAccount(account: MultiPublishingAccountWithRules) {\n        if (uiState.value.addedAccounts.any { it.account.uri == account.account.uri }) return\n        _uiState.update {\n            it.copy(addedAccounts = it.addedAccounts + account.toDefaultUiState())\n        }\n        if (account.rules == null) {\n            loadRuleForAccount(account.account)\n        }\n        updateGlobalRules()\n        updateLocalAccounts()\n    }\n\n    fun onRemoveAccountClick(account: LoggedAccount) {\n        if (uiState.value.addedAccounts.size <= 1) return\n        _uiState.update {\n            it.copy(addedAccounts = it.addedAccounts.filter { it.account.uri != account.uri })\n        }\n        updateGlobalRules()\n        updateLocalAccounts()\n    }\n\n    private fun updateLocalAccounts() {\n        launchInViewModel {\n            val addedAccounts = _uiState.value.addedAccounts\n            selectedAccountPublishingRepo.replace(addedAccounts.map { it.account.uri.toString() })\n        }\n    }\n\n    fun onContentChanged(content: TextFieldValue) {\n        _uiState.update {\n            it.copy(content = content)\n        }\n    }\n\n    fun onSensitiveClick() {\n        _uiState.update {\n            it.copy(sensitive = !it.sensitive)\n        }\n    }\n\n    fun onWarningContentChanged(content: TextFieldValue) {\n        _uiState.update { it.copy(warningContent = content) }\n    }\n\n    fun onMediaAltChanged(media: PublishPostMedia, alt: String) {\n        _uiState.update {\n            it.copy(\n                medias = it.medias.map { m ->\n                    if (m.uri == media.uri) m.copy(alt = alt) else m\n                },\n            )\n        }\n    }\n\n    fun onDeleteMediaClick(media: PublishPostMedia) {\n        _uiState.update {\n            it.copy(medias = it.medias.filter { m -> m.uri != media.uri })\n        }\n    }\n\n    fun onVisibilitySelect(visibility: StatusVisibility) {\n        _uiState.update { it.copy(postVisibility = visibility) }\n    }\n\n    fun onSettingSelected(setting: PostInteractionSetting) {\n        _uiState.update { it.copy(interactionSetting = setting) }\n    }\n\n    private fun loadRuleForAccount(account: LoggedAccount) {\n        launchInViewModel {\n            val rules = loadRules(account) ?: return@launchInViewModel\n            _uiState.update { state ->\n                state.copy(\n                    allAccounts = state.allAccounts.map {\n                        if (it.account.uri == account.uri) {\n                            it.copy(rules = rules)\n                        } else {\n                            it\n                        }\n                    },\n                    addedAccounts = state.addedAccounts.map {\n                        if (it.account.uri == account.uri) {\n                            it.copy(rules = rules)\n                        } else {\n                            it\n                        }\n                    },\n                )\n            }\n        }\n    }\n\n    fun onPublishClick() {\n        val uiState = _uiState.value\n        if (uiState.medias.isEmpty() && uiState.content.text.isEmpty()) {\n            _snackMessage.emitInViewModel(textOf(LocalizedString.postStatusContentIsEmpty))\n            return\n        }\n        launchInViewModel {\n            _uiState.update { it.copy(publishing = true) }\n            publishPostOnMultiAccount(\n                accounts = uiState.addedAccounts.map { it.account },\n                publishingPost = uiState.obtainPost(),\n            ).onFailure { t ->\n                _uiState.update { it.copy(publishing = false) }\n                if (t is PublishingPartFailed) {\n                    _snackMessage.emit(\n                        textOf(LocalizedString.postStatusPartFailed, t.message.orEmpty())\n                    )\n                    _uiState.update { state ->\n                        state.copy(\n                            addedAccounts = state.addedAccounts.filter {\n                                !t.successAccount.contains(it.account.uri.toString())\n                            },\n                        )\n                    }\n                } else {\n                    val errorMessage = textOf(\n                        LocalizedString.postStatusFailed,\n                        t.message.ifNullOrEmpty { \"unknown error\" }.take(180),\n                    )\n                    _snackMessage.emit(errorMessage)\n                }\n            }.onSuccess {\n                _uiState.update { it.copy(publishing = false) }\n                _publishSuccessFlow.emit(Unit)\n            }\n        }\n    }\n\n    private fun MultiAccountPublishingUiState.obtainPost(): PublishingPost {\n        return PublishingPost(\n            content = this.content.text,\n            visibility = this.postVisibility,\n            interactionSetting = this.interactionSetting,\n            sensitive = this.sensitive,\n            warningText = this.warningContent.text,\n            languageCode = this.selectedLanguage.languageCode,\n            medias = this.medias.map { it.convert() }\n        )\n    }\n\n    private fun PublishPostMediaAttachmentFile.convert(): PublishingMedia {\n        return PublishingMedia(\n            file = this.file,\n            alt = this.alt.orEmpty(),\n            isVideo = isVideo,\n        )\n    }\n\n    fun onMediaSelected(medias: List<PlatformUri>) {\n        if (medias.isEmpty()) return\n        launchInViewModel {\n            val fileList = medias.map { async { platformUriHelper.read(it) } }\n                .awaitAll().filterNotNull()\n            val newMedias = if (fileList.first().isVideo) {\n                PublishPostMediaAttachmentFile(\n                    file = fileList.first(),\n                    isVideo = true,\n                    alt = null,\n                ).let { listOf(it) }\n            } else {\n                uiState.value.medias + fileList.map {\n                    PublishPostMediaAttachmentFile(\n                        file = it,\n                        isVideo = false,\n                        alt = null,\n                    )\n                }\n            }\n            _uiState.update { it.copy(medias = newMedias) }\n        }\n    }\n\n    fun onLanguageSelected(lan: String) {\n        _uiState.update { it.copy(selectedLanguage = initLocale(lan)) }\n    }\n\n    private fun updateGlobalRules() {\n        val addedAccount = uiState.value.addedAccounts\n        val maxCharacters = addedAccount.minOf { it.rules.maxCharacters }\n        val maxMediaCount = addedAccount.minOf { it.rules.maxMediaCount }\n        _uiState.update {\n            it.copy(\n                globalRules = it.globalRules.copy(\n                    maxCharacters = maxCharacters,\n                    maxMediaCount = maxMediaCount,\n                )\n            )\n        }\n    }\n\n    private fun LoggedAccount.toDefaultUiState(): MultiPublishingAccountUiState {\n        return MultiPublishingAccountUiState(\n            account = this,\n            rules = MultiAccountPublishingUiState.defaultRules(),\n        )\n    }\n\n    private fun MultiPublishingAccountWithRules.toDefaultUiState(): MultiPublishingAccountUiState {\n        return MultiPublishingAccountUiState(\n            account = this.account,\n            rules = this.rules ?: MultiAccountPublishingUiState.defaultRules(),\n        )\n    }\n\n    private suspend fun loadRules(account: LoggedAccount): PublishBlogRules? {\n        return statusProvider.publishManager.getPublishBlogRules(account).getOrNull()\n    }\n}"
  },
  {
    "path": "commonbiz/sharedscreen/src/commonMain/kotlin/com/zhangke/fread/commonbiz/shared/screen/publish/multi/PublishingAccounts.kt",
    "content": "package com.zhangke.fread.commonbiz.shared.screen.publish.multi\n\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.horizontalScroll\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Delete\nimport androidx.compose.material.icons.filled.Public\nimport androidx.compose.material.icons.rounded.Add\nimport androidx.compose.material3.Card\nimport androidx.compose.material3.HorizontalDivider\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport com.zhangke.framework.composable.SimpleIconButton\nimport com.zhangke.framework.composable.noRippleClick\nimport com.zhangke.framework.utils.getDisplayName\nimport com.zhangke.framework.utils.initLocale\nimport com.zhangke.framework.utils.languageCode\nimport com.zhangke.fread.common.resources.logo\nimport com.zhangke.fread.commonbiz.shared.screen.publish.PublishSettingLabel\nimport com.zhangke.fread.commonbiz.shared.screen.publish.composable.label\nimport com.zhangke.fread.commonbiz.shared.screen.publish.composable.labelIcon\nimport com.zhangke.fread.status.account.LoggedAccount\nimport com.zhangke.fread.status.model.isActivityPub\nimport com.zhangke.fread.status.model.isBluesky\nimport com.zhangke.fread.status.ui.BlogAuthorAvatar\nimport com.zhangke.fread.status.ui.common.RemainingTextStatus\nimport com.zhangke.fread.status.ui.common.SelectAccountDialog\nimport com.zhangke.fread.status.ui.richtext.FreadRichText\nimport org.jetbrains.compose.resources.stringResource\n\n@Composable\nfun PublishingAccounts(\n    modifier: Modifier,\n    uiState: MultiAccountPublishingUiState,\n    onRemoveAccountClick: (LoggedAccount) -> Unit,\n    onAddAccountClick: (MultiPublishingAccountWithRules) -> Unit,\n) {\n    var showSelectAccountPopup by remember { mutableStateOf(false) }\n    Box(modifier = modifier) {\n        Row(\n            modifier = Modifier.fillMaxWidth()\n                .padding(vertical = 8.dp)\n                .horizontalScroll(rememberScrollState())\n                .padding(horizontal = 16.dp),\n            verticalAlignment = Alignment.CenterVertically,\n        ) {\n            for (index in uiState.addedAccounts.indices) {\n                val account = uiState.addedAccounts[index]\n                Box(\n                    modifier = Modifier.size(46.dp)\n                        .noRippleClick { onRemoveAccountClick(account.account) },\n                ) {\n                    BlogAuthorAvatar(\n                        modifier = Modifier.fillMaxSize().padding(2.dp),\n                        imageUrl = account.account.avatar,\n                    )\n                    Image(\n                        imageVector = account.account.platform.protocol.logo,\n                        modifier = Modifier.align(Alignment.BottomEnd).size(16.dp),\n                        contentDescription = null,\n                    )\n                }\n                if (index < uiState.addedAccounts.lastIndex) {\n                    Spacer(modifier = Modifier.width(16.dp))\n                }\n            }\n            SimpleIconButton(\n                modifier = Modifier.padding(start = 8.dp),\n                iconModifier = Modifier.border(\n                    width = 1.dp,\n                    shape = CircleShape,\n                    color = MaterialTheme.colorScheme.tertiary,\n                ),\n                imageVector = Icons.Rounded.Add,\n                tint = MaterialTheme.colorScheme.tertiary,\n                onClick = { showSelectAccountPopup = true },\n                contentDescription = \"Add Account\",\n            )\n        }\n        HorizontalDivider(modifier = Modifier.fillMaxWidth().align(Alignment.BottomCenter))\n    }\n    if (showSelectAccountPopup) {\n        SelectAccountDialog(\n            accountList = uiState.allAccounts.map { it.account },\n            selectedAccounts = uiState.addedAccounts.map { it.account },\n            onDismissRequest = { showSelectAccountPopup = false },\n            onAccountClicked = { account ->\n                showSelectAccountPopup = false\n                val selected = uiState.addedAccounts.any { it.account.uri == account.uri }\n                if (selected) {\n                    onRemoveAccountClick(\n                        uiState.addedAccounts.first { it.account.uri == account.uri }.account\n                    )\n                } else {\n                    onAddAccountClick(uiState.allAccounts.first { it.account.uri == account.uri })\n                }\n            },\n        )\n    }\n}\n\n@Composable\nfun PublishingAccountsOld(\n    modifier: Modifier,\n    uiState: MultiAccountPublishingUiState,\n    onRemoveAccountClick: (LoggedAccount) -> Unit,\n    onAddAccountClick: (MultiPublishingAccountWithRules) -> Unit,\n) {\n    var showSelectAccountPopup by remember { mutableStateOf(false) }\n    Box(\n        modifier = modifier,\n    ) {\n        SimpleIconButton(\n            modifier = Modifier.padding(start = 8.dp, top = 8.dp),\n            iconModifier = Modifier.border(\n                width = 1.dp,\n                shape = CircleShape,\n                color = MaterialTheme.colorScheme.tertiary,\n            ),\n            imageVector = Icons.Rounded.Add,\n            tint = MaterialTheme.colorScheme.tertiary,\n            onClick = { showSelectAccountPopup = true },\n            contentDescription = \"Add Account\",\n        )\n        Column(\n            modifier = Modifier.padding(start = 48.dp),\n        ) {\n            for (index in uiState.addedAccounts.indices) {\n                val account = uiState.addedAccounts[index]\n                Box(\n                    modifier = Modifier.fillMaxWidth().padding(start = 16.dp, end = 16.dp)\n                ) {\n                    AccountItem(\n                        modifier = Modifier.fillMaxWidth(),\n                        uiState = uiState,\n                        accountUiState = account,\n                        onRemoveAccountClick = onRemoveAccountClick,\n                    )\n                }\n                if (index < uiState.addedAccounts.lastIndex) {\n                    Spacer(modifier = Modifier.padding(top = 8.dp))\n                }\n            }\n        }\n    }\n    if (showSelectAccountPopup) {\n        SelectAccountDialog(\n            accountList = uiState.allAccounts.map { it.account },\n            selectedAccounts = uiState.addedAccounts.map { it.account },\n            onDismissRequest = { showSelectAccountPopup = false },\n            onAccountClicked = { account ->\n                showSelectAccountPopup = false\n                onAddAccountClick(uiState.allAccounts.first { it.account.uri == account.uri })\n            },\n        )\n    }\n}\n\n@Composable\nprivate fun AccountItem(\n    modifier: Modifier,\n    uiState: MultiAccountPublishingUiState,\n    accountUiState: MultiPublishingAccountUiState,\n    onRemoveAccountClick: (LoggedAccount) -> Unit,\n) {\n    val account = accountUiState.account\n    Card(\n        modifier = modifier,\n        shape = RoundedCornerShape(16.dp),\n    ) {\n        Column(\n            modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp)\n        ) {\n            Row(\n                modifier = Modifier.fillMaxWidth().padding(start = 16.dp, end = 8.dp),\n            ) {\n                Box(\n                    modifier = Modifier.size(42.dp).align(Alignment.CenterVertically),\n                ) {\n                    BlogAuthorAvatar(\n                        modifier = Modifier.fillMaxSize(),\n                        imageUrl = account.avatar,\n                    )\n                    Image(\n                        imageVector = account.platform.protocol.logo,\n                        modifier = Modifier.align(Alignment.BottomEnd).size(16.dp),\n                        contentDescription = null,\n                    )\n                }\n                Column(\n                    modifier = Modifier.weight(1F)\n                        .padding(start = 8.dp)\n                        .align(Alignment.CenterVertically)\n                ) {\n                    FreadRichText(\n                        modifier = Modifier.fillMaxWidth(),\n                        maxLines = 1,\n                        overflow = TextOverflow.Ellipsis,\n                        content = account.userName,\n                        emojis = account.emojis,\n                        onUrlClick = {},\n                        fontWeight = FontWeight.SemiBold,\n                        fontSize = 16.sp,\n                    )\n                    Text(\n                        modifier = Modifier.fillMaxWidth().padding(top = 2.dp),\n                        text = account.prettyHandle,\n                        maxLines = 1,\n                        style = MaterialTheme.typography.labelMedium,\n                        overflow = TextOverflow.Ellipsis,\n                        color = MaterialTheme.colorScheme.onSurfaceVariant,\n                    )\n                }\n                IconButton(\n                    onClick = { onRemoveAccountClick(account) },\n                ) {\n                    Icon(\n                        imageVector = Icons.Default.Delete,\n                        contentDescription = \"Remove\",\n                    )\n                }\n            }\n            Row(\n                modifier = Modifier.fillMaxWidth().padding(start = 16.dp, top = 8.dp, end = 16.dp),\n                verticalAlignment = Alignment.CenterVertically,\n            ) {\n                if (account.platform.protocol.isBluesky) {\n                    PublishSettingLabel(\n                        modifier = Modifier,\n                        label = uiState.interactionSetting.label,\n                        icon = uiState.interactionSetting.labelIcon,\n                    )\n                } else if (account.platform.protocol.isActivityPub) {\n                    PublishSettingLabel(\n                        modifier = Modifier,\n                        label = stringResource(uiState.postVisibility.describeStringId),\n                        icon = Icons.Default.Public,\n                    )\n                }\n                Spacer(modifier = Modifier.weight(1F))\n                val lan = uiState.selectedLanguage\n                Text(\n                    text = remember(lan) { initLocale(lan.languageCode).getDisplayName(lan) },\n                    style = MaterialTheme.typography.labelMedium,\n                    color = MaterialTheme.colorScheme.primary,\n                )\n                RemainingTextStatus(\n                    modifier = Modifier.padding(start = 8.dp),\n                    maxCount = accountUiState.rules.maxCharacters,\n                    contentLength = uiState.content.text.length,\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "commonbiz/sharedscreen/src/commonMain/kotlin/com/zhangke/fread/commonbiz/shared/screen/search/AbstractSearchStatusScreen.kt",
    "content": "package com.zhangke.fread.commonbiz.shared.screen.search\n\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.foundation.text.KeyboardActions\nimport androidx.compose.foundation.text.KeyboardOptions\nimport androidx.compose.material3.CircularProgressIndicator\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.SnackbarHost\nimport androidx.compose.material3.SnackbarHostState\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextField\nimport androidx.compose.material3.TextFieldDefaults\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.platform.LocalFocusManager\nimport androidx.compose.ui.text.input.ImeAction\nimport androidx.compose.ui.unit.dp\nimport com.zhangke.framework.composable.currentOrThrow\nimport com.zhangke.framework.composable.ConsumeOpenScreenFlow\nimport com.zhangke.framework.composable.ConsumeSnackbarFlow\nimport com.zhangke.framework.composable.Toolbar\nimport com.zhangke.framework.composable.keyboardAsState\nimport com.zhangke.framework.composable.rememberSnackbarHostState\nimport com.zhangke.framework.loadable.lazycolumn.ObserveLoadMore\nimport com.zhangke.framework.nav.LocalNavBackStack\nimport com.zhangke.framework.utils.transparentIndicatorAndContainerColors\nimport com.zhangke.fread.commonbiz.shared.composable.FeedsStatusNode\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.ui.ComposedStatusInteraction\nimport kotlinx.coroutines.delay\nimport org.jetbrains.compose.resources.stringResource\n\n@Composable\nfun AbstractSearchStatusScreen(viewModel: AbstractSearchStatusViewModel) {\n    val backStack = LocalNavBackStack.currentOrThrow\n    val snackbarHostState = rememberSnackbarHostState()\n    val uiState by viewModel.uiState.collectAsState()\n    SearchStatusContent(\n        uiState = uiState,\n        snackbarHostState = snackbarHostState,\n        composedStatusInteraction = viewModel.composedStatusInteraction,\n        onBackClick = backStack::removeLastOrNull,\n        onQueryChanged = viewModel::onQueryChange,\n        onSearchClick = viewModel::onSearchClick,\n        onLoadMore = viewModel::onLoadMore,\n    )\n    ConsumeSnackbarFlow(snackbarHostState, viewModel.errorMessageFlow)\n    ConsumeOpenScreenFlow(viewModel.openScreenFlow)\n}\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nprivate fun SearchStatusContent(\n    uiState: SearchStatusUiState,\n    snackbarHostState: SnackbarHostState,\n    composedStatusInteraction: ComposedStatusInteraction,\n    onBackClick: () -> Unit,\n    onQueryChanged: (String) -> Unit,\n    onSearchClick: () -> Unit,\n    onLoadMore: () -> Unit,\n) {\n    val focusManager = LocalFocusManager.current\n    val keyboardState by keyboardAsState()\n    LaunchedEffect(keyboardState) {\n        if (!keyboardState) {\n            focusManager.clearFocus()\n        }\n    }\n    val focusRequester = remember { FocusRequester() }\n    LaunchedEffect(Unit) {\n        delay(200)\n        focusRequester.requestFocus()\n    }\n    Scaffold(\n        modifier = Modifier.fillMaxSize(),\n        topBar = {\n            TopAppBar(\n                navigationIcon = { Toolbar.BackButton(onBackClick = onBackClick) },\n                title = {\n                    TextField(\n                        modifier = Modifier.fillMaxWidth()\n                            .focusRequester(focusRequester),\n                        value = uiState.query,\n                        onValueChange = onQueryChanged,\n                        placeholder = {\n                            Text(\n                                text = stringResource(LocalizedString.statusUiSearchAccountStatusHint),\n                            )\n                        },\n                        keyboardActions = KeyboardActions(\n                            onSearch = {\n                                focusManager.clearFocus()\n                                onSearchClick()\n                            }\n                        ),\n                        keyboardOptions = KeyboardOptions.Default.copy(\n                            imeAction = ImeAction.Search\n                        ),\n                        trailingIcon = {\n                            if (uiState.searching) {\n                                CircularProgressIndicator(\n                                    modifier = Modifier.size(18.dp),\n                                    strokeWidth = 2.dp,\n                                    color = MaterialTheme.colorScheme.primary,\n                                )\n                            }\n                        },\n                        colors = TextFieldDefaults.transparentIndicatorAndContainerColors,\n                        maxLines = 1,\n                    )\n                },\n            )\n        },\n        snackbarHost = { SnackbarHost(snackbarHostState) },\n    ) { innerPadding ->\n        val listState = rememberLazyListState()\n        ObserveLoadMore(\n            lazyListState = listState,\n            onLoadMore = onLoadMore,\n        )\n        LazyColumn(\n            modifier = Modifier\n                .fillMaxSize()\n                .padding(innerPadding),\n            state = listState,\n        ) {\n            itemsIndexed(uiState.result) { index, status ->\n                FeedsStatusNode(\n                    status = status,\n                    indexInList = index,\n                    composedStatusInteraction = composedStatusInteraction,\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "commonbiz/sharedscreen/src/commonMain/kotlin/com/zhangke/fread/commonbiz/shared/screen/search/AbstractSearchStatusViewModel.kt",
    "content": "package com.zhangke.fread.commonbiz.shared.screen.search\n\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.zhangke.framework.composable.emitTextMessageFromThrowable\nimport com.zhangke.framework.composable.toTextStringOrNull\nimport com.zhangke.framework.ktx.launchInViewModel\nimport com.zhangke.framework.utils.LoadState\nimport com.zhangke.fread.common.adapter.StatusUiStateAdapter\nimport com.zhangke.fread.common.status.StatusUpdater\nimport com.zhangke.fread.commonbiz.shared.feeds.IInteractiveHandler\nimport com.zhangke.fread.commonbiz.shared.feeds.InteractiveHandleResult\nimport com.zhangke.fread.commonbiz.shared.feeds.InteractiveHandler\nimport com.zhangke.fread.commonbiz.shared.feeds.handle\nimport com.zhangke.fread.commonbiz.shared.usecase.RefactorToNewStatusUseCase\nimport com.zhangke.fread.status.StatusProvider\nimport com.zhangke.fread.status.model.StatusUiState\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.update\n\nabstract class AbstractSearchStatusViewModel(\n    private val statusProvider: StatusProvider,\n    private val statusUiStateAdapter: StatusUiStateAdapter,\n    statusUpdater: StatusUpdater,\n    refactorToNewStatus: RefactorToNewStatusUseCase,\n) : ViewModel(), IInteractiveHandler by InteractiveHandler(\n    statusProvider = statusProvider,\n    statusUpdater = statusUpdater,\n    statusUiStateAdapter = statusUiStateAdapter,\n    refactorToNewStatus = refactorToNewStatus,\n) {\n\n    private val _uiState = MutableStateFlow(SearchStatusUiState.default())\n    val uiState = _uiState.asStateFlow()\n\n    private var searchJob: Job? = null\n    private var loadMoreJob: Job? = null\n\n    init {\n        initInteractiveHandler(\n            coroutineScope = viewModelScope,\n            onInteractiveHandleResult = { it.handleResult() },\n        )\n    }\n\n    fun onQueryChange(query: String) {\n        _uiState.update { it.copy(query = query) }\n        if (searchJob?.isActive == true) searchJob?.cancel()\n        doSearch()\n    }\n\n    fun onSearchClick() {\n        doSearch()\n    }\n\n    fun onLoadMore() {\n        if (_uiState.value.searching) return\n        if (_uiState.value.result.isEmpty()) return\n        if (searchJob?.isActive == true) return\n        if (loadMoreJob?.isActive == true) return\n        loadMoreJob = launchInViewModel {\n            _uiState.update { it.copy(loadMoreState = LoadState.Loading) }\n            performSearch(\n                query = _uiState.value.query,\n                loadMore = true,\n            ).onSuccess { statuses ->\n                _uiState.update {\n                    it.copy(\n                        loadMoreState = LoadState.Idle,\n                        result = it.result + statuses,\n                    )\n                }\n            }.onFailure { t ->\n                _uiState.update { it.copy(loadMoreState = LoadState.Failed(t.toTextStringOrNull())) }\n            }\n        }\n    }\n\n    private fun doSearch() {\n        val query = _uiState.value.query\n        if (query.isEmpty() || query.isBlank()) {\n            _uiState.update { it.copy(result = emptyList()) }\n            return\n        }\n        searchJob = launchInViewModel {\n            _uiState.update { it.copy(searching = true) }\n            performSearch(query = query, loadMore = false)\n                .onSuccess { statuses ->\n                    _uiState.update {\n                        it.copy(searching = false, result = statuses)\n                    }\n                }.onFailure { t ->\n                    _uiState.update { it.copy(searching = false) }\n                    mutableErrorMessageFlow.emitTextMessageFromThrowable(t)\n                }\n        }\n    }\n\n    abstract suspend fun performSearch(\n        query: String,\n        loadMore: Boolean,\n    ): Result<List<StatusUiState>>\n\n    private suspend fun InteractiveHandleResult.handleResult() {\n        handle(\n            uiStatusUpdater = { newUiState ->\n                _uiState.update { currentUiState ->\n                    currentUiState.copy(\n                        result = currentUiState.result.map {\n                            if (it.status.intrinsicBlog.id == newUiState.status.intrinsicBlog.id) {\n                                newUiState\n                            } else {\n                                it\n                            }\n                        }\n                    )\n                }\n            },\n            deleteStatus = { statusId ->\n                _uiState.update { state ->\n                    state.copy(\n                        result = state.result.filter {\n                            it.status.id != statusId\n                        }\n                    )\n                }\n            },\n            followStateUpdater = { _, _ -> },\n        )\n    }\n}"
  },
  {
    "path": "commonbiz/sharedscreen/src/commonMain/kotlin/com/zhangke/fread/commonbiz/shared/screen/search/SearchStatusUiState.kt",
    "content": "package com.zhangke.fread.commonbiz.shared.screen.search\n\nimport com.zhangke.framework.utils.LoadState\nimport com.zhangke.fread.status.model.StatusUiState\n\ndata class SearchStatusUiState(\n    val query: String,\n    val searching: Boolean,\n    val loadMoreState: LoadState,\n    val result: List<StatusUiState>,\n) {\n\n    companion object {\n\n        fun default() = SearchStatusUiState(\n            query = \"\",\n            searching = false,\n            loadMoreState = LoadState.Idle,\n            result = emptyList(),\n        )\n    }\n}"
  },
  {
    "path": "commonbiz/sharedscreen/src/commonMain/kotlin/com/zhangke/fread/commonbiz/shared/screen/status/account/SelectAccountOpenStatusScreen.kt",
    "content": "package com.zhangke.fread.commonbiz.shared.screen.status.account\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.pager.HorizontalPager\nimport androidx.compose.foundation.pager.rememberPagerState\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material3.Button\nimport androidx.compose.material3.CircularProgressIndicator\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.HorizontalDivider\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.ModalBottomSheet\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.DisposableEffect\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport com.zhangke.framework.composable.ConsumeFlow\nimport com.zhangke.framework.composable.rememberTransientModalBottomSheetState\nimport com.zhangke.fread.common.utils.GlobalScreenNavigation\nimport com.zhangke.fread.commonbiz.shared.screen.status.context.StatusContextScreenNavKey\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.account.LoggedAccount\nimport com.zhangke.fread.status.model.StatusUiState\nimport com.zhangke.fread.status.ui.user.BasicAccountUi\nimport org.jetbrains.compose.resources.stringResource\nimport org.koin.compose.viewmodel.koinViewModel\n\n@Composable\nfun rememberSelectAccountOpenStatusSheetState(): SelectAccountOpenStatusBottomSheetState {\n    return remember { SelectAccountOpenStatusBottomSheetState() }\n}\n\nclass SelectAccountOpenStatusBottomSheetState {\n\n    internal var statusUiState: StatusUiState? = null\n\n    internal var visible by mutableStateOf(false)\n\n    fun show(status: StatusUiState) {\n        this.statusUiState = status\n        visible = true\n    }\n\n    fun hide() {\n        statusUiState = null\n        visible = false\n    }\n}\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun SelectAccountOpenStatusBottomSheet(\n    state: SelectAccountOpenStatusBottomSheetState,\n) {\n    val viewModel = koinViewModel<SelectAccountOpenStatusViewModel>()\n    if (!state.visible) {\n        viewModel.clearState()\n        return\n    }\n    val status = state.statusUiState ?: return\n    DisposableEffect(status) {\n        viewModel.initialize(status)\n        onDispose {\n            viewModel.clearState()\n        }\n    }\n    val sheetState = rememberTransientModalBottomSheetState()\n    ModalBottomSheet(\n        sheetState = sheetState,\n        onDismissRequest = {\n            state.hide()\n            viewModel.clearState()\n        }\n    ) {\n        SelectAccountOpenStatusScreen(\n            viewModel = viewModel,\n            onClose = {\n                state.hide()\n                viewModel.clearState()\n            },\n        )\n    }\n}\n\n@Composable\nfun SelectAccountOpenStatusScreen(\n    viewModel: SelectAccountOpenStatusViewModel,\n    onClose: () -> Unit,\n) {\n    val uiState by viewModel.uiState.collectAsState()\n    SelectAccountOpenStatusContent(\n        uiState = uiState,\n        onAccountClick = viewModel::onAccountClick,\n        onCancelClick = viewModel::onCancelSearchClick,\n        onSearchFailedClick = viewModel::onSearchFailedClick,\n    )\n    ConsumeFlow(viewModel.searchedStatusFlow) {\n        onClose()\n        GlobalScreenNavigation.navigate(StatusContextScreenNavKey.create(it))\n    }\n}\n\n@Composable\nprivate fun SelectAccountOpenStatusContent(\n    uiState: SelectAccountOpenStatusUiState,\n    onAccountClick: (LoggedAccount) -> Unit,\n    onCancelClick: () -> Unit,\n    onSearchFailedClick: () -> Unit,\n) {\n    Column(\n        modifier = Modifier.fillMaxWidth()\n            .padding(vertical = 16.dp)\n            .padding(bottom = 16.dp)\n            .verticalScroll(rememberScrollState()),\n    ) {\n        Text(\n            modifier = Modifier.padding(horizontal = 16.dp),\n            text = stringResource(LocalizedString.selectAccountOpenStatusTitle),\n            fontSize = 18.sp,\n            maxLines = 1,\n            overflow = TextOverflow.Ellipsis,\n        )\n        Spacer(modifier = Modifier.height(16.dp))\n        if (!uiState.loadingAccounts && uiState.accountList.isEmpty()) {\n            Box(\n                modifier = Modifier.fillMaxSize()\n                    .padding(vertical = 38.dp),\n                contentAlignment = Alignment.Center,\n            ) {\n                Text(\n                    text = stringResource(LocalizedString.selectAccountOpenStatusEmpty)\n                )\n            }\n        } else {\n            val pagerState = rememberPagerState { 2 }\n            LaunchedEffect(uiState.searching) {\n                val target = if (uiState.searching) 1 else 0\n                pagerState.animateScrollToPage(target)\n            }\n            HorizontalPager(\n                modifier = Modifier.fillMaxWidth(),\n                state = pagerState,\n                userScrollEnabled = false,\n            ) { page ->\n                if (page == 0) {\n                    AccountsListContent(\n                        uiState = uiState,\n                        onAccountClick = onAccountClick,\n                    )\n                } else {\n                    SearchContent(\n                        uiState = uiState,\n                        onCancelClick = onCancelClick,\n                        onSearchFailedClick = onSearchFailedClick,\n                    )\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun AccountsListContent(\n    uiState: SelectAccountOpenStatusUiState,\n    onAccountClick: (LoggedAccount) -> Unit,\n) {\n    Column(\n        modifier = Modifier.fillMaxWidth()\n    ) {\n        for (account in uiState.accountList) {\n            BasicAccountUi(\n                modifier = Modifier.fillMaxWidth()\n                    .clickable { onAccountClick(account) },\n                account = account,\n            )\n            HorizontalDivider(thickness = 0.5.dp)\n        }\n    }\n}\n\n@Composable\nprivate fun SearchContent(\n    uiState: SelectAccountOpenStatusUiState,\n    onCancelClick: () -> Unit,\n    onSearchFailedClick: () -> Unit,\n) {\n    Column(\n        modifier = Modifier.fillMaxWidth(),\n        horizontalAlignment = Alignment.CenterHorizontally,\n    ) {\n        Spacer(modifier = Modifier.height(16.dp))\n        if (uiState.searchFailed) {\n            Text(\n                text = stringResource(LocalizedString.selectAccountOpenStatusSearchFailed),\n                style = MaterialTheme.typography.titleMedium,\n            )\n        } else {\n            Text(\n                text = stringResource(\n                    LocalizedString.selectAccountOpenStatusSearchIn,\n                    uiState.searchingAccount?.platform?.name.orEmpty(),\n                ),\n                style = MaterialTheme.typography.titleMedium,\n            )\n            Spacer(modifier = Modifier.height(16.dp))\n            CircularProgressIndicator(\n                modifier = Modifier.size(48.dp)\n            )\n        }\n        Spacer(modifier = Modifier.height(16.dp))\n        Button(\n            onClick = if (uiState.searchFailed) {\n                onSearchFailedClick\n            } else {\n                onCancelClick\n            },\n        ) {\n            Text(\n                text = stringResource(LocalizedString.cancel)\n            )\n        }\n        Spacer(modifier = Modifier.height(16.dp))\n    }\n}\n"
  },
  {
    "path": "commonbiz/sharedscreen/src/commonMain/kotlin/com/zhangke/fread/commonbiz/shared/screen/status/account/SelectAccountOpenStatusUiState.kt",
    "content": "package com.zhangke.fread.commonbiz.shared.screen.status.account\n\nimport com.zhangke.fread.status.account.LoggedAccount\n\ndata class SelectAccountOpenStatusUiState(\n    val loadingAccounts: Boolean,\n    val accountList: List<LoggedAccount>,\n    val searching: Boolean,\n    val searchingAccount: LoggedAccount?,\n    val searchFailed: Boolean,\n) {\n\n    fun reset(): SelectAccountOpenStatusUiState {\n        return SelectAccountOpenStatusUiState(\n            loadingAccounts = false,\n            accountList = emptyList(),\n            searching = false,\n            searchingAccount = null,\n            searchFailed = false,\n        )\n    }\n\n    companion object {\n\n        fun default(loadingAccounts: Boolean): SelectAccountOpenStatusUiState {\n            return SelectAccountOpenStatusUiState(\n                loadingAccounts = loadingAccounts,\n                accountList = emptyList(),\n                searching = false,\n                searchingAccount = null,\n                searchFailed = false,\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "commonbiz/sharedscreen/src/commonMain/kotlin/com/zhangke/fread/commonbiz/shared/screen/status/account/SelectAccountOpenStatusViewModel.kt",
    "content": "package com.zhangke.fread.commonbiz.shared.screen.status.account\n\nimport androidx.lifecycle.ViewModel\nimport com.zhangke.framework.ktx.launchInViewModel\nimport com.zhangke.fread.status.StatusProvider\nimport com.zhangke.fread.status.account.LoggedAccount\nimport com.zhangke.fread.status.model.StatusUiState\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.update\n\nclass SelectAccountOpenStatusViewModel(\n    private val statusProvider: StatusProvider,\n) : ViewModel() {\n\n    private val _uiState = MutableStateFlow(\n        SelectAccountOpenStatusUiState.default(loadingAccounts = false),\n    )\n    val uiState = _uiState\n\n    private val _searchedStatusFlow = MutableSharedFlow<StatusUiState>()\n    val searchedStatusFlow = _searchedStatusFlow\n\n    private var searchJob: Job? = null\n\n    private var blogUrl: String? = null\n\n    fun initialize(statusUiState: StatusUiState) {\n        val protocol = statusUiState.status.platform.protocol\n        val locator = statusUiState.locator\n        this.blogUrl = statusUiState.status.intrinsicBlog.url\n        _uiState.update { it.copy(loadingAccounts = true) }\n        launchInViewModel {\n            val availableAccounts = statusProvider.accountManager\n                .getAllLoggedAccount()\n                .filter { it.platform.protocol == protocol }\n                .filter { it.uri != locator.accountUri }\n            _uiState.update {\n                it.copy(\n                    loadingAccounts = false,\n                    accountList = availableAccounts,\n                )\n            }\n        }\n    }\n\n    fun onAccountClick(account: LoggedAccount) {\n        searchJob?.cancel()\n        _uiState.update {\n            it.copy(\n                searching = true,\n                searchingAccount = account,\n                searchFailed = false,\n            )\n        }\n        searchJob = launchInViewModel {\n            statusProvider.searchEngine\n                .searchStatusByUrl(\n                    protocol = account.platform.protocol,\n                    locator = account.locator,\n                    url = blogUrl.orEmpty(),\n                ).onSuccess { status ->\n                    if (status != null) {\n                        _searchedStatusFlow.emit(status)\n                    } else {\n                        _uiState.update { it.copy(searchFailed = true) }\n                    }\n                }.onFailure {\n                    _uiState.update { it.copy(searchFailed = true) }\n                }\n        }\n    }\n\n    fun onSearchFailedClick() {\n        _uiState.update {\n            it.copy(\n                searchFailed = false,\n                searching = false,\n                searchingAccount = null,\n            )\n        }\n    }\n\n    fun onCancelSearchClick() {\n        searchJob?.cancel()\n        _uiState.update {\n            it.copy(\n                searching = false,\n                searchingAccount = null,\n            )\n        }\n    }\n\n    fun clearState() {\n        _uiState.update { it.reset() }\n        searchJob?.cancel()\n    }\n}\n"
  },
  {
    "path": "commonbiz/sharedscreen/src/commonMain/kotlin/com/zhangke/fread/commonbiz/shared/screen/status/context/StatusContextScreen.kt",
    "content": "package com.zhangke.fread.commonbiz.shared.screen.status.context\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.SnackbarHost\nimport androidx.compose.material3.SnackbarHostState\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.architect.json.globalJson\nimport com.zhangke.framework.blur.LocalBlurController\nimport com.zhangke.framework.blur.applyBlurEffect\nimport com.zhangke.framework.blur.applyBlurSource\nimport com.zhangke.framework.blur.blurEffectContainerColor\nimport com.zhangke.framework.blur.rememberBlurController\nimport com.zhangke.framework.composable.ConsumeOpenScreenFlow\nimport com.zhangke.framework.composable.ConsumeSnackbarFlow\nimport com.zhangke.framework.composable.LoadErrorLineItem\nimport com.zhangke.framework.composable.LoadingLineItem\nimport com.zhangke.framework.composable.SingleRowTopAppBar\nimport com.zhangke.framework.composable.Toolbar\nimport com.zhangke.framework.composable.TopAppBarColors\nimport com.zhangke.framework.composable.currentOrThrow\nimport com.zhangke.framework.composable.inline.InlineVideoLazyColumn\nimport com.zhangke.framework.composable.rememberSnackbarHostState\nimport com.zhangke.framework.composable.textString\nimport com.zhangke.framework.nav.LocalNavBackStack\nimport com.zhangke.fread.common.composable.ErrorContent\nimport com.zhangke.fread.common.composable.ErrorType\nimport com.zhangke.fread.commonbiz.shared.composable.onStatusMediaClick\nimport com.zhangke.fread.commonbiz.shared.screen.status.account.SelectAccountOpenStatusBottomSheet\nimport com.zhangke.fread.commonbiz.shared.screen.status.account.rememberSelectAccountOpenStatusSheetState\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.account.LoggedAccount\nimport com.zhangke.fread.status.blog.Blog\nimport com.zhangke.fread.status.model.BlogTranslationUiState\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.model.StatusUiState\nimport com.zhangke.fread.status.platform.BlogPlatform\nimport com.zhangke.fread.status.ui.BlogAuthorAvatar\nimport com.zhangke.fread.status.ui.ComposedStatusInteraction\nimport com.zhangke.fread.status.ui.StatusListPlaceholder\nimport com.zhangke.fread.status.ui.StatusUi\nimport com.zhangke.fread.status.ui.common.LocalStatusSharedElementConfig\nimport com.zhangke.fread.status.ui.image.OnBlogMediaClick\nimport com.zhangke.fread.status.ui.threads.ThreadsType\nimport kotlinx.serialization.Serializable\nimport org.jetbrains.compose.resources.stringResource\n\n@Serializable\ndata class StatusContextScreenNavKey(\n    val locator: PlatformLocator,\n    val serializedStatus: String? = null,\n    val serializedBlog: String? = null,\n    val blogId: String? = null,\n    val platform: BlogPlatform? = null,\n    val blogTranslationUiState: BlogTranslationUiState? = null,\n) : NavKey {\n\n    companion object {\n\n        fun create(statusUiState: StatusUiState): NavKey {\n            return StatusContextScreenNavKey(\n                locator = statusUiState.locator,\n                serializedStatus = globalJson.encodeToString(\n                    kotlinx.serialization.serializer(),\n                    statusUiState\n                ),\n                blogTranslationUiState = statusUiState.blogTranslationState,\n            )\n        }\n\n        fun create(locator: PlatformLocator, blog: Blog): NavKey {\n            return StatusContextScreenNavKey(\n                locator = locator,\n                serializedBlog = globalJson.encodeToString(\n                    kotlinx.serialization.serializer(),\n                    blog\n                ),\n                blogTranslationUiState = null,\n            )\n        }\n\n        fun create(\n            locator: PlatformLocator,\n            blogId: String,\n            platform: BlogPlatform\n        ): NavKey {\n            return StatusContextScreenNavKey(\n                locator = locator,\n                blogId = blogId,\n                blogTranslationUiState = null,\n                platform = platform,\n            )\n        }\n    }\n}\n\n@Composable\nfun StatusContextScreen(\n    locator: PlatformLocator,\n    serializedStatus: String? = null,\n    serializedBlog: String? = null,\n    blogId: String? = null,\n    platform: BlogPlatform? = null,\n    blogTranslationUiState: BlogTranslationUiState? = null,\n    containerViewModel: StatusContextViewModel,\n) {\n    val backStack = LocalNavBackStack.currentOrThrow\n    val viewModel = containerViewModel.getSubViewModel(\n        locator = locator,\n        anchorStatus = serializedStatus?.let(globalJson::decodeFromString),\n        blog = serializedBlog?.let { globalJson.decodeFromString(it) },\n        blogId = blogId,\n        platform = platform,\n        blogTranslationUiState = blogTranslationUiState,\n    )\n    val uiState by viewModel.uiState.collectAsState()\n    val snackbarHostState = rememberSnackbarHostState()\n    StatusContextContent(\n        uiState = uiState,\n        snackbarHostState = snackbarHostState,\n        onScrolledToAnchor = viewModel::onScrolledToAnchor,\n        onMediaClick = { event ->\n            onStatusMediaClick(\n                navigator = backStack,\n                event = event,\n            )\n        },\n        onBackClick = backStack::removeLastOrNull,\n        onAccountClick = viewModel::onAccountClick,\n        onRetryClick = viewModel::onRetryClick,\n        composedStatusInteraction = viewModel.composedStatusInteraction,\n    )\n    LaunchedEffect(Unit) {\n        viewModel.onPageResume()\n    }\n    ConsumeOpenScreenFlow(viewModel.openScreenFlow)\n    ConsumeSnackbarFlow(snackbarHostState, viewModel.errorMessageFlow)\n}\n\n@Composable\nprivate fun StatusContextContent(\n    uiState: StatusContextUiState,\n    snackbarHostState: SnackbarHostState,\n    onRetryClick: () -> Unit,\n    onScrolledToAnchor: () -> Unit,\n    onBackClick: () -> Unit = {},\n    onAccountClick: (LoggedAccount) -> Unit,\n    onMediaClick: OnBlogMediaClick,\n    composedStatusInteraction: ComposedStatusInteraction,\n) {\n    val blurController = rememberBlurController()\n    val enableBlur = uiState.contextStatus.size > 1\n    CompositionLocalProvider(\n        LocalBlurController provides blurController\n    ) {\n        val surfaceColor = MaterialTheme.colorScheme.surface\n        Scaffold(\n            snackbarHost = {\n                SnackbarHost(hostState = snackbarHostState)\n            },\n            topBar = {\n                SingleRowTopAppBar(\n                    modifier = Modifier.applyBlurEffect(enableBlur, surfaceColor),\n                    colors = TopAppBarColors.default(\n                        containerColor = blurEffectContainerColor(\n                            enabled = enableBlur,\n                            containerColor = surfaceColor,\n                        )\n                    ),\n                    title = {\n                        Text(\n                            text = stringResource(LocalizedString.sharedStatusContextScreenTitle),\n                        )\n                    },\n                    navigationIcon = {\n                        Toolbar.BackButton(onBackClick = onBackClick)\n                    },\n                    actions = {\n                        if (uiState.currentAccount != null) {\n                            BlogAuthorAvatar(\n                                modifier = Modifier.padding(end = 8.dp)\n                                    .size(28.dp),\n                                imageUrl = uiState.currentAccount.avatar,\n                                onClick = { onAccountClick(uiState.currentAccount) },\n                            )\n                        }\n                    }\n                )\n            },\n            content = { contentPaddings ->\n                val contextStatus = uiState.contextStatus\n                if (contextStatus.isEmpty()) {\n                    if (uiState.loading) {\n                        StatusListPlaceholder(modifier = Modifier.fillMaxSize())\n                    } else {\n                        ErrorContent(\n                            modifier = Modifier.fillMaxSize(),\n                            type = ErrorType.NotFound,\n                            errorMessage = uiState.errorMessage?.let { textString(it) },\n                            onRetryClick = onRetryClick,\n                        )\n                    }\n                } else {\n                    val state = rememberLazyListState()\n                    val anchorIndex = uiState.anchorIndex\n                    if (!uiState.loading && uiState.needScrollToAnchor && anchorIndex in 0..contextStatus.lastIndex) {\n                        LaunchedEffect(anchorIndex) {\n                            state.animateScrollToItem(anchorIndex)\n                            onScrolledToAnchor()\n                        }\n                    }\n                    InlineVideoLazyColumn(\n                        modifier = Modifier\n                            .fillMaxSize()\n                            .applyBlurSource(enableBlur),\n                        state = state,\n                        contentPadding = contentPaddings,\n                    ) {\n                        itemsIndexed(\n                            items = contextStatus,\n                        ) { index, statusInContext ->\n                            StatusInContextUi(\n                                modifier = Modifier\n                                    .fillMaxWidth(),\n                                statusInContext = statusInContext,\n                                indexInList = index,\n                                onMediaClick = onMediaClick,\n                                composedStatusInteraction = composedStatusInteraction,\n                            )\n                        }\n                        if (uiState.loading) {\n                            item {\n                                LoadingLineItem(modifier = Modifier.fillMaxWidth())\n                            }\n                        } else if (uiState.errorMessage != null) {\n                            item {\n                                LoadErrorLineItem(\n                                    modifier = Modifier.fillMaxWidth(),\n                                    errorMessage = uiState.errorMessage,\n                                )\n                            }\n                        }\n                    }\n                }\n            },\n        )\n    }\n}\n\n@Composable\nprivate fun StatusInContextUi(\n    modifier: Modifier = Modifier,\n    statusInContext: StatusInContext,\n    indexInList: Int,\n    onMediaClick: OnBlogMediaClick,\n    composedStatusInteraction: ComposedStatusInteraction,\n) {\n    val selectAccountOpenStatusBottomSheetState = rememberSelectAccountOpenStatusSheetState()\n    val preSharedElementConfig = LocalStatusSharedElementConfig.current\n    val statusSharedElementConfig = remember(preSharedElementConfig) {\n        preSharedElementConfig.copy(label = \"status-context\")\n    }\n    CompositionLocalProvider(\n        LocalStatusSharedElementConfig provides statusSharedElementConfig\n    ) {\n        when (statusInContext.type) {\n            StatusInContextType.ANCESTOR -> StatusUi(\n                modifier = modifier.clickable {\n                    composedStatusInteraction.onStatusClick(statusInContext.status)\n                },\n                threadsType = if (indexInList == 0) {\n                    ThreadsType.FIRST_ANCESTOR\n                } else {\n                    ThreadsType.ANCESTOR\n                },\n                status = statusInContext.status,\n                indexInList = indexInList,\n                onMediaClick = onMediaClick,\n                composedStatusInteraction = composedStatusInteraction,\n                onOpenBlogWithOtherAccountClick = {\n                    selectAccountOpenStatusBottomSheetState.show(it)\n                },\n            )\n\n            StatusInContextType.ANCHOR -> StatusUi(\n                modifier = modifier,\n                status = statusInContext.status,\n                indexInList = indexInList,\n                threadsType = if (indexInList == 0) ThreadsType.ANCHOR_FIRST else ThreadsType.ANCHOR,\n                onMediaClick = onMediaClick,\n                detailModel = true,\n                composedStatusInteraction = composedStatusInteraction,\n                onOpenBlogWithOtherAccountClick = {\n                    selectAccountOpenStatusBottomSheetState.show(it)\n                },\n            )\n\n            StatusInContextType.DESCENDANT -> StatusUi(\n                modifier = modifier.clickable {\n                    composedStatusInteraction.onStatusClick(statusInContext.status)\n                },\n                status = statusInContext.status,\n                indexInList = indexInList,\n                onMediaClick = onMediaClick,\n                threadsType = ThreadsType.NONE,\n                composedStatusInteraction = composedStatusInteraction,\n                onOpenBlogWithOtherAccountClick = {\n                    selectAccountOpenStatusBottomSheetState.show(it)\n                },\n            )\n\n            StatusInContextType.DESCENDANT_ANCHOR -> StatusUi(\n                modifier = modifier.clickable {\n                    composedStatusInteraction.onStatusClick(statusInContext.status)\n                },\n                threadsType = ThreadsType.FIRST_ANCESTOR,\n                status = statusInContext.status,\n                indexInList = indexInList,\n                onMediaClick = onMediaClick,\n                composedStatusInteraction = composedStatusInteraction,\n                onOpenBlogWithOtherAccountClick = {\n                    selectAccountOpenStatusBottomSheetState.show(it)\n                },\n            )\n\n            StatusInContextType.DESCENDANT_WITH_ANCESTOR_DESCENDANT -> StatusUi(\n                modifier = modifier.clickable {\n                    composedStatusInteraction.onStatusClick(statusInContext.status)\n                },\n                threadsType = ThreadsType.ANCESTOR,\n                status = statusInContext.status,\n                indexInList = indexInList,\n                onMediaClick = onMediaClick,\n                composedStatusInteraction = composedStatusInteraction,\n                onOpenBlogWithOtherAccountClick = {\n                    selectAccountOpenStatusBottomSheetState.show(it)\n                },\n            )\n\n            StatusInContextType.DESCENDANT_WITH_ANCESTOR -> StatusUi(\n                modifier = modifier.clickable {\n                    composedStatusInteraction.onStatusClick(statusInContext.status)\n                },\n                threadsType = ThreadsType.ANCHOR,\n                status = statusInContext.status,\n                indexInList = indexInList,\n                onMediaClick = onMediaClick,\n                composedStatusInteraction = composedStatusInteraction,\n                onOpenBlogWithOtherAccountClick = {\n                    selectAccountOpenStatusBottomSheetState.show(it)\n                },\n            )\n        }\n    }\n    SelectAccountOpenStatusBottomSheet(selectAccountOpenStatusBottomSheetState)\n}\n"
  },
  {
    "path": "commonbiz/sharedscreen/src/commonMain/kotlin/com/zhangke/fread/commonbiz/shared/screen/status/context/StatusContextSubViewModel.kt",
    "content": "package com.zhangke.fread.commonbiz.shared.screen.status.context\n\nimport com.zhangke.framework.composable.emitInViewModel\nimport com.zhangke.framework.composable.textOf\nimport com.zhangke.framework.composable.toTextStringOrNull\nimport com.zhangke.framework.ktx.launchInViewModel\nimport com.zhangke.framework.lifecycle.SubViewModel\nimport com.zhangke.fread.common.adapter.StatusUiStateAdapter\nimport com.zhangke.fread.common.mixed.MixedStatusRepo\nimport com.zhangke.fread.common.status.StatusUpdater\nimport com.zhangke.fread.commonbiz.shared.feeds.IInteractiveHandler\nimport com.zhangke.fread.commonbiz.shared.feeds.InteractiveHandleResult\nimport com.zhangke.fread.commonbiz.shared.feeds.InteractiveHandler\nimport com.zhangke.fread.commonbiz.shared.usecase.RefactorToNewStatusUseCase\nimport com.zhangke.fread.status.StatusProvider\nimport com.zhangke.fread.status.account.LoggedAccount\nimport com.zhangke.fread.status.author.BlogAuthor\nimport com.zhangke.fread.status.blog.Blog\nimport com.zhangke.fread.status.model.BlogTranslationUiState\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.model.StatusUiState\nimport com.zhangke.fread.status.model.updateFollowingState\nimport com.zhangke.fread.status.platform.BlogPlatform\nimport com.zhangke.fread.status.status.model.DescendantStatus\nimport com.zhangke.fread.status.status.model.Status\nimport com.zhangke.fread.status.status.model.StatusContext\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.update\n\nclass StatusContextSubViewModel(\n    private val mixedStatusRepo: MixedStatusRepo,\n    private val statusProvider: StatusProvider,\n    private val statusUpdater: StatusUpdater,\n    private val refactorToNewStatus: RefactorToNewStatusUseCase,\n    private val statusUiStateAdapter: StatusUiStateAdapter,\n    private val locator: PlatformLocator,\n    anchorStatus: StatusUiState?,\n    blog: Blog?,\n    private val blogId: String?,\n    private val blogTranslationUiState: BlogTranslationUiState?,\n    private val platform: BlogPlatform?,\n) : SubViewModel(), IInteractiveHandler by InteractiveHandler(\n    statusProvider = statusProvider,\n    statusUpdater = statusUpdater,\n    statusUiStateAdapter = statusUiStateAdapter,\n    refactorToNewStatus = refactorToNewStatus,\n) {\n\n    private val _uiState = MutableStateFlow(\n        StatusContextUiState(\n            contextStatus = emptyList(),\n            loading = false,\n            needScrollToAnchor = true,\n            errorMessage = null,\n            currentAccount = null,\n        )\n    )\n    val uiState = _uiState.asStateFlow()\n\n    private val anchorStatus: StatusUiState? = anchorStatus ?: blog?.let {\n        statusUiStateAdapter.toStatusUiStateSnapshot(\n            locator = locator,\n            status = Status.NewBlog(it),\n            blogTranslationState = blogTranslationUiState,\n        )\n    }\n\n    private var anchorAuthorFollowing: Boolean? = null\n\n    init {\n        if (this.anchorStatus != null) {\n            _uiState.update {\n                it.copy(\n                    contextStatus = listOf(\n                        StatusInContext(\n                            type = StatusInContextType.ANCHOR,\n                            status = this.anchorStatus\n                        )\n                    )\n                )\n            }\n        }\n        initInteractiveHandler(\n            coroutineScope = viewModelScope,\n            onInteractiveHandleResult = {\n                when (it) {\n                    is InteractiveHandleResult.UpdateStatus -> {\n                        val innerAnchorStatus = _uiState.value.contextStatus\n                            .firstOrNull { status -> status.type == StatusInContextType.ANCHOR }\n                        if (innerAnchorStatus?.status != it.status) {\n                            updateStatus(it.status)\n                        }\n                    }\n\n                    is InteractiveHandleResult.DeleteStatus -> {\n                        deleteStatus(it.statusId)\n                    }\n\n                    is InteractiveHandleResult.UpdateFollowState -> {\n                        anchorAuthorFollowing = it.following\n                        updateAnchorFollowingState()\n                    }\n                }\n            },\n        )\n        launchInViewModel {\n            statusProvider.accountManager\n                .getAllLoggedAccount()\n                .firstOrNull { it.locator == locator }\n                ?.let { account ->\n                    _uiState.update { state ->\n                        state.copy(currentAccount = account)\n                    }\n                }\n        }\n    }\n\n    fun onPageResume() {\n        launchInViewModel {\n            loadStatusContext()\n        }\n        val anchorAuthor = this.anchorStatus?.status?.intrinsicBlog?.author\n            ?: _uiState.value.anchorStatus?.status?.status?.intrinsicBlog?.author\n        if (anchorAuthor != null) {\n            loadAnchorFollowingState(anchorAuthor)\n        }\n    }\n\n    fun onRetryClick() {\n        onPageResume()\n    }\n\n    fun onScrolledToAnchor() {\n        _uiState.update { state ->\n            state.copy(needScrollToAnchor = false)\n        }\n    }\n\n    fun onAccountClick(account: LoggedAccount) {\n        statusProvider.screenProvider\n            .getUserDetailScreen(\n                locator = locator,\n                uri = account.uri,\n                userId = account.id,\n            )?.let { mutableOpenScreenFlow.emitInViewModel(it) }\n    }\n\n    private suspend fun loadStatusContext() {\n        _uiState.update { it.copy(loading = true) }\n        var anchorStatus = this.anchorStatus\n        if (anchorStatus == null && !blogId.isNullOrEmpty() && platform != null) {\n            anchorStatus = loadStatus(\n                blogId = blogId,\n                blogUri = null,\n                platform = platform,\n            )\n            if (anchorStatus != null) {\n                loadAnchorFollowingState(anchorStatus.status.intrinsicBlog.author)\n            }\n        }\n        if (anchorStatus == null) {\n            _uiState.update { it.copy(errorMessage = textOf(\"Blog not found.\")) }\n            return\n        }\n        statusProvider.statusResolver\n            .getStatusContext(locator, anchorStatus.status)\n            .map { statusContext ->\n                val status = statusContext.status?.let {\n                    statusUpdater.update(\n                        it.copy(\n                            blogTranslationState = blogTranslationUiState ?: it.blogTranslationState\n                        )\n                    )\n                    if (anchorAuthorFollowing != null) {\n                        it.updateFollowingState(anchorAuthorFollowing!!)\n                    } else {\n                        it\n                    }\n                } ?: loadStatus(anchorStatus.status.intrinsicBlog) ?: anchorStatus\n                if (anchorAuthorFollowing != null) {\n                    statusContext.copy(\n                        status = status.updateFollowingState(anchorAuthorFollowing!!)\n                    )\n                } else {\n                    statusContext.copy(status = status)\n                }\n            }\n            .onSuccess { statusContext ->\n                _uiState.update { state ->\n                    state.copy(\n                        contextStatus = buildContextStatus(statusContext),\n                        loading = false,\n                        errorMessage = null,\n                    )\n                }\n                statusUpdater.update(statusContext.status!!)\n            }.onFailure {\n                _uiState.update { state ->\n                    state.copy(\n                        loading = false,\n                        errorMessage = it.toTextStringOrNull(),\n                    )\n                }\n            }\n    }\n\n    private suspend fun loadStatus(\n        blog: Blog\n    ): StatusUiState? {\n        return loadStatus(\n            blogId = blog.id,\n            blogUri = blog.url,\n            platform = blog.platform,\n        )\n    }\n\n    private suspend fun loadStatus(\n        blogId: String?,\n        blogUri: String?,\n        platform: BlogPlatform,\n    ): StatusUiState? {\n        return statusProvider.statusResolver\n            .getStatus(\n                locator = locator,\n                blogId = blogId,\n                blogUri = blogUri,\n                platform = platform,\n            ).map {\n                it.copy(blogTranslationState = blogTranslationUiState ?: it.blogTranslationState)\n                if (anchorAuthorFollowing != null) {\n                    it.updateFollowingState(anchorAuthorFollowing!!)\n                } else {\n                    it\n                }\n            }\n            .onSuccess { statusUpdater.update(it) }\n            .getOrNull()\n    }\n\n    private fun buildContextStatus(\n        statusContext: StatusContext,\n    ): List<StatusInContext> {\n        val contextStatus = mutableListOf<StatusInContext>()\n        contextStatus += statusContext.ancestors.sortedBy { it.status.createAt.epochMillis }\n            .map { StatusInContext(it, StatusInContextType.ANCESTOR) }\n        statusContext.status?.let {\n            contextStatus += StatusInContext(it, StatusInContextType.ANCHOR)\n        }\n        for (descendant in statusContext.descendants) {\n            contextStatus.addAll(descendant.expandToContextStatus(null))\n        }\n        return contextStatus\n    }\n\n    private fun DescendantStatus.expandToContextStatus(\n        parentType: StatusInContextType?,\n    ): List<StatusInContext> {\n        val type = if (this.descendantStatus != null) {\n            if (parentType == null) {\n                StatusInContextType.DESCENDANT_ANCHOR\n            } else {\n                StatusInContextType.DESCENDANT_WITH_ANCESTOR_DESCENDANT\n            }\n        } else {\n            if (parentType == null) {\n                StatusInContextType.DESCENDANT\n            } else {\n                StatusInContextType.DESCENDANT_WITH_ANCESTOR\n            }\n        }\n        val list = mutableListOf<StatusInContext>()\n        val statusInContext = StatusInContext(status, type)\n        list.add(statusInContext)\n        if (this.descendantStatus != null) {\n            list.addAll(this.descendantStatus!!.expandToContextStatus(type))\n        }\n        return list\n    }\n\n    private suspend fun updateStatus(newStatus: StatusUiState) {\n        mixedStatusRepo.updateStatus(newStatus)\n        _uiState.update { state ->\n            val contextStatus = state.contextStatus.map { item ->\n                item.copy(\n                    status = if (item.status.status.intrinsicBlog.id == newStatus.status.intrinsicBlog.id) {\n                        newStatus\n                    } else {\n                        item.status\n                    }\n                )\n            }\n            state.copy(contextStatus = contextStatus)\n        }\n        updateAnchorFollowingState()\n    }\n\n    private fun loadAnchorFollowingState(author: BlogAuthor) {\n        launchInViewModel {\n            statusProvider.statusResolver\n                .isFollowing(\n                    locator = locator,\n                    target = author,\n                )?.onSuccess { following ->\n                    anchorAuthorFollowing = following\n                    updateAnchorFollowingState()\n                }\n        }\n    }\n\n    private fun updateAnchorFollowingState() {\n        _uiState.update { state ->\n            state.copy(\n                contextStatus = state.contextStatus.map { item ->\n                    if (item.type == StatusInContextType.ANCHOR && anchorAuthorFollowing != null) {\n                        item.copy(\n                            status = item.status.updateFollowingState(anchorAuthorFollowing!!)\n                        )\n                    } else {\n                        item\n                    }\n                }\n            )\n        }\n    }\n\n    private fun deleteStatus(statusId: String) {\n        _uiState.update { state ->\n            state.copy(\n                contextStatus = state.contextStatus.filter { it.status.status.id != statusId }\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "commonbiz/sharedscreen/src/commonMain/kotlin/com/zhangke/fread/commonbiz/shared/screen/status/context/StatusContextUiState.kt",
    "content": "package com.zhangke.fread.commonbiz.shared.screen.status.context\n\nimport com.zhangke.framework.composable.TextString\nimport com.zhangke.fread.status.account.LoggedAccount\nimport com.zhangke.fread.status.model.StatusUiState\n\ndata class StatusContextUiState(\n    val contextStatus: List<StatusInContext>,\n    val loading: Boolean,\n    val needScrollToAnchor: Boolean,\n    val currentAccount: LoggedAccount?,\n    val errorMessage: TextString?,\n) {\n\n    val anchorIndex: Int get() = contextStatus.indexOfFirst { it.type == StatusInContextType.ANCHOR }\n\n    val anchorStatus: StatusInContext? get() = contextStatus.getOrNull(anchorIndex)\n}\n\ndata class StatusInContext(\n    val status: StatusUiState,\n    val type: StatusInContextType,\n)\n\nenum class StatusInContextType {\n\n    ANCHOR,\n    ANCESTOR,\n    DESCENDANT,// no descendant\n    DESCENDANT_ANCHOR,\n    DESCENDANT_WITH_ANCESTOR_DESCENDANT,\n    DESCENDANT_WITH_ANCESTOR,\n}\n"
  },
  {
    "path": "commonbiz/sharedscreen/src/commonMain/kotlin/com/zhangke/fread/commonbiz/shared/screen/status/context/StatusContextViewModel.kt",
    "content": "package com.zhangke.fread.commonbiz.shared.screen.status.context\n\nimport com.zhangke.framework.lifecycle.ContainerViewModel\nimport com.zhangke.fread.common.adapter.StatusUiStateAdapter\nimport com.zhangke.fread.common.mixed.MixedStatusRepo\nimport com.zhangke.fread.common.status.StatusUpdater\nimport com.zhangke.fread.commonbiz.shared.usecase.RefactorToNewStatusUseCase\nimport com.zhangke.fread.status.StatusProvider\nimport com.zhangke.fread.status.blog.Blog\nimport com.zhangke.fread.status.model.BlogTranslationUiState\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.model.StatusUiState\nimport com.zhangke.fread.status.platform.BlogPlatform\n\nclass StatusContextViewModel (\n    private val mixedStatusRepo: MixedStatusRepo,\n    private val statusProvider: StatusProvider,\n    private val statusUpdater: StatusUpdater,\n    private val statusUiStateAdapter: StatusUiStateAdapter,\n    private val refactorToNewStatus: RefactorToNewStatusUseCase,\n) : ContainerViewModel<StatusContextSubViewModel, StatusContextViewModel.Params>() {\n\n    override fun createSubViewModel(params: Params): StatusContextSubViewModel {\n        return StatusContextSubViewModel(\n            mixedStatusRepo = mixedStatusRepo,\n            statusProvider = statusProvider,\n            statusUpdater = statusUpdater,\n            statusUiStateAdapter = statusUiStateAdapter,\n            refactorToNewStatus = refactorToNewStatus,\n            locator = params.locator,\n            anchorStatus = params.anchorStatus,\n            blog = params.blog,\n            blogId = params.blogId,\n            blogTranslationUiState = params.blogTranslationUiState,\n            platform = params.platform,\n        )\n    }\n\n    fun getSubViewModel(\n        locator: PlatformLocator,\n        anchorStatus: StatusUiState?,\n        blog: Blog?,\n        blogId: String?,\n        blogTranslationUiState: BlogTranslationUiState?,\n        platform: BlogPlatform?,\n    ): StatusContextSubViewModel {\n        return obtainSubViewModel(\n            Params(\n                locator = locator,\n                anchorStatus = anchorStatus,\n                blog = blog,\n                blogId = blogId,\n                blogTranslationUiState = blogTranslationUiState,\n                platform = platform,\n            )\n        )\n    }\n\n    class Params(\n        val locator: PlatformLocator,\n        val anchorStatus: StatusUiState?,\n        val blog: Blog?,\n        val blogId: String?,\n        val blogTranslationUiState: BlogTranslationUiState?,\n        val platform: BlogPlatform?,\n    ) : SubViewModelParams() {\n\n        override val key: String = anchorStatus?.status?.id + blog?.id + locator + blogId + platform\n    }\n}\n"
  },
  {
    "path": "commonbiz/sharedscreen/src/commonMain/kotlin/com/zhangke/fread/commonbiz/shared/screen/video/FullVideoScreen.kt",
    "content": "package com.zhangke.fread.commonbiz.shared.screen.video\n\n\nimport androidx.compose.runtime.Composable\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.composable.currentOrThrow\nimport com.zhangke.framework.nav.LocalNavBackStack\nimport com.zhangke.framework.utils.toPlatformUri\nimport com.zhangke.fread.status.ui.video.full.FullScreenVideoPlayer\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class FullVideoScreenNavKey(val uri: String): NavKey\n\n@Composable\nfun FullVideoScreen(uri: String) {\n    val backStack = LocalNavBackStack.currentOrThrow\n    FullScreenVideoPlayer(\n        uri = uri.toPlatformUri(),\n        onBackClick = backStack::removeLastOrNull,\n    )\n}\n"
  },
  {
    "path": "commonbiz/sharedscreen/src/commonMain/kotlin/com/zhangke/fread/commonbiz/shared/usecase/PublishPostOnMultiAccountUseCase.kt",
    "content": "package com.zhangke.fread.commonbiz.shared.usecase\n\nimport com.zhangke.framework.utils.exceptionOrThrow\nimport com.zhangke.fread.status.StatusProvider\nimport com.zhangke.fread.status.account.LoggedAccount\nimport com.zhangke.fread.status.publish.PublishingPost\nimport kotlinx.coroutines.async\nimport kotlinx.coroutines.awaitAll\nimport kotlinx.coroutines.supervisorScope\n\nclass PublishPostOnMultiAccountUseCase (\n    private val statusProvider: StatusProvider,\n) {\n\n    suspend operator fun invoke(\n        accounts: List<LoggedAccount>,\n        publishingPost: PublishingPost,\n    ): Result<Unit> {\n        val publishManager = statusProvider.publishManager\n        val results = supervisorScope {\n            accounts.map {\n                async { it to publishManager.publish(it, publishingPost) }\n            }.awaitAll()\n        }\n        if (results.any { it.second.isFailure }) {\n            val e = results.first { it.second.isFailure }.second.exceptionOrThrow()\n            val successAccount = results.filter { it.second.isSuccess }.map { it.first }\n            val failedAccounts = results.filter { it.second.isFailure }.map { it.first }\n            return Result.failure(\n                PublishingPartFailed(\n                    successAccount = successAccount.map { it.uri.toString() },\n                    failedAccounts = failedAccounts.map { it.uri.toString() },\n                    e = e,\n                )\n            )\n        } else {\n            return Result.success(Unit)\n        }\n    }\n} class PublishingPartFailed(\n    val successAccount: List<String>,\n    val failedAccounts: List<String>,\n    val e: Throwable,\n) : RuntimeException()"
  },
  {
    "path": "commonbiz/sharedscreen/src/commonMain/kotlin/com/zhangke/fread/commonbiz/shared/usecase/RefactorToNewBlogUseCase.kt",
    "content": "package com.zhangke.fread.commonbiz.shared.usecase\n\nimport com.zhangke.fread.status.status.model.Status\n\nclass RefactorToNewBlogUseCase () {\n\n    operator fun invoke(status: Status): Status.NewBlog {\n        return when (status) {\n            is Status.NewBlog -> status\n            is Status.Reblog -> Status.NewBlog(\n                blog = status.reblog,\n            )\n        }\n    }\n}"
  },
  {
    "path": "commonbiz/sharedscreen/src/commonMain/kotlin/com/zhangke/fread/commonbiz/shared/usecase/RefactorToNewStatusUseCase.kt",
    "content": "package com.zhangke.fread.commonbiz.shared.usecase\n\nimport com.zhangke.fread.status.model.StatusUiState\n\nclass RefactorToNewStatusUseCase (\n    private val refactorToNewBlog: RefactorToNewBlogUseCase,\n) {\n\n    operator fun invoke(status: StatusUiState): StatusUiState {\n        return status.copy(\n            status = refactorToNewBlog(status.status),\n        )\n    }\n}"
  },
  {
    "path": "commonbiz/sharedscreen/src/commonMain/kotlin/com/zhangke/fread/commonbiz/shared/utils/LoadableStatusController.kt",
    "content": "package com.zhangke.fread.commonbiz.shared.utils\n\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.composable.TextString\nimport com.zhangke.framework.controller.CommonLoadableController\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.model.StatusUiState\nimport com.zhangke.fread.status.richtext.preParse\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.SharedFlow\nimport kotlinx.coroutines.launch\n\nopen class LoadableStatusController(\n    protected val coroutineScope: CoroutineScope,\n) {\n\n    private val loadableController = CommonLoadableController<StatusUiState>(\n        coroutineScope,\n        onPostSnackMessage = {\n            coroutineScope.launch {\n                mutableErrorMessageFlow.emit(it)\n            }\n        },\n    )\n\n    val mutableUiState = loadableController.mutableUiState\n    val uiState = loadableController.uiState\n\n    private val mutableErrorMessageFlow = MutableSharedFlow<TextString>()\n    val errorMessageFlow: SharedFlow<TextString> = mutableErrorMessageFlow\n\n    private val _openScreenFlow = MutableSharedFlow<NavKey>()\n    val openScreenFlow: SharedFlow<NavKey> get() = _openScreenFlow\n\n    open fun onRefresh(\n        locator: PlatformLocator,\n        refreshFunction: suspend () -> Result<List<StatusUiState>>,\n    ) {\n        loadableController.onRefresh {\n            refreshFunction().map { list ->\n                list.preParse()\n                list\n            }\n        }\n    }\n\n    open fun onLoadMore(\n        locator: PlatformLocator,\n        loadMoreFunction: suspend (maxId: String) -> Result<List<StatusUiState>>,\n    ) {\n        val latestId = loadableController.uiState.value.dataList.lastOrNull()?.status?.id ?: return\n        loadableController.onLoadMore {\n            loadMoreFunction(latestId).map { list ->\n                list.preParse()\n                list\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "commonbiz/sharedscreen/src/iosMain/kotlin/com/zhangke/fread/commonbiz/shared/SharedScreenIosModule.kt",
    "content": "package com.zhangke.fread.commonbiz.shared\n\nimport androidx.room.Room\nimport androidx.sqlite.driver.bundled.BundledSQLiteDriver\nimport com.zhangke.fread.common.documentDirectory\nimport com.zhangke.fread.commonbiz.shared.db.SelectedAccountPublishingDatabase\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.IO\nimport org.koin.core.module.Module\n\nactual fun Module.createPlatformModule() {\n    single<SelectedAccountPublishingDatabase> {\n        val dbFilePath = getDBFilePath(SelectedAccountPublishingDatabase.DB_NAME)\n        Room.databaseBuilder<SelectedAccountPublishingDatabase>(\n            name = dbFilePath,\n        ).setDriver(BundledSQLiteDriver())\n            .setQueryCoroutineContext(Dispatchers.IO)\n            .build()\n    }\n}\n\nprivate fun getDBFilePath(dbName: String): String {\n    return documentDirectory() + \"/$dbName\"\n}\n"
  },
  {
    "path": "commonbiz/sharedscreen/src/iosMain/kotlin/com/zhangke/fread/commonbiz/shared/composable/WebViewPreviewer.ios.kt",
    "content": "package com.zhangke.fread.commonbiz.shared.composable\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\n\n@Composable\nactual fun WebViewPreviewer(html: String, modifier: Modifier) {\n    // TODO: Not implemented yet\n}"
  },
  {
    "path": "commonbiz/sharedscreen/src/iosMain/kotlin/com/zhangke/fread/commonbiz/shared/screen/ImageViewerScreen.ios.kt",
    "content": "package com.zhangke.fread.commonbiz.shared.screen\n\nimport com.seiko.imageloader.model.ImageResult\n\ninternal actual fun ImageResult.aspectRatio(): Float? {\n    return when (this) {\n        is ImageResult.OfBitmap -> bitmap.width.toFloat() / bitmap.height\n        is ImageResult.OfImage -> image.width.toFloat() / image.height\n        else -> null\n    }\n}"
  },
  {
    "path": "commonbiz/status-ui/.gitignore",
    "content": "/build"
  },
  {
    "path": "commonbiz/status-ui/build.gradle.kts",
    "content": "plugins {\n    id(\"fread.project.framework.kmp\")\n    id(\"com.google.devtools.ksp\")\n}\n\nandroid {\n    namespace = \"com.zhangke.fread.statusui\"\n    sourceSets {\n        getByName(\"main\") {\n            res.srcDirs(\"src/commonMain/res\")\n            resources.srcDirs(\"src/commonMain/resources\")\n        }\n    }\n}\n\nkotlin {\n    sourceSets {\n        commonMain {\n            dependencies {\n                implementation(project(path = \":framework\"))\n                implementation(project(path = \":commonbiz:common\"))\n                implementation(project(\":commonbiz:analytics\"))\n                implementation(project(path = \":bizframework:status-provider\"))\n\n                implementation(compose.components.resources)\n\n                implementation(libs.arrow.core)\n\n                implementation(libs.androidx.annotation)\n                implementation(libs.imageLoader)\n                implementation(libs.ktml)\n\n                implementation(libs.krouter.runtime)\n                implementation(libs.androidx.constraintlayout.compose.kmp)\n\n                implementation(libs.haze)\n                implementation(libs.haze.materials)\n\n                implementation(project(\":thirds:halilibo-richtext-ui\"))\n                implementation(project(\":thirds:halilibo-richtext-material3\"))\n            }\n        }\n        commonTest {\n            dependencies {\n                implementation(kotlin(\"test\"))\n            }\n        }\n        androidMain {\n            dependencies {\n                implementation(compose.preview)\n                implementation(libs.androidx.core.ktx)\n                implementation(libs.androidx.appcompat)\n                implementation(libs.bundles.androidx.media3)\n\n                implementation(libs.okhttp3)\n                implementation(libs.okhttp3.logging)\n\n                implementation(libs.auto.service.annotations)\n            }\n        }\n    }\n}\n\ndependencies {\n    add(\"kspAndroid\", libs.androidx.room.compiler)\n    add(\"kspAndroid\", libs.auto.service.ksp)\n}\n\ncompose {\n    resources {\n        publicResClass = true\n        packageOfResClass = \"com.zhangke.fread.statusui\"\n        generateResClass = always\n    }\n}\n"
  },
  {
    "path": "commonbiz/status-ui/consumer-rules.pro",
    "content": ""
  },
  {
    "path": "commonbiz/status-ui/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguardFiles setting in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n\n# Uncomment this to preserve the line number information for\n# debugging stack traces.\n#-keepattributes SourceFile,LineNumberTable\n\n# If you keep the line number information, uncomment this to\n# hide the original source file name.\n#-renamesourcefileattribute SourceFile"
  },
  {
    "path": "commonbiz/status-ui/src/androidMain/kotlin/com/zhangke/fread/status/ui/StatusPlaceHolder.preview.kt",
    "content": "package com.zhangke.fread.status.ui\n\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.tooling.preview.Preview\nimport com.zhangke.framework.architect.theme.FreadTheme\n\n@Preview\n@Composable\nfun StatusPlaceHolderPreview() {\n    FreadTheme {\n        StatusPlaceHolder(\n            modifier = Modifier.fillMaxWidth(),\n        )\n    }\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/androidMain/kotlin/com/zhangke/fread/status/ui/common/FormattingTimeText.kt",
    "content": "package com.zhangke.fread.status.ui.common\n\nimport androidx.compose.material3.LocalTextStyle\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.produceState\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.text.TextLayoutResult\nimport androidx.compose.ui.text.TextStyle\nimport androidx.compose.ui.text.font.FontFamily\nimport androidx.compose.ui.text.font.FontStyle\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.text.style.TextDecoration\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.TextUnit\nimport com.zhangke.fread.status.utils.DateTimeFormatter\nimport java.util.Date\n\n@Composable\nfun FormattingTimeText(\n    date: Date,\n    modifier: Modifier = Modifier,\n    color: Color = Color.Unspecified,\n    fontSize: TextUnit = TextUnit.Unspecified,\n    fontStyle: FontStyle? = null,\n    fontWeight: FontWeight? = null,\n    fontFamily: FontFamily? = null,\n    letterSpacing: TextUnit = TextUnit.Unspecified,\n    textDecoration: TextDecoration? = null,\n    textAlign: TextAlign? = null,\n    lineHeight: TextUnit = TextUnit.Unspecified,\n    overflow: TextOverflow = TextOverflow.Clip,\n    softWrap: Boolean = true,\n    maxLines: Int = Int.MAX_VALUE,\n    minLines: Int = 1,\n    onTextLayout: ((TextLayoutResult) -> Unit)? = null,\n    style: TextStyle = LocalTextStyle.current\n) {\n    val timeText by produceState(\"\") {\n        value = DateTimeFormatter.format(date.time)\n    }\n    Text(\n        text = timeText,\n        modifier = modifier,\n        color = color,\n        fontSize = fontSize,\n        fontStyle = fontStyle,\n        fontWeight = fontWeight,\n        fontFamily = fontFamily,\n        letterSpacing = letterSpacing,\n        textDecoration = textDecoration,\n        textAlign = textAlign,\n        lineHeight = lineHeight,\n        overflow = overflow,\n        softWrap = softWrap,\n        maxLines = maxLines,\n        minLines = minLines,\n        onTextLayout = onTextLayout,\n        style = style,\n    )\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/androidMain/kotlin/com/zhangke/fread/status/ui/poll/BlogPollOption.preview.kt",
    "content": "package com.zhangke.fread.status.ui.poll\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\n\n\n@Preview(backgroundColor = 0xffffffff)\n@Composable\nprivate fun PreviewMoreLineBlogPollOption() {\n    Column(\n        modifier = Modifier\n            .fillMaxSize()\n            .background(color = Color.White)\n            .padding(horizontal = 15.dp),\n        horizontalAlignment = Alignment.CenterHorizontally,\n        verticalArrangement = Arrangement.Center,\n    ) {\n        BlogPollOption(\n            modifier = Modifier\n                .fillMaxWidth(),\n            optionContent = \"12344\",\n            selected = true,\n            votable = true,\n            showProgress = true,\n            progress = 0F,\n            onClick = {},\n        )\n        BlogPollOption(\n            modifier = Modifier\n                .fillMaxWidth()\n                .padding(top = 10.dp),\n            optionContent = \"12344\",\n            selected = true,\n            votable = true,\n            showProgress = true,\n            progress = 0F,\n            onClick = {},\n        )\n        BlogPollOption(\n            modifier = Modifier\n                .fillMaxWidth()\n                .padding(top = 10.dp),\n            optionContent = \"12344\",\n            selected = true,\n            votable = true,\n            progress = 0F,\n            showProgress = true,\n            onClick = {},\n        )\n        BlogPollOption(\n            modifier = Modifier\n                .fillMaxWidth()\n                .padding(top = 10.dp),\n            optionContent = \"123441234412344123441234412344123441234412344123441234412344123441234412344123441234412344123441234412344123441234412344123441234412344\",\n            progress = 0.5F,\n            selected = true,\n            votable = true,\n            showProgress = true,\n            onClick = {},\n        )\n    }\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/androidMain/kotlin/com/zhangke/fread/status/ui/utils/ScreenSize.android.kt",
    "content": "package com.zhangke.fread.status.ui.utils\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.ReadOnlyComposable\nimport androidx.compose.ui.platform.LocalConfiguration\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\n\n@ReadOnlyComposable\n@Composable\nactual fun getScreenWidth(): Dp {\n    val configuration = LocalConfiguration.current\n    return configuration.screenWidthDp.dp\n}\n\n@ReadOnlyComposable\n@Composable\nactual fun getScreenHeight(): Dp {\n    val configuration = LocalConfiguration.current\n    return configuration.screenHeightDp.dp\n}"
  },
  {
    "path": "commonbiz/status-ui/src/androidMain/kotlin/com/zhangke/fread/status/ui/video/BlogVideos.kt",
    "content": "package com.zhangke.fread.status.ui.video\n\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.unit.dp\nimport com.zhangke.framework.blurhash.blurhash\nimport com.zhangke.framework.utils.toPlatformUri\nimport com.zhangke.fread.status.blog.BlogMedia\nimport com.zhangke.fread.status.ui.image.BlogMediaClickEvent\nimport com.zhangke.fread.status.ui.image.OnBlogMediaClick\nimport com.zhangke.fread.status.ui.image.decideAspect\nimport com.zhangke.fread.status.ui.video.inline.InlineVideo\n\n@Composable\nactual fun BlogVideos(\n    mediaList: List<BlogMedia>,\n    hideContent: Boolean,\n    indexInList: Int,\n    onMediaClick: OnBlogMediaClick,\n) {\n    val videoMedia = mediaList.first()\n    SingleBlogInlineVideo(\n        videoMedia = videoMedia,\n        hideContent = hideContent,\n        indexInList = indexInList,\n        onMediaClick = onMediaClick,\n    )\n}\n\n@Composable\nprivate fun SingleBlogInlineVideo(\n    videoMedia: BlogMedia,\n    hideContent: Boolean,\n    indexInList: Int,\n    onMediaClick: OnBlogMediaClick,\n) {\n    val aspect = videoMedia.meta.decideAspect(1.78F)\n    val modifier = Modifier\n        .clip(RoundedCornerShape(8.dp))\n        .fillMaxWidth()\n        .blurhash(videoMedia.blurhash)\n    Box(\n        modifier = modifier\n    ) {\n        if (!hideContent) {\n            InlineVideo(\n                aspectRatio = aspect,\n                coverImage = videoMedia.previewUrl,\n                indexInList = indexInList,\n                uri = remember(videoMedia.url) {\n                    videoMedia.url.toPlatformUri()\n                },\n                onClick = {\n                    onMediaClick(\n                        BlogMediaClickEvent.BlogVideoClickEvent(\n                            index = indexInList,\n                            media = videoMedia,\n                        )\n                    )\n                },\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/androidMain/kotlin/com/zhangke/fread/status/ui/video/LocalInlineVideoPlayer.kt",
    "content": "package com.zhangke.fread.status.ui.video\n\nimport androidx.compose.runtime.ProvidableCompositionLocal\nimport androidx.compose.runtime.staticCompositionLocalOf\nimport androidx.media3.exoplayer.ExoPlayer\n\nval LocalInlineVideoPlayer: ProvidableCompositionLocal<ExoPlayer?> =\n    staticCompositionLocalOf { null }\n\nfun provideExoPlayer(){\n\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/androidMain/kotlin/com/zhangke/fread/status/ui/video/inline/InlineVideo.kt",
    "content": "package com.zhangke.fread.status.ui.video.inline\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.aspectRatio\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.PlayCircleOutline\nimport androidx.compose.material.icons.filled.VolumeOff\nimport androidx.compose.material.icons.filled.VolumeUp\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport androidx.media3.common.util.UnstableApi\nimport com.seiko.imageloader.ui.AutoSizeImage\nimport com.zhangke.framework.composable.SimpleIconButton\nimport com.zhangke.framework.composable.inline.LocalPlayableIndexRecorder\nimport com.zhangke.framework.composable.noRippleClick\nimport com.zhangke.framework.composable.video.VideoPlayer\nimport com.zhangke.framework.composable.video.rememberVideoPlayerController\nimport com.zhangke.framework.utils.PlatformUri\nimport com.zhangke.fread.common.config.LocalFreadConfigManager\n\n@androidx.annotation.OptIn(UnstableApi::class)\n@Composable\nfun InlineVideo(\n    aspectRatio: Float?,\n    coverImage: String?,\n    indexInList: Int,\n    style: InlineVideoPlayerStyle = InlineVideoPlayerDefault.defaultStyle,\n    uri: PlatformUri,\n    onClick: () -> Unit,\n) {\n    val playableIndexRecorder = LocalPlayableIndexRecorder.current\n    playableIndexRecorder?.recordePlayableIndex(indexInList)\n    val playWhenReady =\n        playableIndexRecorder != null && playableIndexRecorder.currentActiveIndex == indexInList\n    InlineVideoShell(\n        aspectRatio = aspectRatio ?: style.defaultMediaAspect,\n        style = style,\n    ) {\n        val freadConfigManager = LocalFreadConfigManager.current\n        InlineVideoPlayer(\n            uri = uri,\n            coverImage = coverImage,\n            autoPlay = freadConfigManager.autoPlayInlineVideo,\n            playWhenReady = playWhenReady,\n            onClick = onClick,\n            onPlayManually = { playableIndexRecorder?.changeActiveIndex(indexInList) },\n        )\n    }\n}\n\n@Composable\nprivate fun InlineVideoShell(\n    aspectRatio: Float,\n    style: InlineVideoPlayerStyle,\n    content: @Composable () -> Unit,\n) {\n    val fixedAspectRatio = aspectRatio\n        .coerceAtLeast(style.minAspect)\n        .coerceAtMost(style.maxAspect)\n    Box(\n        modifier = Modifier\n            .fillMaxWidth()\n            .aspectRatio(fixedAspectRatio),\n        content = {\n            content()\n        },\n    )\n}\n\n@androidx.annotation.OptIn(UnstableApi::class)\n@Composable\nprivate fun InlineVideoPlayer(\n    uri: PlatformUri,\n    coverImage: String?,\n    autoPlay: Boolean,\n    playWhenReady: Boolean,\n    onClick: () -> Unit,\n    onPlayManually: () -> Unit,\n) {\n    Box(\n        modifier = Modifier\n            .fillMaxSize()\n            .background(Color.Black)\n            .noRippleClick(onClick = onClick)\n    ) {\n        if (autoPlay && playWhenReady) {\n            val videoController = rememberVideoPlayerController(\n                mediaUrl = uri.toString(),\n                initialContentScale = ContentScale.Fit,\n                autoPlay = true,\n                initialMuted = true,\n                isLooping = false,\n            )\n            VideoPlayer(\n                modifier = Modifier.fillMaxSize().noRippleClick(onClick = onClick),\n                controller = videoController,\n            )\n            InlineVideoControlPanel(\n                playEnded = videoController.hasPlaybackEnded,\n                mute = videoController.isMuted,\n                onPlayClick = {\n                    onPlayManually()\n                    videoController.play()\n                },\n                onMuteClick = { mute ->\n                    if (mute) {\n                        videoController.mute()\n                    } else {\n                        videoController.unmute()\n                    }\n                },\n            )\n        } else {\n            Box(modifier = Modifier.fillMaxSize()) {\n                AutoSizeImage(\n                    url = coverImage.orEmpty(),\n                    modifier = Modifier\n                        .fillMaxSize(),\n                    contentScale = ContentScale.Crop,\n                    contentDescription = null,\n                )\n                PlayVideoIconButton(\n                    modifier = Modifier\n                        .size(36.dp)\n                        .align(Alignment.Center),\n                    onClick = {\n                        if (autoPlay) {\n                            onPlayManually()\n                        } else {\n                            onClick()\n                        }\n                    },\n                )\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun InlineVideoControlPanel(\n    playEnded: Boolean,\n    mute: Boolean,\n    onPlayClick: () -> Unit,\n    onMuteClick: (mute: Boolean) -> Unit,\n) {\n    Box(modifier = Modifier.fillMaxSize()) {\n        if (playEnded) {\n            PlayVideoIconButton(\n                modifier = Modifier\n                    .size(42.dp)\n                    .align(Alignment.Center),\n                onClick = onPlayClick,\n            )\n        }\n        IconButton(\n            modifier = Modifier\n                .align(Alignment.BottomEnd)\n                .padding(end = 10.dp, bottom = 6.dp),\n            onClick = { onMuteClick(!mute) },\n        ) {\n            val icon = if (mute) {\n                Icons.Default.VolumeOff\n            } else {\n                Icons.Default.VolumeUp\n            }\n            Icon(\n                imageVector = icon,\n                contentDescription = if (mute) \"unmute\" else \"mute\",\n                tint = Color.White,\n            )\n        }\n    }\n}\n\n@Composable\ninternal fun PlayVideoIconButton(\n    modifier: Modifier,\n    onClick: () -> Unit,\n) {\n    SimpleIconButton(\n        modifier = modifier,\n        onClick = onClick,\n        imageVector = Icons.Default.PlayCircleOutline,\n        tint = Color.White,\n        contentDescription = \"Play\",\n    )\n}\n\ndata class InlineVideoPlayerStyle(\n    val radius: Dp,\n    val defaultMediaAspect: Float,\n    val minAspect: Float,\n    val maxAspect: Float,\n)\n\nobject InlineVideoPlayerDefault {\n\n    val defaultStyle = InlineVideoPlayerStyle(\n        radius = 8.dp,\n        defaultMediaAspect = 1.0F,\n        maxAspect = 2.5F,\n        minAspect = 0.7F,\n    )\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/composeResources/drawable/ic_drag_indicator.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n  <path\n      android:pathData=\"M11,18C11,19.1 10.1,20 9,20C7.9,20 7,19.1 7,18C7,16.9 7.9,16 9,16C10.1,16 11,16.9 11,18ZM9,10C7.9,10 7,10.9 7,12C7,13.1 7.9,14 9,14C10.1,14 11,13.1 11,12C11,10.9 10.1,10 9,10ZM9,4C7.9,4 7,4.9 7,6C7,7.1 7.9,8 9,8C10.1,8 11,7.1 11,6C11,4.9 10.1,4 9,4ZM15,8C16.1,8 17,7.1 17,6C17,4.9 16.1,4 15,4C13.9,4 13,4.9 13,6C13,7.1 13.9,8 15,8ZM15,10C13.9,10 13,10.9 13,12C13,13.1 13.9,14 15,14C16.1,14 17,13.1 17,12C17,10.9 16.1,10 15,10ZM15,16C13.9,16 13,16.9 13,18C13,19.1 13.9,20 15,20C16.1,20 17,19.1 17,18C17,16.9 16.1,16 15,16Z\"\n      android:fillColor=\"#000000\"/>\n</vector>\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/composeResources/drawable/ic_format_quote.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960\"\n    android:viewportHeight=\"960\">\n  <path\n      android:fillColor=\"#000000\"\n      android:pathData=\"M228,720L320,560Q320,560 320,560Q320,560 320,560Q254,560 207,513Q160,466 160,400Q160,334 207,287Q254,240 320,240Q386,240 433,287Q480,334 480,400Q480,423 474.5,442.5Q469,462 458,480L320,720L228,720ZM588,720L680,560Q680,560 680,560Q680,560 680,560Q614,560 567,513Q520,466 520,400Q520,334 567,287Q614,240 680,240Q746,240 793,287Q840,334 840,400Q840,423 834.5,442.5Q829,462 818,480L680,720L588,720ZM320,460Q345,460 362.5,442.5Q380,425 380,400Q380,375 362.5,357.5Q345,340 320,340Q295,340 277.5,357.5Q260,375 260,400Q260,425 277.5,442.5Q295,460 320,460ZM680,460Q705,460 722.5,442.5Q740,425 740,400Q740,375 722.5,357.5Q705,340 680,340Q655,340 637.5,357.5Q620,375 620,400Q620,425 637.5,442.5Q655,460 680,460ZM680,400Q680,400 680,400Q680,400 680,400Q680,400 680,400Q680,400 680,400Q680,400 680,400Q680,400 680,400Q680,400 680,400Q680,400 680,400ZM320,400Q320,400 320,400Q320,400 320,400Q320,400 320,400Q320,400 320,400Q320,400 320,400Q320,400 320,400Q320,400 320,400Q320,400 320,400Z\"/>\n</vector>\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/composeResources/drawable/ic_format_quote_in_left.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960\"\n    android:viewportHeight=\"960\">\n  <group\n      android:pivotX=\"480\"\n      android:pivotY=\"480\"\n      android:rotation=\"180\">\n    <path\n        android:fillColor=\"#000000\"\n        android:pathData=\"M228,720L320,560Q320,560 320,560Q320,560 320,560Q254,560 207,513Q160,466 160,400Q160,334 207,287Q254,240 320,240Q386,240 433,287Q480,334 480,400Q480,423 474.5,442.5Q469,462 458,480L320,720L228,720ZM588,720L680,560Q680,560 680,560Q680,560 680,560Q614,560 567,513Q520,466 520,400Q520,334 567,287Q614,240 680,240Q746,240 793,287Q840,334 840,400Q840,423 834.5,442.5Q829,462 818,480L680,720L588,720ZM320,460Q345,460 362.5,442.5Q380,425 380,400Q380,375 362.5,357.5Q345,340 320,340Q295,340 277.5,357.5Q260,375 260,400Q260,425 277.5,442.5Q295,460 320,460ZM680,460Q705,460 722.5,442.5Q740,425 740,400Q740,375 722.5,357.5Q705,340 680,340Q655,340 637.5,357.5Q620,375 620,400Q620,425 637.5,442.5Q655,460 680,460ZM680,400Q680,400 680,400Q680,400 680,400Q680,400 680,400Q680,400 680,400Q680,400 680,400Q680,400 680,400Q680,400 680,400Q680,400 680,400ZM320,400Q320,400 320,400Q320,400 320,400Q320,400 320,400Q320,400 320,400Q320,400 320,400Q320,400 320,400Q320,400 320,400Q320,400 320,400Z\"/>\n  </group>\n</vector>\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/composeResources/drawable/ic_mode_edit.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n  <path\n      android:pathData=\"M19.06,3.59L20.41,4.94C21.2,5.72 21.2,6.99 20.41,7.77L7.18,21H3V16.82L13.4,6.41L16.23,3.59C17.01,2.81 18.28,2.81 19.06,3.59ZM5,19L6.41,19.06L16.23,9.23L14.82,7.82L5,17.64V19Z\"\n      android:fillColor=\"#000000\"\n      android:fillType=\"evenOdd\"/>\n</vector>\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/composeResources/drawable/ic_more.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"23dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"23\">\n  <path\n      android:pathData=\"M12,8.423C12.825,8.423 13.5,7.748 13.5,6.923C13.5,6.098 12.825,5.423 12,5.423C11.175,5.423 10.5,6.098 10.5,6.923C10.5,7.748 11.175,8.423 12,8.423ZM12,9.923C11.175,9.923 10.5,10.598 10.5,11.423C10.5,12.248 11.175,12.923 12,12.923C12.825,12.923 13.5,12.248 13.5,11.423C13.5,10.598 12.825,9.923 12,9.923ZM12,14.423C11.175,14.423 10.5,15.098 10.5,15.923C10.5,16.748 11.175,17.423 12,17.423C12.825,17.423 13.5,16.748 13.5,15.923C13.5,15.098 12.825,14.423 12,14.423Z\"\n      android:fillColor=\"#868686\"/>\n</vector>\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/composeResources/drawable/ic_post_status_spoiler.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n  <group>\n    <clip-path\n        android:pathData=\"M0,0h24v24h-24z\"/>\n    <path\n        android:pathData=\"M2.738,20.225C1.966,21.559 2.928,23.227 4.469,23.227H21.531C23.072,23.227 24.034,21.559 23.262,20.225L14.731,5.49C13.96,4.159 12.04,4.159 11.269,5.49L2.738,20.225ZM14.091,18.864C14.091,19.466 13.602,19.955 13,19.955C12.398,19.955 11.909,19.466 11.909,18.864C11.909,18.261 12.398,17.773 13,17.773C13.602,17.773 14.091,18.261 14.091,18.864ZM14.091,14.5C14.091,15.102 13.602,15.591 13,15.591C12.398,15.591 11.909,15.102 11.909,14.5V12.318C11.909,11.716 12.398,11.227 13,11.227C13.602,11.227 14.091,11.716 14.091,12.318V14.5Z\"\n        android:fillColor=\"#000000\"/>\n  </group>\n</vector>\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/composeResources/drawable/ic_share.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n  <path\n      android:pathData=\"M18,16.12C17.24,16.12 16.56,16.42 16.04,16.89L8.91,12.74C8.96,12.51 9,12.28 9,12.04C9,11.8 8.96,11.57 8.91,11.34L15.96,7.23C16.5,7.73 17.21,8.04 18,8.04C19.66,8.04 21,6.7 21,5.04C21,3.38 19.66,2.04 18,2.04C16.34,2.04 15,3.38 15,5.04C15,5.28 15.04,5.51 15.09,5.74L8.04,9.85C7.5,9.35 6.79,9.04 6,9.04C4.34,9.04 3,10.38 3,12.04C3,13.7 4.34,15.04 6,15.04C6.79,15.04 7.5,14.73 8.04,14.23L15.16,18.39C15.11,18.6 15.08,18.82 15.08,19.04C15.08,20.65 16.39,21.96 18,21.96C19.61,21.96 20.92,20.65 20.92,19.04C20.92,17.43 19.61,16.12 18,16.12ZM18,4.04C18.55,4.04 19,4.49 19,5.04C19,5.59 18.55,6.04 18,6.04C17.45,6.04 17,5.59 17,5.04C17,4.49 17.45,4.04 18,4.04ZM6,13.04C5.45,13.04 5,12.59 5,12.04C5,11.49 5.45,11.04 6,11.04C6.55,11.04 7,11.49 7,12.04C7,12.59 6.55,13.04 6,13.04ZM18,20.06C17.45,20.06 17,19.61 17,19.06C17,18.51 17.45,18.06 18,18.06C18.55,18.06 19,18.51 19,19.06C19,19.61 18.55,20.06 18,20.06Z\"\n      android:fillColor=\"#000000\"/>\n</vector>\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/composeResources/drawable/ic_status_comment.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n  <path\n      android:pathData=\"M12.5,3.5C16.91,3.5 20.5,7.09 20.5,11.5C20.5,15.91 16.91,19.5 12.5,19.5C11.32,19.5 10.16,19.24 9.07,18.72C8.8,18.59 8.51,18.53 8.21,18.53C8.02,18.53 7.83,18.56 7.65,18.61L4.45,19.55L5.39,16.35C5.53,15.88 5.49,15.37 5.28,14.93C4.76,13.84 4.5,12.68 4.5,11.5C4.5,7.09 8.09,3.5 12.5,3.5ZM12.5,1.5C6.98,1.5 2.5,5.98 2.5,11.5C2.5,13.04 2.86,14.48 3.47,15.79L1.5,22.5L8.21,20.53C9.52,21.14 10.96,21.5 12.5,21.5C18.02,21.5 22.5,17.02 22.5,11.5C22.5,5.98 18.02,1.5 12.5,1.5Z\"\n      android:fillColor=\"#000000\"/>\n</vector>\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/composeResources/drawable/ic_status_forward.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n  <path\n      android:pathData=\"M18.628,8.355C15.318,8.359 12.63,11.052 12.634,14.362C12.634,14.577 12.646,14.79 12.668,15L10.66,15C10.66,15 10.645,14.785 10.641,14.676C10.636,14.573 10.634,14.469 10.634,14.364C10.629,9.944 14.205,6.36 18.625,6.355L18.622,3.355L22.626,7.351L18.631,11.355L18.628,8.355Z\"\n      android:fillColor=\"#000000\"/>\n  <path\n      android:pathData=\"M10.4,3L10.357,3C9.273,3 8.399,3 7.691,3.058C6.963,3.117 6.322,3.243 5.73,3.545C4.789,4.024 4.024,4.789 3.545,5.73C3.243,6.322 3.117,6.963 3.058,7.691C3,8.399 3,9.273 3,10.357L3,10.4L3,13.6L3,13.643C3,14.727 3,15.601 3.058,16.309C3.117,17.038 3.243,17.678 3.545,18.27C4.024,19.211 4.789,19.976 5.73,20.455C6.322,20.757 6.963,20.883 7.691,20.942C8.399,21 9.273,21 10.357,21L10.357,21L10.4,21L13.6,21L13.643,21L13.643,21C14.727,21 15.601,21 16.309,20.942C17.038,20.883 17.678,20.757 18.27,20.455C19.211,19.976 19.976,19.211 20.455,18.27C20.757,17.678 20.883,17.038 20.942,16.309C20.974,15.924 20.988,15.49 20.994,15L18.994,15C18.988,15.447 18.975,15.821 18.949,16.146C18.899,16.751 18.807,17.099 18.673,17.362C18.385,17.927 17.927,18.385 17.362,18.673C17.099,18.807 16.751,18.899 16.146,18.949C15.529,18.999 14.737,19 13.6,19L10.4,19C9.263,19 8.471,18.999 7.854,18.949C7.249,18.899 6.901,18.807 6.638,18.673C6.074,18.385 5.615,17.927 5.327,17.362C5.193,17.099 5.101,16.751 5.051,16.146C5.001,15.529 5,14.737 5,13.6L5,10.4C5,9.263 5.001,8.471 5.051,7.854C5.101,7.249 5.193,6.901 5.327,6.638C5.615,6.074 6.074,5.615 6.638,5.327C6.901,5.193 7.249,5.101 7.854,5.051C8.471,5.001 9.263,5 10.4,5L12,5L12,3L10.4,3Z\"\n      android:fillColor=\"#000000\"\n      android:fillType=\"evenOdd\"/>\n</vector>\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/composeResources/drawable/img_banner_background.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"412dp\"\n    android:height=\"171dp\"\n    android:viewportWidth=\"412\"\n    android:viewportHeight=\"171\">\n  <path\n      android:pathData=\"M6,24.41L18.24,24.41A6,6 0,0 1,24.24 30.41L24.24,42.65A6,6 0,0 1,18.24 48.65L6,48.65A6,6 0,0 1,0 42.65L0,30.41A6,6 0,0 1,6 24.41z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.12\"/>\n  <path\n      android:pathData=\"M6,72.88L18.24,72.88A6,6 0,0 1,24.24 78.88L24.24,91.12A6,6 0,0 1,18.24 97.12L6,97.12A6,6 0,0 1,0 91.12L0,78.88A6,6 0,0 1,6 72.88z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.12\"/>\n  <path\n      android:pathData=\"M6,121.35L18.24,121.35A6,6 0,0 1,24.24 127.35L24.24,139.59A6,6 0,0 1,18.24 145.59L6,145.59A6,6 0,0 1,0 139.59L0,127.35A6,6 0,0 1,6 121.35z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.06\"/>\n  <path\n      android:pathData=\"M6,48.65L18.24,48.65A6,6 0,0 1,24.24 54.65L24.24,66.88A6,6 0,0 1,18.24 72.88L6,72.88A6,6 0,0 1,0 66.88L0,54.65A6,6 0,0 1,6 48.65z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.18\"/>\n  <path\n      android:pathData=\"M6,97.12L18.24,97.12A6,6 0,0 1,24.24 103.12L24.24,115.35A6,6 0,0 1,18.24 121.35L6,121.35A6,6 0,0 1,0 115.35L0,103.12A6,6 0,0 1,6 97.12z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.072\"/>\n  <path\n      android:pathData=\"M6,0.18L18.24,0.18A6,6 0,0 1,24.24 6.18L24.24,18.41A6,6 0,0 1,18.24 24.41L6,24.41A6,6 0,0 1,0 18.41L0,6.18A6,6 0,0 1,6 0.18z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.24\"/>\n  <path\n      android:pathData=\"M6,145.59L18.24,145.59A6,6 0,0 1,24.24 151.59L24.24,163.82A6,6 0,0 1,18.24 169.82L6,169.82A6,6 0,0 1,0 163.82L0,151.59A6,6 0,0 1,6 145.59z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.012\"/>\n  <path\n      android:pathData=\"M30.24,24.41L42.47,24.41A6,6 0,0 1,48.47 30.41L48.47,42.65A6,6 0,0 1,42.47 48.65L30.24,48.65A6,6 0,0 1,24.24 42.65L24.24,30.41A6,6 0,0 1,30.24 24.41z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.18\"/>\n  <path\n      android:pathData=\"M30.24,72.88L42.47,72.88A6,6 0,0 1,48.47 78.88L48.47,91.12A6,6 0,0 1,42.47 97.12L30.24,97.12A6,6 0,0 1,24.24 91.12L24.24,78.88A6,6 0,0 1,30.24 72.88z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.132\"/>\n  <path\n      android:pathData=\"M30.24,121.35L42.47,121.35A6,6 0,0 1,48.47 127.35L48.47,139.59A6,6 0,0 1,42.47 145.59L30.24,145.59A6,6 0,0 1,24.24 139.59L24.24,127.35A6,6 0,0 1,30.24 121.35z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.03\"/>\n  <path\n      android:pathData=\"M30.24,48.65L42.47,48.65A6,6 0,0 1,48.47 54.65L48.47,66.88A6,6 0,0 1,42.47 72.88L30.24,72.88A6,6 0,0 1,24.24 66.88L24.24,54.65A6,6 0,0 1,30.24 48.65z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.084\"/>\n  <path\n      android:pathData=\"M30.24,97.12L42.47,97.12A6,6 0,0 1,48.47 103.12L48.47,115.35A6,6 0,0 1,42.47 121.35L30.24,121.35A6,6 0,0 1,24.24 115.35L24.24,103.12A6,6 0,0 1,30.24 97.12z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.096\"/>\n  <path\n      android:pathData=\"M30.24,0.18L42.47,0.18A6,6 0,0 1,48.47 6.18L48.47,18.41A6,6 0,0 1,42.47 24.41L30.24,24.41A6,6 0,0 1,24.24 18.41L24.24,6.18A6,6 0,0 1,30.24 0.18z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.24\"/>\n  <path\n      android:pathData=\"M30.24,145.59L42.47,145.59A6,6 0,0 1,48.47 151.59L48.47,163.82A6,6 0,0 1,42.47 169.82L30.24,169.82A6,6 0,0 1,24.24 163.82L24.24,151.59A6,6 0,0 1,30.24 145.59z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.06\"/>\n  <path\n      android:pathData=\"M54.47,24.41L66.71,24.41A6,6 0,0 1,72.71 30.41L72.71,42.65A6,6 0,0 1,66.71 48.65L54.47,48.65A6,6 0,0 1,48.47 42.65L48.47,30.41A6,6 0,0 1,54.47 24.41z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.12\"/>\n  <path\n      android:pathData=\"M54.47,72.88L66.71,72.88A6,6 0,0 1,72.71 78.88L72.71,91.12A6,6 0,0 1,66.71 97.12L54.47,97.12A6,6 0,0 1,48.47 91.12L48.47,78.88A6,6 0,0 1,54.47 72.88z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.048\"/>\n  <path\n      android:pathData=\"M54.47,121.35L66.71,121.35A6,6 0,0 1,72.71 127.35L72.71,139.59A6,6 0,0 1,66.71 145.59L54.47,145.59A6,6 0,0 1,48.47 139.59L48.47,127.35A6,6 0,0 1,54.47 121.35z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.06\"/>\n  <path\n      android:pathData=\"M54.47,48.65L66.71,48.65A6,6 0,0 1,72.71 54.65L72.71,66.88A6,6 0,0 1,66.71 72.88L54.47,72.88A6,6 0,0 1,48.47 66.88L48.47,54.65A6,6 0,0 1,54.47 48.65z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.18\"/>\n  <path\n      android:pathData=\"M54.47,97.12L66.71,97.12A6,6 0,0 1,72.71 103.12L72.71,115.35A6,6 0,0 1,66.71 121.35L54.47,121.35A6,6 0,0 1,48.47 115.35L48.47,103.12A6,6 0,0 1,54.47 97.12z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.084\"/>\n  <path\n      android:pathData=\"M54.47,0.18L66.71,0.18A6,6 0,0 1,72.71 6.18L72.71,18.41A6,6 0,0 1,66.71 24.41L54.47,24.41A6,6 0,0 1,48.47 18.41L48.47,6.18A6,6 0,0 1,54.47 0.18z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.204\"/>\n  <path\n      android:pathData=\"M54.47,145.59L66.71,145.59A6,6 0,0 1,72.71 151.59L72.71,163.82A6,6 0,0 1,66.71 169.82L54.47,169.82A6,6 0,0 1,48.47 163.82L48.47,151.59A6,6 0,0 1,54.47 145.59z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.012\"/>\n  <path\n      android:pathData=\"M78.71,24.41L90.94,24.41A6,6 0,0 1,96.94 30.41L96.94,42.65A6,6 0,0 1,90.94 48.65L78.71,48.65A6,6 0,0 1,72.71 42.65L72.71,30.41A6,6 0,0 1,78.71 24.41z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.264\"/>\n  <path\n      android:pathData=\"M78.71,72.88L90.94,72.88A6,6 0,0 1,96.94 78.88L96.94,91.12A6,6 0,0 1,90.94 97.12L78.71,97.12A6,6 0,0 1,72.71 91.12L72.71,78.88A6,6 0,0 1,78.71 72.88z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.096\"/>\n  <path\n      android:pathData=\"M78.71,121.35L90.94,121.35A6,6 0,0 1,96.94 127.35L96.94,139.59A6,6 0,0 1,90.94 145.59L78.71,145.59A6,6 0,0 1,72.71 139.59L72.71,127.35A6,6 0,0 1,78.71 121.35z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.06\"/>\n  <path\n      android:pathData=\"M78.71,48.65L90.94,48.65A6,6 0,0 1,96.94 54.65L96.94,66.88A6,6 0,0 1,90.94 72.88L78.71,72.88A6,6 0,0 1,72.71 66.88L72.71,54.65A6,6 0,0 1,78.71 48.65z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.24\"/>\n  <path\n      android:pathData=\"M78.71,97.12L90.94,97.12A6,6 0,0 1,96.94 103.12L96.94,115.35A6,6 0,0 1,90.94 121.35L78.71,121.35A6,6 0,0 1,72.71 115.35L72.71,103.12A6,6 0,0 1,78.71 97.12z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.06\"/>\n  <path\n      android:pathData=\"M78.71,0.18L90.94,0.18A6,6 0,0 1,96.94 6.18L96.94,18.41A6,6 0,0 1,90.94 24.41L78.71,24.41A6,6 0,0 1,72.71 18.41L72.71,6.18A6,6 0,0 1,78.71 0.18z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.12\"/>\n  <path\n      android:pathData=\"M78.71,145.59L90.94,145.59A6,6 0,0 1,96.94 151.59L96.94,163.82A6,6 0,0 1,90.94 169.82L78.71,169.82A6,6 0,0 1,72.71 163.82L72.71,151.59A6,6 0,0 1,78.71 145.59z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.012\"/>\n  <path\n      android:pathData=\"M102.94,24.41L115.18,24.41A6,6 0,0 1,121.18 30.41L121.18,42.65A6,6 0,0 1,115.18 48.65L102.94,48.65A6,6 0,0 1,96.94 42.65L96.94,30.41A6,6 0,0 1,102.94 24.41z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.12\"/>\n  <path\n      android:pathData=\"M102.94,72.88L115.18,72.88A6,6 0,0 1,121.18 78.88L121.18,91.12A6,6 0,0 1,115.18 97.12L102.94,97.12A6,6 0,0 1,96.94 91.12L96.94,78.88A6,6 0,0 1,102.94 72.88z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.048\"/>\n  <path\n      android:pathData=\"M102.94,121.35L115.18,121.35A6,6 0,0 1,121.18 127.35L121.18,139.59A6,6 0,0 1,115.18 145.59L102.94,145.59A6,6 0,0 1,96.94 139.59L96.94,127.35A6,6 0,0 1,102.94 121.35z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.084\"/>\n  <path\n      android:pathData=\"M102.94,48.65L115.18,48.65A6,6 0,0 1,121.18 54.65L121.18,66.88A6,6 0,0 1,115.18 72.88L102.94,72.88A6,6 0,0 1,96.94 66.88L96.94,54.65A6,6 0,0 1,102.94 48.65z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.18\"/>\n  <path\n      android:pathData=\"M102.94,97.12L115.18,97.12A6,6 0,0 1,121.18 103.12L121.18,115.35A6,6 0,0 1,115.18 121.35L102.94,121.35A6,6 0,0 1,96.94 115.35L96.94,103.12A6,6 0,0 1,102.94 97.12z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.12\"/>\n  <path\n      android:pathData=\"M102.94,0.18L115.18,0.18A6,6 0,0 1,121.18 6.18L121.18,18.41A6,6 0,0 1,115.18 24.41L102.94,24.41A6,6 0,0 1,96.94 18.41L96.94,6.18A6,6 0,0 1,102.94 0.18z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.24\"/>\n  <path\n      android:pathData=\"M102.94,145.59L115.18,145.59A6,6 0,0 1,121.18 151.59L121.18,163.82A6,6 0,0 1,115.18 169.82L102.94,169.82A6,6 0,0 1,96.94 163.82L96.94,151.59A6,6 0,0 1,102.94 145.59z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.06\"/>\n  <path\n      android:pathData=\"M127.18,24.41L139.41,24.41A6,6 0,0 1,145.41 30.41L145.41,42.65A6,6 0,0 1,139.41 48.65L127.18,48.65A6,6 0,0 1,121.18 42.65L121.18,30.41A6,6 0,0 1,127.18 24.41z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.12\"/>\n  <path\n      android:pathData=\"M127.18,72.88L139.41,72.88A6,6 0,0 1,145.41 78.88L145.41,91.12A6,6 0,0 1,139.41 97.12L127.18,97.12A6,6 0,0 1,121.18 91.12L121.18,78.88A6,6 0,0 1,127.18 72.88z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.18\"/>\n  <path\n      android:pathData=\"M127.18,121.35L139.41,121.35A6,6 0,0 1,145.41 127.35L145.41,139.59A6,6 0,0 1,139.41 145.59L127.18,145.59A6,6 0,0 1,121.18 139.59L121.18,127.35A6,6 0,0 1,127.18 121.35z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.048\"/>\n  <path\n      android:pathData=\"M127.18,48.65L139.41,48.65A6,6 0,0 1,145.41 54.65L145.41,66.88A6,6 0,0 1,139.41 72.88L127.18,72.88A6,6 0,0 1,121.18 66.88L121.18,54.65A6,6 0,0 1,127.18 48.65z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.24\"/>\n  <path\n      android:pathData=\"M127.18,97.12L139.41,97.12A6,6 0,0 1,145.41 103.12L145.41,115.35A6,6 0,0 1,139.41 121.35L127.18,121.35A6,6 0,0 1,121.18 115.35L121.18,103.12A6,6 0,0 1,127.18 97.12z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.072\"/>\n  <path\n      android:pathData=\"M127.18,0.18L139.41,0.18A6,6 0,0 1,145.41 6.18L145.41,18.41A6,6 0,0 1,139.41 24.41L127.18,24.41A6,6 0,0 1,121.18 18.41L121.18,6.18A6,6 0,0 1,127.18 0.18z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.24\"/>\n  <path\n      android:pathData=\"M127,146L139.24,146A6,6 0,0 1,145.24 152L145.24,164.24A6,6 0,0 1,139.24 170.24L127,170.24A6,6 0,0 1,121 164.24L121,152A6,6 0,0 1,127 146z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.024\"/>\n  <path\n      android:pathData=\"M151.41,24.41L163.65,24.41A6,6 0,0 1,169.65 30.41L169.65,42.65A6,6 0,0 1,163.65 48.65L151.41,48.65A6,6 0,0 1,145.41 42.65L145.41,30.41A6,6 0,0 1,151.41 24.41z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.18\"/>\n  <path\n      android:pathData=\"M151.41,72.88L163.65,72.88A6,6 0,0 1,169.65 78.88L169.65,91.12A6,6 0,0 1,163.65 97.12L151.41,97.12A6,6 0,0 1,145.41 91.12L145.41,78.88A6,6 0,0 1,151.41 72.88z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.132\"/>\n  <path\n      android:pathData=\"M151.41,121.35L163.65,121.35A6,6 0,0 1,169.65 127.35L169.65,139.59A6,6 0,0 1,163.65 145.59L151.41,145.59A6,6 0,0 1,145.41 139.59L145.41,127.35A6,6 0,0 1,151.41 121.35z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.06\"/>\n  <path\n      android:pathData=\"M151.41,48.65L163.65,48.65A6,6 0,0 1,169.65 54.65L169.65,66.88A6,6 0,0 1,163.65 72.88L151.41,72.88A6,6 0,0 1,145.41 66.88L145.41,54.65A6,6 0,0 1,151.41 48.65z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.18\"/>\n  <path\n      android:pathData=\"M151.41,97.12L163.65,97.12A6,6 0,0 1,169.65 103.12L169.65,115.35A6,6 0,0 1,163.65 121.35L151.41,121.35A6,6 0,0 1,145.41 115.35L145.41,103.12A6,6 0,0 1,151.41 97.12z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.12\"/>\n  <path\n      android:pathData=\"M151.41,0.18L163.65,0.18A6,6 0,0 1,169.65 6.18L169.65,18.41A6,6 0,0 1,163.65 24.41L151.41,24.41A6,6 0,0 1,145.41 18.41L145.41,6.18A6,6 0,0 1,151.41 0.18z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.18\"/>\n  <path\n      android:pathData=\"M151.41,145.59L163.65,145.59A6,6 0,0 1,169.65 151.59L169.65,163.82A6,6 0,0 1,163.65 169.82L151.41,169.82A6,6 0,0 1,145.41 163.82L145.41,151.59A6,6 0,0 1,151.41 145.59z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.012\"/>\n  <path\n      android:pathData=\"M175.65,24.41L187.88,24.41A6,6 0,0 1,193.88 30.41L193.88,42.65A6,6 0,0 1,187.88 48.65L175.65,48.65A6,6 0,0 1,169.65 42.65L169.65,30.41A6,6 0,0 1,175.65 24.41z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.12\"/>\n  <path\n      android:pathData=\"M175.65,72.88L187.88,72.88A6,6 0,0 1,193.88 78.88L193.88,91.12A6,6 0,0 1,187.88 97.12L175.65,97.12A6,6 0,0 1,169.65 91.12L169.65,78.88A6,6 0,0 1,175.65 72.88z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.084\"/>\n  <path\n      android:pathData=\"M175.65,121.35L187.88,121.35A6,6 0,0 1,193.88 127.35L193.88,139.59A6,6 0,0 1,187.88 145.59L175.65,145.59A6,6 0,0 1,169.65 139.59L169.65,127.35A6,6 0,0 1,175.65 121.35z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.024\"/>\n  <path\n      android:pathData=\"M175.65,48.65L187.88,48.65A6,6 0,0 1,193.88 54.65L193.88,66.88A6,6 0,0 1,187.88 72.88L175.65,72.88A6,6 0,0 1,169.65 66.88L169.65,54.65A6,6 0,0 1,175.65 48.65z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.12\"/>\n  <path\n      android:pathData=\"M175.65,97.12L187.88,97.12A6,6 0,0 1,193.88 103.12L193.88,115.35A6,6 0,0 1,187.88 121.35L175.65,121.35A6,6 0,0 1,169.65 115.35L169.65,103.12A6,6 0,0 1,175.65 97.12z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.12\"/>\n  <path\n      android:pathData=\"M175.65,0.18L187.88,0.18A6,6 0,0 1,193.88 6.18L193.88,18.41A6,6 0,0 1,187.88 24.41L175.65,24.41A6,6 0,0 1,169.65 18.41L169.65,6.18A6,6 0,0 1,175.65 0.18z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.276\"/>\n  <path\n      android:pathData=\"M175.65,145.59L187.88,145.59A6,6 0,0 1,193.88 151.59L193.88,163.82A6,6 0,0 1,187.88 169.82L175.65,169.82A6,6 0,0 1,169.65 163.82L169.65,151.59A6,6 0,0 1,175.65 145.59z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.012\"/>\n  <path\n      android:pathData=\"M199.88,24.41L212.12,24.41A6,6 0,0 1,218.12 30.41L218.12,42.65A6,6 0,0 1,212.12 48.65L199.88,48.65A6,6 0,0 1,193.88 42.65L193.88,30.41A6,6 0,0 1,199.88 24.41z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.264\"/>\n  <path\n      android:pathData=\"M199.88,72.88L212.12,72.88A6,6 0,0 1,218.12 78.88L218.12,91.12A6,6 0,0 1,212.12 97.12L199.88,97.12A6,6 0,0 1,193.88 91.12L193.88,78.88A6,6 0,0 1,199.88 72.88z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.18\"/>\n  <path\n      android:pathData=\"M199.88,121.35L212.12,121.35A6,6 0,0 1,218.12 127.35L218.12,139.59A6,6 0,0 1,212.12 145.59L199.88,145.59A6,6 0,0 1,193.88 139.59L193.88,127.35A6,6 0,0 1,199.88 121.35z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.036\"/>\n  <path\n      android:pathData=\"M199.88,48.65L212.12,48.65A6,6 0,0 1,218.12 54.65L218.12,66.88A6,6 0,0 1,212.12 72.88L199.88,72.88A6,6 0,0 1,193.88 66.88L193.88,54.65A6,6 0,0 1,199.88 48.65z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.132\"/>\n  <path\n      android:pathData=\"M199.88,97.12L212.12,97.12A6,6 0,0 1,218.12 103.12L218.12,115.35A6,6 0,0 1,212.12 121.35L199.88,121.35A6,6 0,0 1,193.88 115.35L193.88,103.12A6,6 0,0 1,199.88 97.12z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.048\"/>\n  <path\n      android:pathData=\"M199.88,0.18L212.12,0.18A6,6 0,0 1,218.12 6.18L218.12,18.41A6,6 0,0 1,212.12 24.41L199.88,24.41A6,6 0,0 1,193.88 18.41L193.88,6.18A6,6 0,0 1,199.88 0.18z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.18\"/>\n  <path\n      android:pathData=\"M199.88,145.59L212.12,145.59A6,6 0,0 1,218.12 151.59L218.12,163.82A6,6 0,0 1,212.12 169.82L199.88,169.82A6,6 0,0 1,193.88 163.82L193.88,151.59A6,6 0,0 1,199.88 145.59z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.012\"/>\n  <path\n      android:pathData=\"M224.12,24.41L236.35,24.41A6,6 0,0 1,242.35 30.41L242.35,42.65A6,6 0,0 1,236.35 48.65L224.12,48.65A6,6 0,0 1,218.12 42.65L218.12,30.41A6,6 0,0 1,224.12 24.41z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.12\"/>\n  <path\n      android:pathData=\"M224.12,72.88L236.35,72.88A6,6 0,0 1,242.35 78.88L242.35,91.12A6,6 0,0 1,236.35 97.12L224.12,97.12A6,6 0,0 1,218.12 91.12L218.12,78.88A6,6 0,0 1,224.12 72.88z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.108\"/>\n  <path\n      android:pathData=\"M224.12,121.35L236.35,121.35A6,6 0,0 1,242.35 127.35L242.35,139.59A6,6 0,0 1,236.35 145.59L224.12,145.59A6,6 0,0 1,218.12 139.59L218.12,127.35A6,6 0,0 1,224.12 121.35z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.06\"/>\n  <path\n      android:pathData=\"M224.12,48.65L236.35,48.65A6,6 0,0 1,242.35 54.65L242.35,66.88A6,6 0,0 1,236.35 72.88L224.12,72.88A6,6 0,0 1,218.12 66.88L218.12,54.65A6,6 0,0 1,224.12 48.65z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.18\"/>\n  <path\n      android:pathData=\"M224.12,97.12L236.35,97.12A6,6 0,0 1,242.35 103.12L242.35,115.35A6,6 0,0 1,236.35 121.35L224.12,121.35A6,6 0,0 1,218.12 115.35L218.12,103.12A6,6 0,0 1,224.12 97.12z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.06\"/>\n  <path\n      android:pathData=\"M224.12,0.18L236.35,0.18A6,6 0,0 1,242.35 6.18L242.35,18.41A6,6 0,0 1,236.35 24.41L224.12,24.41A6,6 0,0 1,218.12 18.41L218.12,6.18A6,6 0,0 1,224.12 0.18z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.24\"/>\n  <path\n      android:pathData=\"M224.12,145.59L236.35,145.59A6,6 0,0 1,242.35 151.59L242.35,163.82A6,6 0,0 1,236.35 169.82L224.12,169.82A6,6 0,0 1,218.12 163.82L218.12,151.59A6,6 0,0 1,224.12 145.59z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.012\"/>\n  <path\n      android:pathData=\"M248.35,24.41L260.59,24.41A6,6 0,0 1,266.59 30.41L266.59,42.65A6,6 0,0 1,260.59 48.65L248.35,48.65A6,6 0,0 1,242.35 42.65L242.35,30.41A6,6 0,0 1,248.35 24.41z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.24\"/>\n  <path\n      android:pathData=\"M248.35,72.88L260.59,72.88A6,6 0,0 1,266.59 78.88L266.59,91.12A6,6 0,0 1,260.59 97.12L248.35,97.12A6,6 0,0 1,242.35 91.12L242.35,78.88A6,6 0,0 1,248.35 72.88z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.048\"/>\n  <path\n      android:pathData=\"M248.35,48.65L260.59,48.65A6,6 0,0 1,266.59 54.65L266.59,66.88A6,6 0,0 1,260.59 72.88L248.35,72.88A6,6 0,0 1,242.35 66.88L242.35,54.65A6,6 0,0 1,248.35 48.65z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.18\"/>\n  <path\n      android:pathData=\"M248.35,97.12L260.59,97.12A6,6 0,0 1,266.59 103.12L266.59,115.35A6,6 0,0 1,260.59 121.35L248.35,121.35A6,6 0,0 1,242.35 115.35L242.35,103.12A6,6 0,0 1,248.35 97.12z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.12\"/>\n  <path\n      android:pathData=\"M248.35,0.18L260.59,0.18A6,6 0,0 1,266.59 6.18L266.59,18.41A6,6 0,0 1,260.59 24.41L248.35,24.41A6,6 0,0 1,242.35 18.41L242.35,6.18A6,6 0,0 1,248.35 0.18z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.24\"/>\n  <path\n      android:pathData=\"M248.35,145.59L260.59,145.59A6,6 0,0 1,266.59 151.59L266.59,163.82A6,6 0,0 1,260.59 169.82L248.35,169.82A6,6 0,0 1,242.35 163.82L242.35,151.59A6,6 0,0 1,248.35 145.59z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.048\"/>\n  <path\n      android:pathData=\"M272.59,24.41L284.82,24.41A6,6 0,0 1,290.82 30.41L290.82,42.65A6,6 0,0 1,284.82 48.65L272.59,48.65A6,6 0,0 1,266.59 42.65L266.59,30.41A6,6 0,0 1,272.59 24.41z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.24\"/>\n  <path\n      android:pathData=\"M272.59,72.88L284.82,72.88A6,6 0,0 1,290.82 78.88L290.82,91.12A6,6 0,0 1,284.82 97.12L272.59,97.12A6,6 0,0 1,266.59 91.12L266.59,78.88A6,6 0,0 1,272.59 72.88z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.09\"/>\n  <path\n      android:pathData=\"M272.59,121.35L284.82,121.35A6,6 0,0 1,290.82 127.35L290.82,139.59A6,6 0,0 1,284.82 145.59L272.59,145.59A6,6 0,0 1,266.59 139.59L266.59,127.35A6,6 0,0 1,272.59 121.35z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.036\"/>\n  <path\n      android:pathData=\"M272.59,48.65L284.82,48.65A6,6 0,0 1,290.82 54.65L290.82,66.88A6,6 0,0 1,284.82 72.88L272.59,72.88A6,6 0,0 1,266.59 66.88L266.59,54.65A6,6 0,0 1,272.59 48.65z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.12\"/>\n  <path\n      android:pathData=\"M272.59,97.12L284.82,97.12A6,6 0,0 1,290.82 103.12L290.82,115.35A6,6 0,0 1,284.82 121.35L272.59,121.35A6,6 0,0 1,266.59 115.35L266.59,103.12A6,6 0,0 1,272.59 97.12z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.06\"/>\n  <path\n      android:pathData=\"M272.59,0.18L284.82,0.18A6,6 0,0 1,290.82 6.18L290.82,18.41A6,6 0,0 1,284.82 24.41L272.59,24.41A6,6 0,0 1,266.59 18.41L266.59,6.18A6,6 0,0 1,272.59 0.18z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.3\"/>\n  <path\n      android:pathData=\"M272.59,145.59L284.82,145.59A6,6 0,0 1,290.82 151.59L290.82,163.82A6,6 0,0 1,284.82 169.82L272.59,169.82A6,6 0,0 1,266.59 163.82L266.59,151.59A6,6 0,0 1,272.59 145.59z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.012\"/>\n  <path\n      android:pathData=\"M296.82,24.41L309.06,24.41A6,6 0,0 1,315.06 30.41L315.06,42.65A6,6 0,0 1,309.06 48.65L296.82,48.65A6,6 0,0 1,290.82 42.65L290.82,30.41A6,6 0,0 1,296.82 24.41z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.24\"/>\n  <path\n      android:pathData=\"M296.82,72.88L309.06,72.88A6,6 0,0 1,315.06 78.88L315.06,91.12A6,6 0,0 1,309.06 97.12L296.82,97.12A6,6 0,0 1,290.82 91.12L290.82,78.88A6,6 0,0 1,296.82 72.88z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.072\"/>\n  <path\n      android:pathData=\"M296.82,121.35L309.06,121.35A6,6 0,0 1,315.06 127.35L315.06,139.59A6,6 0,0 1,309.06 145.59L296.82,145.59A6,6 0,0 1,290.82 139.59L290.82,127.35A6,6 0,0 1,296.82 121.35z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.06\"/>\n  <path\n      android:pathData=\"M296.82,48.65L309.06,48.65A6,6 0,0 1,315.06 54.65L315.06,66.88A6,6 0,0 1,309.06 72.88L296.82,72.88A6,6 0,0 1,290.82 66.88L290.82,54.65A6,6 0,0 1,296.82 48.65z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.18\"/>\n  <path\n      android:pathData=\"M296.82,97.12L309.06,97.12A6,6 0,0 1,315.06 103.12L315.06,115.35A6,6 0,0 1,309.06 121.35L296.82,121.35A6,6 0,0 1,290.82 115.35L290.82,103.12A6,6 0,0 1,296.82 97.12z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.12\"/>\n  <path\n      android:pathData=\"M296.82,0.18L309.06,0.18A6,6 0,0 1,315.06 6.18L315.06,18.41A6,6 0,0 1,309.06 24.41L296.82,24.41A6,6 0,0 1,290.82 18.41L290.82,6.18A6,6 0,0 1,296.82 0.18z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.24\"/>\n  <path\n      android:pathData=\"M296.82,145.59L309.06,145.59A6,6 0,0 1,315.06 151.59L315.06,163.82A6,6 0,0 1,309.06 169.82L296.82,169.82A6,6 0,0 1,290.82 163.82L290.82,151.59A6,6 0,0 1,296.82 145.59z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.012\"/>\n  <path\n      android:pathData=\"M321.06,24.41L333.3,24.41A6,6 0,0 1,339.3 30.41L339.3,42.65A6,6 0,0 1,333.3 48.65L321.06,48.65A6,6 0,0 1,315.06 42.65L315.06,30.41A6,6 0,0 1,321.06 24.41z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.12\"/>\n  <path\n      android:pathData=\"M321.06,72.88L333.3,72.88A6,6 0,0 1,339.3 78.88L339.3,91.12A6,6 0,0 1,333.3 97.12L321.06,97.12A6,6 0,0 1,315.06 91.12L315.06,78.88A6,6 0,0 1,321.06 72.88z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.156\"/>\n  <path\n      android:pathData=\"M321.06,121.35L333.3,121.35A6,6 0,0 1,339.3 127.35L339.3,139.59A6,6 0,0 1,333.3 145.59L321.06,145.59A6,6 0,0 1,315.06 139.59L315.06,127.35A6,6 0,0 1,321.06 121.35z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.024\"/>\n  <path\n      android:pathData=\"M321.06,48.65L333.3,48.65A6,6 0,0 1,339.3 54.65L339.3,66.88A6,6 0,0 1,333.3 72.88L321.06,72.88A6,6 0,0 1,315.06 66.88L315.06,54.65A6,6 0,0 1,321.06 48.65z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.06\"/>\n  <path\n      android:pathData=\"M321.06,97.12L333.3,97.12A6,6 0,0 1,339.3 103.12L339.3,115.35A6,6 0,0 1,333.3 121.35L321.06,121.35A6,6 0,0 1,315.06 115.35L315.06,103.12A6,6 0,0 1,321.06 97.12z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.06\"/>\n  <path\n      android:pathData=\"M321.06,0.18L333.3,0.18A6,6 0,0 1,339.3 6.18L339.3,18.41A6,6 0,0 1,333.3 24.41L321.06,24.41A6,6 0,0 1,315.06 18.41L315.06,6.18A6,6 0,0 1,321.06 0.18z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.24\"/>\n  <path\n      android:pathData=\"M321.06,145.59L333.3,145.59A6,6 0,0 1,339.3 151.59L339.3,163.82A6,6 0,0 1,333.3 169.82L321.06,169.82A6,6 0,0 1,315.06 163.82L315.06,151.59A6,6 0,0 1,321.06 145.59z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.012\"/>\n  <path\n      android:pathData=\"M345.3,24.41L357.53,24.41A6,6 0,0 1,363.53 30.41L363.53,42.65A6,6 0,0 1,357.53 48.65L345.3,48.65A6,6 0,0 1,339.3 42.65L339.3,30.41A6,6 0,0 1,345.3 24.41z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.24\"/>\n  <path\n      android:pathData=\"M345.3,72.88L357.53,72.88A6,6 0,0 1,363.53 78.88L363.53,91.12A6,6 0,0 1,357.53 97.12L345.3,97.12A6,6 0,0 1,339.3 91.12L339.3,78.88A6,6 0,0 1,345.3 72.88z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.072\"/>\n  <path\n      android:pathData=\"M345.3,121.35L357.53,121.35A6,6 0,0 1,363.53 127.35L363.53,139.59A6,6 0,0 1,357.53 145.59L345.3,145.59A6,6 0,0 1,339.3 139.59L339.3,127.35A6,6 0,0 1,345.3 121.35z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.06\"/>\n  <path\n      android:pathData=\"M345.3,48.65L357.53,48.65A6,6 0,0 1,363.53 54.65L363.53,66.88A6,6 0,0 1,357.53 72.88L345.3,72.88A6,6 0,0 1,339.3 66.88L339.3,54.65A6,6 0,0 1,345.3 48.65z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.12\"/>\n  <path\n      android:pathData=\"M345.3,97.12L357.53,97.12A6,6 0,0 1,363.53 103.12L363.53,115.35A6,6 0,0 1,357.53 121.35L345.3,121.35A6,6 0,0 1,339.3 115.35L339.3,103.12A6,6 0,0 1,345.3 97.12z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.12\"/>\n  <path\n      android:pathData=\"M345.3,0.18L357.53,0.18A6,6 0,0 1,363.53 6.18L363.53,18.41A6,6 0,0 1,357.53 24.41L345.3,24.41A6,6 0,0 1,339.3 18.41L339.3,6.18A6,6 0,0 1,345.3 0.18z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.24\"/>\n  <path\n      android:pathData=\"M345.3,145.59L357.53,145.59A6,6 0,0 1,363.53 151.59L363.53,163.82A6,6 0,0 1,357.53 169.82L345.3,169.82A6,6 0,0 1,339.3 163.82L339.3,151.59A6,6 0,0 1,345.3 145.59z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.048\"/>\n  <path\n      android:pathData=\"M369.53,24.41L381.77,24.41A6,6 0,0 1,387.77 30.41L387.77,42.65A6,6 0,0 1,381.77 48.65L369.53,48.65A6,6 0,0 1,363.53 42.65L363.53,30.41A6,6 0,0 1,369.53 24.41z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.12\"/>\n  <path\n      android:pathData=\"M369.53,72.88L381.77,72.88A6,6 0,0 1,387.77 78.88L387.77,91.12A6,6 0,0 1,381.77 97.12L369.53,97.12A6,6 0,0 1,363.53 91.12L363.53,78.88A6,6 0,0 1,369.53 72.88z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.108\"/>\n  <path\n      android:pathData=\"M369.53,121.35L381.77,121.35A6,6 0,0 1,387.77 127.35L387.77,139.59A6,6 0,0 1,381.77 145.59L369.53,145.59A6,6 0,0 1,363.53 139.59L363.53,127.35A6,6 0,0 1,369.53 121.35z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.12\"/>\n  <path\n      android:pathData=\"M369.53,48.65L381.77,48.65A6,6 0,0 1,387.77 54.65L387.77,66.88A6,6 0,0 1,381.77 72.88L369.53,72.88A6,6 0,0 1,363.53 66.88L363.53,54.65A6,6 0,0 1,369.53 48.65z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.18\"/>\n  <path\n      android:pathData=\"M369.53,97.12L381.77,97.12A6,6 0,0 1,387.77 103.12L387.77,115.35A6,6 0,0 1,381.77 121.35L369.53,121.35A6,6 0,0 1,363.53 115.35L363.53,103.12A6,6 0,0 1,369.53 97.12z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.084\"/>\n  <path\n      android:pathData=\"M369.53,0.18L381.77,0.18A6,6 0,0 1,387.77 6.18L387.77,18.41A6,6 0,0 1,381.77 24.41L369.53,24.41A6,6 0,0 1,363.53 18.41L363.53,6.18A6,6 0,0 1,369.53 0.18z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.18\"/>\n  <path\n      android:pathData=\"M369.53,145.59L381.77,145.59A6,6 0,0 1,387.77 151.59L387.77,163.82A6,6 0,0 1,381.77 169.82L369.53,169.82A6,6 0,0 1,363.53 163.82L363.53,151.59A6,6 0,0 1,369.53 145.59z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.012\"/>\n  <path\n      android:pathData=\"M393.77,24.41L406,24.41A6,6 0,0 1,412 30.41L412,42.65A6,6 0,0 1,406 48.65L393.77,48.65A6,6 0,0 1,387.77 42.65L387.77,30.41A6,6 0,0 1,393.77 24.41z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.12\"/>\n  <path\n      android:pathData=\"M393.77,72.88L406,72.88A6,6 0,0 1,412 78.88L412,91.12A6,6 0,0 1,406 97.12L393.77,97.12A6,6 0,0 1,387.77 91.12L387.77,78.88A6,6 0,0 1,393.77 72.88z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.204\"/>\n  <path\n      android:pathData=\"M393.77,121.35L406,121.35A6,6 0,0 1,412 127.35L412,139.59A6,6 0,0 1,406 145.59L393.77,145.59A6,6 0,0 1,387.77 139.59L387.77,127.35A6,6 0,0 1,393.77 121.35z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.06\"/>\n  <path\n      android:pathData=\"M393.77,48.65L406,48.65A6,6 0,0 1,412 54.65L412,66.88A6,6 0,0 1,406 72.88L393.77,72.88A6,6 0,0 1,387.77 66.88L387.77,54.65A6,6 0,0 1,393.77 48.65z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.12\"/>\n  <path\n      android:pathData=\"M393.77,97.12L406,97.12A6,6 0,0 1,412 103.12L412,115.35A6,6 0,0 1,406 121.35L393.77,121.35A6,6 0,0 1,387.77 115.35L387.77,103.12A6,6 0,0 1,393.77 97.12z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.096\"/>\n  <path\n      android:pathData=\"M393.77,0.18L406,0.18A6,6 0,0 1,412 6.18L412,18.41A6,6 0,0 1,406 24.41L393.77,24.41A6,6 0,0 1,387.77 18.41L387.77,6.18A6,6 0,0 1,393.77 0.18z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.24\"/>\n  <path\n      android:pathData=\"M393.77,145.59L406,145.59A6,6 0,0 1,412 151.59L412,163.82A6,6 0,0 1,406 169.82L393.77,169.82A6,6 0,0 1,387.77 163.82L387.77,151.59A6,6 0,0 1,393.77 145.59z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.036\"/>\n</vector>\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/composeResources/drawable/status_ui_baseline_visibility_off_24.xml",
    "content": "<vector android:height=\"24dp\" android:tint=\"#000000\"\n    android:viewportHeight=\"24\" android:viewportWidth=\"24\"\n    android:width=\"24dp\" xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path android:fillColor=\"@android:color/white\" android:pathData=\"M12,7c2.76,0 5,2.24 5,5 0,0.65 -0.13,1.26 -0.36,1.83l2.92,2.92c1.51,-1.26 2.7,-2.89 3.43,-4.75 -1.73,-4.39 -6,-7.5 -11,-7.5 -1.4,0 -2.74,0.25 -3.98,0.7l2.16,2.16C10.74,7.13 11.35,7 12,7zM2,4.27l2.28,2.28 0.46,0.46C3.08,8.3 1.78,10.02 1,12c1.73,4.39 6,7.5 11,7.5 1.55,0 3.03,-0.3 4.38,-0.84l0.42,0.42L19.73,22 21,20.73 3.27,3 2,4.27zM7.53,9.8l1.55,1.55c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.66 1.34,3 3,3 0.22,0 0.44,-0.03 0.65,-0.08l1.55,1.55c-0.67,0.33 -1.41,0.53 -2.2,0.53 -2.76,0 -5,-2.24 -5,-5 0,-0.79 0.2,-1.53 0.53,-2.2zM11.84,9.02l3.15,3.15 0.02,-0.16c0,-1.66 -1.34,-3 -3,-3l-0.17,0.01z\"/>\n</vector>\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/BlogAuthorAvatar.kt",
    "content": "package com.zhangke.fread.status.ui\n\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.unit.dp\nimport com.seiko.imageloader.model.ImageAction\nimport com.seiko.imageloader.model.ImageRequest\nimport com.seiko.imageloader.rememberImageActionPainter\nimport com.seiko.imageloader.ui.AutoSizeBox\nimport com.zhangke.framework.composable.freadPlaceholder\n\n@Composable\nfun BlogAuthorAvatar(\n    modifier: Modifier,\n    reblogAvatar: String?,\n    authorAvatar: String?,\n    onClick: (() -> Unit)? = null,\n) {\n    if (reblogAvatar.isNullOrEmpty()) {\n        BlogAuthorAvatar(\n            modifier = modifier,\n            onClick = onClick,\n            imageUrl = authorAvatar,\n        )\n    } else {\n        Box(modifier = modifier) {\n            Box(\n                modifier = Modifier\n                    .fillMaxSize()\n                    .padding(end = 6.dp, bottom = 6.dp)\n            ) {\n                BlogAuthorAvatar(\n                    modifier = Modifier.fillMaxSize(),\n                    onClick = onClick,\n                    imageUrl = authorAvatar,\n                )\n            }\n            BlogAuthorAvatar(\n                modifier = Modifier\n                    .size(18.dp)\n                    .align(Alignment.BottomEnd),\n                onClick = onClick,\n                imageUrl = reblogAvatar,\n            )\n        }\n    }\n}\n\n@Composable\nfun BlogAuthorAvatar(\n    modifier: Modifier,\n    imageUrl: String?,\n    onClick: (() -> Unit)? = null,\n) {\n    AutoSizeBox(\n        request = remember(imageUrl) {\n            ImageRequest(imageUrl.orEmpty())\n        },\n    ) { action ->\n        Image(\n            painter = rememberImageActionPainter(action),\n            contentDescription = \"Avatar\",\n            modifier = modifier\n                .clip(CircleShape)\n                .freadPlaceholder(action !is ImageAction.Success)\n                .let {\n                    if (onClick == null) {\n                        it\n                    } else {\n                        it.clickable { onClick() }\n                    }\n                },\n        )\n    }\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/BlogAuthorUi.kt",
    "content": "package com.zhangke.fread.status.ui\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.material3.HorizontalDivider\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport com.zhangke.framework.composable.StyledTextButton\nimport com.zhangke.framework.composable.TextButtonStyle\nimport com.zhangke.fread.common.browser.LocalActivityBrowserLauncher\nimport com.zhangke.fread.common.browser.launchWebTabInApp\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.author.BlogAuthor\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.ui.richtext.FreadRichText\nimport com.zhangke.fread.status.ui.style.StatusInfoStyleDefaults\nimport org.jetbrains.compose.resources.stringResource\n\n@Composable\nfun BlogAuthorUi(\n    modifier: Modifier,\n    author: BlogAuthor,\n    onClick: (BlogAuthor) -> Unit,\n    onUrlClick: (String) -> Unit,\n) {\n    Column(modifier = modifier.clickable { onClick(author) }) {\n        Row(\n            modifier = Modifier.fillMaxWidth(),\n        ) {\n            BlogAuthorAvatar(\n                modifier = Modifier\n                    .padding(start = 16.dp, top = 8.dp)\n                    .size(StatusInfoStyleDefaults.avatarSize),\n                imageUrl = author.avatar,\n            )\n            Column(\n                modifier = Modifier.weight(1F)\n                    .padding(start = 16.dp, top = 8.dp, end = 16.dp),\n            ) {\n                FreadRichText(\n                    modifier = Modifier.fillMaxWidth(),\n                    richText = author.humanizedName,\n                    maxLines = 1,\n                    overflow = TextOverflow.Ellipsis,\n                    fontSize = 16.sp,\n                    onUrlClick = onUrlClick,\n                )\n                Text(\n                    modifier = Modifier.fillMaxWidth().padding(top = 2.dp),\n                    textAlign = TextAlign.Start,\n                    text = author.webFinger.toString(),\n                    maxLines = 1,\n                    overflow = TextOverflow.Ellipsis,\n                    style = MaterialTheme.typography.labelMedium,\n                )\n                FreadRichText(\n                    modifier = Modifier.fillMaxWidth().padding(top = 4.dp),\n                    content = author.description,\n                    emojis = author.emojis,\n                    mentions = emptyList(),\n                    tags = emptyList(),\n                    onHashtagClick = {},\n                    onMentionClick = {},\n                    maxLines = 1,\n                    onUrlClick = onUrlClick,\n                )\n            }\n        }\n        HorizontalDivider(modifier = Modifier.fillMaxWidth().padding(top = 8.dp))\n    }\n}\n\n@Composable\nfun RecommendAuthorUi(\n    modifier: Modifier,\n    locator: PlatformLocator,\n    author: BlogAuthor,\n    following: Boolean,\n    composedStatusInteraction: ComposedStatusInteraction,\n) {\n    BaseBlogAuthor(\n        modifier = modifier,\n        locator = locator,\n        author = author,\n        following = following,\n        composedStatusInteraction = composedStatusInteraction,\n    )\n}\n\n@Composable\nprivate fun BaseBlogAuthor(\n    modifier: Modifier,\n    locator: PlatformLocator,\n    author: BlogAuthor,\n    following: Boolean,\n    composedStatusInteraction: ComposedStatusInteraction,\n) {\n    val browserLauncher = LocalActivityBrowserLauncher.current\n    val coroutineScope = rememberCoroutineScope()\n    Box(\n        modifier = modifier.fillMaxWidth()\n            .clickable { composedStatusInteraction.onUserInfoClick(locator, author) },\n    ) {\n        Row(\n            modifier = Modifier.padding(bottom = 8.dp)\n        ) {\n            BlogAuthorAvatar(\n                modifier = Modifier\n                    .padding(start = 16.dp, top = 8.dp)\n                    .size(StatusInfoStyleDefaults.avatarSize),\n                imageUrl = author.avatar,\n            )\n            Column(\n                modifier = Modifier\n                    .padding(start = 16.dp, top = 2.dp, end = 8.dp)\n                    .fillMaxWidth()\n            ) {\n                Row(modifier = Modifier.fillMaxWidth()) {\n                    Column(modifier = Modifier.weight(1F).align(Alignment.CenterVertically)) {\n                        FreadRichText(\n                            modifier = Modifier.fillMaxWidth(),\n                            richText = author.humanizedName,\n                            maxLines = 1,\n                            overflow = TextOverflow.Ellipsis,\n                            fontSize = 16.sp,\n                            onUrlClick = {\n                                browserLauncher.launchWebTabInApp(coroutineScope, it, locator)\n                            },\n                        )\n\n                        Text(\n                            modifier = Modifier.fillMaxWidth().padding(top = 2.dp),\n                            textAlign = TextAlign.Start,\n                            text = author.webFinger.toString(),\n                            maxLines = 1,\n                            overflow = TextOverflow.Ellipsis,\n                            style = MaterialTheme.typography.labelMedium,\n                        )\n                    }\n\n                    StyledTextButton(\n                        modifier = Modifier.align(Alignment.CenterVertically),\n                        text = if (following) {\n                            stringResource(LocalizedString.statusUiUnfollow)\n                        } else {\n                            stringResource(LocalizedString.statusUiFollow)\n                        },\n                        style = TextButtonStyle.STANDARD,\n                        onClick = {\n                            if (following) {\n                                composedStatusInteraction.onUnfollowClick(locator, author)\n                            } else {\n                                composedStatusInteraction.onFollowClick(locator, author)\n                            }\n                        },\n                    )\n                }\n\n                FreadRichText(\n                    modifier = Modifier\n                        .padding(end = 8.dp),\n                    content = author.description,\n                    emojis = author.emojis,\n                    mentions = emptyList(),\n                    tags = emptyList(),\n                    onMentionClick = {\n                        composedStatusInteraction.onMentionClick(locator, it)\n                    },\n                    onHashtagClick = {\n                        composedStatusInteraction.onHashtagInStatusClick(locator, it)\n                    },\n                    overflow = TextOverflow.Ellipsis,\n                    maxLines = 3,\n                    onUrlClick = {\n                        browserLauncher.launchWebTabInApp(coroutineScope, it, locator)\n                    },\n                )\n            }\n        }\n        HorizontalDivider(modifier = Modifier.fillMaxWidth())\n    }\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/BlogContent.kt",
    "content": "package com.zhangke.fread.status.ui\n\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.expandVertically\nimport androidx.compose.animation.shrinkVertically\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.wrapContentHeight\nimport androidx.compose.foundation.text.selection.SelectionContainer\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.drawBehind\nimport androidx.compose.ui.geometry.CornerRadius\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.geometry.Rect\nimport androidx.compose.ui.geometry.RoundRect\nimport androidx.compose.ui.geometry.Size\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.Path\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.TextUnit\nimport androidx.compose.ui.unit.dp\nimport com.zhangke.framework.architect.theme.inverseOnSurfaceDark\nimport com.zhangke.framework.composable.noRippleClick\nimport com.zhangke.framework.utils.toPx\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.blog.Blog\nimport com.zhangke.fread.status.blog.BlogPoll\nimport com.zhangke.fread.status.model.BlogTranslationUiState\nimport com.zhangke.fread.status.model.HashtagInStatus\nimport com.zhangke.fread.status.model.Mention\nimport com.zhangke.fread.status.model.isRss\nimport com.zhangke.fread.status.richtext.RichText\nimport com.zhangke.fread.status.richtext.buildRichText\nimport com.zhangke.fread.status.ui.common.BlogTranslateLabel\nimport com.zhangke.fread.status.ui.embed.BlogEmbedsUi\nimport com.zhangke.fread.status.ui.image.OnBlogMediaClick\nimport com.zhangke.fread.status.ui.label.StatusBottomEditedLabel\nimport com.zhangke.fread.status.ui.label.StatusBottomInteractionLabel\nimport com.zhangke.fread.status.ui.label.StatusBottomTimeLabel\nimport com.zhangke.fread.status.ui.media.BlogMedias\nimport com.zhangke.fread.status.ui.poll.BlogPoll\nimport com.zhangke.fread.status.ui.richtext.FreadRichText\nimport com.zhangke.fread.status.ui.style.LocalStatusUiConfig\nimport com.zhangke.fread.status.ui.style.StatusStyle\nimport com.zhangke.fread.status.ui.style.StatusStyle.ContentStyle\n\n/**\n * 博客正文部分，仅包含内容，投票，媒体，链接预览卡片。\n */\n@Composable\nfun BlogContent(\n    modifier: Modifier,\n    blog: Blog,\n    isOwner: Boolean?,\n    style: StatusStyle,\n    indexOfFeeds: Int,\n    sharedElementId: String? = null,\n    onBlogClick: (Blog) -> Unit,\n    onMediaClick: OnBlogMediaClick = {},\n    blogTranslationState: BlogTranslationUiState = BlogTranslationUiState.DEFAULT,\n    onVoted: (List<BlogPoll.Option>) -> Unit = {},\n    onHashtagInStatusClick: (HashtagInStatus) -> Unit = {},\n    onMaybeHashtagClick: (String) -> Unit,\n    onBoostedClick: ((String) -> Unit)? = null,\n    onFavouritedClick: ((String) -> Unit)? = null,\n    onUrlClick: (url: String) -> Unit = {},\n    onMentionClick: (Mention) -> Unit = {},\n    onMentionDidClick: (String) -> Unit = {},\n    onShowOriginalClick: () -> Unit,\n    onUnavailableQuoteClick: (String) -> Unit,\n    detailModel: Boolean = false,\n    editedTime: String? = null,\n) {\n    Column(\n        modifier = modifier,\n    ) {\n        BlogTranslateLabel(\n            modifier = Modifier,\n            style = style,\n            blogTranslationState = blogTranslationState,\n            onShowOriginalClick = onShowOriginalClick,\n        )\n        if (detailModel) {\n            SelectionContainer {\n                Column {\n                    BlogTextContentSection(\n                        blog = blog,\n                        blogTranslationState = blogTranslationState,\n                        style = style.contentStyle,\n                        onHashtagInStatusClick = onHashtagInStatusClick,\n                        onMentionClick = onMentionClick,\n                        onMentionDidClick = onMentionDidClick,\n                        onMaybeHashtagClick = onMaybeHashtagClick,\n                        onUrlClick = onUrlClick,\n                    )\n                }\n            }\n        } else {\n            BlogTextContentSection(\n                blog = blog,\n                blogTranslationState = blogTranslationState,\n                style = style.contentStyle,\n                onHashtagInStatusClick = onHashtagInStatusClick,\n                onMentionClick = onMentionClick,\n                onMentionDidClick = onMentionDidClick,\n                onUrlClick = onUrlClick,\n                onMaybeHashtagClick = onMaybeHashtagClick,\n            )\n        }\n        val sensitive = blog.sensitive || blog.sensitiveByFilter\n        if (blog.poll != null) {\n            BlogPoll(\n                modifier = Modifier\n                    .padding(top = style.contentStyle.contentVerticalSpacing)\n                    .fillMaxWidth(),\n                poll = blog.poll!!,\n                isSelf = isOwner,\n                blogTranslationState = blogTranslationState,\n                onVoted = {\n                    onVoted(it)\n                },\n            )\n        } else if (blog.mediaList.isNotEmpty()) {\n            BlogMedias(\n                modifier = Modifier\n                    .padding(top = style.contentStyle.contentVerticalSpacing)\n                    .fillMaxWidth(),\n                mediaList = blog.mediaList,\n                sharedElementId = sharedElementId ?: blog.id,\n                blogTranslationState = blogTranslationState,\n                indexInList = indexOfFeeds,\n                sensitive = sensitive,\n                onMediaClick = onMediaClick,\n            )\n        } else if (blog.embeds.isNotEmpty()) {\n            BlogEmbedsUi(\n                modifier = Modifier\n                    .padding(top = style.contentStyle.contentVerticalSpacing)\n                    .fillMaxWidth(),\n                embeds = blog.embeds,\n                style = style,\n                onContentClick = onBlogClick,\n                onUrlClick = onUrlClick,\n                onUnavailableQuoteClick = onUnavailableQuoteClick,\n            )\n        }\n\n        if (detailModel) {\n            StatusBottomTimeLabel(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .padding(top = style.contentStyle.contentVerticalSpacing),\n                blog = blog,\n                specificTime = blog.formattedCreateAt,\n                style = style,\n                onUrlClick = onUrlClick,\n            )\n            if (!editedTime.isNullOrEmpty()) {\n                StatusBottomEditedLabel(\n                    modifier = Modifier\n                        .padding(top = style.contentStyle.contentVerticalSpacing),\n                    editedAt = editedTime,\n                    style = style,\n                )\n            }\n            if (blog.like.likedCount != null && blog.forward.forwardCount != null) {\n                StatusBottomInteractionLabel(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .padding(top = style.contentStyle.contentVerticalSpacing),\n                    boostedCount = blog.forward.forwardCount!!,\n                    favouritedCount = blog.like.likedCount!!,\n                    style = style,\n                    onBoostedClick = { onBoostedClick?.invoke(blog.id) },\n                    onFavouritedClick = { onFavouritedClick?.invoke(blog.id) },\n                )\n            }\n        }\n    }\n}\n\n@Composable\nfun BlogTextContentSection(\n    blog: Blog,\n    style: ContentStyle,\n    blogTranslationState: BlogTranslationUiState? = null,\n    onHashtagInStatusClick: (HashtagInStatus) -> Unit = {},\n    onMaybeHashtagClick: (String) -> Unit = {},\n    onMentionClick: (Mention) -> Unit = {},\n    onMentionDidClick: (String) -> Unit = {},\n    onUrlClick: (url: String) -> Unit = {},\n) {\n    val contentMaxLine: Int = if (blog.platform.protocol.isRss) {\n        style.maxLine\n    } else {\n        Int.MAX_VALUE\n    }\n    val showWarning = blog.spoilerText.isNotEmpty() || blog.sensitiveByFilter\n    val spoilerText = blog.spoilerText\n    if (showWarning) {\n        val statusConfig = LocalStatusUiConfig.current\n        var hideContent by rememberSaveable(\n            showWarning,\n            spoilerText,\n            statusConfig.alwaysShowSensitiveContent,\n        ) {\n            mutableStateOf(!statusConfig.alwaysShowSensitiveContent)\n        }\n        val humanizedSpoilerText = if (blogTranslationState?.showingTranslation == true) {\n            blogTranslationState.blogTranslation!!.getHumanizedSpoilerText(blog)\n        } else if (blog.spoilerText.isNotEmpty()) {\n            blog.humanizedSpoilerText\n        } else {\n            val text =\n                org.jetbrains.compose.resources.stringResource(\n                    LocalizedString.statusUiSensitiveByFilter,\n                    blog.filtered!!.first().title,\n                )\n            remember { buildRichText(document = text, type = blog.richTextType) }\n        }\n        SpoilerText(\n            modifier = Modifier,\n            hideContent = hideContent,\n            spoilerText = humanizedSpoilerText,\n            fontSize = style.contentSize,\n            onShowContent = { hideContent = false },\n            onHideContent = { hideContent = true },\n            onHashtagInStatusClick = onHashtagInStatusClick,\n            onMentionClick = onMentionClick,\n            onMentionDidClick = onMentionDidClick,\n            onUrlClick = onUrlClick,\n        )\n        if (blog.content.isNotEmpty()) {\n            AnimatedVisibility(\n                visible = !hideContent,\n                enter = expandVertically(),\n                exit = shrinkVertically(),\n            ) {\n                val humanizedContent = if (blogTranslationState?.showingTranslation == true) {\n                    blogTranslationState.blogTranslation!!.getHumanizedContent(blog)\n                } else {\n                    blog.humanizedContent\n                }\n                FreadRichText(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .padding(top = 4.dp)\n                        .wrapContentHeight(),\n                    richText = humanizedContent,\n                    maxLines = contentMaxLine,\n                    onMentionClick = onMentionClick,\n                    onMentionDidClick = onMentionDidClick,\n                    onHashtagClick = onHashtagInStatusClick,\n                    onMaybeHashtagClick = onMaybeHashtagClick,\n                    onUrlClick = onUrlClick,\n                    fontSize = style.contentSize,\n                )\n            }\n        }\n    } else {\n        if (!blog.title.isNullOrEmpty()) {\n            Text(\n                modifier = Modifier,\n                text = blog.title!!,\n                fontWeight = FontWeight.Bold,\n                fontSize = style.titleSize,\n                overflow = TextOverflow.Ellipsis,\n                maxLines = 1,\n                style = MaterialTheme.typography.titleMedium,\n            )\n        }\n        if (!blog.description.isNullOrEmpty()) {\n            val topPadding = if (blog.title.isNullOrEmpty()) {\n                style.contentVerticalSpacing\n            } else {\n                style.contentVerticalSpacing / 2\n            }\n            FreadRichText(\n                modifier = Modifier\n                    .padding(top = topPadding),\n                richText = blog.humanizedDescription,\n                maxLines = contentMaxLine,\n                onMentionClick = onMentionClick,\n                onMentionDidClick = onMentionDidClick,\n                onHashtagClick = onHashtagInStatusClick,\n                onMaybeHashtagClick = onMaybeHashtagClick,\n                onUrlClick = onUrlClick,\n                fontSize = style.contentSize,\n            )\n        }\n        if (\n            blog.title.isNullOrEmpty() &&\n            blog.description.isNullOrEmpty() &&\n            blog.content.isNotEmpty()\n        ) {\n            val humanizedContent = if (blogTranslationState?.showingTranslation == true) {\n                blogTranslationState.blogTranslation!!.getHumanizedContent(blog)\n            } else {\n                blog.humanizedContent\n            }\n            FreadRichText(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .wrapContentHeight(),\n                richText = humanizedContent,\n                maxLines = contentMaxLine,\n                onMentionClick = onMentionClick,\n                onMentionDidClick = onMentionDidClick,\n                onHashtagClick = onHashtagInStatusClick,\n                onMaybeHashtagClick = onMaybeHashtagClick,\n                fontSize = style.contentSize,\n                onUrlClick = onUrlClick,\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun SpoilerText(\n    modifier: Modifier,\n    hideContent: Boolean,\n    spoilerText: RichText,\n    fontSize: TextUnit,\n    onShowContent: () -> Unit,\n    onHideContent: () -> Unit,\n    onUrlClick: (url: String) -> Unit,\n    onHashtagInStatusClick: (HashtagInStatus) -> Unit,\n    onMentionClick: (Mention) -> Unit,\n    onMentionDidClick: (String) -> Unit,\n) {\n    Box(\n        modifier = modifier\n            .fillMaxWidth()\n            .wrapContentHeight()\n            .drawSpoilerBackground()\n            .noRippleClick {\n                if (hideContent) {\n                    onShowContent()\n                } else {\n                    onHideContent()\n                }\n            }\n    ) {\n        FreadRichText(\n            modifier = Modifier\n                .fillMaxWidth()\n                .wrapContentHeight()\n                .padding(start = 16.dp, top = 22.dp, end = 16.dp, bottom = 6.dp),\n            richText = spoilerText,\n            color = inverseOnSurfaceDark,\n            onMentionClick = onMentionClick,\n            onMentionDidClick = onMentionDidClick,\n            onHashtagClick = onHashtagInStatusClick,\n            onUrlClick = onUrlClick,\n            fontSize = fontSize,\n        )\n    }\n}\n\n@Composable\nfun Modifier.drawSpoilerBackground(): Modifier {\n    val edgeColor = Color(0xFFFFB84D)\n    val backgroundColor = Color(0xFFFFEED3)\n    val edgeWidth = 8.dp.toPx()\n    val cornerRadiiPx = 6.dp.toPx()\n    val cornerRadius = CornerRadius(cornerRadiiPx, cornerRadiiPx)\n    return this.drawBehind {\n        val canvasWidth = size.width\n        val canvasHeight = size.height\n        val startEdge = Path().apply {\n            addRoundRect(\n                RoundRect(\n                    rect = Rect(\n                        offset = Offset.Zero,\n                        Size(width = edgeWidth, height = canvasHeight),\n                    ),\n                    topLeft = cornerRadius,\n                    bottomLeft = cornerRadius,\n                )\n            )\n        }\n        val endEdge = Path().apply {\n            addRoundRect(\n                RoundRect(\n                    rect = Rect(\n                        offset = Offset(x = canvasWidth - edgeWidth, y = 0F),\n                        Size(width = edgeWidth, height = canvasHeight),\n                    ),\n                    topRight = cornerRadius,\n                    bottomRight = cornerRadius,\n                )\n            )\n        }\n        drawPath(startEdge, edgeColor)\n        drawPath(endEdge, edgeColor)\n        drawRect(\n            color = backgroundColor,\n            topLeft = Offset(x = edgeWidth, y = 0F),\n            size = size.copy(width = canvasWidth - edgeWidth * 2),\n        )\n        var pointStartOffset = 18.dp.toPx()\n        val pointRadii = 1.5.dp.toPx()\n        repeat(3) {\n            drawCircle(\n                color = Color.Black.copy(alpha = 0.8F),\n                radius = pointRadii,\n                center = Offset(x = pointStartOffset, y = 14.dp.toPx())\n            )\n            pointStartOffset += pointRadii + 6.dp.toPx()\n        }\n    }\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/BlogDivider.kt",
    "content": "package com.zhangke.fread.status.ui\n\nimport androidx.compose.material3.HorizontalDivider\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\n\n@Composable\nfun BlogDivider(\n    modifier: Modifier = Modifier,\n) {\n    HorizontalDivider(\n        modifier = modifier,\n        thickness = 0.5.dp\n    )\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/BlogUi.kt",
    "content": "package com.zhangke.fread.status.ui\n\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.layout.onGloballyPositioned\nimport androidx.compose.ui.layout.positionInParent\nimport androidx.compose.ui.unit.dp\nimport com.zhangke.fread.common.handler.LocalTextHandler\nimport com.zhangke.fread.status.author.BlogAuthor\nimport com.zhangke.fread.status.blog.Blog\nimport com.zhangke.fread.status.blog.BlogPoll\nimport com.zhangke.fread.status.model.BlogTranslationUiState\nimport com.zhangke.fread.status.model.HashtagInStatus\nimport com.zhangke.fread.status.model.Mention\nimport com.zhangke.fread.status.model.StatusActionType\nimport com.zhangke.fread.status.ui.action.StatusBottomInteractionPanel\nimport com.zhangke.fread.status.ui.image.OnBlogMediaClick\nimport com.zhangke.fread.status.ui.style.StatusStyle\nimport com.zhangke.fread.status.ui.threads.ThreadsType\nimport com.zhangke.fread.status.ui.threads.threads\n\n@Composable\nfun BlogUi(\n    modifier: Modifier,\n    blog: Blog,\n    blogTranslationState: BlogTranslationUiState,\n    isOwner: Boolean?,\n    logged: Boolean?,\n    indexInList: Int,\n    sharedElementId: String? = null,\n    style: StatusStyle,\n    topLabels: List<@Composable () -> Unit>,\n    reblogAuthor: BlogAuthor? = null,\n    onInteractive: (StatusActionType, Blog) -> Unit,\n    onMediaClick: OnBlogMediaClick,\n    onUserInfoClick: (BlogAuthor) -> Unit,\n    onVoted: (List<BlogPoll.Option>) -> Unit,\n    onHashtagInStatusClick: (HashtagInStatus) -> Unit,\n    onMaybeHashtagClick: (String) -> Unit,\n    onUrlClick: (url: String) -> Unit,\n    onMentionClick: (Mention) -> Unit,\n    onMentionDidClick: (String) -> Unit,\n    onShowOriginalClick: () -> Unit,\n    onBlogClick: (Blog) -> Unit,\n    onTranslateClick: () -> Unit,\n    continueThreadLabelHeight: Int? = null,\n    onBoostedClick: ((String) -> Unit)? = null,\n    onFavouritedClick: ((String) -> Unit)? = null,\n    onFollowClick: ((BlogAuthor) -> Unit)? = null,\n    detailModel: Boolean = false,\n    showDivider: Boolean = true,\n    showBottomPanel: Boolean = true,\n    showMoreOperationIcon: Boolean = true,\n    threadsType: ThreadsType = ThreadsType.NONE,\n    onOpenBlogWithOtherAccountClick: (Blog) -> Unit = {},\n    onUnavailableQuoteClick: (String) -> Unit = {},\n) {\n    val textHandler = LocalTextHandler.current\n    var infoToTopSpacing: Float? by remember { mutableStateOf(null) }\n    Column(\n        modifier = modifier\n            .fillMaxWidth()\n            .threads(\n                threadsType = threadsType,\n                infoToTopSpacing = infoToTopSpacing,\n                style = style,\n                continueThreadLabelHeight = continueThreadLabelHeight,\n            )\n    ) {\n        if (topLabels.isNotEmpty()) {\n            Spacer(modifier = Modifier.height(style.containerTopPadding / 2))\n        }\n        topLabels.forEachIndexed { index, composable ->\n            composable()\n            Spacer(modifier = Modifier.height(style.infolineToTopLabelPadding))\n        }\n        val infoTopPadding = if (topLabels.isEmpty()) {\n            style.containerTopPadding\n        } else {\n            0.dp\n        }\n        StatusInfoLine(\n            modifier = Modifier\n                .padding(top = infoTopPadding)\n                .fillMaxWidth()\n                .let {\n                    if (threadsType != ThreadsType.NONE && threadsType != ThreadsType.UNSPECIFIED) {\n                        it.onGloballyPositioned { coordinates ->\n                            if (coordinates.positionInParent().y != infoToTopSpacing) {\n                                infoToTopSpacing = coordinates.positionInParent().y\n                            }\n                        }\n                    } else {\n                        it\n                    }\n                },\n            blog = blog,\n            blogTranslationState = blogTranslationState,\n            displayTime = blog.formattingDisplayTime.formattedTime(),\n            visibility = blog.visibility,\n            isOwner = isOwner,\n            showMoreOperationIcon = showMoreOperationIcon,\n            allowToShowFollowButton = isOwner == false && detailModel,\n            onInteractive = onInteractive,\n            onUserInfoClick = onUserInfoClick,\n            onUrlClick = onUrlClick,\n            onFollowClick = onFollowClick,\n            style = style,\n            reblogAuthor = reblogAuthor,\n            editedAt = blog.editedAt?.instant,\n            onTranslateClick = onTranslateClick,\n            onOpenBlogWithOtherAccountClick = onOpenBlogWithOtherAccountClick,\n            showOpenBlogWithOtherAccountBtn = true,\n        )\n        BlogContent(\n            modifier = Modifier\n                .fillMaxWidth()\n                .padding(\n                    start = style.containerStartPadding + style.contentStyle.startPadding,\n                    top = style.contentStyle.contentVerticalSpacing,\n                    end = style.containerEndPadding,\n                ),\n            blog = blog,\n            isOwner = isOwner,\n            blogTranslationState = blogTranslationState,\n            detailModel = detailModel,\n            indexOfFeeds = indexInList,\n            sharedElementId = sharedElementId,\n            style = style,\n            onMediaClick = onMediaClick,\n            onVoted = onVoted,\n            onUrlClick = onUrlClick,\n            onBoostedClick = onBoostedClick,\n            onFavouritedClick = onFavouritedClick,\n            editedTime = blog.formattedEditAt,\n            onHashtagInStatusClick = onHashtagInStatusClick,\n            onMentionClick = onMentionClick,\n            onMentionDidClick = onMentionDidClick,\n            onShowOriginalClick = onShowOriginalClick,\n            onBlogClick = onBlogClick,\n            onMaybeHashtagClick = onMaybeHashtagClick,\n            onUnavailableQuoteClick = onUnavailableQuoteClick,\n        )\n        if (showBottomPanel) {\n            StatusBottomInteractionPanel(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .padding(\n                        start = style.containerStartPadding / 2 + style.bottomPanelStyle.startPadding,\n                        top = style.contentStyle.contentVerticalSpacing,\n                        end = style.containerEndPadding / 2\n                    ),\n                style = style,\n                blog = blog,\n                logged = logged,\n                onInteractive = { type, blog ->\n                    if (type == StatusActionType.SHARE) {\n                        textHandler.shareUrl(blog.link, blog.content)\n                        return@StatusBottomInteractionPanel\n                    }\n                    onInteractive(type, blog)\n                },\n            )\n        }\n        Spacer(\n            modifier = Modifier\n                .fillMaxWidth()\n                .height(style.containerBottomPadding)\n        )\n        if (showDivider) {\n            BlogDivider()\n        }\n    }\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/ComposedStatusInteraction.kt",
    "content": "package com.zhangke.fread.status.ui\n\nimport com.zhangke.fread.status.author.BlogAuthor\nimport com.zhangke.fread.status.blog.Blog\nimport com.zhangke.fread.status.blog.BlogPoll\nimport com.zhangke.fread.status.model.Hashtag\nimport com.zhangke.fread.status.model.HashtagInStatus\nimport com.zhangke.fread.status.model.Mention\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.model.StatusActionType\nimport com.zhangke.fread.status.model.StatusProviderProtocol\nimport com.zhangke.fread.status.model.StatusUiState\nimport com.zhangke.fread.status.platform.BlogPlatform\n\ninterface ComposedStatusInteraction {\n\n    fun onStatusInteractive(status: StatusUiState, type: StatusActionType)\n    fun onUserInfoClick(locator: PlatformLocator, blogAuthor: BlogAuthor)\n    fun onVoted(status: StatusUiState, blogPollOptions: List<BlogPoll.Option>)\n    fun onHashtagInStatusClick(locator: PlatformLocator, hashtagInStatus: HashtagInStatus)\n    fun onHashtagClick(locator: PlatformLocator, tag: Hashtag)\n    fun onMaybeHashtagClick(\n        locator: PlatformLocator,\n        protocol: StatusProviderProtocol,\n        hashtag: String\n    )\n\n    fun onMentionClick(locator: PlatformLocator, mention: Mention)\n    fun onMentionClick(locator: PlatformLocator, did: String, protocol: StatusProviderProtocol)\n    fun onStatusClick(status: StatusUiState)\n    fun onBlogClick(locator: PlatformLocator, blog: Blog)\n    fun onBlogIdClick(locator: PlatformLocator, platform: BlogPlatform, blogId: String)\n    fun onBlockClick(locator: PlatformLocator, blog: Blog)\n    fun onFollowClick(locator: PlatformLocator, target: BlogAuthor)\n    fun onUnfollowClick(locator: PlatformLocator, target: BlogAuthor)\n    fun onBoostedClick(locator: PlatformLocator, status: StatusUiState)\n    fun onFavouritedClick(locator: PlatformLocator, status: StatusUiState)\n    fun onTranslateClick(locator: PlatformLocator, status: StatusUiState)\n    fun onShowOriginalClick(status: StatusUiState)\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/StatusInfoLine.kt",
    "content": "@file:OptIn(ExperimentalTime::class)\n\npackage com.zhangke.fread.status.ui\n\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.heightIn\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.wrapContentWidth\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Lock\nimport androidx.compose.material.icons.filled.LockOpen\nimport androidx.compose.material3.Button\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport com.zhangke.framework.composable.noRippleClick\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.author.BlogAuthor\nimport com.zhangke.fread.status.blog.Blog\nimport com.zhangke.fread.status.model.BlogTranslationUiState\nimport com.zhangke.fread.status.model.Relationships\nimport com.zhangke.fread.status.model.StatusActionType\nimport com.zhangke.fread.status.model.StatusVisibility\nimport com.zhangke.fread.status.ui.action.StatusMoreInteractionIcon\nimport com.zhangke.fread.status.ui.richtext.FreadRichText\nimport com.zhangke.fread.status.ui.style.StatusStyle\nimport kotlinx.datetime.Instant\nimport org.jetbrains.compose.resources.stringResource\nimport kotlin.time.ExperimentalTime\n\n/**\n * Status 头部信息行，主要包括头像，\n * 用户名，Handle，时间，更多按钮等。\n */\n@Composable\nfun StatusInfoLine(\n    modifier: Modifier,\n    blog: Blog,\n    blogTranslationState: BlogTranslationUiState,\n    isOwner: Boolean?,\n    displayTime: String,\n    style: StatusStyle,\n    visibility: StatusVisibility,\n    allowToShowFollowButton: Boolean,\n    showMoreOperationIcon: Boolean = true,\n    onUrlClick: (url: String) -> Unit = {},\n    onInteractive: (StatusActionType, Blog) -> Unit = { _, _ -> },\n    onUserInfoClick: ((BlogAuthor) -> Unit)? = null,\n    onFollowClick: ((BlogAuthor) -> Unit)? = null,\n    onTranslateClick: () -> Unit = {},\n    reblogAuthor: BlogAuthor? = null,\n    editedAt: Instant? = null,\n    onOpenBlogWithOtherAccountClick: (Blog) -> Unit = {},\n    showOpenBlogWithOtherAccountBtn: Boolean = true,\n) {\n    val blogAuthor = blog.author\n    Row(\n        modifier = modifier.padding(start = style.containerStartPadding),\n    ) {\n        BlogAuthorAvatar(\n            modifier = Modifier\n                .size(style.infoLineStyle.avatarSize),\n            onClick = {\n                onUserInfoClick?.invoke(blogAuthor)\n            },\n            reblogAvatar = reblogAuthor?.avatar,\n            authorAvatar = blogAuthor.avatar,\n        )\n        Column(\n            modifier = Modifier\n                .weight(1F)\n                .padding(start = style.infoLineStyle.nameToAvatarSpacing, end = 6.dp),\n        ) {\n            FreadRichText(\n                modifier = Modifier\n                    .wrapContentWidth(unbounded = false)\n                    .noRippleClick(enabled = onUserInfoClick != null) {\n                        onUserInfoClick?.invoke(blogAuthor)\n                    },\n                maxLines = 1,\n                overflow = TextOverflow.Ellipsis,\n                richText = blogAuthor.humanizedName,\n                onUrlClick = onUrlClick,\n                fontSize = style.infoLineStyle.nameSize,\n            )\n            Row(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .padding(top = 1.dp),\n                verticalAlignment = Alignment.CenterVertically,\n            ) {\n                if (visibility == StatusVisibility.PRIVATE ||\n                    visibility == StatusVisibility.UNLISTED ||\n                    visibility == StatusVisibility.DIRECT\n                ) {\n                    Icon(\n                        modifier = Modifier\n                            .padding(end = 4.dp)\n                            .size(14.dp),\n                        imageVector = if (visibility == StatusVisibility.UNLISTED) {\n                            Icons.Default.LockOpen\n                        } else {\n                            Icons.Default.Lock\n                        },\n                        contentDescription = null,\n                    )\n                }\n                val fontColor = style.secondaryFontColor\n                if (editedAt != null) {\n                    Text(\n                        modifier = Modifier\n                            .padding(end = 4.dp),\n                        text = stringResource(LocalizedString.statusUiInfoLabelEdited),\n                        style = style.infoLineStyle.descStyle,\n                        maxLines = 1,\n                        color = fontColor,\n                        overflow = TextOverflow.Ellipsis,\n                    )\n                }\n                Text(\n                    modifier = Modifier,\n                    text = displayTime,\n                    style = style.infoLineStyle.descStyle,\n                    maxLines = 1,\n                    color = fontColor,\n                    overflow = TextOverflow.Ellipsis,\n                )\n                Text(\n                    modifier = Modifier.padding(start = 4.dp),\n                    text = blogAuthor.prettyHandle,\n                    style = style.infoLineStyle.descStyle,\n                    maxLines = 1,\n                    color = fontColor,\n                    overflow = TextOverflow.Ellipsis,\n                )\n            }\n        }\n\n        val relationships = blogAuthor.relationships\n        if (allowToShowFollowButton && (relationships?.following == false)) {\n            FollowButton(\n                modifier = Modifier\n                    .align(Alignment.Top)\n                    .heightIn(min = 20.dp)\n                    .padding(end = 4.dp),\n                relationships = relationships,\n                contentPadding = PaddingValues(horizontal = 6.dp, vertical = 4.dp),\n                onFollowClick = { onFollowClick?.invoke(blogAuthor) },\n            )\n        }\n\n        if (showMoreOperationIcon) {\n            val moreIconAlign = if (allowToShowFollowButton) {\n                Alignment.CenterVertically\n            } else {\n                Alignment.Top\n            }\n            StatusMoreInteractionIcon(\n                modifier = Modifier\n                    .align(moreIconAlign)\n                    .padding(end = style.containerEndPadding / 2),\n                blog = blog,\n                isOwner = isOwner,\n                blogTranslationState = blogTranslationState,\n                style = style,\n                onActionClick = onInteractive,\n                onTranslateClick = onTranslateClick,\n                showOpenBlogWithOtherAccountBtn = showOpenBlogWithOtherAccountBtn,\n                onOpenBlogWithOtherAccountClick = onOpenBlogWithOtherAccountClick,\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun FollowButton(\n    modifier: Modifier,\n    relationships: Relationships,\n    contentPadding: PaddingValues,\n    onFollowClick: () -> Unit,\n) {\n    Button(\n        modifier = modifier,\n        onClick = onFollowClick,\n        contentPadding = contentPadding,\n    ) {\n        Text(\n            text = if (relationships.followedBy) {\n                stringResource(LocalizedString.statusUiUserDetailRelationshipFollowBack)\n            } else {\n                stringResource(LocalizedString.statusUiUserDetailRelationshipNotFollow)\n            },\n        )\n    }\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/StatusPlaceHolder.kt",
    "content": "package com.zhangke.fread.status.ui\n\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.unit.dp\nimport com.zhangke.framework.composable.LocalContentPadding\nimport com.zhangke.framework.composable.freadPlaceholder\nimport com.zhangke.fread.status.ui.style.StatusInfoStyleDefaults\n\n@Composable\nfun StatusPlaceHolder(\n    modifier: Modifier = Modifier,\n) {\n    Row(modifier = modifier) {\n        Spacer(Modifier.width(16.dp))\n        Box(\n            modifier = Modifier\n                .padding(top = 8.dp)\n                .clip(CircleShape)\n                .size(StatusInfoStyleDefaults.avatarSize)\n                .freadPlaceholder(true),\n        )\n        Spacer(Modifier.width(8.dp))\n        Column(Modifier.weight(1f)) {\n            Spacer(Modifier.height(4.dp))\n            Box(\n                modifier = Modifier\n                    .size(100.dp, 16.dp)\n                    .freadPlaceholder(true),\n            )\n            Spacer(Modifier.height(2.dp))\n            Box(\n                modifier = Modifier\n                    .size(200.dp, 12.dp)\n                    .freadPlaceholder(true),\n            )\n            Spacer(Modifier.height(4.dp))\n            repeat(3) {\n                Box(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .padding(top = 4.dp)\n                        .height(14.dp)\n                        .freadPlaceholder(true),\n                )\n            }\n            Spacer(Modifier.height(8.dp))\n        }\n        Spacer(Modifier.width(16.dp))\n    }\n}\n\n@Composable\nfun StatusListPlaceholder(\n    modifier: Modifier = Modifier,\n) {\n    Box(modifier = modifier.fillMaxSize().padding(LocalContentPadding.current)) {\n        Column(\n            modifier = Modifier.fillMaxSize(),\n        ) {\n            repeat(8) {\n                StatusPlaceHolder(modifier = Modifier.fillMaxWidth())\n                Box(modifier = Modifier.height(6.dp))\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/StatusUi.kt",
    "content": "package com.zhangke.fread.status.ui\n\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport com.zhangke.fread.common.browser.LocalActivityBrowserLauncher\nimport com.zhangke.fread.common.browser.launchWebTabInApp\nimport com.zhangke.fread.status.author.BlogAuthor\nimport com.zhangke.fread.status.model.BlogFiltered\nimport com.zhangke.fread.status.model.StatusUiState\nimport com.zhangke.fread.status.model.StatusVisibility\nimport com.zhangke.fread.status.status.model.Status\nimport com.zhangke.fread.status.ui.image.OnBlogMediaClick\nimport com.zhangke.fread.status.ui.label.ContinueThread\nimport com.zhangke.fread.status.ui.label.ReblogTopLabel\nimport com.zhangke.fread.status.ui.label.StatusMentionOnlyLabel\nimport com.zhangke.fread.status.ui.label.StatusPinnedLabel\nimport com.zhangke.fread.status.ui.style.LocalStatusUiConfig\nimport com.zhangke.fread.status.ui.style.StatusStyle\nimport com.zhangke.fread.status.ui.threads.ThreadsType\n\n@Composable\nfun StatusUi(\n    modifier: Modifier = Modifier,\n    status: StatusUiState,\n    indexInList: Int,\n    sharedElementId: String? = null,\n    style: StatusStyle = LocalStatusUiConfig.current.contentStyle,\n    onMediaClick: OnBlogMediaClick,\n    composedStatusInteraction: ComposedStatusInteraction,\n    detailModel: Boolean = false,\n    showDivider: Boolean = true,\n    threadsType: ThreadsType = ThreadsType.UNSPECIFIED,\n    onOpenBlogWithOtherAccountClick: (StatusUiState) -> Unit = {},\n) {\n    val browserLauncher = LocalActivityBrowserLauncher.current\n    if (status.status.intrinsicBlog.filtered?.firstOrNull()?.action == BlogFiltered.FilterAction.HIDE) {\n        Box(modifier = modifier.size(1.dp))\n        return\n    }\n    val fixedThreadType =\n        if (threadsType == ThreadsType.UNSPECIFIED && status.status.intrinsicBlog.isReply) {\n            ThreadsType.CONTINUED_THREAD\n        } else {\n            threadsType\n        }\n    val rawStatus = status.status\n    val resolvedSharedElementId = sharedElementId ?: rawStatus.id\n    var continueThreadHeight: Int? by remember { mutableStateOf(null) }\n    val coroutineScope = rememberCoroutineScope()\n    Box(modifier = modifier) {\n        BlogUi(\n            modifier = Modifier,\n            blog = rawStatus.intrinsicBlog,\n            isOwner = status.isOwner,\n            logged = status.logged,\n            blogTranslationState = status.blogTranslationState,\n            continueThreadLabelHeight = continueThreadHeight,\n            topLabels = getStatusTopLabel(\n                isReblog = rawStatus is Status.Reblog,\n                pinned = rawStatus.intrinsicBlog.pinned,\n                isReply = rawStatus.intrinsicBlog.isReply,\n                author = rawStatus.triggerAuthor,\n                style = style,\n                threadsType = fixedThreadType,\n                mentionOnly = rawStatus.intrinsicBlog.visibility == StatusVisibility.DIRECT,\n                onUserInfoClick = {\n                    composedStatusInteraction.onUserInfoClick(status.locator, it)\n                },\n                onContinueThreadHeightChanged = { continueThreadHeight = it }\n            ),\n            indexInList = indexInList,\n            sharedElementId = resolvedSharedElementId,\n            threadsType = fixedThreadType,\n            detailModel = detailModel,\n            style = if (detailModel) style else style.contentIndentStyle(),\n            onInteractive = { type, _ ->\n                composedStatusInteraction.onStatusInteractive(status, type)\n            },\n            showDivider = showDivider && threadsType != ThreadsType.ANCESTOR && threadsType != ThreadsType.FIRST_ANCESTOR,\n            onMediaClick = onMediaClick,\n            onUserInfoClick = {\n                composedStatusInteraction.onUserInfoClick(status.locator, it)\n            },\n            onVoted = { options ->\n                composedStatusInteraction.onVoted(status, options)\n            },\n            onHashtagInStatusClick = {\n                composedStatusInteraction.onHashtagInStatusClick(status.locator, it)\n            },\n            onMentionClick = {\n                composedStatusInteraction.onMentionClick(status.locator, it)\n            },\n            onMentionDidClick = {\n                composedStatusInteraction.onMentionClick(\n                    locator = status.locator,\n                    did = it,\n                    protocol = status.status.platform.protocol,\n                )\n            },\n            onFollowClick = {\n                composedStatusInteraction.onFollowClick(status.locator, it)\n            },\n            onUrlClick = {\n                browserLauncher.launchWebTabInApp(coroutineScope, it, status.locator)\n            },\n            onBoostedClick = {\n                composedStatusInteraction.onBoostedClick(status.locator, status)\n            },\n            onFavouritedClick = {\n                composedStatusInteraction.onFavouritedClick(status.locator, status)\n            },\n            onShowOriginalClick = {\n                composedStatusInteraction.onShowOriginalClick(status)\n            },\n            onTranslateClick = {\n                composedStatusInteraction.onTranslateClick(status.locator, status)\n            },\n            onBlogClick = {\n                composedStatusInteraction.onBlockClick(status.locator, it)\n            },\n            onMaybeHashtagClick = {\n                composedStatusInteraction.onMaybeHashtagClick(\n                    locator = status.locator,\n                    protocol = status.status.platform.protocol,\n                    hashtag = it,\n                )\n            },\n            onOpenBlogWithOtherAccountClick = {\n                onOpenBlogWithOtherAccountClick(status)\n            },\n            onUnavailableQuoteClick = {\n                composedStatusInteraction.onBlogIdClick(\n                    locator = status.locator,\n                    platform = status.status.platform,\n                    blogId = rawStatus.id,\n                )\n            },\n        )\n    }\n}\n\nfun getStatusTopLabel(\n    style: StatusStyle,\n    threadsType: ThreadsType,\n    isReblog: Boolean,\n    pinned: Boolean,\n    isReply: Boolean,\n    author: BlogAuthor,\n    mentionOnly: Boolean,\n    onUserInfoClick: (blogAuthor: BlogAuthor) -> Unit,\n    onContinueThreadHeightChanged: (height: Int) -> Unit,\n): List<@Composable () -> Unit> {\n    val labels = mutableListOf<@Composable () -> Unit>()\n    if (isReblog) {\n        labels += {\n            ReblogTopLabel(\n                author = author,\n                style = style,\n                onAuthorClick = onUserInfoClick,\n            )\n        }\n    } else if (threadsType == ThreadsType.CONTINUED_THREAD && isReply) {\n        labels += {\n            ContinueThread(style = style, onHeightChanged = onContinueThreadHeightChanged)\n        }\n    }\n    if (mentionOnly) {\n        labels += {\n            StatusMentionOnlyLabel(\n                modifier = Modifier,\n                style = style,\n            )\n        }\n    }\n    if (pinned) {\n        labels += {\n            StatusPinnedLabel(\n                style = style,\n            )\n        }\n    }\n    return labels\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/action/ModalDropdownMenuItem.kt",
    "content": "package com.zhangke.fread.status.ui.action\n\nimport androidx.compose.material3.DropdownMenuItem\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MenuDefaults\nimport androidx.compose.material3.MenuItemColors\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.graphics.vector.ImageVector\n\n@Composable\nfun ModalDropdownMenuItem(\n    imageVector: ImageVector,\n    text: String,\n    colors: MenuItemColors = MenuDefaults.itemColors(),\n    onClick: () -> Unit,\n) {\n    DropdownMenuItem(\n        text = {\n            Text(text = text)\n        },\n        colors = colors,\n        leadingIcon = {\n            Icon(\n                imageVector = imageVector,\n                contentDescription = text,\n            )\n        },\n        onClick = onClick,\n    )\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/action/StatusActions.kt",
    "content": "package com.zhangke.fread.status.ui.action\n\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.CloudQueue\nimport androidx.compose.material.icons.filled.ContentCopy\nimport androidx.compose.material.icons.filled.Language\nimport androidx.compose.material.icons.filled.PersonSearch\nimport androidx.compose.runtime.Composable\nimport com.zhangke.fread.localization.LocalizedString\nimport org.jetbrains.compose.resources.stringResource\n\n@Composable\nfun DropDownCopyLinkItem(\n    onClick: () -> Unit,\n) {\n    ModalDropdownMenuItem(\n        text = stringResource(LocalizedString.statusUiInteractionCopyUrl),\n        imageVector = Icons.Default.ContentCopy,\n        onClick = onClick,\n    )\n}\n\n@Composable\nfun DropDownOpenInBrowserItem(onClick: () -> Unit) {\n    ModalDropdownMenuItem(\n        text = stringResource(LocalizedString.statusUiInteractionOpenInBrowser),\n        imageVector = Icons.Default.Language,\n        onClick = onClick,\n    )\n}\n\n@Composable\nfun DropDownOpenOriginalInstanceItem(onClick: () -> Unit) {\n    ModalDropdownMenuItem(\n        text = stringResource(LocalizedString.statusUiInteractionOpenOriginalInstance),\n        imageVector = Icons.Default.CloudQueue,\n        onClick = onClick,\n    )\n}\n\n@Composable\nfun DropDownOpenStatusByOtherAccountItem(onClick: () -> Unit) {\n    ModalDropdownMenuItem(\n        text = stringResource(LocalizedString.statusUiInteractionOpenBlogByOtherAccount),\n        imageVector = Icons.Default.PersonSearch,\n        onClick = onClick,\n    )\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/action/StatusBottomInteractionPanel.kt",
    "content": "package com.zhangke.fread.status.ui.action\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButtonDefaults\nimport androidx.compose.material3.LocalContentColor\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.ModalBottomSheet\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.semantics.Role\nimport androidx.compose.ui.unit.dp\nimport com.zhangke.framework.composable.rememberTransientModalBottomSheetState\nimport com.zhangke.fread.status.blog.Blog\nimport com.zhangke.fread.status.model.StatusActionType\nimport com.zhangke.fread.status.ui.style.StatusStyle\nimport kotlinx.coroutines.launch\n\n@Composable\nfun StatusBottomInteractionPanel(\n    modifier: Modifier = Modifier,\n    style: StatusStyle,\n    blog: Blog,\n    logged: Boolean?,\n    onInteractive: (StatusActionType, Blog) -> Unit,\n) {\n    Row(\n        modifier = modifier.fillMaxWidth(),\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        ForwardActionIcon(\n            modifier = Modifier,\n            blog = blog,\n            style = style,\n            logged = logged,\n            onInteractive = onInteractive,\n        )\n        Spacer(modifier = Modifier.weight(1F))\n        StatusActionIcon(\n            modifier = Modifier,\n            imageVector = replyIcon(),\n            enabled = logged == true && blog.reply.support,\n            style = style,\n            contentDescription = replyAlt(),\n            text = blog.reply.repliesCount?.countToLabel(),\n            highLight = false,\n            onClick = { onInteractive(StatusActionType.REPLY, blog) },\n        )\n        Spacer(modifier = Modifier.weight(1F))\n        StatusActionIcon(\n            modifier = Modifier,\n            imageVector = likeIcon(blog.like.liked == true),\n            enabled = logged == true && blog.like.support,\n            style = style,\n            contentDescription = likeAlt(),\n            text = blog.like.likedCount?.countToLabel(),\n            highLight = blog.like.liked == true,\n            onClick = { onInteractive(StatusActionType.LIKE, blog) },\n        )\n        if (blog.bookmark.support) {\n            Spacer(modifier = Modifier.weight(1F))\n            StatusActionIcon(\n                modifier = Modifier,\n                imageVector = bookmarkIcon(blog.bookmark.bookmarked == true),\n                enabled = logged == true,\n                style = style,\n                contentDescription = bookmarkAlt(blog.bookmark.bookmarked == true),\n                text = null,\n                highLight = blog.bookmark.bookmarked == true,\n                onClick = { onInteractive(StatusActionType.BOOKMARK, blog) },\n            )\n        }\n        Spacer(modifier = Modifier.weight(1F))\n        StatusActionIcon(\n            modifier = Modifier,\n            imageVector = shareIcon(),\n            enabled = true,\n            style = style,\n            contentDescription = shareAlt(),\n            text = null,\n            highLight = false,\n            contentAlignment = Alignment.CenterEnd,\n            onClick = { onInteractive(StatusActionType.SHARE, blog) },\n        )\n    }\n}\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nprivate fun ForwardActionIcon(\n    modifier: Modifier,\n    blog: Blog,\n    style: StatusStyle,\n    logged: Boolean?,\n    onInteractive: (StatusActionType, Blog) -> Unit,\n) {\n    var showForwardDialog by remember { mutableStateOf(false) }\n    val coroutineScope = rememberCoroutineScope()\n    val sheetState = rememberTransientModalBottomSheetState()\n    val highlight = blog.forward.forward == true\n    StatusActionIcon(\n        modifier = modifier,\n        imageVector = forwardIcon(),\n        enabled = logged == true && (blog.forward.support || blog.quote.support),\n        style = style,\n        contentDescription = forwardAlt(),\n        text = blog.forward.forwardCount?.countToLabel(),\n        highLight = highlight,\n        contentAlignment = Alignment.CenterStart,\n        onClick = {\n            if (!blog.quote.support) {\n                onInteractive(StatusActionType.FORWARD, blog)\n            } else {\n                showForwardDialog = true\n            }\n        },\n    )\n    if (showForwardDialog) {\n        ModalBottomSheet(\n            sheetState = sheetState,\n            onDismissRequest = { showForwardDialog = false },\n        ) {\n            Column(\n                modifier = Modifier.fillMaxWidth(),\n            ) {\n                val contentColor = if (highlight) {\n                    MaterialTheme.colorScheme.tertiary\n                } else {\n                    LocalContentColor.current\n                }\n                Row(\n                    modifier = Modifier.fillMaxWidth()\n                        .clickable {\n                            coroutineScope.launch {\n                                sheetState.hide()\n                                showForwardDialog = false\n                                onInteractive(StatusActionType.FORWARD, blog)\n                            }\n                        }\n                        .padding(vertical = 16.dp, horizontal = 16.dp),\n                    verticalAlignment = Alignment.CenterVertically,\n                ) {\n                    Icon(\n                        modifier = Modifier.size(18.dp),\n                        imageVector = forwardIcon(),\n                        contentDescription = forwardAlt(),\n                        tint = contentColor,\n                    )\n                    Spacer(modifier = Modifier.size(8.dp))\n                    Text(\n                        modifier = Modifier.padding(start = 2.dp),\n                        text = if (highlight) {\n                            unforwardAlt()\n                        } else {\n                            forwardAlt()\n                        },\n                        maxLines = 1,\n                        color = contentColor,\n                    )\n                }\n                val quoteContentColor = if (blog.quote.enabled) {\n                    IconButtonDefaults.iconButtonColors().contentColor\n                } else {\n                    IconButtonDefaults.iconButtonColors().disabledContentColor\n                }\n                CompositionLocalProvider(LocalContentColor provides quoteContentColor) {\n                    Row(\n                        modifier = Modifier.fillMaxWidth()\n                            .clickable(\n                                enabled = blog.quote.enabled,\n                                role = Role.Button,\n                            ) {\n                                coroutineScope.launch {\n                                    sheetState.hide()\n                                    showForwardDialog = false\n                                    onInteractive(StatusActionType.QUOTE, blog)\n                                }\n                            }\n                            .padding(vertical = 16.dp, horizontal = 16.dp),\n                        verticalAlignment = Alignment.CenterVertically,\n                    ) {\n                        Icon(\n                            modifier = Modifier.size(18.dp),\n                            imageVector = quoteInLeftIcon(),\n                            contentDescription = quoteAlt(),\n                        )\n                        Spacer(modifier = Modifier.size(8.dp))\n                        Text(\n                            modifier = Modifier.padding(start = 2.dp),\n                            text = quoteAlt(),\n                            maxLines = 1,\n                        )\n                    }\n                }\n                Spacer(modifier = Modifier.height(16.dp))\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun StatusActionIcon(\n    modifier: Modifier = Modifier,\n    imageVector: ImageVector,\n    enabled: Boolean,\n    contentDescription: String,\n    style: StatusStyle,\n    text: String? = null,\n    highLight: Boolean,\n    onClick: () -> Unit,\n    contentAlignment: Alignment = Alignment.Center,\n) {\n    StatusIconButton(\n        modifier = modifier.height(style.bottomPanelStyle.iconSize),\n        onClick = onClick,\n        enabled = enabled,\n        contentAlignment = contentAlignment,\n    ) {\n        Row(\n            modifier.padding(horizontal = 6.dp),\n            verticalAlignment = Alignment.CenterVertically,\n        ) {\n            val contentColor = if (highLight) {\n                MaterialTheme.colorScheme.tertiary\n            } else {\n                LocalContentColor.current\n            }\n            Icon(\n                modifier = Modifier.size(18.dp),\n                imageVector = imageVector,\n                contentDescription = contentDescription,\n                tint = contentColor,\n            )\n            if (text != null) {\n                Text(\n                    modifier = Modifier.padding(start = 2.dp),\n                    text = text,\n                    maxLines = 1,\n                    color = contentColor,\n                    style = MaterialTheme.typography.labelMedium,\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/action/StatusIconButton.kt",
    "content": "package com.zhangke.fread.status.ui.action\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.sizeIn\nimport androidx.compose.material3.IconButtonColors\nimport androidx.compose.material3.IconButtonDefaults\nimport androidx.compose.material3.LocalContentColor\nimport androidx.compose.material3.LocalMinimumInteractiveComponentSize\nimport androidx.compose.material3.ripple\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.semantics.Role\nimport androidx.compose.ui.unit.coerceAtLeast\nimport androidx.compose.ui.unit.dp\n\n@Composable\nfun StatusIconButton(\n    onClick: () -> Unit,\n    modifier: Modifier = Modifier,\n    enabled: Boolean = true,\n    colors: IconButtonColors = IconButtonDefaults.iconButtonColors(),\n    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },\n    contentAlignment: Alignment = Alignment.Center,\n    content: @Composable () -> Unit,\n) {\n    val size = LocalMinimumInteractiveComponentSize.current.coerceAtLeast(0.dp)\n    Box(\n        modifier = modifier\n            .sizeIn(minWidth = size, minHeight = size)\n            .background(color = if (enabled) colors.containerColor else colors.disabledContainerColor)\n            .clickable(\n                onClick = onClick,\n                enabled = enabled,\n                role = Role.Button,\n                interactionSource = interactionSource,\n                indication = ripple(\n                    bounded = false,\n                    radius = 20.dp,\n                )\n            ),\n        contentAlignment = contentAlignment,\n    ) {\n        val contentColor = if (enabled) colors.contentColor else colors.disabledContentColor\n        CompositionLocalProvider(LocalContentColor provides contentColor, content = content)\n    }\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/action/StatusInteractiveExts.kt",
    "content": "package com.zhangke.fread.status.ui.action\n\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Bookmark\nimport androidx.compose.material.icons.filled.BookmarkBorder\nimport androidx.compose.material.icons.filled.Delete\nimport androidx.compose.material.icons.filled.Edit\nimport androidx.compose.material.icons.filled.Favorite\nimport androidx.compose.material.icons.filled.FavoriteBorder\nimport androidx.compose.material.icons.filled.PushPin\nimport androidx.compose.material.icons.outlined.PushPin\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport com.zhangke.framework.utils.formatToHumanReadable\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.statusui.Res\nimport com.zhangke.fread.statusui.ic_format_quote\nimport com.zhangke.fread.statusui.ic_format_quote_in_left\nimport com.zhangke.fread.statusui.ic_share\nimport com.zhangke.fread.statusui.ic_status_comment\nimport com.zhangke.fread.statusui.ic_status_forward\nimport org.jetbrains.compose.resources.stringResource\nimport org.jetbrains.compose.resources.vectorResource\n\n@Composable\nfun likeIcon(liked: Boolean): ImageVector {\n    return if (liked) Icons.Default.Favorite else Icons.Default.FavoriteBorder\n}\n\n@Composable\ninternal fun forwardIcon(): ImageVector {\n    return vectorResource(Res.drawable.ic_status_forward)\n}\n\n@Composable\nfun quoteIcon(): ImageVector {\n    return vectorResource(Res.drawable.ic_format_quote)\n}\n\n@Composable\nfun quoteInLeftIcon(): ImageVector {\n    return vectorResource(Res.drawable.ic_format_quote_in_left)\n}\n\n@Composable\nfun replyIcon(): ImageVector {\n    return vectorResource(Res.drawable.ic_status_comment)\n}\n\n@Composable\ninternal fun bookmarkIcon(bookmarked: Boolean): ImageVector {\n    return if (bookmarked) Icons.Default.Bookmark else Icons.Default.BookmarkBorder\n}\n\n@Composable\ninternal fun deleteIcon(): ImageVector {\n    return Icons.Default.Delete\n}\n\n@Composable\ninternal fun shareIcon(): ImageVector {\n    return vectorResource(Res.drawable.ic_share)\n}\n\n@Composable\nfun pinIcon(pinned: Boolean): ImageVector {\n    return if (pinned) Icons.Default.PushPin else Icons.Outlined.PushPin\n}\n\n@Composable\ninternal fun editIcon(): ImageVector {\n    return Icons.Default.Edit\n}\n\n@Composable\nfun likeAlt(): String {\n    return stringResource(LocalizedString.statusUiLike)\n}\n\n@Composable\ninternal fun forwardAlt(): String {\n    return stringResource(LocalizedString.status_ui_repost)\n}\n\n@Composable\ninternal fun unforwardAlt(): String {\n    return stringResource(LocalizedString.status_ui_action_unforward)\n}\n\n@Composable\ninternal fun quoteAlt(): String {\n    return stringResource(LocalizedString.statusUiQuote)\n}\n\n@Composable\ninternal fun replyAlt(): String {\n    return stringResource(LocalizedString.statusUiComment)\n}\n\n@Composable\ninternal fun bookmarkAlt(bookmarked: Boolean): String {\n    return if (bookmarked) stringResource(LocalizedString.statusUiUnbookmark) else stringResource(\n        LocalizedString.statusUiBookmark\n    )\n}\n\n@Composable\ninternal fun deleteAlt(): String {\n    return stringResource(LocalizedString.statusUiDelete)\n}\n\n@Composable\ninternal fun shareAlt(): String {\n    return stringResource(LocalizedString.statusUiShare)\n}\n\n@Composable\nfun pinAlt(pinned: Boolean): String {\n    return if (pinned) stringResource(LocalizedString.statusUiUnpin) else stringResource(\n        LocalizedString.statusUiPin\n    )\n}\n\n@Composable\ninternal fun editAlt(): String {\n    return stringResource(LocalizedString.statusUiEdit)\n}\n\ninternal fun Long.countToLabel(): String? {\n    return when {\n        this <= 0 -> null\n        else -> this.formatToHumanReadable()\n    }\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/action/StatusMoreInteractionPanel.kt",
    "content": "package com.zhangke.fread.status.ui.action\n\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Language\nimport androidx.compose.material3.DropdownMenuItem\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport com.zhangke.framework.composable.FreadDialog\nimport com.zhangke.framework.composable.PopupMenu\nimport com.zhangke.fread.common.browser.LocalActivityBrowserLauncher\nimport com.zhangke.fread.common.handler.LocalTextHandler\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.blog.Blog\nimport com.zhangke.fread.status.model.BlogTranslationUiState\nimport com.zhangke.fread.status.model.StatusActionType\nimport com.zhangke.fread.status.ui.style.StatusStyle\nimport com.zhangke.fread.statusui.Res\nimport com.zhangke.fread.statusui.ic_more\nimport kotlinx.coroutines.launch\nimport org.jetbrains.compose.resources.stringResource\nimport org.jetbrains.compose.resources.vectorResource\n\n@Composable\nfun StatusMoreInteractionIcon(\n    modifier: Modifier,\n    blog: Blog,\n    isOwner: Boolean?,\n    blogTranslationState: BlogTranslationUiState,\n    style: StatusStyle,\n    onActionClick: (StatusActionType, Blog) -> Unit,\n    onTranslateClick: () -> Unit,\n    onOpenBlogWithOtherAccountClick: (Blog) -> Unit,\n    showOpenBlogWithOtherAccountBtn: Boolean = true,\n) {\n    var showMorePopup by remember {\n        mutableStateOf(false)\n    }\n    Box(modifier = modifier) {\n        StatusIconButton(\n            modifier = Modifier\n                .size(style.bottomPanelStyle.iconSize),\n            onClick = { showMorePopup = !showMorePopup },\n        ) {\n            Icon(\n                imageVector = vectorResource(Res.drawable.ic_more),\n                contentDescription = \"More Options\"\n            )\n        }\n\n        PopupMenu(\n            expanded = showMorePopup,\n            onDismissRequest = { showMorePopup = false },\n        ) {\n            AdditionalMoreOptions(\n                blog = blog,\n                blogTranslationState = blogTranslationState,\n                onDismissRequest = { showMorePopup = false },\n                onTranslateClick = onTranslateClick,\n                onOpenBlogWithOtherAccountClick = onOpenBlogWithOtherAccountClick,\n                showOpenBlogWithOtherAccountBtn = showOpenBlogWithOtherAccountBtn,\n            )\n\n            if (isOwner == true) {\n                InteractionItem(\n                    type = StatusActionType.PIN,\n                    icon = pinIcon(blog.pinned),\n                    actionName = pinAlt(blog.pinned),\n                    onDismissRequest = { showMorePopup = false },\n                    onActionClick = { onActionClick(it, blog) },\n                )\n\n                InteractionItem(\n                    type = StatusActionType.DELETE,\n                    icon = deleteIcon(),\n                    actionName = deleteAlt(),\n                    onDismissRequest = { showMorePopup = false },\n                    onActionClick = { onActionClick(it, blog) },\n                )\n                if (blog.supportEdit) {\n                    InteractionItem(\n                        type = StatusActionType.EDIT,\n                        icon = editIcon(),\n                        actionName = editAlt(),\n                        onDismissRequest = { showMorePopup = false },\n                        onActionClick = { onActionClick(it, blog) },\n                    )\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun InteractionItem(\n    type: StatusActionType,\n    actionName: String,\n    icon: ImageVector,\n    onDismissRequest: () -> Unit,\n    onActionClick: (StatusActionType) -> Unit,\n) {\n    var showDeleteConfirmDialog by remember(type) {\n        mutableStateOf(false)\n    }\n    DropdownMenuItem(\n        text = { Text(text = actionName) },\n        leadingIcon = {\n            Icon(\n                imageVector = icon,\n                contentDescription = actionName,\n            )\n        },\n        onClick = {\n            if (type == StatusActionType.DELETE) {\n                showDeleteConfirmDialog = true\n            } else {\n                onDismissRequest()\n                onActionClick(type)\n            }\n        },\n    )\n    if (showDeleteConfirmDialog) {\n        FreadDialog(\n            onDismissRequest = {\n                onDismissRequest()\n                showDeleteConfirmDialog = false\n            },\n            contentText = stringResource(LocalizedString.statusUiDeleteStatusConfirm),\n            onNegativeClick = {\n                onDismissRequest()\n                showDeleteConfirmDialog = false\n            },\n            onPositiveClick = {\n                onDismissRequest()\n                showDeleteConfirmDialog = false\n                onActionClick(type)\n            },\n        )\n    }\n}\n\n@Composable\nprivate fun AdditionalMoreOptions(\n    blog: Blog,\n    blogTranslationState: BlogTranslationUiState,\n    onDismissRequest: () -> Unit,\n    onTranslateClick: () -> Unit,\n    onOpenBlogWithOtherAccountClick: (Blog) -> Unit,\n    showOpenBlogWithOtherAccountBtn: Boolean,\n) {\n    val textHandler = LocalTextHandler.current\n    val browserLauncher = LocalActivityBrowserLauncher.current\n    val coroutineScope = rememberCoroutineScope()\n    DropDownOpenInBrowserItem {\n        onDismissRequest()\n        coroutineScope.launch {\n            browserLauncher.launchWebTabInApp(blog.link, checkAppSupportPage = false)\n        }\n    }\n    DropDownCopyLinkItem {\n        onDismissRequest()\n        textHandler.copyText(blog.link)\n    }\n    if (showOpenBlogWithOtherAccountBtn) {\n        DropDownOpenStatusByOtherAccountItem {\n            onDismissRequest()\n            onOpenBlogWithOtherAccountClick(blog)\n        }\n    }\n    if (blogTranslationState.support) {\n        ModalDropdownMenuItem(\n            text = stringResource(LocalizedString.statusUiInteractionTranslate),\n            imageVector = Icons.Default.Language,\n            onClick = {\n                onDismissRequest()\n                onTranslateClick()\n            },\n        )\n    }\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/bar/EditContentTopBar.kt",
    "content": "package com.zhangke.fread.status.ui.bar\n\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Delete\nimport androidx.compose.material.icons.filled.Edit\nimport androidx.compose.material3.OutlinedTextField\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport com.zhangke.framework.composable.FreadDialog\nimport com.zhangke.framework.composable.SimpleIconButton\nimport com.zhangke.framework.composable.Toolbar\nimport com.zhangke.fread.localization.LocalizedString\nimport org.jetbrains.compose.resources.stringResource\n\n@Composable\nfun EditContentTopBar(\n    contentName: String,\n    onBackClick: () -> Unit,\n    onNameEdit: (String) -> Unit,\n    onDeleteClick: () -> Unit,\n) {\n\n    var showDeleteConfirmDialog by remember {\n        mutableStateOf(false)\n    }\n    var showEditNameDialog by remember {\n        mutableStateOf(false)\n    }\n    Toolbar(\n        title = contentName,\n        onBackClick = onBackClick,\n        actions = {\n            SimpleIconButton(\n                onClick = {\n                    showEditNameDialog = true\n                },\n                imageVector = Icons.Default.Edit,\n                contentDescription = \"Edit content name\",\n            )\n            SimpleIconButton(\n                onClick = {\n                    showDeleteConfirmDialog = true\n                },\n                imageVector = Icons.Default.Delete,\n                contentDescription = \"Delete content\",\n            )\n        }\n    )\n    if (showDeleteConfirmDialog) {\n        FreadDialog(\n            onDismissRequest = { showDeleteConfirmDialog = false },\n            contentText = stringResource(LocalizedString.statusUiEditContentDeleteDialogContent),\n            onNegativeClick = {\n                showDeleteConfirmDialog = false\n            },\n            onPositiveClick = {\n                showDeleteConfirmDialog = false\n                onDeleteClick()\n            },\n        )\n    }\n    if (showEditNameDialog) {\n        EditContentNameDialog(\n            name = contentName,\n            onConfirmClick = onNameEdit,\n            onDismissRequest = { showEditNameDialog = false },\n        )\n    }\n}\n\n@Composable\nprivate fun EditContentNameDialog(\n    name: String,\n    onConfirmClick: (String) -> Unit,\n    onDismissRequest: () -> Unit,\n) {\n    var inputtingNote by remember { mutableStateOf(name) }\n    FreadDialog(\n        onDismissRequest = {\n            onDismissRequest()\n        },\n        title = stringResource(LocalizedString.statusUiEditContentNameTitle),\n        content = {\n            OutlinedTextField(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .padding(start = 16.dp, top = 8.dp, bottom = 8.dp, end = 16.dp),\n                value = inputtingNote,\n                onValueChange = {\n                    inputtingNote = it\n                },\n                label = {\n                    Text(\n                        text = stringResource(LocalizedString.statusUiEditContentNameLabel)\n                    )\n                },\n                placeholder = {\n                    Text(\n                        text = stringResource(LocalizedString.statusUiEditContentNameHint)\n                    )\n                },\n            )\n        },\n        onNegativeClick = {\n            onDismissRequest()\n        },\n        onPositiveClick = {\n            if (inputtingNote.isNotEmpty()) {\n                onDismissRequest()\n                onConfirmClick(inputtingNote)\n            }\n        },\n    )\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/common/BlogTranslaction.kt",
    "content": "package com.zhangke.fread.status.ui.common\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material3.HorizontalDivider\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.model.BlogTranslationUiState\nimport com.zhangke.fread.status.ui.style.StatusStyle\nimport org.jetbrains.compose.resources.stringResource\n\n@Composable\nfun BlogTranslateLabel(\n    modifier: Modifier,\n    style: StatusStyle,\n    blogTranslationState: BlogTranslationUiState?,\n    onShowOriginalClick: () -> Unit,\n) {\n    if (blogTranslationState == null || !blogTranslationState.support) return\n    if (!blogTranslationState.translating && !blogTranslationState.showingTranslation) return\n    Row(\n        modifier = modifier\n            .padding(vertical = style.contentStyle.contentVerticalSpacing),\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        HorizontalDivider(\n            modifier = Modifier.weight(1F)\n        )\n        if (blogTranslationState.showingTranslation) {\n            Text(\n                modifier = Modifier\n                    .clickable {\n                        onShowOriginalClick()\n                    }\n                    .padding(horizontal = 16.dp),\n                text = stringResource(LocalizedString.statusUiTranslateShowOriginal),\n                style = style.infoLineStyle.descStyle,\n                color = MaterialTheme.colorScheme.primary,\n            )\n        } else {\n            Text(\n                modifier = Modifier\n                    .padding(horizontal = 16.dp),\n                text = stringResource(LocalizedString.statusUiTranslating),\n                style = style.infoLineStyle.descStyle,\n            )\n        }\n        HorizontalDivider(\n            modifier = Modifier.weight(1F)\n        )\n    }\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/common/ContentToolbar.kt",
    "content": "package com.zhangke.fread.status.ui.common\n\nimport androidx.compose.foundation.gestures.detectTapGestures\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.filled.ArrowForward\nimport androidx.compose.material.icons.filled.Menu\nimport androidx.compose.material.icons.filled.Refresh\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBarScrollBehavior\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.input.pointer.pointerInput\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport com.zhangke.framework.blur.applyBlurEffect\nimport com.zhangke.framework.composable.ScrollTopAppBar\nimport com.zhangke.framework.composable.ScrollTopAppBarColors\nimport com.zhangke.framework.composable.SimpleIconButton\nimport com.zhangke.framework.composable.ToolbarTokens\nimport com.zhangke.framework.composable.noRippleClick\nimport com.zhangke.fread.status.account.LoggedAccount\nimport dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi\n\n@OptIn(ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class)\n@Composable\nfun ContentToolbar(\n    modifier: Modifier = Modifier,\n    title: String,\n    account: LoggedAccount?,\n    showAccountInfo: Boolean,\n    showNextIcon: Boolean,\n    showRefreshButton: Boolean,\n    scrollBehavior: TopAppBarScrollBehavior,\n    onMenuClick: () -> Unit,\n    onRefreshClick: () -> Unit,\n    onNextClick: () -> Unit,\n    onTitleClick: () -> Unit,\n    onDoubleClick: (() -> Unit)? = null,\n) {\n    val scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer\n    val surfaceColor = MaterialTheme.colorScheme.surface\n    ScrollTopAppBar(\n        modifier = modifier.applyBlurEffect(containerColor = surfaceColor)\n            .pointerInput(onDoubleClick) {\n                detectTapGestures(onDoubleTap = { onDoubleClick?.invoke() })\n            },\n        scrollBehavior = scrollBehavior,\n        colors = ScrollTopAppBarColors.default(scrolledContainerColor = scrolledContainerColor),\n        navigationIcon = {\n            SimpleIconButton(\n                onClick = onMenuClick,\n                imageVector = Icons.Default.Menu,\n                contentDescription = \"Menu\",\n            )\n        },\n        title = {\n            Column {\n                Text(\n                    modifier = Modifier.noRippleClick { onTitleClick() },\n                    text = title,\n                    style = ToolbarTokens.titleTextStyle,\n                    maxLines = 1,\n                    overflow = TextOverflow.Ellipsis,\n                )\n                if (account != null && showAccountInfo) {\n                    Text(\n                        modifier = Modifier.padding(top = 1.dp),\n                        text = account.prettyHandle,\n                        maxLines = 1,\n                        overflow = TextOverflow.Ellipsis,\n                        style = MaterialTheme.typography.labelSmall,\n                        color = MaterialTheme.colorScheme.onSurfaceVariant,\n                    )\n                }\n            }\n        },\n        actions = {\n            if (showRefreshButton) {\n                SimpleIconButton(\n                    modifier = Modifier,\n                    onClick = {\n                        onRefreshClick()\n                    },\n                    imageVector = Icons.Default.Refresh,\n                    contentDescription = \"Next Content\"\n                )\n            }\n            if (showNextIcon) {\n                SimpleIconButton(\n                    modifier = Modifier,\n                    onClick = {\n                        onNextClick()\n                    },\n                    imageVector = Icons.AutoMirrored.Filled.ArrowForward,\n                    contentDescription = \"Next Content\"\n                )\n            }\n        },\n    )\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/common/DetailHeaderContent.kt",
    "content": "package com.zhangke.fread.status.ui.common\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.widthIn\nimport androidx.compose.foundation.text.selection.SelectionContainer\nimport androidx.compose.material3.LocalTextStyle\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.text.buildAnnotatedString\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport androidx.constraintlayout.compose.ConstraintLayout\nimport androidx.constraintlayout.compose.Dimension\nimport com.zhangke.framework.composable.freadPlaceholder\nimport com.zhangke.framework.utils.Log\nimport com.zhangke.fread.status.richtext.RichText\nimport com.zhangke.fread.status.ui.richtext.SelectableRichText\n\n@Composable\nfun DetailHeaderContent(\n    progress: Float,\n    loading: Boolean,\n    banner: String?,\n    avatar: String?,\n    title: RichText?,\n    description: RichText?,\n    acctLine: @Composable () -> Unit,\n    followInfo: @Composable () -> Unit,\n    onBannerClick: () -> Unit,\n    onAvatarClick: () -> Unit,\n    onUrlClick: (String) -> Unit,\n    onMaybeHashtagClick: (String) -> Unit,\n    privateNote: String? = null,\n    action: (@Composable () -> Unit)? = null,\n    bottomArea: (@Composable () -> Unit)? = null,\n) {\n    SelectionContainer {\n        Surface(modifier = Modifier.fillMaxWidth()) {\n            ConstraintLayout(\n                modifier = Modifier.fillMaxWidth(),\n            ) {\n                val (bannerRef, avatarRef, relationBtnRef, nameRef) = createRefs()\n                val (acctRef, privateNoteRef, noteRef, followRef) = createRefs()\n                val bottomAreaRef = createRef()\n                ProgressedBanner(\n                    modifier = Modifier\n                        .clickable { onBannerClick() }\n                        .constrainAs(bannerRef) {\n                            top.linkTo(parent.top)\n                            start.linkTo(parent.start)\n                            end.linkTo(parent.end)\n                            width = Dimension.fillToConstraints\n                        },\n                    url = banner,\n                )\n\n                // avatar\n                ProgressedAvatar(\n                    modifier = Modifier.constrainAs(avatarRef) {\n                        start.linkTo(parent.start, 16.dp)\n                        top.linkTo(bannerRef.bottom)\n                        bottom.linkTo(bannerRef.bottom)\n                    },\n                    avatar = avatar,\n                    progress = progress,\n                    loading = loading,\n                    onAvatarClick = onAvatarClick,\n                )\n\n                // relationship button\n                if (action == null || loading) {\n                    Box(\n                        modifier = Modifier.constrainAs(relationBtnRef) {\n                            top.linkTo(bannerRef.bottom, 16.dp)\n                            end.linkTo(parent.end, 16.dp)\n                            width = Dimension.value(0.dp)\n                        }\n                    )\n                } else {\n                    Box(\n                        modifier = Modifier.constrainAs(relationBtnRef) {\n                            top.linkTo(bannerRef.bottom, 4.dp)\n                            end.linkTo(parent.end, 16.dp)\n                        }\n                    ) {\n                        action()\n                    }\n                }\n\n                // title\n                SelectableRichText(\n                    modifier = Modifier\n                        .freadPlaceholder(loading)\n                        .constrainAs(nameRef) {\n                            top.linkTo(avatarRef.bottom, 8.dp)\n                            start.linkTo(parent.start, 16.dp)\n                            if (loading) {\n                                width = Dimension.value(68.dp)\n                            } else {\n                                end.linkTo(parent.end, 16.dp)\n                                width = Dimension.fillToConstraints\n                            }\n                        },\n                    richText = title ?: RichText.empty,\n                    maxLines = 1,\n                    overflow = TextOverflow.Ellipsis,\n                    fontSize = 18.sp,\n                    onUrlClick = onUrlClick,\n                )\n\n                // acct line\n                Box(\n                    modifier = Modifier\n                        .widthIn(min = 36.dp)\n                        .freadPlaceholder(loading)\n                        .constrainAs(acctRef) {\n                            top.linkTo(nameRef.bottom, 6.dp)\n                            start.linkTo(nameRef.start)\n                            width = Dimension.wrapContent\n                        },\n                    contentAlignment = Alignment.CenterStart,\n                ) {\n                    acctLine()\n                }\n\n                // private note\n                if (privateNote.isNullOrEmpty()) {\n                    Box(modifier = Modifier.constrainAs(privateNoteRef) {\n                        top.linkTo(acctRef.bottom)\n                        start.linkTo(nameRef.start)\n                    })\n                } else {\n                    Box(\n                        modifier = Modifier.constrainAs(privateNoteRef) {\n                            top.linkTo(acctRef.bottom, 6.dp)\n                            start.linkTo(nameRef.start)\n                            end.linkTo(parent.end, 16.dp)\n                            width = Dimension.fillToConstraints\n                        },\n                    ) {\n                        val privateNoteStr = buildAnnotatedString {\n                            val prefix = \"NOTE: \"\n                            append(prefix)\n                            append(privateNote)\n                        }\n                        SelectionContainer {\n                            Text(\n                                modifier = Modifier.fillMaxWidth(),\n                                textAlign = TextAlign.Start,\n                                text = privateNoteStr,\n                                style = MaterialTheme.typography.labelLarge,\n                            )\n                        }\n                    }\n                }\n\n                // description\n                SelectableRichText(\n                    modifier = Modifier\n                        .freadPlaceholder(loading)\n                        .fillMaxWidth()\n                        .constrainAs(noteRef) {\n                            top.linkTo(privateNoteRef.bottom, 6.dp)\n                            start.linkTo(nameRef.start)\n                            end.linkTo(parent.end, 16.dp)\n                            width = Dimension.fillToConstraints\n                        },\n                    richText = description ?: RichText.empty,\n                    onUrlClick = onUrlClick,\n                    fontSize = 14.sp,\n                    onMaybeHashtagClick = onMaybeHashtagClick,\n                )\n\n                // follow info line\n                Box(\n                    modifier = Modifier\n                        .padding(top = 4.dp)\n                        .freadPlaceholder(loading)\n                        .constrainAs(followRef) {\n                            top.linkTo(noteRef.bottom)\n                            start.linkTo(noteRef.start)\n                            width = Dimension.wrapContent\n                        },\n                ) {\n                    followInfo()\n                }\n\n                // bottom area\n                Box(\n                    modifier = Modifier.Companion.constrainAs(bottomAreaRef) {\n                        top.linkTo(followRef.bottom, 8.dp)\n                        start.linkTo(followRef.start)\n                        end.linkTo(parent.end, 16.dp)\n                        bottom.linkTo(parent.bottom, 8.dp)\n                        width = Dimension.fillToConstraints\n                    },\n                ) {\n                    bottomArea?.invoke()\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/common/DetailPageScaffold.kt",
    "content": "package com.zhangke.fread.status.ui.common\n\nimport androidx.compose.foundation.layout.BoxScope\nimport androidx.compose.foundation.layout.RowScope\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.navigationBarsPadding\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.SnackbarHost\nimport androidx.compose.material3.SnackbarHostState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.MutableState\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport com.zhangke.framework.blur.BlurController\nimport com.zhangke.framework.blur.LocalBlurController\nimport com.zhangke.framework.blur.rememberBlurController\nimport com.zhangke.framework.composable.LocalContentPadding\nimport com.zhangke.framework.composable.LocalSnackbarHostState\nimport com.zhangke.framework.composable.collapsable.ScrollUpTopBarLayout\nimport com.zhangke.framework.composable.plusContentPadding\nimport com.zhangke.fread.status.richtext.RichText\n\n@Composable\nfun DetailPageScaffold(\n    modifier: Modifier,\n    snackbarHostState: SnackbarHostState,\n    title: RichText,\n    avatar: String,\n    banner: String?,\n    description: RichText?,\n    privateNote: String?,\n    loading: Boolean,\n    contentCanScrollBackward: MutableState<Boolean>,\n    onBannerClick: () -> Unit,\n    onAvatarClick: () -> Unit,\n    onUrlClick: (String) -> Unit,\n    onMaybeHashtagClick: (String) -> Unit,\n    onBackClick: () -> Unit,\n    topBarActions: @Composable RowScope.() -> Unit,\n    handleLine: @Composable () -> Unit,\n    followInfoLine: @Composable () -> Unit,\n    topDetailContentAction: (@Composable () -> Unit)? = null,\n    bottomArea: (@Composable () -> Unit)? = null,\n    content: @Composable (progress: Float) -> Unit,\n) {\n    DetailPageScaffold(\n        modifier = modifier,\n        snackbarHostState = snackbarHostState,\n        title = title,\n        contentCanScrollBackward = contentCanScrollBackward,\n        onBackClick = onBackClick,\n        topBarActions = topBarActions,\n        topDetailContent = { progress ->\n            DetailHeaderContent(\n                progress = progress,\n                loading = loading,\n                banner = banner,\n                avatar = avatar,\n                title = title,\n                description = description,\n                acctLine = handleLine,\n                followInfo = followInfoLine,\n                onBannerClick = onBannerClick,\n                onAvatarClick = onAvatarClick,\n                onUrlClick = onUrlClick,\n                onMaybeHashtagClick = onMaybeHashtagClick,\n                privateNote = privateNote,\n                action = topDetailContentAction,\n                bottomArea = bottomArea,\n            )\n        },\n        content = content,\n    )\n}\n\n@Composable\nfun DetailPageScaffold(\n    modifier: Modifier,\n    snackbarHostState: SnackbarHostState,\n    title: RichText,\n    contentCanScrollBackward: MutableState<Boolean>,\n    onBackClick: () -> Unit,\n    topBarActions: @Composable RowScope.() -> Unit,\n    topDetailContent: @Composable BoxScope.(Float) -> Unit,\n    content: @Composable (progress: Float) -> Unit,\n) {\n    Scaffold(\n        modifier = modifier,\n        snackbarHost = {\n            SnackbarHost(\n                modifier = Modifier.navigationBarsPadding(),\n                hostState = snackbarHostState,\n            )\n        },\n        contentWindowInsets = WindowInsets(0, 0, 0, 0),\n    ) { innerPaddings ->\n        val blurController = rememberBlurController()\n        CompositionLocalProvider(\n            LocalSnackbarHostState provides snackbarHostState,\n            LocalBlurController provides blurController,\n        ) {\n            ScrollUpTopBarLayout(\n                modifier = Modifier\n                    .fillMaxSize()\n                    .padding(innerPaddings),\n                topBarContent = { progress ->\n                    DetailTopBar(\n                        progress = progress,\n                        title = title,\n                        onBackClick = onBackClick,\n                        actions = topBarActions,\n                    )\n                },\n                headerContent = { progress ->\n                    topDetailContent(progress)\n                },\n                contentCanScrollBackward = contentCanScrollBackward,\n            ) { paddingValues, progress ->\n                CompositionLocalProvider(\n                    LocalContentPadding provides plusContentPadding(paddingValues),\n                ) {\n                    content(progress)\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/common/DetailTopBar.kt",
    "content": "package com.zhangke.fread.status.ui.common\n\nimport androidx.compose.foundation.layout.RowScope\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.statusBars\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.lerp\nimport androidx.compose.ui.unit.sp\nimport com.zhangke.framework.blur.applyBlurEffect\nimport com.zhangke.framework.blur.blurEffectContainerColor\nimport com.zhangke.framework.composable.SingleRowTopAppBar\nimport com.zhangke.framework.composable.Toolbar\nimport com.zhangke.framework.composable.TopAppBarColors\nimport com.zhangke.fread.common.browser.LocalActivityBrowserLauncher\nimport com.zhangke.fread.status.richtext.RichText\nimport com.zhangke.fread.status.ui.richtext.FreadRichText\nimport kotlinx.coroutines.launch\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun DetailTopBar(\n    progress: Float,\n    title: RichText,\n    onBackClick: () -> Unit,\n    actions: @Composable RowScope.() -> Unit,\n) {\n    val progressTopBarContainerColor = MaterialTheme.colorScheme.surface.copy(progress)\n    val blurEnabled = progress >= 1F\n    val onTopBarColor = lerp(\n        start = MaterialTheme.colorScheme.inverseOnSurface,\n        stop = MaterialTheme.colorScheme.onSurface,\n        fraction = progress,\n    )\n    val browserLauncher = LocalActivityBrowserLauncher.current\n    val coroutineScope = rememberCoroutineScope()\n    SingleRowTopAppBar(\n        modifier = Modifier.applyBlurEffect(\n            enabled = blurEnabled,\n            containerColor = progressTopBarContainerColor,\n        ),\n        title = {\n            if (progress >= 1F) {\n                FreadRichText(\n                    modifier = Modifier,\n                    richText = title,\n                    fontSize = 22.sp,\n                    maxLines = 1,\n                    onUrlClick = {\n                        coroutineScope.launch {\n                            browserLauncher.launchWebTabInApp(it)\n                        }\n                    },\n                )\n            }\n        },\n        navigationIcon = {\n            Toolbar.BackButton(onBackClick = onBackClick)\n        },\n        windowInsets = WindowInsets.statusBars,\n        colors = TopAppBarColors.default(\n            containerColor = blurEffectContainerColor(\n                enabled = blurEnabled,\n                containerColor = progressTopBarContainerColor,\n            ),\n            navigationIconContentColor = onTopBarColor,\n            actionIconContentColor = onTopBarColor,\n        ),\n        actions = actions,\n    )\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/common/HomeContentTabsTopBar.kt",
    "content": "package com.zhangke.fread.status.ui.common\n\nimport androidx.compose.foundation.gestures.detectTapGestures\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.RowScope\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.filled.ArrowForward\nimport androidx.compose.material.icons.filled.Menu\nimport androidx.compose.material.icons.filled.Refresh\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBarScrollBehavior\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.composed\nimport androidx.compose.ui.input.pointer.pointerInput\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport com.zhangke.framework.composable.SimpleIconButton\nimport com.zhangke.framework.composable.TabsTopAppBar\nimport com.zhangke.framework.composable.TabsTopAppBarColors\nimport com.zhangke.framework.composable.ToolbarTokens\nimport com.zhangke.framework.composable.noRippleClick\nimport com.zhangke.fread.status.account.LoggedAccount\n\n@Composable\n@OptIn(ExperimentalMaterial3Api::class)\nfun HomeContentTabsTopBar(\n    modifier: Modifier = Modifier,\n    selectedTabIndex: Int,\n    scrollBehavior: TopAppBarScrollBehavior,\n    tabCount: Int,\n    tabContent: @Composable (index: Int) -> Unit,\n    onTabClick: (index: Int) -> Unit,\n    navigationIcon: @Composable () -> Unit = {},\n    title: @Composable () -> Unit,\n    actions: @Composable RowScope.() -> Unit = {},\n) {\n    val scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer\n    TabsTopAppBar(\n        modifier = modifier,\n        navigationIcon = navigationIcon,\n        title = title,\n        actions = actions,\n        selectedTabIndex = selectedTabIndex,\n        scrollBehavior = scrollBehavior,\n        colors = TabsTopAppBarColors.default(scrolledContainerColor = scrolledContainerColor),\n        tabContent = tabContent,\n        tabCount = tabCount,\n        onTabClick = onTabClick,\n    )\n}\n\n@Composable\n@OptIn(ExperimentalMaterial3Api::class)\nfun HomeContentTabsTopBar(\n    modifier: Modifier = Modifier,\n    title: String,\n    account: LoggedAccount?,\n    showAccountInfo: Boolean,\n    selectedTabIndex: Int,\n    tabTitles: List<String>,\n    scrollBehavior: TopAppBarScrollBehavior,\n    showNextIcon: Boolean = false,\n    showRefreshButton: Boolean = false,\n    onMenuClick: () -> Unit,\n    onRefreshClick: () -> Unit,\n    onNextClick: () -> Unit,\n    onTitleClick: () -> Unit,\n    onDoubleClick: (() -> Unit)? = null,\n    onTabClick: (index: Int) -> Unit,\n) {\n    HomeContentTabsTopBar(\n        modifier = modifier.doubleTapToScrollTop(onDoubleClick),\n        selectedTabIndex = selectedTabIndex,\n        tabCount = tabTitles.size,\n        scrollBehavior = scrollBehavior,\n        onTabClick = onTabClick,\n        navigationIcon = {\n            SimpleIconButton(\n                onClick = onMenuClick,\n                imageVector = Icons.Default.Menu,\n                contentDescription = \"Menu\",\n            )\n        },\n        title = {\n            Column(\n                modifier = Modifier\n            ) {\n                Text(\n                    modifier = Modifier.titleClickable(onTitleClick),\n                    text = title,\n                    maxLines = 1,\n                    style = ToolbarTokens.titleTextStyle,\n                    overflow = TextOverflow.Ellipsis,\n                )\n                if (account != null && showAccountInfo) {\n                    Text(\n                        modifier = Modifier.padding(top = 1.dp),\n                        text = account.prettyHandle,\n                        maxLines = 1,\n                        overflow = TextOverflow.Ellipsis,\n                        style = MaterialTheme.typography.labelSmall,\n                        color = MaterialTheme.colorScheme.onSurfaceVariant,\n                    )\n                }\n            }\n        },\n        actions = {\n            if (showRefreshButton) {\n                SimpleIconButton(\n                    onClick = onRefreshClick,\n                    imageVector = Icons.Default.Refresh,\n                    contentDescription = \"Refresh\",\n                )\n            }\n            if (showNextIcon) {\n                SimpleIconButton(\n                    onClick = onNextClick,\n                    imageVector = Icons.AutoMirrored.Filled.ArrowForward,\n                    contentDescription = \"Next Content\",\n                )\n            }\n        },\n        tabContent = { index ->\n            Text(\n                text = tabTitles[index],\n                maxLines = 1,\n            )\n        },\n    )\n}\n\n@Composable\nprivate fun Modifier.titleClickable(onClick: () -> Unit): Modifier {\n    return this.noRippleClick { onClick() }\n}\n\nfun Modifier.doubleTapToScrollTop(onDoubleClick: (() -> Unit)?): Modifier = composed {\n    if (onDoubleClick == null) {\n        this\n    } else {\n        this.pointerInput(onDoubleClick) {\n            detectTapGestures(onDoubleTap = { onDoubleClick() })\n        }\n    }\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/common/LinkPreviewCard.kt",
    "content": "package com.zhangke.fread.status.ui.common\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.ColumnScope\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.aspectRatio\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Close\nimport androidx.compose.material.icons.filled.Description\nimport androidx.compose.material3.HorizontalDivider\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.IconButtonColors\nimport androidx.compose.material3.IconButtonDefaults\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport com.seiko.imageloader.model.ImageRequest\nimport com.seiko.imageloader.ui.AutoSizeImage\nimport com.zhangke.framework.composable.TextWithIcon\nimport com.zhangke.framework.composable.freadPlaceholder\nimport com.zhangke.framework.ktx.ifNullOrEmpty\nimport com.zhangke.framework.utils.LinkPreviewInfo\nimport com.zhangke.fread.localization.LocalizedString\nimport org.jetbrains.compose.resources.stringResource\n\n@Composable\nfun LinkPreviewCard(\n    modifier: Modifier,\n    card: DetectedLinkCard,\n    onRemoveClick: () -> Unit,\n) {\n    when (card) {\n        is DetectedLinkCard.Deleted -> {\n            Box(modifier = modifier)\n        }\n\n        is DetectedLinkCard.Loading -> {\n            PreviewCardLoading(\n                modifier = modifier,\n                link = card.link,\n                onRemoveClick = onRemoveClick,\n            )\n        }\n\n        is DetectedLinkCard.Loaded -> {\n            PreviewCardLoaded(\n                modifier = modifier,\n                link = card.link,\n                info = card.info,\n                onRemoveClick = onRemoveClick,\n            )\n        }\n\n        is DetectedLinkCard.Failure -> {\n            PreviewCardFailure(\n                modifier = modifier,\n                link = card.link,\n                throwable = card.throwable,\n                onRemoveClick = onRemoveClick,\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun PreviewCardLoading(modifier: Modifier, link: String, onRemoveClick: () -> Unit) {\n    PreviewCardContainer(\n        modifier = modifier,\n        onRemoveClick = onRemoveClick,\n        link = link,\n    ) {\n        Box(\n            modifier = Modifier.height(16.dp)\n                .fillMaxWidth(0.3F)\n                .freadPlaceholder(true),\n        )\n        Box(\n            modifier = Modifier.padding(top = 16.dp)\n                .height(16.dp)\n                .fillMaxWidth(0.6F)\n                .freadPlaceholder(true),\n        )\n        Box(\n            modifier = Modifier.padding(top = 16.dp)\n                .height(16.dp)\n                .fillMaxWidth(0.9F)\n                .freadPlaceholder(true),\n        )\n    }\n}\n\n@Composable\nprivate fun PreviewCardLoaded(\n    modifier: Modifier,\n    link: String,\n    info: LinkPreviewInfo,\n    onRemoveClick: () -> Unit,\n) {\n    if (info.image.isNullOrEmpty()) {\n        Row(\n            modifier = modifier.height(86.dp).previewCardBorder(),\n        ) {\n            Box(\n                modifier = Modifier\n                    .padding(end = 10.dp)\n                    .fillMaxHeight()\n                    .aspectRatio(1F)\n                    .background(\n                        color = MaterialTheme.colorScheme.surfaceDim,\n                        shape = RoundedCornerShape(8.dp),\n                    ),\n            ) {\n                Icon(\n                    modifier = Modifier\n                        .align(Alignment.Center)\n                        .size(36.dp),\n                    imageVector = Icons.Default.Description,\n                    contentDescription = \"Link\",\n                    tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7F),\n                )\n            }\n            Column(\n                modifier = Modifier\n                    .weight(1F)\n                    .padding(end = 8.dp),\n                verticalArrangement = Arrangement.Center,\n            ) {\n                Text(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .padding(horizontal = 16.dp),\n                    textAlign = TextAlign.Start,\n                    text = info.title,\n                    maxLines = 2,\n                    fontSize = 16.sp,\n                    fontWeight = FontWeight.SemiBold,\n                    overflow = TextOverflow.Ellipsis,\n                )\n                Text(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .padding(top = 2.dp)\n                        .padding(horizontal = 16.dp),\n                    textAlign = TextAlign.Start,\n                    text = info.description.ifNullOrEmpty { info.siteName.ifNullOrEmpty { link } },\n                    maxLines = 3,\n                    fontSize = 14.sp,\n                    overflow = TextOverflow.Ellipsis,\n                )\n            }\n            DeleteButton(\n                modifier = Modifier.padding(top = 8.dp, end = 8.dp),\n                onClick = onRemoveClick,\n            )\n        }\n    } else {\n        Column(\n            modifier = modifier.previewCardBorder()\n        ) {\n            Box(\n                modifier = Modifier.fillMaxWidth().aspectRatio(2F),\n            ) {\n                AutoSizeImage(\n                    remember(info.image) {\n                        ImageRequest(info.image.orEmpty())\n                    },\n                    modifier = Modifier\n                        .clip(RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp))\n                        .fillMaxSize(),\n                    contentScale = ContentScale.Crop,\n                    contentDescription = \"Preview Image\",\n                )\n                DeleteButton(\n                    modifier = Modifier.align(Alignment.TopEnd)\n                        .padding(top = 8.dp, end = 8.dp),\n                    onClick = onRemoveClick,\n                )\n            }\n            Text(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .padding(top = 8.dp)\n                    .padding(horizontal = 16.dp),\n                textAlign = TextAlign.Start,\n                text = info.title,\n                maxLines = 2,\n                fontSize = 16.sp,\n                fontWeight = FontWeight.SemiBold,\n                overflow = TextOverflow.Ellipsis,\n            )\n            if (!info.description.isNullOrEmpty()) {\n                Text(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .padding(top = 4.dp)\n                        .padding(horizontal = 16.dp),\n                    textAlign = TextAlign.Start,\n                    text = info.description.orEmpty(),\n                    maxLines = 3,\n                    fontSize = 14.sp,\n                    overflow = TextOverflow.Ellipsis,\n                )\n            }\n            HorizontalDivider(\n                modifier = Modifier.fillMaxWidth().padding(start = 16.dp, top = 12.dp, end = 16.dp)\n            )\n            TextWithIcon(\n                modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),\n                text = info.siteName.ifNullOrEmpty { info.url },\n                color = MaterialTheme.colorScheme.onSurfaceVariant,\n                style = MaterialTheme.typography.labelSmall,\n                maxLines = 1,\n                overflow = TextOverflow.Ellipsis,\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun PreviewCardFailure(\n    modifier: Modifier,\n    link: String,\n    throwable: Throwable?,\n    onRemoveClick: () -> Unit,\n) {\n    PreviewCardContainer(\n        modifier = modifier,\n        onRemoveClick = onRemoveClick,\n        link = link,\n    ) {\n        Text(\n            modifier = Modifier.padding(top = 16.dp).align(Alignment.CenterHorizontally),\n            text = stringResource(LocalizedString.loadMoreError),\n            fontWeight = FontWeight.SemiBold,\n        )\n        if (!throwable?.message.isNullOrEmpty()) {\n            Text(\n                modifier = Modifier.padding(top = 8.dp).align(Alignment.CenterHorizontally),\n                text = throwable.message.orEmpty(),\n                style = MaterialTheme.typography.labelMedium,\n                color = MaterialTheme.colorScheme.onSurfaceVariant,\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun PreviewCardContainer(\n    modifier: Modifier,\n    link: String,\n    onRemoveClick: () -> Unit,\n    content: @Composable ColumnScope.() -> Unit,\n) {\n    Box(modifier = modifier.previewCardBorder()) {\n        Text(\n            modifier = Modifier.align(Alignment.TopCenter)\n                .fillMaxWidth()\n                .padding(start = 46.dp, top = 16.dp, end = 46.dp),\n            text = link,\n            textAlign = TextAlign.Center,\n            maxLines = 1,\n            color = MaterialTheme.colorScheme.onSurfaceVariant,\n            overflow = TextOverflow.Ellipsis,\n        )\n        DeleteButton(\n            modifier = Modifier.align(Alignment.TopEnd).padding(top = 8.dp, end = 8.dp),\n            onClick = onRemoveClick,\n        )\n        Column(\n            modifier = Modifier.fillMaxWidth()\n                .padding(start = 16.dp, top = 46.dp, end = 16.dp, bottom = 16.dp),\n        ) {\n            content()\n        }\n    }\n}\n\n@Composable\nprivate fun DeleteButton(\n    modifier: Modifier,\n    onClick: () -> Unit,\n    colors: IconButtonColors = IconButtonDefaults.iconButtonColors(\n        containerColor = MaterialTheme.colorScheme.secondaryContainer,\n        contentColor = MaterialTheme.colorScheme.onSecondaryContainer,\n    ),\n) {\n    IconButton(\n        modifier = modifier,\n        onClick = onClick,\n        colors = colors,\n    ) {\n        Icon(\n            imageVector = Icons.Default.Close,\n            contentDescription = stringResource(LocalizedString.statusUiDelete),\n        )\n    }\n}\n\n@Composable\nprivate fun Modifier.previewCardBorder(): Modifier {\n    return this.border(\n        width = 1.dp,\n        shape = RoundedCornerShape(8.dp),\n        color = MaterialTheme.colorScheme.outlineVariant,\n    )\n}\n\nsealed interface DetectedLinkCard {\n\n    val link: String\n\n    data class Loading(override val link: String) : DetectedLinkCard\n\n    data class Loaded(override val link: String, val info: LinkPreviewInfo) : DetectedLinkCard\n\n    data class Failure(override val link: String, val throwable: Throwable?) : DetectedLinkCard\n\n    data class Deleted(override val link: String) : DetectedLinkCard\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/common/NestedTabConnection.kt",
    "content": "package com.zhangke.fread.status.ui.common\n\nimport androidx.compose.foundation.lazy.LazyListState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.staticCompositionLocalOf\nimport com.zhangke.fread.status.model.FreadContent\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.SharedFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.asSharedFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.launch\nimport kotlin.time.Duration.Companion.milliseconds\n\nval LocalNestedTabConnection = staticCompositionLocalOf {\n    NestedTabConnection()\n}\n\nclass NestedTabConnection {\n\n    companion object {\n\n        private val IMMERSIVE_MODE_DELAY = 500.milliseconds\n    }\n\n    private val _switchToNextTabFlow = MutableSharedFlow<Unit>()\n    val switchToNextTabFlow: SharedFlow<Unit> get() = _switchToNextTabFlow.asSharedFlow()\n\n    private val _openDrawerFlow = MutableSharedFlow<Unit>()\n    val openDrawerFlow: SharedFlow<Unit> get() = _openDrawerFlow.asSharedFlow()\n\n    private val _inImmersiveFlow = MutableStateFlow(false)\n    val inImmersiveFlow: StateFlow<Boolean> get() = _inImmersiveFlow.asStateFlow()\n\n    private val _scrollToContentTabFlow = MutableSharedFlow<FreadContent>()\n    val scrollToContentTabFlow: SharedFlow<FreadContent> get() = _scrollToContentTabFlow.asSharedFlow()\n\n    private val _scrollToTopFlow = MutableSharedFlow<Unit>()\n    val scrollToTopFlow: SharedFlow<Unit> get() = _scrollToTopFlow.asSharedFlow()\n\n    private val _contentScrollInpProgress = MutableStateFlow(false)\n    val contentScrollInpProgress: StateFlow<Boolean> get() = _contentScrollInpProgress.asStateFlow()\n\n    private val _refreshFlow = MutableSharedFlow<Unit>()\n    val refreshFlow: SharedFlow<Unit> get() = _refreshFlow.asSharedFlow()\n\n    private var toggleImmersiveJob: Job? = null\n\n    suspend fun switchToNextTab() {\n        _switchToNextTabFlow.emit(Unit)\n    }\n\n    suspend fun openDrawer() {\n        _openDrawerFlow.emit(Unit)\n    }\n\n    fun openImmersiveMode(coroutineScope: CoroutineScope) {\n        toggleImmersiveJob?.cancel()\n        toggleImmersiveJob = coroutineScope.launch {\n            delay(IMMERSIVE_MODE_DELAY)\n            _inImmersiveFlow.emit(true)\n        }\n    }\n\n    fun closeImmersiveMode(coroutineScope: CoroutineScope) {\n        toggleImmersiveJob?.cancel()\n        toggleImmersiveJob = coroutineScope.launch {\n            delay(IMMERSIVE_MODE_DELAY)\n            _inImmersiveFlow.emit(false)\n        }\n    }\n\n    fun updateContentScrollInProgress(scrollInProgress: Boolean) {\n        _contentScrollInpProgress.value = scrollInProgress\n    }\n\n    suspend fun scrollToContentTab(contentConfig: FreadContent) {\n        _scrollToContentTabFlow.emit(contentConfig)\n    }\n\n    suspend fun scrollToTop() {\n        _scrollToTopFlow.emit(Unit)\n    }\n\n    suspend fun refresh() {\n        _refreshFlow.emit(Unit)\n    }\n}\n\n@Composable\nfun ObserveScrollInProgressForConnection(lazyListState: LazyListState) {\n    val nestedTabConnection = LocalNestedTabConnection.current\n    LaunchedEffect(lazyListState.isScrollInProgress) {\n        nestedTabConnection.updateContentScrollInProgress(lazyListState.isScrollInProgress)\n    }\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/common/NewStatusNotifyBar.kt",
    "content": "package com.zhangke.fread.status.ui.common\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.ArrowUpward\nimport androidx.compose.material3.Button\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport com.zhangke.fread.localization.LocalizedString\nimport org.jetbrains.compose.resources.stringResource\n\n@Composable\nfun NewStatusNotifyBar(\n    modifier: Modifier,\n    onClick: () -> Unit,\n) {\n    Button(\n        modifier = modifier\n            .height(40.dp)\n            .clickable { onClick() },\n        shape = RoundedCornerShape(20.dp),\n        onClick = onClick,\n    ) {\n        Row(\n            verticalAlignment = Alignment.CenterVertically,\n        ) {\n            Icon(\n                modifier = Modifier.size(18.dp),\n                imageVector = Icons.Default.ArrowUpward,\n                contentDescription = \"Scroll Up\",\n            )\n            val tip = stringResource(LocalizedString.statusUiNewStatus)\n            Text(\n                modifier = Modifier.padding(start = 4.dp),\n                text = tip,\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/common/ObserveMaxReadItem.kt",
    "content": "package com.zhangke.fread.status.ui.common\n\nimport androidx.compose.foundation.lazy.LazyListState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\n\n@Composable\nfun ObserveMinReadItem(listState: LazyListState, onReadMinIndex: (index: Int) -> Unit) {\n    var minIndex by remember {\n        mutableIntStateOf(Int.MAX_VALUE)\n    }\n    val firstVisibleItemIndex by remember { derivedStateOf { listState.firstVisibleItemIndex } }\n    if (firstVisibleItemIndex < minIndex && !listState.isScrollInProgress) {\n        minIndex = firstVisibleItemIndex\n        LaunchedEffect(minIndex) {\n            onReadMinIndex(minIndex)\n        }\n    }\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/common/ObserveScrollStopedPosition.kt",
    "content": "package com.zhangke.fread.status.ui.common\n\nimport androidx.compose.foundation.lazy.LazyListState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.snapshotFlow\nimport kotlinx.coroutines.flow.distinctUntilChanged\n\n@Composable\nfun ObserveScrollStopedPosition(\n    listState: LazyListState,\n    onPositionChanged: (position: Int) -> Unit,\n) {\n    LaunchedEffect(listState) {\n        snapshotFlow { listState.isScrollInProgress }\n            .distinctUntilChanged()\n            .collect { inProgress ->\n                if (!inProgress) {\n                    val index = listState.firstVisibleItemIndex\n                    onPositionChanged(index)\n                }\n            }\n    }\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/common/PostStatusTextVisualTransformation.kt",
    "content": "package com.zhangke.fread.status.ui.common\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.text.AnnotatedString\nimport androidx.compose.ui.text.SpanStyle\nimport androidx.compose.ui.text.buildAnnotatedString\nimport androidx.compose.ui.text.input.OffsetMapping\nimport androidx.compose.ui.text.input.TransformedText\nimport androidx.compose.ui.text.input.VisualTransformation\nimport com.zhangke.fread.common.utils.HashtagTextUtils\nimport com.zhangke.fread.common.utils.LinkTextUtils\nimport com.zhangke.fread.common.utils.MentionTextUtil\n\nclass PostStatusTextVisualTransformation(\n    private val highLightColor: Color,\n    private val enableMentions: Boolean = true,\n    private val allowHashtagInHashtag: Boolean = false,\n) : VisualTransformation {\n\n    override fun filter(text: AnnotatedString): TransformedText {\n        val hashtags = HashtagTextUtils.findHashtags(text.text, allowHashtagInHashtag)\n        val mentions =\n            if (enableMentions) MentionTextUtil.findMentionList(text.text) else emptyList()\n        val links = LinkTextUtils.findLinks(text.text)\n        val highlightList = hashtags + mentions + links\n        return TransformedText(\n            text = buildAnnotatedString {\n                append(text)\n                highlightList.forEach {\n                    addStyle(\n                        style = SpanStyle(\n                            color = highLightColor,\n                        ),\n                        start = it.start,\n                        end = it.end,\n                    )\n                }\n            },\n            offsetMapping = OffsetMapping.Identity,\n        )\n    }\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/common/ProgressedAvatar.kt",
    "content": "package com.zhangke.fread.status.ui.common\n\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.draw.scale\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.unit.dp\nimport com.seiko.imageloader.ui.AutoSizeImage\nimport com.zhangke.framework.composable.freadPlaceholder\n\n@Composable\nfun ProgressedAvatar(\n    modifier: Modifier,\n    avatar: String?,\n    loading: Boolean,\n    progress: Float,\n    onAvatarClick: () -> Unit,\n) {\n    AutoSizeImage(\n        url = avatar.orEmpty(),\n        modifier = modifier\n            .scale(1F - progress)\n            .clip(CircleShape)\n            .border(2.dp, Color.White, CircleShape)\n            .clickable { onAvatarClick() }\n            .freadPlaceholder(loading)\n            .size(80.dp),\n        contentScale = ContentScale.Crop,\n        contentDescription = \"avatar\",\n    )\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/common/ProgressedBanner.kt",
    "content": "package com.zhangke.fread.status.ui.common\n\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.aspectRatio\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.drawWithContent\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.layout.ContentScale\nimport com.seiko.imageloader.ui.AutoSizeImage\nimport com.zhangke.fread.statusui.Res\nimport com.zhangke.fread.statusui.img_banner_background\nimport org.jetbrains.compose.resources.painterResource\n\nprivate const val BANNER_ASPECT = 2.6F\n\n@Composable\nfun ProgressedBanner(\n    modifier: Modifier,\n    url: String?,\n) {\n    Box(modifier = modifier.aspectRatio(BANNER_ASPECT)) {\n        Image(\n            modifier = Modifier.fillMaxSize(),\n            painter = painterResource(Res.drawable.img_banner_background),\n            contentDescription = null,\n            contentScale = ContentScale.Crop,\n        )\n        val maskColor = MaterialTheme.colorScheme.inverseSurface\n        AutoSizeImage(\n            url.orEmpty(),\n            modifier = Modifier\n                .fillMaxSize()\n                .drawWithContent {\n                    drawContent()\n                    drawRect(\n                        brush = Brush.verticalGradient(\n                            colors = listOf(\n                                maskColor.copy(alpha = 0.3F),\n                                maskColor.copy(alpha = 0F),\n                            ),\n                        ),\n                    )\n                },\n            contentScale = ContentScale.Crop,\n            contentDescription = \"banner\",\n        )\n    }\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/common/PublishingFab.kt",
    "content": "package com.zhangke.fread.status.ui.common\n\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.scaleIn\nimport androidx.compose.animation.scaleOut\nimport androidx.compose.animation.slideInVertically\nimport androidx.compose.animation.slideOutVertically\nimport androidx.compose.foundation.layout.navigationBarsPadding\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Edit\nimport androidx.compose.material3.FloatingActionButton\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport com.zhangke.framework.composable.contentBottomPadding\n\n@Composable\nfun PublishingFab(\n    visible: Boolean,\n    modifier: Modifier = Modifier,\n    onPublishClick: () -> Unit,\n    bottomPadding: Dp = 32.dp,\n) {\n    AnimatedVisibility(\n        modifier = modifier\n            .navigationBarsPadding()\n            .contentBottomPadding()\n            .padding(bottom = bottomPadding),\n        visible = visible,\n        enter = scaleIn() + slideInVertically(initialOffsetY = { it }),\n        exit = scaleOut() + slideOutVertically(targetOffsetY = { it }),\n    ) {\n        FloatingActionButton(\n            onClick = onPublishClick,\n            containerColor = MaterialTheme.colorScheme.surface,\n            contentColor = MaterialTheme.colorScheme.primary,\n            shape = CircleShape,\n        ) {\n            Icon(\n                imageVector = Icons.Default.Edit,\n                contentDescription = \"Post Micro Blog\",\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/common/RelationshipStateButton.kt",
    "content": "package com.zhangke.fread.status.ui.common\n\nimport androidx.compose.material3.Button\nimport androidx.compose.material3.ButtonDefaults\nimport androidx.compose.material3.FilledTonalButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport com.zhangke.framework.composable.AlertConfirmDialog\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.model.Relationships\nimport org.jetbrains.compose.resources.stringResource\n\n@Composable\nfun RelationshipStateButton(\n    modifier: Modifier,\n    relationship: Relationships,\n    onUnblockClick: () -> Unit,\n    onFollowClick: () -> Unit,\n    onUnfollowClick: () -> Unit,\n    onCancelFollowRequestClick: () -> Unit,\n) {\n    var showUnfollowDialog by remember { mutableStateOf(false) }\n    when {\n        relationship.blocking -> {\n            var showDialog by remember { mutableStateOf(false) }\n            Button(\n                modifier = modifier,\n                onClick = { showDialog = true },\n            ) {\n                Text(text = stringResource(LocalizedString.statusUiUserDetailRelationshipBlocking))\n            }\n            if (showDialog) {\n                AlertConfirmDialog(\n                    content = stringResource(LocalizedString.statusUiRelationshipBtnDialogContentCancelBlocking),\n                    onConfirm = onUnblockClick,\n                    onDismissRequest = { showDialog = false }\n                )\n            }\n        }\n\n        relationship.requested == true -> {\n            var showDialog by remember { mutableStateOf(false) }\n            FilledTonalButton(\n                modifier = modifier,\n                onClick = { showDialog = true },\n                colors = ButtonDefaults.outlinedButtonColors(\n                    containerColor = MaterialTheme.colorScheme.secondaryContainer,\n                ),\n            ) {\n                Text(text = stringResource(LocalizedString.statusUiUserDetailRelationshipRequested))\n            }\n            if (showDialog) {\n                AlertConfirmDialog(\n                    content = stringResource(LocalizedString.statusUiRelationshipBtnDialogContentCancelFollowRequest),\n                    onConfirm = onCancelFollowRequestClick,\n                    onDismissRequest = { showDialog = false }\n                )\n            }\n        }\n\n        relationship.following && relationship.followedBy -> {\n            FilledTonalButton(\n                modifier = modifier,\n                onClick = { showUnfollowDialog = true },\n                colors = ButtonDefaults.outlinedButtonColors(\n                    containerColor = MaterialTheme.colorScheme.secondaryContainer,\n                ),\n            ) {\n                Text(text = stringResource(LocalizedString.statusUiUserDetailRelationshipMutuals))\n            }\n        }\n\n        relationship.following -> {\n            FilledTonalButton(\n                modifier = modifier,\n                onClick = { showUnfollowDialog = true },\n            ) {\n                Text(text = stringResource(LocalizedString.statusUiUserDetailRelationshipFollowing))\n            }\n        }\n\n        relationship.followedBy -> {\n            Button(\n                modifier = modifier,\n                onClick = onFollowClick,\n            ) {\n                Text(text = stringResource(LocalizedString.statusUiUserDetailRelationshipFollowBack))\n            }\n        }\n\n        else -> {\n            Button(\n                modifier = modifier,\n                onClick = onFollowClick,\n            ) {\n                Text(text = stringResource(LocalizedString.statusUiUserDetailRelationshipNotFollow))\n            }\n        }\n    }\n    if (showUnfollowDialog) {\n        AlertConfirmDialog(\n            content = stringResource(LocalizedString.statusUiRelationshipBtnDialogContentCancelFollow),\n            onConfirm = onUnfollowClick,\n            onDismissRequest = { showUnfollowDialog = false }\n        )\n    }\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/common/RemainingTextStatus.kt",
    "content": "package com.zhangke.fread.status.ui.common\n\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.material.Text\nimport androidx.compose.material3.CircularProgressIndicator\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\n\n@Composable\nfun RemainingTextStatus(\n    modifier: Modifier,\n    maxCount: Int,\n    contentLength: Int,\n) {\n    Row(\n        modifier = modifier,\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        val remainingCount = maxCount - contentLength\n        val overLength = remainingCount < 0\n        val fontColor = if (overLength) {\n            MaterialTheme.colorScheme.error\n        } else {\n            MaterialTheme.colorScheme.primary\n        }\n        Text(\n            text = \"${maxCount - contentLength}\",\n            style = MaterialTheme.typography.labelSmall,\n            color = fontColor,\n            maxLines = 1,\n        )\n        CircularProgressIndicator(\n            modifier = Modifier.padding(start = 4.dp)\n                .size(20.dp),\n            progress = { contentLength / maxCount.toFloat() },\n            trackColor = MaterialTheme.colorScheme.secondaryContainer,\n            color = fontColor,\n        )\n    }\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/common/SelectAccountDialog.kt",
    "content": "package com.zhangke.fread.status.ui.common\n\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Check\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.window.Dialog\nimport com.zhangke.framework.composable.noRippleClick\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.account.LoggedAccount\nimport com.zhangke.fread.status.ui.BlogAuthorAvatar\nimport com.zhangke.fread.status.ui.richtext.RichTextWithIcon\nimport org.jetbrains.compose.resources.stringResource\n\n@Composable\nfun SelectAccountDialog(\n    accountList: List<LoggedAccount>,\n    selectedAccounts: List<LoggedAccount>,\n    onDismissRequest: () -> Unit,\n    onAccountClicked: (LoggedAccount) -> Unit,\n) {\n    Dialog(onDismissRequest = onDismissRequest) {\n        Surface(\n            shape = RoundedCornerShape(16.dp),\n            shadowElevation = 6.dp,\n        ) {\n            Column(\n                modifier = Modifier.fillMaxWidth()\n                    .padding(16.dp)\n                    .verticalScroll(rememberScrollState()),\n            ) {\n                Text(\n                    modifier = Modifier,\n                    text = stringResource(LocalizedString.statusUiSwitchAccountDialogTitle),\n                    style = MaterialTheme.typography.titleMedium\n                        .copy(fontWeight = FontWeight.SemiBold),\n                )\n                for (account in accountList) {\n                    Spacer(modifier = Modifier.height(16.dp))\n                    SelectableAccount(\n                        account = account,\n                        selected = selectedAccounts.any { account.uri == it.uri },\n                        onClick = {\n                            onDismissRequest()\n                            onAccountClicked(account)\n                        },\n                    )\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun SelectableAccount(\n    account: LoggedAccount,\n    selected: Boolean,\n    onClick: (LoggedAccount) -> Unit,\n) {\n    Row(\n        modifier = Modifier.fillMaxWidth().noRippleClick { onClick(account) },\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        BlogAuthorAvatar(\n            modifier = Modifier.size(42.dp),\n            imageUrl = account.avatar,\n        )\n        Column(\n            modifier = Modifier.padding(start = 16.dp).weight(1F),\n        ) {\n            RichTextWithIcon(\n                text = account.humanizedName,\n                style = MaterialTheme.typography.titleMedium\n                    .copy(fontWeight = FontWeight.SemiBold),\n                maxLines = 1,\n                overflow = TextOverflow.Ellipsis,\n                endIcon = {\n                    if (selected) {\n                        Spacer(modifier = Modifier.width(2.dp))\n                        Icon(\n                            modifier = Modifier.size(18.dp),\n                            imageVector = Icons.Default.Check,\n                            tint = MaterialTheme.colorScheme.primary,\n                            contentDescription = null,\n                        )\n                    }\n                }\n            )\n            Text(\n                text = account.prettyHandle,\n                style = MaterialTheme.typography.labelMedium,\n                color = MaterialTheme.colorScheme.onSurfaceVariant,\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/common/StatusSharedElementConfig.kt",
    "content": "package com.zhangke.fread.status.ui.common\n\nimport androidx.compose.runtime.ProvidableCompositionLocal\nimport androidx.compose.runtime.compositionLocalOf\n\ndata class StatusSharedElementConfig(\n    val wholeBlogEnabled: Boolean,\n    val imageAttachmentEnabled: Boolean,\n    val label: String,\n) {\n\n    fun buildImageKey(key: String): String {\n        return buildKey(label, key)\n    }\n\n    companion object {\n\n        fun default(): StatusSharedElementConfig {\n            return StatusSharedElementConfig(\n                wholeBlogEnabled = true,\n                imageAttachmentEnabled = true,\n                label = \"feeds\"\n            )\n        }\n\n        fun buildKey(label: String, key: String): String {\n            return \"$label-$key\"\n        }\n    }\n}\n\nval LocalStatusSharedElementConfig: ProvidableCompositionLocal<StatusSharedElementConfig> =\n    compositionLocalOf { StatusSharedElementConfig.default() }\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/common/UserFollowLine.kt",
    "content": "package com.zhangke.fread.status.ui.common\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.text.AnnotatedString\nimport androidx.compose.ui.text.SpanStyle\nimport androidx.compose.ui.text.buildAnnotatedString\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.TextUnit\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport com.zhangke.framework.utils.formatToHumanReadable\nimport com.zhangke.fread.localization.LocalizedString\nimport org.jetbrains.compose.resources.StringResource\nimport org.jetbrains.compose.resources.stringResource\n\n@Composable\nfun UserFollowLine(\n    modifier: Modifier,\n    followersCount: Long?,\n    followingCount: Long?,\n    statusesCount: Long?,\n    isHighlightBigger: Boolean = true,\n    onFollowerClick: (() -> Unit)? = null,\n    onFollowingClick: (() -> Unit)? = null,\n) {\n    Row(\n        modifier = modifier,\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        CountInfoItem(\n            count = followersCount,\n            descId = LocalizedString.statusUiUserDetailFollowerInfo,\n            onClick = onFollowerClick,\n            isHighlightBigger = isHighlightBigger,\n        )\n        Text(\n            modifier = Modifier\n                .padding(horizontal = 4.dp),\n            text = \"·\",\n            style = MaterialTheme.typography.bodySmall,\n        )\n        CountInfoItem(\n            count = followingCount,\n            descId = LocalizedString.statusUiUserDetailFollowingInfo,\n            onClick = onFollowingClick,\n            isHighlightBigger = isHighlightBigger,\n        )\n        Text(\n            modifier = Modifier\n                .padding(horizontal = 4.dp),\n            text = \"·\",\n            style = MaterialTheme.typography.bodySmall,\n        )\n        CountInfoItem(\n            count = statusesCount,\n            isHighlightBigger = isHighlightBigger,\n            descId = LocalizedString.statusUiUserDetailPosts,\n        )\n    }\n}\n\n@Composable\nprivate fun CountInfoItem(\n    count: Long?,\n    descId: StringResource,\n    isHighlightBigger: Boolean,\n    onClick: (() -> Unit)? = null,\n) {\n    val descSuffix = stringResource(descId)\n    val info = remember(count) {\n        if (count == null) {\n            buildAnnotatedString { append(\"    \") }\n        } else {\n            buildCountedDesc(count, descSuffix, isHighlightBigger)\n        }\n    }\n    Text(\n        modifier = Modifier.clickable(count != null && onClick != null) {\n            onClick?.invoke()\n        },\n        text = info,\n        style = MaterialTheme.typography.bodySmall,\n    )\n}\n\nprivate fun buildCountedDesc(\n    count: Long,\n    desc: String,\n    isHighlightBigger: Boolean,\n): AnnotatedString {\n    val formattedCount = count.formatToHumanReadable()\n    return buildAnnotatedString {\n        append(formattedCount)\n\n        addStyle(\n            style = SpanStyle(\n                fontSize = if (isHighlightBigger) 16.sp else TextUnit.Unspecified,\n                fontWeight = FontWeight.Medium,\n            ),\n            start = 0,\n            end = formattedCount.length,\n        )\n        append(\" \")\n        append(desc)\n    }\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/embed/BlogEmbedsUi.kt",
    "content": "package com.zhangke.fread.status.ui.embed\n\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.DividerDefaults\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport com.zhangke.fread.status.blog.Blog\nimport com.zhangke.fread.status.blog.BlogEmbed\nimport com.zhangke.fread.status.ui.style.StatusStyle\n\n@Composable\ninternal fun BlogEmbedsUi(\n    modifier: Modifier,\n    embeds: List<BlogEmbed>,\n    style: StatusStyle,\n    onContentClick: (Blog) -> Unit,\n    onUrlClick: (url: String) -> Unit,\n    onUnavailableQuoteClick: (String) -> Unit,\n) {\n    if (embeds.isEmpty()) return\n    embeds.forEach { embed ->\n        BlogEmbedUi(\n            modifier = modifier\n                .padding(top = style.contentStyle.contentVerticalSpacing),\n            embed = embed,\n            style = style,\n            onUrlClick = onUrlClick,\n            onContentClick = onContentClick,\n            onUnavailableQuoteClick = onUnavailableQuoteClick,\n        )\n    }\n}\n\n@Composable\nprivate fun BlogEmbedUi(\n    modifier: Modifier,\n    embed: BlogEmbed,\n    style: StatusStyle,\n    onContentClick: (Blog) -> Unit,\n    onUrlClick: (url: String) -> Unit,\n    onUnavailableQuoteClick: (String) -> Unit,\n) {\n    when (embed) {\n        is BlogEmbed.Link -> {\n            StatusEmbedLinkUi(\n                modifier = modifier\n                    .embedBorder()\n                    .fillMaxWidth(),\n                linkEmbed = embed,\n                style = style.cardStyle,\n                onCardClick = { onUrlClick(embed.url) },\n            )\n        }\n\n        is BlogEmbed.Blog -> {\n            BlogInEmbedding(\n                modifier = modifier\n                    .embedBorder()\n                    .padding(8.dp),\n                blog = embed.blog,\n                style = style,\n                onContentClick = onContentClick,\n            )\n        }\n\n        is BlogEmbed.UnavailableQuote -> {\n            UnavailableQuoteInEmbedding(\n                modifier = modifier.embedBorder().padding(16.dp),\n                unavailableQuote = embed,\n                onContentClick = onUnavailableQuoteClick,\n            )\n        }\n    }\n}\n\n@Composable\nfun Modifier.embedBorder(): Modifier {\n    return this.border(\n        width = 1.dp,\n        color = DividerDefaults.color,\n        shape = RoundedCornerShape(8.dp),\n    )\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/embed/BlogInEmbedding.kt",
    "content": "package com.zhangke.fread.status.ui.embed\n\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport com.zhangke.framework.composable.noRippleClick\nimport com.zhangke.fread.status.blog.Blog\nimport com.zhangke.fread.status.blog.BlogEmbed\nimport com.zhangke.fread.status.ui.BlogAuthorAvatar\nimport com.zhangke.fread.status.ui.BlogTextContentSection\nimport com.zhangke.fread.status.ui.media.BlogMedias\nimport com.zhangke.fread.status.ui.publish.NameAndAccountInfo\nimport com.zhangke.fread.status.ui.style.StatusStyle\n\n@Composable\nfun BlogInEmbedding(\n    modifier: Modifier,\n    blog: Blog,\n    style: StatusStyle,\n    onContentClick: (Blog) -> Unit = {},\n) {\n    Row(\n        modifier = modifier.noRippleClick { onContentClick(blog) },\n    ) {\n        BlogAuthorAvatar(\n            modifier = Modifier.size(style.infoLineStyle.avatarSize),\n            imageUrl = blog.author.avatar,\n        )\n        Column(\n            modifier = Modifier.weight(1F).padding(start = 8.dp),\n        ) {\n            // Name\\handle\\time row\n            Row(\n                modifier = Modifier.fillMaxWidth().padding(top = 2.dp),\n            ) {\n                NameAndAccountInfo(\n                    modifier = Modifier.weight(1F)\n                        .alignByBaseline(),\n                    humanizedName = blog.author.humanizedName,\n                    handle = blog.author.prettyHandle,\n                    style = style,\n                )\n                Text(\n                    modifier = Modifier.padding(start = 4.dp)\n                        .alignByBaseline(),\n                    text = blog.formattingDisplayTime.formattedTime(),\n                    style = style.infoLineStyle.descStyle,\n                    color = style.secondaryFontColor,\n                )\n            }\n\n            // text content and images\n            Row(\n                modifier = Modifier.fillMaxWidth().padding(top = 2.dp),\n            ) {\n                Column(modifier = Modifier.weight(3F)) {\n                    BlogTextContentSection(\n                        blog = blog,\n                        style = style.contentStyle.copy(\n                            maxLine = 10\n                        ),\n                    )\n                }\n                if (blog.mediaList.isNotEmpty()) {\n                    Spacer(modifier = Modifier.size(8.dp))\n                    BlogMedias(\n                        modifier = Modifier.weight(1F),\n                        mediaList = blog.mediaList,\n                        sharedElementId = blog.id,\n                        indexInList = 0,\n                        sensitive = blog.sensitive,\n                        onMediaClick = {},\n                        showAlt = false,\n                    )\n                }\n            }\n\n            // link card\n            val linkEmbed = blog.embeds.firstNotNullOfOrNull { it as? BlogEmbed.Link }\n            if (linkEmbed != null) {\n                Spacer(modifier = Modifier.height(style.contentStyle.contentVerticalSpacing))\n                StatusEmbedLinkUi(\n                    modifier = Modifier.fillMaxWidth()\n                        .embedBorder(),\n                    style = style.cardStyle,\n                    linkEmbed = linkEmbed,\n                    onCardClick = {},\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/embed/StatusEmbedLinkUi.kt",
    "content": "package com.zhangke.fread.status.ui.embed\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.aspectRatio\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Description\nimport androidx.compose.material.icons.filled.PlayArrow\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.IconButtonDefaults\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport com.seiko.imageloader.model.ImageRequest\nimport com.seiko.imageloader.ui.AutoSizeImage\nimport com.zhangke.framework.blurhash.blurhash\nimport com.zhangke.fread.status.blog.BlogEmbed\nimport com.zhangke.fread.status.ui.style.StatusStyle\n\n@Composable\nfun StatusEmbedLinkUi(\n    modifier: Modifier,\n    linkEmbed: BlogEmbed.Link,\n    style: StatusStyle.CardStyle,\n    onCardClick: (BlogEmbed.Link) -> Unit,\n) {\n    val containerModifier = modifier\n        .clickable { onCardClick(linkEmbed) }\n        .padding(bottom = style.contentVerticalPadding)\n    if (linkEmbed.image.isNullOrEmpty().not()) {\n        Column(modifier = containerModifier) {\n            if (linkEmbed.image.isNullOrEmpty().not()) {\n                Box(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .aspectRatio(linkEmbed.aspectRatio)\n                ) {\n                    AutoSizeImage(\n                        remember(linkEmbed.image) {\n                            ImageRequest(linkEmbed.image.orEmpty())\n                        },\n                        modifier = Modifier\n                            .clip(RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp))\n                            .fillMaxSize()\n                            .blurhash(linkEmbed.blurhash),\n                        contentScale = ContentScale.Crop,\n                        contentDescription = \"Preview Image\",\n                    )\n                    if (linkEmbed.video) {\n                        IconButton(\n                            modifier = Modifier\n                                .size(32.dp)\n                                .align(Alignment.Center),\n                            onClick = {\n                                onCardClick(linkEmbed)\n                            },\n                            colors = IconButtonDefaults.iconButtonColors(\n                                containerColor = MaterialTheme.colorScheme.primaryContainer,\n                                contentColor = MaterialTheme.colorScheme.onPrimaryContainer,\n                            ),\n                        ) {\n                            Icon(\n                                imageVector = Icons.Default.PlayArrow,\n                                contentDescription = \"Play Video\",\n                            )\n                        }\n                    }\n                }\n            }\n            Spacer(modifier = Modifier.height(style.imageBottomPadding))\n            PreviewCardTexts(linkEmbed, style, 2)\n        }\n    } else {\n        Row(\n            modifier = containerModifier\n                .height(86.dp)\n                .padding(top = style.contentVerticalPadding),\n        ) {\n            Column(\n                modifier = Modifier\n                    .weight(1F)\n                    .padding(end = 8.dp)\n            ) {\n                PreviewCardTexts(linkEmbed, style, 1)\n            }\n            Box(\n                modifier = Modifier\n                    .padding(end = 10.dp)\n                    .fillMaxHeight()\n                    .aspectRatio(1F)\n                    .background(\n                        color = MaterialTheme.colorScheme.surfaceDim,\n                        shape = RoundedCornerShape(8.dp),\n                    ),\n            ) {\n                Icon(\n                    modifier = Modifier\n                        .align(Alignment.Center)\n                        .size(36.dp),\n                    imageVector = Icons.Default.Description,\n                    contentDescription = \"Link\",\n                    tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7F),\n                )\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun PreviewCardTexts(\n    card: BlogEmbed.Link,\n    style: StatusStyle.CardStyle,\n    maxLine: Int,\n) {\n    if (!card.providerName.isNullOrEmpty()) {\n        Text(\n            modifier = Modifier.padding(horizontal = 16.dp),\n            text = card.providerName!!,\n            style = style.descStyle,\n            maxLines = 1,\n            overflow = TextOverflow.Ellipsis,\n        )\n        Spacer(modifier = Modifier.height(4.dp))\n    }\n    Text(\n        modifier = Modifier\n            .fillMaxWidth()\n            .padding(horizontal = 16.dp),\n        textAlign = TextAlign.Start,\n        text = card.title,\n        style = style.titleStyle,\n        maxLines = maxLine,\n        overflow = TextOverflow.Ellipsis,\n    )\n    if (card.description.isNotEmpty()) {\n        Spacer(modifier = Modifier.height(4.dp))\n        Text(\n            modifier = Modifier\n                .fillMaxWidth()\n                .padding(horizontal = 16.dp),\n            textAlign = TextAlign.Start,\n            text = card.description,\n            style = style.descStyle,\n            maxLines = maxLine,\n            overflow = TextOverflow.Ellipsis,\n        )\n    }\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/embed/UnavailableQuoteInEmbedding.kt",
    "content": "package com.zhangke.fread.status.ui.embed\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material3.Card\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.blog.BlogEmbed\nimport org.jetbrains.compose.resources.stringResource\n\n@Composable\nfun UnavailableQuoteInEmbedding(\n    modifier: Modifier,\n    unavailableQuote: BlogEmbed.UnavailableQuote,\n    onContentClick: (String) -> Unit,\n) {\n    Column(\n        modifier = modifier,\n        horizontalAlignment = Alignment.CenterHorizontally,\n        verticalArrangement = Arrangement.Center,\n    ) {\n        Card(\n            modifier = Modifier.fillMaxWidth(),\n            onClick = {\n                if (!unavailableQuote.blogId.isNullOrEmpty()) {\n                    onContentClick(unavailableQuote.blogId!!)\n                }\n            },\n            enabled = !unavailableQuote.blogId.isNullOrEmpty(),\n        ) {\n            Box(\n                modifier = Modifier.fillMaxWidth().padding(16.dp),\n                contentAlignment = Alignment.Center,\n            ) {\n                Text(\n                    text = stringResource(LocalizedString.status_ui_embed_quote_unavailable),\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/hashtag/HashtagUi.kt",
    "content": "package com.zhangke.fread.status.ui.hashtag\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.heightIn\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.material3.DividerDefaults\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.drawscope.Stroke\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport com.zhangke.framework.composable.BezierCurve\nimport com.zhangke.framework.composable.BezierCurveStyle\nimport com.zhangke.framework.composable.freadPlaceholder\nimport com.zhangke.framework.composable.textString\nimport com.zhangke.framework.utils.toPx\nimport com.zhangke.fread.status.model.Hashtag\n\n@Composable\nfun HashtagUi(\n    tag: Hashtag,\n    onClick: (Hashtag) -> Unit,\n    modifier: Modifier = Modifier,\n) {\n    Row(\n        modifier = modifier\n            .fillMaxWidth()\n            .heightIn(min = 80.dp)\n            .clickable {\n                onClick(tag)\n            },\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        Column(\n            modifier = Modifier\n                .weight(1F)\n                .fillMaxWidth()\n                .padding(start = 15.dp, end = 15.dp)\n        ) {\n            Text(\n                text = tag.name,\n                fontSize = 16.sp,\n            )\n            Text(\n                modifier = Modifier.padding(top = 3.dp),\n                text = textString(tag.description),\n                fontSize = 12.sp,\n            )\n        }\n\n        BezierCurve(\n            modifier = Modifier\n                .padding(end = 15.dp)\n                .size(width = 70.dp, height = 40.dp),\n            points = tag.history.history.reversed(),\n            minPoint = tag.history.min,\n            maxPoint = tag.history.max,\n            style = BezierCurveStyle.StrokeAndFill(\n                fillBrush = SolidColor(MaterialTheme.colorScheme.primary),\n                strokeBrush = SolidColor(Color.White),\n                stroke = Stroke(width = 1.dp.toPx()),\n            )\n        )\n    }\n}\n\n@Composable\nfun HashtagUiPlaceholder() {\n    Row(\n        modifier = Modifier\n            .fillMaxWidth()\n            .heightIn(min = 80.dp),\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        Column(\n            modifier = Modifier\n                .weight(1F)\n                .fillMaxWidth()\n                .padding(start = 15.dp, end = 15.dp)\n        ) {\n            Box(\n                modifier = Modifier\n                    .size(width = 100.dp, height = 16.dp)\n                    .freadPlaceholder(true)\n            )\n            Spacer(modifier = Modifier.height(4.dp))\n            Box(\n                modifier = Modifier\n                    .size(width = 200.dp, height = 16.dp)\n                    .freadPlaceholder(true)\n            )\n        }\n\n        BezierCurve(\n            modifier = Modifier\n                .padding(end = 15.dp)\n                .size(width = 70.dp, height = 40.dp),\n            points = listOf(0.4F, 0.5F, 0.4F, 0.5F, 0.6F, 0.7F),\n            minPoint = 0.3F,\n            maxPoint = 0.7F,\n            style = BezierCurveStyle.StrokeAndFill(\n                fillBrush = SolidColor(DividerDefaults.color),\n                strokeBrush = SolidColor(DividerDefaults.color),\n                stroke = Stroke(width = 1.dp.toPx()),\n            )\n        )\n    }\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/image/BlogImageMedia.kt",
    "content": "package com.zhangke.fread.status.ui.image\n\nimport androidx.compose.animation.ExperimentalSharedTransitionApi\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.layout.wrapContentHeight\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.foundation.text.selection.SelectionContainer\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.PlayCircleOutline\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.ModalBottomSheet\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport com.seiko.imageloader.LocalImageLoader\nimport com.seiko.imageloader.model.ImageRequest\nimport com.seiko.imageloader.ui.AutoSizeImage\nimport com.zhangke.framework.blurhash.blurhash\nimport com.zhangke.framework.composable.rememberTransientModalBottomSheetState\nimport com.zhangke.framework.imageloader.executeSafety\nimport com.zhangke.framework.ktx.ifNullOrEmpty\nimport com.zhangke.framework.nav.sharedElement\nimport com.zhangke.fread.status.blog.BlogMedia\nimport com.zhangke.fread.status.blog.BlogMediaMeta\nimport com.zhangke.fread.status.blog.BlogMediaType\nimport com.zhangke.fread.status.ui.common.LocalStatusSharedElementConfig\nimport com.zhangke.fread.status.ui.common.StatusSharedElementConfig\n\ntypealias OnBlogMediaClick = (BlogMediaClickEvent) -> Unit\n\nsealed interface BlogMediaClickEvent {\n\n    data class BlogImageClickEvent(\n        val index: Int,\n        val mediaList: List<ClickedBlogMedia>,\n    ) : BlogMediaClickEvent\n\n    data class BlogVideoClickEvent(\n        val index: Int,\n        val media: BlogMedia,\n    ) : BlogMediaClickEvent\n}\n\ndata class ClickedBlogMedia(\n    val media: BlogMedia,\n    val sharedElementKey: String,\n)\n\n/**\n * Image and Gifv\n */\n@OptIn(ExperimentalSharedTransitionApi::class)\n@Composable\nfun BlogImageMedias(\n    mediaList: List<BlogMedia>,\n    sharedElementId: String,\n    containerWidth: Dp,\n    hideContent: Boolean,\n    style: BlogImageMediaStyle = BlogImageMediaDefault.defaultStyle,\n    onMediaClick: OnBlogMediaClick,\n    showAlt: Boolean = true,\n) {\n    val sharedElementConfig = LocalStatusSharedElementConfig.current\n    val fixedMediaList = remember(mediaList, sharedElementId, sharedElementConfig) {\n        mediaList.map {\n            ClickedBlogMedia(\n                media = it,\n                sharedElementKey = buildSharedElementKey(\n                    config = sharedElementConfig,\n                    sharedElementId = sharedElementId,\n                    media = it,\n                ),\n            )\n        }\n    }\n    val aspectList = mediaList.take(6).map { it.meta.decideAspect(style.defaultMediaAspect) }\n    BlogImageLayout(\n        modifier = Modifier.clip(RoundedCornerShape(style.radius)),\n        containerWidth = containerWidth,\n        aspectList = aspectList,\n        style = style,\n        itemContent = { index ->\n            val media = fixedMediaList[index]\n            BlogImage(\n                modifier = Modifier\n                    .fillMaxSize()\n                    .clickable(enabled = !hideContent) {\n                        onMediaClick(\n                            BlogMediaClickEvent.BlogImageClickEvent(\n                                index = index,\n                                mediaList = fixedMediaList,\n                            )\n                        )\n                    },\n                clickableMedia = media,\n                hideContent = hideContent,\n                showAlt = showAlt,\n            )\n        }\n    )\n}\n\nprivate fun buildSharedElementKey(\n    config: StatusSharedElementConfig,\n    sharedElementId: String,\n    media: BlogMedia,\n): String {\n    return config.buildImageKey(\"$sharedElementId-${media.url}\")\n}\n\n/**\n * For Image or gif\n */\n@Composable\ninternal fun BlogImageLayout(\n    modifier: Modifier,\n    containerWidth: Dp,\n    aspectList: List<Float>,\n    style: BlogImageMediaStyle,\n    itemContent: @Composable (index: Int) -> Unit,\n) {\n    when (aspectList.size) {\n        1 -> SingleBlogImageLayout(\n            modifier = modifier,\n            style = style,\n            aspect = aspectList.first(),\n            itemContent = { itemContent(0) },\n        )\n\n        2 -> DoubleBlogImageLayout(\n            modifier = modifier,\n            style = style,\n            aspectList = aspectList,\n            itemContent = itemContent,\n        )\n\n        3 -> TripleImageMediaLayout(\n            modifier = modifier,\n            containerWidth = containerWidth,\n            style = style,\n            aspectList = aspectList,\n            itemContent = itemContent,\n        )\n\n        4 -> QuadrupleImageMediaLayout(\n            modifier = modifier,\n            containerWidth = containerWidth,\n            aspectList = aspectList,\n            style = style,\n            itemContent = itemContent,\n        )\n\n        5 -> FivefoldImageMediaLayout(\n            modifier = modifier,\n            containerWidth = containerWidth,\n            aspectList = aspectList,\n            style = style,\n            itemContent = itemContent,\n        )\n\n        6 -> SixfoldImageMediaLayout(\n            modifier = modifier,\n            aspectList = aspectList,\n            style = style,\n            itemContent = itemContent,\n        )\n    }\n}\n\n@OptIn(ExperimentalMaterial3Api::class, ExperimentalSharedTransitionApi::class)\n@Composable\ninternal fun BlogImage(\n    modifier: Modifier,\n    clickableMedia: ClickedBlogMedia,\n    hideContent: Boolean,\n    showAlt: Boolean,\n) {\n    val media = clickableMedia.media\n    val imageUrl = if (media.type == BlogMediaType.GIFV) media.previewUrl else media.url\n    if (hideContent) {\n        val imageLoader = LocalImageLoader.current\n        LaunchedEffect(media) {\n            imageLoader.executeSafety(\n                ImageRequest {\n                    data(imageUrl)\n                }\n            )\n        }\n    }\n\n    Box(modifier = modifier.blurhash(media.blurhash)) {\n        if (!hideContent) {\n            BlogAutoSizeImage(\n                modifier = Modifier,\n                imageUrl = imageUrl,\n                description = media.description,\n                sharedElementKey = clickableMedia.sharedElementKey,\n            )\n        }\n        if (media.type == BlogMediaType.GIFV) {\n            Icon(\n                modifier = Modifier\n                    .size(32.dp)\n                    .align(Alignment.Center),\n                imageVector = Icons.Default.PlayCircleOutline,\n                tint = Color.White,\n                contentDescription = \"Play\",\n            )\n        }\n        if (showAlt && !media.description.isNullOrEmpty()) {\n            var showBottomSheet by remember { mutableStateOf(false) }\n            Surface(\n                modifier = Modifier.align(Alignment.BottomStart)\n                    .padding(start = 2.dp, bottom = 2.dp),\n                onClick = {\n                    showBottomSheet = true\n                },\n                shape = RoundedCornerShape(4.dp),\n                enabled = true,\n                color = Color.Black.copy(alpha = 0.6F),\n                contentColor = Color.White,\n                content = {\n                    Text(\n                        modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),\n                        text = \"ALT\",\n                        style = MaterialTheme.typography.labelSmall,\n                    )\n                },\n            )\n\n            if (showBottomSheet) {\n                val sheetState = rememberTransientModalBottomSheetState()\n                ModalBottomSheet(\n                    sheetState = sheetState,\n                    onDismissRequest = { showBottomSheet = false },\n                ) {\n                    Box(\n                        modifier = Modifier\n                            .fillMaxWidth()\n                            .padding(start = 16.dp, top = 8.dp, end = 16.dp, bottom = 28.dp)\n                            .wrapContentHeight()\n                    ) {\n                        SelectionContainer {\n                            Text(text = media.description.orEmpty())\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun BlogAutoSizeImage(\n    modifier: Modifier,\n    imageUrl: String?,\n    description: String?,\n    sharedElementKey: String,\n) {\n    val sharedElementConfig = LocalStatusSharedElementConfig.current\n    val finalModifier = modifier.then(\n        if (sharedElementConfig.imageAttachmentEnabled) {\n            Modifier.sharedElement(sharedElementKey)\n        } else {\n            Modifier\n        }\n    )\n    AutoSizeImage(\n        request = remember {\n            ImageRequest {\n                data(imageUrl)\n            }\n        },\n        modifier = finalModifier.fillMaxSize(),\n        contentScale = ContentScale.Crop,\n        contentDescription = description.ifNullOrEmpty { \"Blog Image Media\" },\n    )\n}\n\n@Composable\ninternal fun VerticalSpacer(height: Dp) {\n    Spacer(\n        modifier = Modifier\n            .width(1.dp)\n            .height(height)\n    )\n}\n\n@Composable\ninternal fun HorizontalSpacer(width: Dp) {\n    Spacer(\n        modifier = Modifier\n            .width(width)\n            .height(1.dp)\n    )\n}\n\ninternal fun BlogMediaMeta?.decideAspect(defaultMediaAspect: Float): Float {\n    val metaAspect = when (this) {\n        is BlogMediaMeta.ImageMeta -> this.original?.aspect\n        is BlogMediaMeta.GifvMeta -> this.aspect ?: this.original?.aspect\n        is BlogMediaMeta.VideoMeta -> this.aspect ?: this.original?.aspect\n        else -> null\n    }\n    return metaAspect ?: defaultMediaAspect\n}\n\ndata class BlogImageMediaStyle(\n    val radius: Dp,\n    val horizontalDivider: Dp,\n    val verticalDivider: Dp,\n    val defaultMediaAspect: Float,\n    val minAspect: Float,\n    val maxAspect: Float,\n    val maxWeightInHorizontal: Float,\n    val minWeightInHorizontal: Float,\n    val maxWeightInHorizontalThreshold: Float,\n    val quadrupleHorizontalThreshold: Float,\n    val quadrupleVerticalThreshold: Float,\n    val sixfoldAspect: Float,\n)\n\nobject BlogImageMediaDefault {\n\n    val defaultStyle = BlogImageMediaStyle(\n        radius = 8.dp,\n        horizontalDivider = 4.dp,\n        verticalDivider = 4.dp,\n        defaultMediaAspect = 1F,\n        minAspect = 0.6F,\n        maxAspect = 3F,\n        maxWeightInHorizontal = 0.75F,\n        minWeightInHorizontal = 0.5F,\n        maxWeightInHorizontalThreshold = 0.6F,\n        quadrupleHorizontalThreshold = 0.67F,\n        quadrupleVerticalThreshold = 1.5F,\n        sixfoldAspect = 1.5F,\n    )\n}\n\ninternal fun BlogImageMediaStyle.getCompliantAspect(aspect: Float): Float {\n    return aspect.coerceAtLeast(minAspect).coerceAtMost(maxAspect)\n}\n\n/**\n * Image more than two, and is horizontal arrange.\n */\ninternal fun BlogImageMediaStyle.decideFirstImageWeightInHorizontalMode(aspect: Float): Float {\n    if (aspect >= 1F) {\n        return minWeightInHorizontal\n    }\n    if (aspect <= maxWeightInHorizontalThreshold) return maxWeightInHorizontal\n    val floatingWeight = maxWeightInHorizontal - minWeightInHorizontal\n    val factor = 1F / aspect - 1F\n    val weight = minWeightInHorizontal + floatingWeight * factor\n    return weight.coerceAtLeast(minWeightInHorizontal).coerceAtMost(maxWeightInHorizontal)\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/image/DoubleBlogImageLayout.kt",
    "content": "package com.zhangke.fread.status.ui.image\n\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.aspectRatio\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport com.zhangke.framework.ktx.second\n\n@Composable\ninternal fun DoubleBlogImageLayout(\n    modifier: Modifier = Modifier,\n    style: BlogImageMediaStyle,\n    aspectList: List<Float>,\n    itemContent: @Composable (index: Int) -> Unit,\n) {\n    val firstAspect = aspectList.first()\n    val secondAspect = aspectList.second()\n    val firstFixedAspect = style.getCompliantAspect(firstAspect)\n    val secondFixedAspect = style.getCompliantAspect(secondAspect)\n    if (firstAspect > 1 && secondAspect > 1) {\n        // vertical arrange\n        Column(\n            modifier = modifier\n                .fillMaxWidth()\n        ) {\n            Box(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .aspectRatio(firstFixedAspect)\n            ) {\n                itemContent(0)\n            }\n            VerticalSpacer(style.verticalDivider)\n            Box(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .aspectRatio(secondFixedAspect)\n            ) {\n                itemContent(1)\n            }\n        }\n    } else {\n        // horizontal arrange\n        Row(\n            modifier = modifier\n                .fillMaxWidth()\n        ) {\n            Box(\n                modifier = Modifier\n                    .weight(firstFixedAspect)\n                    .aspectRatio(firstFixedAspect)\n            ) {\n                itemContent(0)\n            }\n            HorizontalSpacer(style.horizontalDivider)\n            Box(\n                modifier = Modifier\n                    .weight(secondFixedAspect)\n                    .aspectRatio(secondFixedAspect)\n            ) {\n                itemContent(1)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/image/FivefoldImageMediaFrameLayout.kt",
    "content": "package com.zhangke.fread.status.ui.image\n\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.layout.onGloballyPositioned\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.unit.Dp\nimport com.zhangke.framework.ktx.averageDropFirst\nimport com.zhangke.framework.ktx.second\nimport com.zhangke.framework.ktx.third\nimport com.zhangke.framework.utils.pxToDp\n\n@Composable\ninternal fun FivefoldImageMediaLayout(\n    modifier: Modifier,\n    containerWidth: Dp,\n    aspectList: List<Float>,\n    style: BlogImageMediaStyle,\n    itemContent: @Composable (index: Int) -> Unit,\n) {\n    val density = LocalDensity.current\n    val firstAspect = aspectList.first()\n    if (firstAspect < 1F) {\n        HorizontalImageMediaFrameLayout(\n            modifier = modifier,\n            containerWidth = containerWidth,\n            style = style,\n            startAspect = firstAspect,\n            startContent = {\n                var mainlyWidth: Dp? by remember {\n                    mutableStateOf(null)\n                }\n                Box(\n                    modifier = Modifier\n                        .fillMaxSize()\n                        .onGloballyPositioned {\n                            mainlyWidth = it.size.width.pxToDp(density)\n                        }\n                ) {\n                    if (mainlyWidth != null) {\n                        val verticalList = remember {\n                            mutableListOf<Float>().apply {\n                                add(aspectList.first())\n                                add(aspectList[3])\n                                add(aspectList[4])\n                            }\n                        }\n                        VerticalImageMediaFrameLayout(\n                            modifier = Modifier,\n                            containerWidth = mainlyWidth!!,\n                            style = style,\n                            topAspect = firstAspect,\n                            fixedHeight = true,\n                            bottomAspect = verticalList.averageDropFirst(1).toFloat(),\n                            topContent = {\n                                itemContent(0)\n                            },\n                            bottomContent = {\n                                HorizontalImageMediaListLayout(\n                                    modifier = Modifier.fillMaxSize(),\n                                    style = style,\n                                    dropFirst = 1,\n                                    aspectList = verticalList,\n                                    itemContent = { index ->\n                                        itemContent(2 + index)\n                                    }\n                                )\n                            },\n                        )\n                    }\n                }\n            },\n            endContent = {\n                VerticalImageMediaListLayout(\n                    modifier = Modifier.fillMaxSize(),\n                    style = style,\n                    dropFirst = 1,\n                    aspectList = aspectList.take(3),\n                    itemContent = itemContent,\n                )\n            },\n        )\n    } else {\n        val bottomAspectList = aspectList.drop(3)\n        VerticalImageMediaFrameLayout(\n            modifier = modifier,\n            containerWidth = containerWidth,\n            style = style,\n            topAspect = aspectList.first(),\n            bottomAspect = bottomAspectList.average().toFloat(),\n            topContent = {\n                var mainlyWidth: Dp? by remember {\n                    mutableStateOf(null)\n                }\n                Box(\n                    modifier = Modifier\n                        .fillMaxSize()\n                        .onGloballyPositioned {\n                            mainlyWidth = it.size.width.pxToDp(density)\n                        }\n                ) {\n                    if (mainlyWidth != null) {\n                        HorizontalImageMediaFrameLayout(\n                            modifier = Modifier,\n                            containerWidth = mainlyWidth!!,\n                            style = style,\n                            fixedHeight = true,\n                            startAspect = aspectList.first(),\n                            startContent = {\n                                itemContent(0)\n                            },\n                            endContent = {\n                                VerticalImageMediaListLayout(\n                                    modifier = Modifier.fillMaxSize(),\n                                    style = style,\n                                    dropFirst = 0,\n                                    aspectList = listOf(aspectList.second(), aspectList.third()),\n                                    itemContent = { index ->\n                                        itemContent(1 + index)\n                                    }\n                                )\n                            }\n                        )\n                    }\n                }\n            },\n            bottomContent = {\n                HorizontalImageMediaListLayout(\n                    modifier = Modifier.fillMaxSize(),\n                    style = style,\n                    dropFirst = 0,\n                    aspectList = bottomAspectList,\n                    itemContent = { index ->\n                        itemContent(3 + index)\n                    }\n                )\n            },\n        )\n    }\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/image/HorizontalImageMediaFrameLayout.kt",
    "content": "package com.zhangke.fread.status.ui.image\n\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.Dp\n\n/**\n * As shown below:\n *       ---------------   --------\n *      ｜              ｜ ｜       ｜\n *      ｜              ｜ ｜       ｜\n *      ｜              ｜ ｜       ｜\n *      ｜              ｜ ｜       ｜\n *      ｜              ｜ ｜       ｜\n *      ｜              ｜ ｜       ｜\n *      ｜              ｜ ｜       ｜\n *      ｜              ｜ ｜       ｜\n *       ---------------   ---------\n */\n@Composable\ninternal fun HorizontalImageMediaFrameLayout(\n    modifier: Modifier,\n    containerWidth: Dp,\n    fixedHeight: Boolean = false,\n    style: BlogImageMediaStyle,\n    startAspect: Float,\n    startContent: @Composable () -> Unit,\n    endContent: @Composable () -> Unit,\n) {\n    val startFixedAspect = style.getCompliantAspect(startAspect)\n    val firstImageWidthWeight = style.decideFirstImageWeightInHorizontalMode(startFixedAspect)\n    val remainderContainerWidth = containerWidth - style.horizontalDivider\n    val firstImageWidth = remainderContainerWidth * firstImageWidthWeight\n    val finalModifier = if (fixedHeight) {\n        modifier.fillMaxHeight()\n    } else {\n        val firstImageHeight = firstImageWidth / startFixedAspect\n        modifier.height(firstImageHeight)\n    }\n    Box(\n        modifier = finalModifier\n            .fillMaxWidth()\n    ) {\n        Box(\n            modifier = Modifier\n                .width(firstImageWidth)\n                .fillMaxHeight()\n        ) {\n            startContent()\n        }\n        Box(\n            modifier = Modifier\n                .align(Alignment.CenterEnd)\n                .width(remainderContainerWidth - firstImageWidth)\n                .fillMaxHeight()\n        ) {\n            endContent()\n        }\n    }\n}\n\n/**\n * As shown below:\n *       ---------------   --------\n *      ｜              ｜ ｜       ｜\n *      ｜              ｜ ｜       ｜\n *      ｜              ｜ ｜       ｜\n *      ｜              ｜  --------\n *      ｜              ｜  --------\n *      ｜              ｜ ｜       ｜\n *      ｜              ｜ ｜       ｜\n *      ｜              ｜ ｜       ｜\n *      ｜              ｜ ｜       ｜\n *       ---------------   ---------\n */\n@Composable\ninternal fun HorizontalImageMediaFrameLayout(\n    modifier: Modifier,\n    containerWidth: Dp,\n    style: BlogImageMediaStyle,\n    aspectList: List<Float>,\n    itemContent: @Composable (index: Int) -> Unit,\n) {\n    HorizontalImageMediaFrameLayout(\n        modifier = modifier,\n        containerWidth = containerWidth,\n        style = style,\n        startAspect = aspectList.first(),\n        startContent = {\n            itemContent(0)\n        },\n        endContent = {\n            VerticalImageMediaListLayout(\n                modifier = Modifier,\n                dropFirst = 1,\n                style = style,\n                aspectList = aspectList,\n                itemContent = itemContent,\n            )\n        }\n    )\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/image/HorizontalImageMediaListLayout.kt",
    "content": "package com.zhangke.fread.status.ui.image\n\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\n\n@Composable\ninternal fun HorizontalImageMediaListLayout(\n    modifier: Modifier,\n    style: BlogImageMediaStyle,\n    dropFirst: Int,\n    aspectList: List<Float>,\n    itemContent: @Composable (index: Int) -> Unit,\n) {\n    Row(\n        modifier = modifier\n    ) {\n        for (index in dropFirst until aspectList.size) {\n            Box(\n                modifier = Modifier\n                    .fillMaxHeight()\n                    .weight(1F)\n            ) {\n                itemContent(index)\n            }\n            if (index < aspectList.lastIndex) {\n                HorizontalSpacer(width = style.horizontalDivider)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/image/QuadrupleImageMediaLayout.kt",
    "content": "package com.zhangke.fread.status.ui.image\n\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.aspectRatio\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.Dp\n\n@Composable\ninternal fun QuadrupleImageMediaLayout(\n    modifier: Modifier = Modifier,\n    containerWidth: Dp,\n    aspectList: List<Float>,\n    style: BlogImageMediaStyle,\n    itemContent: @Composable (index: Int) -> Unit,\n) {\n    val firstAspect = aspectList.first()\n    if (firstAspect > style.quadrupleHorizontalThreshold &&\n        firstAspect < style.quadrupleVerticalThreshold\n    ) {\n        // Grid arrange\n        QuadrupleGridLayout(\n            modifier = modifier,\n            aspectList = aspectList,\n            style = style,\n            itemContent = itemContent,\n        )\n    } else if (firstAspect <= style.quadrupleHorizontalThreshold) {\n        // horizontal arrange\n        HorizontalImageMediaFrameLayout(\n            modifier = modifier,\n            containerWidth = containerWidth,\n            style = style,\n            aspectList = aspectList,\n            itemContent = itemContent,\n        )\n    } else {\n        // vertical arrange\n        VerticalImageMediaFrameLayout(\n            modifier = modifier,\n            containerWidth = containerWidth,\n            style = style,\n            aspectList = aspectList,\n            itemContent = itemContent,\n        )\n    }\n}\n\n@Composable\nprivate fun QuadrupleGridLayout(\n    modifier: Modifier,\n    aspectList: List<Float>,\n    style: BlogImageMediaStyle,\n    itemContent: @Composable (index: Int) -> Unit,\n) {\n    val firstAspect = aspectList.first()\n    val fixedFirstAspect = style.getCompliantAspect(firstAspect)\n    Column(\n        modifier = modifier\n            .fillMaxWidth()\n    ) {\n        Row(modifier = Modifier.fillMaxWidth()) {\n            Box(\n                modifier = Modifier\n                    .weight(1F)\n                    .aspectRatio(fixedFirstAspect)\n            ) {\n                itemContent(0)\n            }\n            HorizontalSpacer(width = style.horizontalDivider)\n            Box(\n                modifier = Modifier\n                    .weight(1F)\n                    .aspectRatio(fixedFirstAspect)\n            ) {\n                itemContent(1)\n            }\n        }\n        VerticalSpacer(height = style.verticalDivider)\n        Row(modifier = Modifier.fillMaxWidth()) {\n            Box(\n                modifier = Modifier\n                    .weight(1F)\n                    .aspectRatio(fixedFirstAspect)\n            ) {\n                itemContent(2)\n            }\n            HorizontalSpacer(width = style.horizontalDivider)\n            Box(\n                modifier = Modifier\n                    .weight(1F)\n                    .aspectRatio(fixedFirstAspect)\n            ) {\n                itemContent(3)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/image/SingleBlogImageLayout.kt",
    "content": "package com.zhangke.fread.status.ui.image\n\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.aspectRatio\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\n\n@Composable\ninternal fun SingleBlogImageLayout(\n    modifier: Modifier,\n    style: BlogImageMediaStyle,\n    aspect: Float,\n    itemContent: @Composable () -> Unit,\n) {\n    val fixedAspect = style.getCompliantAspect(aspect)\n    Box(\n        modifier = modifier\n            .fillMaxWidth()\n            .aspectRatio(fixedAspect),\n        contentAlignment = Alignment.Center,\n    ) {\n        itemContent()\n    }\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/image/SixfoldImageMediaLayout.kt",
    "content": "package com.zhangke.fread.status.ui.image\n\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.aspectRatio\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\n\n@Composable\ninternal fun SixfoldImageMediaLayout(\n    modifier: Modifier = Modifier,\n    aspectList: List<Float>,\n    style: BlogImageMediaStyle,\n    itemContent: @Composable (index: Int) -> Unit,\n) {\n    Column(\n        modifier = modifier\n            .fillMaxWidth()\n            .aspectRatio(style.sixfoldAspect)\n    ) {\n        HorizontalImageMediaList(\n            modifier = Modifier\n                .fillMaxWidth()\n                .weight(1F),\n            dropFirst = 0,\n            aspectList = aspectList.take(3),\n            style = style,\n            itemContent = itemContent,\n        )\n        VerticalSpacer(height = style.verticalDivider)\n        HorizontalImageMediaList(\n            modifier = Modifier\n                .fillMaxWidth()\n                .weight(1F),\n            dropFirst = 3,\n            aspectList = aspectList,\n            style = style,\n            itemContent = itemContent,\n        )\n    }\n}\n\n@Composable\nprivate fun HorizontalImageMediaList(\n    modifier: Modifier,\n    dropFirst: Int,\n    aspectList: List<Float>,\n    style: BlogImageMediaStyle,\n    itemContent: @Composable (index: Int) -> Unit,\n) {\n    Row(modifier = modifier) {\n        for (index in dropFirst until aspectList.size) {\n            Box(\n                modifier = Modifier\n                    .fillMaxHeight()\n                    .weight(1F)\n            ) {\n                itemContent(index)\n            }\n            if (index < aspectList.lastIndex) {\n                HorizontalSpacer(width = style.horizontalDivider)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/image/TripleImageMediaLayout.kt",
    "content": "package com.zhangke.fread.status.ui.image\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.Dp\n\n@Composable\ninternal fun TripleImageMediaLayout(\n    modifier: Modifier = Modifier,\n    containerWidth: Dp,\n    style: BlogImageMediaStyle,\n    aspectList: List<Float>,\n    itemContent: @Composable (index: Int) -> Unit,\n) {\n    val firstAspect = aspectList.first()\n    if (firstAspect <= 1F) {\n        // horizontal arrange\n        HorizontalImageMediaFrameLayout(\n            modifier = modifier,\n            containerWidth = containerWidth,\n            style = style,\n            aspectList = aspectList,\n            itemContent = itemContent,\n        )\n    } else {\n        // vertical arrange\n        VerticalImageMediaFrameLayout(\n            modifier = modifier,\n            containerWidth = containerWidth,\n            style = style,\n            aspectList = aspectList,\n            itemContent = itemContent,\n        )\n    }\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/image/VerticalImageMediaFrameLayout.kt",
    "content": "package com.zhangke.fread.status.ui.image\n\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.coerceAtMost\nimport com.zhangke.framework.ktx.averageDropFirst\n\n/**\n * As shown below:\n *       ---------------------\n *      ｜                    ｜\n *      ｜                    ｜\n *      ｜                    ｜\n *      ｜                    ｜\n *      ｜                    ｜\n *      ｜                    ｜\n *       ---------------------\n *       ---------------------\n *      ｜                    ｜\n *       ---------------------\n */\n@Composable\ninternal fun VerticalImageMediaFrameLayout(\n    modifier: Modifier,\n    containerWidth: Dp,\n    style: BlogImageMediaStyle,\n    topAspect: Float,\n    bottomAspect: Float,\n    fixedHeight: Boolean = false,\n    topContent: @Composable () -> Unit,\n    bottomContent: @Composable () -> Unit,\n) {\n    val fixedTopAspect = style.getCompliantAspect(topAspect)\n    val fixedBottomAspect = style.getCompliantAspect(bottomAspect)\n    val finalModifier = if (fixedHeight) {\n        modifier.fillMaxHeight()\n    } else {\n        val minHeight = containerWidth / style.maxAspect\n        val maxHeight = containerWidth / style.minAspect\n        val bottomHeight = containerWidth / fixedBottomAspect\n        val topHeight = containerWidth / fixedTopAspect\n        val reckonTotalHeight = style.verticalDivider + bottomHeight + topHeight\n        val totalHeight = reckonTotalHeight.coerceAtLeast(minHeight).coerceAtMost(maxHeight)\n        modifier.height(totalHeight)\n    }\n    Column(\n        modifier = finalModifier\n            .fillMaxWidth(),\n    ) {\n        Box(\n            modifier = Modifier\n                .fillMaxWidth()\n                .weight(1F / fixedTopAspect)\n        ) {\n            topContent()\n        }\n        VerticalSpacer(height = style.verticalDivider)\n        Box(\n            modifier = Modifier\n                .fillMaxWidth()\n                .weight(1F / fixedBottomAspect)\n        ) {\n            bottomContent()\n        }\n    }\n}\n\n/**\n * As shown below:\n *       ---------------------\n *      ｜                    ｜\n *      ｜                    ｜\n *      ｜                    ｜\n *      ｜                    ｜\n *      ｜                    ｜\n *      ｜                    ｜\n *       ---------------------\n *       ----    ------   -----\n *      ｜   ｜  ｜    ｜  ｜   ｜\n *       ----    ------   -----\n */\n@Composable\ninternal fun VerticalImageMediaFrameLayout(\n    modifier: Modifier,\n    containerWidth: Dp,\n    style: BlogImageMediaStyle,\n    aspectList: List<Float>,\n    itemContent: @Composable (index: Int) -> Unit,\n) {\n    VerticalImageMediaFrameLayout(\n        modifier = modifier,\n        containerWidth = containerWidth,\n        style = style,\n        topAspect = aspectList.first(),\n        bottomAspect = aspectList.averageDropFirst(1).toFloat(),\n        topContent = {\n            itemContent(0)\n        },\n        bottomContent = {\n            HorizontalImageMediaListLayout(\n                modifier = Modifier,\n                style = style,\n                dropFirst = 1,\n                aspectList = aspectList,\n                itemContent = itemContent,\n            )\n        }\n    )\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/image/VerticalImageMediaListLayout.kt",
    "content": "package com.zhangke.fread.status.ui.image\n\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\n\n@Composable\ninternal fun VerticalImageMediaListLayout(\n    modifier: Modifier,\n    style: BlogImageMediaStyle,\n    dropFirst: Int,\n    aspectList: List<Float>,\n    itemContent: @Composable (index: Int) -> Unit,\n) {\n    Column(modifier = modifier) {\n        for (index in dropFirst until aspectList.size) {\n            Box(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .weight(1F)\n            ) {\n                itemContent(index)\n            }\n            if (index < aspectList.lastIndex) {\n                VerticalSpacer(height = style.verticalDivider)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/label/StatusBottomEditedLabel.kt",
    "content": "package com.zhangke.fread.status.ui.label\n\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.text.style.TextAlign\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.ui.style.StatusStyle\nimport org.jetbrains.compose.resources.stringResource\n\n@Composable\nfun StatusBottomEditedLabel(\n    modifier: Modifier,\n    editedAt: String,\n    style: StatusStyle,\n) {\n    Text(\n        modifier = modifier,\n        text = stringResource(LocalizedString.statusUiBottomLabelEditedAt, editedAt),\n        textAlign = TextAlign.Start,\n        color = style.secondaryFontColor,\n        style = style.bottomLabelStyle.textStyle,\n    )\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/label/StatusBottomInteractionLabel.kt",
    "content": "package com.zhangke.fread.status.ui.label\n\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.text.AnnotatedString\nimport androidx.compose.ui.text.SpanStyle\nimport androidx.compose.ui.text.TextStyle\nimport androidx.compose.ui.text.buildAnnotatedString\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.isSpecified\nimport androidx.compose.ui.unit.sp\nimport com.zhangke.framework.composable.noRippleClick\nimport com.zhangke.framework.utils.formatToHumanReadable\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.ui.style.StatusStyle\nimport org.jetbrains.compose.resources.stringResource\n\n@Composable\nfun StatusBottomInteractionLabel(\n    modifier: Modifier,\n    boostedCount: Long,\n    favouritedCount: Long,\n    style: StatusStyle,\n    onBoostedClick: () -> Unit,\n    onFavouritedClick: () -> Unit,\n) {\n    Row(\n        modifier = modifier,\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n\n        val boostedText = buildHighlightLabelText(\n            highlight = boostedCount.formatToHumanReadable(),\n            wholeText = stringResource(\n                LocalizedString.statusUiInteractionLabelBoostedCount,\n                boostedCount.formatToHumanReadable(),\n            ),\n            style = style.bottomLabelStyle.textStyle,\n            highLightColor = MaterialTheme.colorScheme.onSurface,\n        )\n        Text(\n            modifier = Modifier.noRippleClick { onBoostedClick() },\n            text = boostedText,\n            color = style.secondaryFontColor,\n            style = style.bottomLabelStyle.textStyle,\n        )\n\n        val favouritedText = buildHighlightLabelText(\n            highlight = favouritedCount.formatToHumanReadable(),\n            wholeText = stringResource(\n                LocalizedString.statusUiInteractionLabelFavouritedCount,\n                favouritedCount.formatToHumanReadable(),\n            ),\n            style = style.bottomLabelStyle.textStyle,\n            highLightColor = MaterialTheme.colorScheme.onSurface,\n        )\n        Text(\n            modifier = Modifier\n                .padding(start = 6.dp)\n                .noRippleClick { onFavouritedClick() },\n            text = favouritedText,\n            color = style.secondaryFontColor,\n            style = style.bottomLabelStyle.textStyle,\n        )\n    }\n}\n\nprivate fun buildHighlightLabelText(\n    highlight: String,\n    wholeText: String,\n    style: TextStyle,\n    highLightColor: Color,\n): AnnotatedString {\n    return buildAnnotatedString {\n        append(wholeText)\n        val startIndex = wholeText.indexOf(highlight)\n        val endIndex = startIndex + highlight.length\n        if (startIndex in 0..<endIndex && endIndex <= wholeText.length) {\n            val highlightFontSize = if (style.fontSize.isSpecified) {\n                style.fontSize * 1.2\n            } else {\n                16.sp\n            }\n            addStyle(\n                style = SpanStyle(\n                    fontSize = highlightFontSize,\n                    fontWeight = FontWeight.Medium,\n                    color = highLightColor,\n                ),\n                start = startIndex,\n                end = endIndex,\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/label/StatusBottomTimeLabel.kt",
    "content": "package com.zhangke.fread.status.ui.label\n\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport com.zhangke.framework.composable.noRippleClick\nimport com.zhangke.fread.status.blog.Blog\nimport com.zhangke.fread.status.ui.style.StatusStyle\n\n@Composable\nfun StatusBottomTimeLabel(\n    modifier: Modifier,\n    blog: Blog,\n    specificTime: String,\n    style: StatusStyle,\n    onUrlClick: (String) -> Unit,\n) {\n    Row(\n        modifier = modifier,\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        Text(\n            text = specificTime,\n            style = style.bottomLabelStyle.textStyle,\n            color = style.secondaryFontColor,\n        )\n        if (blog.application?.name.isNullOrEmpty().not()) {\n            Text(\n                modifier = Modifier.noRippleClick(\n                    enabled = !blog.application?.website.isNullOrEmpty()\n                ) {\n                    onUrlClick(blog.application!!.website!!)\n                },\n                color = style.secondaryFontColor,\n                text = \"  •  ${blog.application!!.name}\",\n                style = style.bottomLabelStyle.textStyle,\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/label/StatusTopLabel.kt",
    "content": "package com.zhangke.fread.status.ui.label\n\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.AlternateEmail\nimport androidx.compose.material.icons.filled.PushPin\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.layout.onSizeChanged\nimport androidx.compose.ui.text.style.TextDecoration\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport com.zhangke.framework.composable.noRippleClick\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.author.BlogAuthor\nimport com.zhangke.fread.status.ui.richtext.FreadRichText\nimport com.zhangke.fread.status.ui.style.StatusStyle\nimport com.zhangke.fread.statusui.Res\nimport com.zhangke.fread.statusui.ic_status_forward\nimport org.jetbrains.compose.resources.stringResource\nimport org.jetbrains.compose.resources.vectorResource\n\n@Composable\nfun StatusMentionOnlyLabel(\n    modifier: Modifier,\n    style: StatusStyle,\n) {\n    IconWithTextLabel(\n        modifier = modifier,\n        icon = Icons.Default.AlternateEmail,\n        text = stringResource(LocalizedString.statusUiVisibilityMentionedOnly),\n        style = style,\n        color = MaterialTheme.colorScheme.primary,\n    )\n}\n\n@Composable\nfun ReblogTopLabel(\n    author: BlogAuthor,\n    style: StatusStyle,\n    onAuthorClick: (BlogAuthor) -> Unit,\n) {\n    val startPadding =\n        style.containerStartPadding + style.infoLineStyle.avatarSize - style.topLabelStyle.iconSize\n    Row(\n        modifier = Modifier\n            .fillMaxWidth()\n            .padding(\n                start = startPadding,\n                end = style.containerEndPadding,\n            )\n            .noRippleClick { onAuthorClick(author) },\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        Icon(\n            modifier = Modifier.size(style.topLabelStyle.iconSize),\n            imageVector = vectorResource(Res.drawable.ic_status_forward),\n            contentDescription = null,\n            tint = style.secondaryFontColor,\n        )\n        FreadRichText(\n            modifier = Modifier.padding(start = 6.dp),\n            richText = author.humanizedName,\n            maxLines = 1,\n            onHashtagClick = {},\n            onMentionClick = {},\n            onUrlClick = {},\n            fontSize = style.topLabelStyle.textSize,\n            color = style.secondaryFontColor,\n        )\n        Text(\n            modifier = Modifier.padding(start = 4.dp),\n            text = stringResource(LocalizedString.status_ui_reposted),\n            maxLines = 1,\n            style = MaterialTheme.typography.bodySmall,\n            fontSize = style.topLabelStyle.textSize,\n            color = style.secondaryFontColor,\n        )\n    }\n}\n\n@Composable\nfun StatusPinnedLabel(\n    modifier: Modifier = Modifier,\n    style: StatusStyle,\n) {\n    IconWithTextLabel(\n        modifier = modifier,\n        icon = Icons.Default.PushPin,\n        text = stringResource(LocalizedString.statusUiLabelPinned),\n        style = style,\n    )\n}\n\n@Composable\nfun ContinueThread(\n    style: StatusStyle,\n    onHeightChanged: (Int) -> Unit,\n) {\n    val startPadding =\n        style.containerStartPadding + style.infoLineStyle.avatarSize + style.infoLineStyle.nameToAvatarSpacing\n    Text(\n        modifier = Modifier.padding(\n            start = startPadding,\n            end = style.containerEndPadding,\n        ).onSizeChanged { onHeightChanged(it.height) },\n        maxLines = 1,\n        text = stringResource(LocalizedString.statusUiTopLabelContinuedThread),\n        color = style.secondaryFontColor,\n        fontSize = style.topLabelStyle.textSize,\n        textDecoration = TextDecoration.Underline,\n    )\n}\n\n@Composable\nprivate fun IconWithTextLabel(\n    modifier: Modifier,\n    icon: ImageVector,\n    text: String,\n    style: StatusStyle,\n    color: Color = style.secondaryFontColor,\n) {\n    val startPadding =\n        style.containerStartPadding + style.infoLineStyle.avatarSize - style.topLabelStyle.iconSize\n    Row(\n        modifier = modifier\n            .fillMaxWidth()\n            .padding(\n                start = startPadding,\n                end = style.containerEndPadding,\n            ),\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        Icon(\n            modifier = Modifier.size(style.topLabelStyle.iconSize),\n            imageVector = icon,\n            contentDescription = text,\n            tint = color,\n        )\n        Text(\n            modifier = Modifier.padding(start = style.infoLineStyle.nameToAvatarSpacing),\n            maxLines = 1,\n            text = text,\n            color = color,\n            fontSize = style.topLabelStyle.textSize,\n        )\n    }\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/media/BlogMedias.kt",
    "content": "package com.zhangke.fread.status.ui.media\n\nimport androidx.compose.animation.ExperimentalSharedTransitionApi\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.VisibilityOff\nimport androidx.compose.material3.ButtonDefaults\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.IconButtonDefaults\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.layout.onGloballyPositioned\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport com.zhangke.framework.utils.pxToDp\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.blog.BlogMedia\nimport com.zhangke.fread.status.blog.BlogMediaType\nimport com.zhangke.fread.status.model.BlogTranslationUiState\nimport com.zhangke.fread.status.ui.image.BlogImageMedias\nimport com.zhangke.fread.status.ui.image.BlogMediaClickEvent\nimport com.zhangke.fread.status.ui.image.OnBlogMediaClick\nimport com.zhangke.fread.status.ui.style.LocalStatusUiConfig\nimport com.zhangke.fread.status.ui.video.BlogVideos\nimport org.jetbrains.compose.resources.stringResource\n\nprivate var cachedContainerWidth: Dp? = null\n\n@OptIn(ExperimentalSharedTransitionApi::class)\n@Composable\nfun BlogMedias(\n    modifier: Modifier,\n    mediaList: List<BlogMedia>,\n    sharedElementId: String,\n    indexInList: Int,\n    sensitive: Boolean,\n    onMediaClick: OnBlogMediaClick,\n    showAlt: Boolean = true,\n    blogTranslationState: BlogTranslationUiState? = null,\n) {\n    val density = LocalDensity.current\n    var containerWidth: Dp? by remember {\n        mutableStateOf(cachedContainerWidth)\n    }\n    val statusConfig = LocalStatusUiConfig.current\n    var hideContent by rememberSaveable(\n        sensitive,\n        mediaList,\n        statusConfig.alwaysShowSensitiveContent,\n    ) {\n        mutableStateOf(sensitive && !statusConfig.alwaysShowSensitiveContent)\n    }\n    Box(\n        modifier = modifier\n            .onGloballyPositioned {\n                containerWidth = it.size.width.pxToDp(density)\n                cachedContainerWidth = containerWidth\n            }\n    ) {\n        if (containerWidth != null) {\n            BlogMediaContent(\n                mediaList = mediaList,\n                sharedElementId = sharedElementId,\n                blogTranslationState = blogTranslationState,\n                hideContent = hideContent,\n                indexInList = indexInList,\n                containerWidth = containerWidth!!,\n                onMediaClick = onMediaClick,\n                showAlt = showAlt,\n            )\n        }\n        if (sensitive) {\n            if (hideContent) {\n                TextButton(\n                    modifier = Modifier.align(Alignment.Center),\n                    onClick = {\n                        hideContent = false\n                    },\n                    colors = ButtonDefaults.textButtonColors(\n                        containerColor = Color.Black.copy(alpha = 0.5F),\n                        contentColor = Color.White,\n                    ),\n                ) {\n                    Text(stringResource(LocalizedString.statusUiImageSensitiveLabel))\n                }\n            } else {\n                IconButton(\n                    onClick = {\n                        hideContent = true\n                    },\n                    colors = IconButtonDefaults.iconButtonColors(\n                        containerColor = Color.Black.copy(alpha = 0.3F),\n                        contentColor = Color.White,\n                    ),\n                ) {\n                    Icon(\n                        modifier = Modifier.padding(2.dp),\n                        imageVector = Icons.Default.VisibilityOff,\n                        contentDescription = \"Hide Content\",\n                        tint = Color.White,\n                    )\n                }\n            }\n        }\n    }\n}\n\n@OptIn(ExperimentalSharedTransitionApi::class)\n@Composable\nprivate fun BlogMediaContent(\n    mediaList: List<BlogMedia>,\n    sharedElementId: String,\n    blogTranslationState: BlogTranslationUiState?,\n    hideContent: Boolean,\n    indexInList: Int,\n    containerWidth: Dp,\n    showAlt: Boolean,\n    onMediaClick: OnBlogMediaClick,\n) {\n    if (mediaList.firstOrNull()?.type == BlogMediaType.VIDEO) {\n        BlogVideos(\n            mediaList = mediaList,\n            indexInList = indexInList,\n            hideContent = hideContent,\n            onMediaClick = onMediaClick,\n        )\n    } else {\n        val imageMediaList =\n            mediaList.filter { it.type == BlogMediaType.IMAGE || it.type == BlogMediaType.GIFV }\n        BlogImageMedias(\n            mediaList = imageMediaList,\n            sharedElementId = sharedElementId,\n            hideContent = hideContent,\n            containerWidth = containerWidth,\n            onMediaClick = {\n                onMediaClick(it.transformTranslatedEvent(blogTranslationState))\n            },\n            showAlt = showAlt,\n        )\n    }\n}\n\nprivate fun BlogMediaClickEvent.transformTranslatedEvent(\n    blogTranslationState: BlogTranslationUiState?,\n): BlogMediaClickEvent {\n    val imageEvent = (this as? BlogMediaClickEvent.BlogImageClickEvent) ?: return this\n    if (blogTranslationState == null) return this\n    if (!blogTranslationState.showingTranslation) return this\n    val attachment = blogTranslationState.blogTranslation?.attachments ?: return this\n    val mediaList = imageEvent.mediaList\n    if (attachment.size != mediaList.size) return this\n    val newMediaList = mediaList.map { media ->\n        val description =\n            attachment.firstOrNull { it.id == media.media.id }?.description\n                ?: media.media.description\n        media.copy(\n            media = media.media.copy(description = description),\n        )\n    }\n    return imageEvent.copy(\n        mediaList = newMediaList,\n    )\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/placeholder/ListWithAvatarPlaceholder.kt",
    "content": "package com.zhangke.fread.status.ui.placeholder\n\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.unit.dp\nimport com.zhangke.framework.composable.freadPlaceholder\n\n@Composable\nfun TitleWithAvatarItemPlaceholder(\n    modifier: Modifier,\n) {\n    Row(\n        modifier = modifier.height(48.dp).padding(horizontal = 16.dp),\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        Box(\n            modifier = Modifier.size(36.dp)\n                .clip(RoundedCornerShape(6.dp))\n                .freadPlaceholder(true),\n        )\n        Spacer(modifier = Modifier.width(16.dp))\n        Box(\n            modifier = Modifier.size(width = 180.dp, height = 18.dp)\n                .clip(RoundedCornerShape(4.dp))\n                .freadPlaceholder(true),\n        )\n    }\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/poll/BlogPoll.kt",
    "content": "package com.zhangke.fread.status.ui.poll\n\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.blog.BlogPoll\nimport com.zhangke.fread.status.model.BlogTranslationUiState\nimport org.jetbrains.compose.resources.stringResource\n\n@Composable\nfun BlogPoll(\n    modifier: Modifier,\n    poll: BlogPoll,\n    isSelf: Boolean?,\n    blogTranslationState: BlogTranslationUiState,\n    onVoted: (List<BlogPoll.Option>) -> Unit,\n) {\n    // 显示投票占比，满足任意条件：发帖人、投票结束、已投票\n    Column(modifier = modifier) {\n        if (poll.multiple) {\n            MultipleChoicePoll(poll, isSelf == true, blogTranslationState, onVoted)\n        } else {\n            SingleChoicePoll(poll, isSelf == true, blogTranslationState, onVoted)\n        }\n        if (poll.expired) {\n            val count = poll.votesCount\n            val finishedTip = if (count <= 1) {\n                stringResource(LocalizedString.statusUiPollVoteFinishedTip, count)\n            } else {\n                stringResource(LocalizedString.statusUiPollVotesFinishedTip, count)\n            }\n            Text(\n                modifier = Modifier.padding(start = 4.dp, top = 8.dp),\n                text = finishedTip,\n                style = MaterialTheme.typography.labelMedium,\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/poll/BlogPollOption.kt",
    "content": "package com.zhangke.fread.status.ui.poll\n\nimport androidx.annotation.FloatRange\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.heightIn\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.widthIn\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Check\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.vector.rememberVectorPainter\nimport androidx.compose.ui.layout.onSizeChanged\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.unit.IntSize\nimport androidx.compose.ui.unit.dp\nimport com.zhangke.framework.composable.SlickRoundCornerShape\nimport com.zhangke.framework.utils.pxToDp\nimport kotlin.math.roundToInt\n\n@Composable\ninternal fun BlogPollOption(\n    modifier: Modifier,\n    selected: Boolean,\n    votable: Boolean,\n    showProgress: Boolean,\n    optionContent: String,\n    @FloatRange(from = 0.0, to = 1.0) progress: Float,\n    onClick: () -> Unit,\n) {\n    val density = LocalDensity.current\n    val colorScheme = MaterialTheme.colorScheme\n    var containerSize: IntSize? by remember {\n        mutableStateOf(null)\n    }\n    val borderWidth = 1.dp\n    val cornerRadius = 21.dp\n    Box(\n        modifier = modifier\n            .onSizeChanged {\n                if (it != containerSize) {\n                    containerSize = it\n                }\n            }\n            .heightIn(min = 42.dp)\n            .clip(RoundedCornerShape(cornerRadius))\n            .border(\n                width = borderWidth,\n                color = MaterialTheme.colorScheme.onSurfaceVariant,\n                shape = RoundedCornerShape(cornerRadius),\n            )\n            .clickable(votable) { onClick() },\n    ) {\n        val fixedContainerSize = containerSize\n        if (showProgress && fixedContainerSize != null && progress > 0F) {\n            val progressWidth = fixedContainerSize.width * progress.coerceAtMost(1F)\n            Box(\n                modifier = Modifier\n                    .align(Alignment.CenterStart)\n                    .padding(start = borderWidth)\n                    .size(\n                        height = fixedContainerSize.height.pxToDp(density),\n                        width = progressWidth.pxToDp(density),\n                    )\n                    .clip(SlickRoundCornerShape(cornerRadius))\n                    .background(color = Color.Blue.copy(alpha = 0.3F)),\n            )\n        }\n        Row(\n            modifier = Modifier\n                .padding(start = 18.dp, end = 15.dp, top = 10.dp, bottom = 10.dp)\n                .align(Alignment.CenterStart)\n                .fillMaxWidth(),\n            verticalAlignment = Alignment.CenterVertically,\n        ) {\n            Text(\n                modifier = Modifier.widthIn(max = 250.dp),\n                text = optionContent,\n                color = colorScheme.primary,\n            )\n            if (selected) {\n                Icon(\n                    modifier = Modifier\n                        .padding(start = 6.dp)\n                        .size(14.dp),\n                    painter = rememberVectorPainter(Icons.Default.Check),\n                    contentDescription = \"\",\n                )\n            }\n        }\n        if (showProgress) {\n            Text(\n                modifier = Modifier\n                    .align(Alignment.CenterEnd)\n                    .padding(end = 20.dp),\n                text = \"${(progress * 100).roundToInt()} %\",\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/poll/MultipleChoicePoll.kt",
    "content": "package com.zhangke.fread.status.ui.poll\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.mutableStateMapOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.unit.dp\nimport com.zhangke.framework.composable.SlickRoundCornerShape\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.model.BlogTranslationUiState\nimport com.zhangke.fread.status.blog.BlogPoll\nimport org.jetbrains.compose.resources.stringResource\n\n@Composable\ninternal fun MultipleChoicePoll(\n    poll: BlogPoll,\n    isSelf: Boolean,\n    blogTranslationState: BlogTranslationUiState,\n    onVoted: (List<BlogPoll.Option>) -> Unit,\n) {\n    val indexToSelected = remember(poll) {\n        val map = mutableMapOf<Int, Boolean>()\n        poll.options.forEachIndexed { index, _ ->\n            map[index] = poll.ownVotes.contains(index)\n        }\n        mutableStateMapOf(*map.map { it.key to it.value }.toTypedArray())\n    }\n    val translatedPoll = blogTranslationState.blogTranslation?.poll\n    val pollIsInVotable = !poll.expired && poll.voted == false && !isSelf\n    val sum = poll.options.sumOf { it.votesCount ?: 0 }.toFloat()\n    poll.options.forEachIndexed { index, option ->\n        val votesCount = option.votesCount?.toFloat() ?: 0F\n        val progress = if (votesCount > 0) votesCount / sum else 0F\n        var optionContent: String = option.title\n        if (blogTranslationState.showingTranslation) {\n            translatedPoll?.options?.getOrNull(index)?.title?.let {\n                optionContent = it\n            }\n        }\n        val selected = indexToSelected[index] ?: false\n        BlogPollOption(\n            modifier = Modifier.fillMaxWidth(),\n            optionContent = optionContent,\n            selected = selected,\n            votable = pollIsInVotable,\n            showProgress = isSelf || selected || poll.expired,\n            progress = progress,\n            onClick = {\n                indexToSelected[index] = indexToSelected[index]?.not() ?: false\n            },\n        )\n        if (index < poll.options.lastIndex) {\n            Spacer(modifier = Modifier.size(width = 1.dp, height = 10.dp))\n        }\n    }\n    if (pollIsInVotable) {\n        val votable = indexToSelected.map { it.value }.contains(true)\n        val backgroundColor = if (votable) {\n            Color.Blue.copy(alpha = 0.6F)\n        } else {\n            Color.Gray.copy(alpha = 0.6F)\n        }\n        Box(\n            modifier = Modifier\n                .fillMaxWidth()\n                .padding(top = 15.dp)\n                .height(42.dp)\n                .clip(SlickRoundCornerShape(21.dp))\n                .background(backgroundColor)\n                .clickable(votable) {\n                    val votedOptions = indexToSelected\n                        .filter { it.value }\n                        .map { it.key }\n                        .map { poll.options[it] }\n                    onVoted(votedOptions)\n                },\n        ) {\n            Text(\n                modifier = Modifier.align(Alignment.Center),\n                text = stringResource(LocalizedString.statusUiPollVote),\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/poll/SingleChoicePoll.kt",
    "content": "package com.zhangke.fread.status.ui.poll\n\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport com.zhangke.fread.status.model.BlogTranslationUiState\nimport com.zhangke.fread.status.blog.BlogPoll\n\n@Composable\ninternal fun SingleChoicePoll(\n    poll: BlogPoll,\n    isSelf: Boolean,\n    blogTranslationState: BlogTranslationUiState,\n    onVoted: (List<BlogPoll.Option>) -> Unit,\n) {\n    val sum = poll.options.sumOf { it.votesCount ?: 0 }.toFloat()\n    val translatedPoll = blogTranslationState.blogTranslation?.poll\n    val pollIsInVotable = !poll.expired && poll.voted == false && !isSelf\n    poll.options.forEachIndexed { index, option ->\n        val votesCount = option.votesCount?.toFloat() ?: 0F\n        val progress = if (votesCount > 0) votesCount / sum else 0F\n        val selected = poll.ownVotes.contains(index)\n        val showProgress = isSelf || poll.expired || selected\n        var optionContent: String = option.title\n        if (blogTranslationState.showingTranslation) {\n            translatedPoll?.options?.getOrNull(index)?.title?.let {\n                optionContent = it\n            }\n        }\n        BlogPollOption(\n            modifier = Modifier.fillMaxWidth(),\n            optionContent = optionContent,\n            selected = selected,\n            votable = pollIsInVotable,\n            showProgress = showProgress,\n            progress = progress,\n            onClick = {\n                onVoted(listOf(option))\n            },\n        )\n        if (index < poll.options.lastIndex) {\n            Spacer(modifier = Modifier.size(width = 1.dp, height = 10.dp))\n        }\n    }\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/publish/BlogInQuoting.kt",
    "content": "package com.zhangke.fread.status.ui.publish\n\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.rotate\nimport androidx.compose.ui.unit.dp\nimport com.zhangke.fread.status.blog.Blog\nimport com.zhangke.fread.status.ui.action.quoteIcon\nimport com.zhangke.fread.status.ui.embed.BlogInEmbedding\nimport com.zhangke.fread.status.ui.embed.embedBorder\nimport com.zhangke.fread.status.ui.style.StatusStyle\n\n@Composable\nfun BlogInQuoting(\n    modifier: Modifier,\n    blog: Blog,\n    style: StatusStyle,\n) {\n    Column(\n        modifier = modifier,\n    ) {\n        Icon(\n            modifier = Modifier.rotate(180F),\n            imageVector = quoteIcon(),\n            tint = MaterialTheme.colorScheme.outline,\n            contentDescription = null,\n        )\n        Spacer(Modifier.height(8.dp))\n        BlogInEmbedding(\n            modifier = Modifier\n                .embedBorder()\n                .padding(8.dp),\n            blog = blog,\n            style = style,\n        )\n    }\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/publish/NameAndAccountInfo.kt",
    "content": "package com.zhangke.fread.status.ui.publish\n\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport com.zhangke.framework.composable.TwoTextsInRow\nimport com.zhangke.fread.status.richtext.RichText\nimport com.zhangke.fread.status.ui.richtext.FreadRichText\nimport com.zhangke.fread.status.ui.style.StatusStyle\n\n@Composable\nfun NameAndAccountInfo(\n    modifier: Modifier,\n    humanizedName: RichText,\n    handle: String,\n    style: StatusStyle,\n) {\n    TwoTextsInRow(\n        firstText = {\n            FreadRichText(\n                modifier = Modifier,\n                maxLines = 1,\n                overflow = TextOverflow.Ellipsis,\n                richText = humanizedName,\n                onUrlClick = {},\n                fontWeight = FontWeight.SemiBold,\n                fontSize = style.infoLineStyle.nameSize,\n            )\n        },\n        secondText = {\n            Text(\n                modifier = Modifier,\n                maxLines = 1,\n                overflow = TextOverflow.Ellipsis,\n                text = handle,\n                color = style.secondaryFontColor,\n                style = style.infoLineStyle.descStyle,\n            )\n        },\n        spacing = 2.dp,\n        modifier = modifier,\n    )\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/publish/PublishBlogStyle.kt",
    "content": "package com.zhangke.fread.status.ui.publish\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport com.zhangke.fread.status.ui.style.StatusStyle\nimport com.zhangke.fread.status.ui.style.StatusStyles\n\ndata class PublishBlogStyle(\n    val topPadding: Dp,\n    val startPadding: Dp,\n    val endPadding: Dp,\n    val statusStyle: StatusStyle,\n)\n\nobject PublishBlogStyleDefault {\n\n    @Composable\n    fun defaultStyle(): PublishBlogStyle {\n        val statusStyle = StatusStyles.medium()\n        return PublishBlogStyle(\n            topPadding = 24.dp,\n            startPadding = 16.dp,\n            endPadding = 16.dp,\n            statusStyle = statusStyle,\n        )\n    }\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/richtext/FreadRichText.kt",
    "content": "package com.zhangke.fread.status.ui.richtext\n\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.text.InlineTextContent\nimport androidx.compose.foundation.text.selection.SelectionContainer\nimport androidx.compose.material3.LocalTextStyle\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.DisposableEffect\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.text.Placeholder\nimport androidx.compose.ui.text.PlaceholderVerticalAlign\nimport androidx.compose.ui.text.TextLayoutResult\nimport androidx.compose.ui.text.TextStyle\nimport androidx.compose.ui.text.font.FontFamily\nimport androidx.compose.ui.text.font.FontStyle\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.text.style.TextDecoration\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.TextUnit\nimport androidx.compose.ui.unit.em\nimport androidx.compose.ui.unit.sp\nimport com.seiko.imageloader.rememberImagePainter\nimport com.zhangke.fread.status.model.Emoji\nimport com.zhangke.fread.status.model.HashtagInStatus\nimport com.zhangke.fread.status.model.Mention\nimport com.zhangke.fread.status.richtext.RichText\nimport com.zhangke.fread.status.richtext.buildRichText\nimport com.zhangke.fread.status.richtext.model.RichLinkTarget\n\n@Composable\nfun FreadRichText(\n    content: String,\n    modifier: Modifier = Modifier,\n    mentions: List<Mention> = emptyList(),\n    emojis: List<Emoji> = emptyList(),\n    tags: List<HashtagInStatus> = emptyList(),\n    onMentionClick: (Mention) -> Unit = {},\n    onHashtagClick: (HashtagInStatus) -> Unit = {},\n    onUrlClick: (url: String) -> Unit = {},\n    // layoutDirection: LayoutDirection = LocalLayoutDirection.current,\n    overflow: TextOverflow = TextOverflow.Ellipsis,\n    maxLines: Int = Int.MAX_VALUE,\n    fontSize: TextUnit = TextUnit.Unspecified,\n    fontStyle: FontStyle? = null,\n    fontWeight: FontWeight? = null,\n    textAlign: TextAlign? = null,\n) {\n    val richText = remember(content, mentions) {\n        buildRichText(\n            document = content,\n            mentions = mentions,\n            hashTags = tags,\n            emojis = emojis,\n        )\n    }\n    FreadRichText(\n        modifier = modifier,\n        richText = richText,\n        // layoutDirection = layoutDirection,\n        overflow = overflow,\n        maxLines = maxLines,\n        fontStyle = fontStyle,\n        fontWeight = fontWeight,\n        textAlign = textAlign,\n        onMentionClick = onMentionClick,\n        onHashtagClick = onHashtagClick,\n        fontSize = fontSize,\n        onUrlClick = onUrlClick,\n    )\n}\n\n@Composable\nfun FreadRichText(\n    modifier: Modifier,\n    richText: RichText,\n    color: Color = Color.Unspecified,\n    fontStyle: FontStyle? = null,\n    fontWeight: FontWeight? = null,\n    lineHeight: TextUnit = 1.5.em,\n    textAlign: TextAlign? = null,\n    fontFamily: FontFamily? = null,\n    letterSpacing: TextUnit = TextUnit.Unspecified,\n    softWrap: Boolean = true,\n    textDecoration: TextDecoration? = null,\n    onMentionClick: (Mention) -> Unit = {},\n    onMentionDidClick: (String) -> Unit = {},\n    onHashtagClick: (HashtagInStatus) -> Unit = {},\n    onMaybeHashtagClick: (String) -> Unit = {},\n    onUrlClick: (url: String) -> Unit = {},\n    // layoutDirection: LayoutDirection = LocalLayoutDirection.current,\n    overflow: TextOverflow = TextOverflow.Ellipsis,\n    maxLines: Int = Int.MAX_VALUE,\n    fontSize: TextUnit = TextUnit.Unspecified,\n    onTextLayout: (TextLayoutResult) -> Unit = {},\n    style: TextStyle = LocalTextStyle.current\n) {\n    DisposableEffect(richText) {\n        richText.onLinkTargetClick = { target ->\n            when (target) {\n                is RichLinkTarget.UrlTarget -> onUrlClick(target.url)\n                is RichLinkTarget.MentionTarget -> onMentionClick(target.mention)\n                is RichLinkTarget.MentionDidTarget -> onMentionDidClick(target.did)\n                is RichLinkTarget.HashtagTarget -> onHashtagClick(target.hashtag)\n                is RichLinkTarget.MaybeHashtagTarget -> onMaybeHashtagClick(target.hashtag)\n            }\n        }\n        onDispose {\n            richText.onLinkTargetClick = null\n        }\n    }\n\n    Text(\n        text = richText.parse(),\n        modifier = modifier,\n        color = color,\n        overflow = overflow,\n        maxLines = maxLines,\n        fontStyle = fontStyle,\n        lineHeight = lineHeight,\n        fontWeight = fontWeight,\n        textAlign = textAlign,\n        fontSize = fontSize,\n        fontFamily = fontFamily,\n        letterSpacing = letterSpacing,\n        softWrap = softWrap,\n        textDecoration = textDecoration,\n        onTextLayout = onTextLayout,\n        style = style,\n        inlineContent = rememberInlineContent(richText.emojis),\n    )\n}\n\n@Composable\nfun SelectableRichText(\n    modifier: Modifier,\n    richText: RichText,\n    color: Color = Color.Unspecified,\n    onMentionClick: (Mention) -> Unit = {},\n    onHashtagClick: (HashtagInStatus) -> Unit = {},\n    onMaybeHashtagClick: (String) -> Unit = {},\n    onUrlClick: (url: String) -> Unit = {},\n    // layoutDirection: LayoutDirection = LocalLayoutDirection.current,\n    overflow: TextOverflow = TextOverflow.Ellipsis,\n    maxLines: Int = Int.MAX_VALUE,\n    fontSize: TextUnit = TextUnit.Unspecified,\n) {\n    Box(modifier = modifier) {\n        SelectionContainer {\n            FreadRichText(\n                modifier = Modifier,\n                richText = richText,\n                color = color,\n                onMentionClick = onMentionClick,\n                onHashtagClick = onHashtagClick,\n                onMaybeHashtagClick = onMaybeHashtagClick,\n                onUrlClick = onUrlClick,\n                overflow = overflow,\n                maxLines = maxLines,\n                fontSize = fontSize,\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun rememberInlineContent(\n    emojis: List<Emoji>,\n): Map<String, InlineTextContent> {\n    return remember(emojis) {\n        val emojiContent = InlineTextContent(\n            placeholder = Placeholder(\n                width = 1.5.em,\n                height = 1.5.em,\n                placeholderVerticalAlign = PlaceholderVerticalAlign.TextBottom,\n            ),\n        ) { shortCode ->\n            val fixedShortCode = shortCode.removePrefix(\":\").removeSuffix(\":\")\n            val emoji = emojis.firstOrNull { it.shortcode == fixedShortCode }\n            if (emoji != null) {\n                Image(\n                    painter = rememberImagePainter(emoji.url),\n                    contentDescription = null,\n                    modifier = Modifier.fillMaxSize(),\n                )\n            } else {\n                Text(fixedShortCode)\n            }\n        }\n        mapOf(\"emoji\" to emojiContent)\n    }\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/richtext/RichTextWithIcon.kt",
    "content": "package com.zhangke.fread.status.ui.richtext\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.material3.LocalTextStyle\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.text.TextLayoutResult\nimport androidx.compose.ui.text.TextStyle\nimport androidx.compose.ui.text.font.FontFamily\nimport androidx.compose.ui.text.font.FontStyle\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.text.style.TextDecoration\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.TextUnit\nimport androidx.compose.ui.unit.TextUnitType\nimport com.zhangke.fread.status.model.HashtagInStatus\nimport com.zhangke.fread.status.model.Mention\nimport com.zhangke.fread.status.richtext.RichText\n\n@Composable\nfun RichTextWithIcon(\n    modifier: Modifier = Modifier,\n    text: RichText,\n    color: Color = Color.Unspecified,\n    fontSize: TextUnit = TextUnit.Unspecified,\n    fontStyle: FontStyle? = null,\n    fontWeight: FontWeight? = null,\n    fontFamily: FontFamily? = null,\n    letterSpacing: TextUnit = TextUnit.Unspecified,\n    textDecoration: TextDecoration? = null,\n    textAlign: TextAlign? = null,\n    lineHeight: TextUnit = TextUnit.Unspecified,\n    overflow: TextOverflow = TextOverflow.Clip,\n    softWrap: Boolean = true,\n    maxLines: Int = Int.MAX_VALUE,\n    startIcon: (@Composable () -> Unit)? = null,\n    endIcon: (@Composable () -> Unit)? = null,\n    onTextLayout: (TextLayoutResult) -> Unit = {},\n    onMentionClick: (Mention) -> Unit = {},\n    onMentionDidClick: (String) -> Unit = {},\n    onHashtagClick: (HashtagInStatus) -> Unit = {},\n    onMaybeHashtagClick: (String) -> Unit = {},\n    onUrlClick: (url: String) -> Unit = {},\n    style: TextStyle = LocalTextStyle.current,\n) {\n    Row(\n        modifier = modifier,\n        horizontalArrangement = Arrangement.Center,\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        startIcon?.invoke()\n        FreadRichText(\n            modifier = Modifier,\n            richText = text,\n            color = color,\n            fontStyle = fontStyle,\n            fontWeight = fontWeight,\n            lineHeight = lineHeight,\n            textAlign = textAlign,\n            onMentionClick = onMentionClick,\n            onMentionDidClick = onMentionDidClick,\n            onHashtagClick = onHashtagClick,\n            onMaybeHashtagClick = onMaybeHashtagClick,\n            onUrlClick = onUrlClick,\n            overflow = overflow,\n            maxLines = maxLines,\n            fontSize = fontSize,\n            fontFamily = fontFamily,\n            letterSpacing = letterSpacing,\n            textDecoration = textDecoration,\n            softWrap = softWrap,\n            onTextLayout = onTextLayout,\n            style = style,\n        )\n        endIcon?.invoke()\n    }\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/source/BlogPlatformUi.kt",
    "content": "package com.zhangke.fread.status.ui.source\n\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.aspectRatio\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.material3.Button\nimport androidx.compose.material3.Card\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport com.seiko.imageloader.ui.AutoSizeImage\nimport com.zhangke.fread.common.resources.logo\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.platform.BlogPlatform\nimport com.zhangke.fread.status.platform.PlatformSnapshot\nimport com.zhangke.fread.status.ui.richtext.FreadRichText\nimport com.zhangke.fread.statusui.Res\nimport com.zhangke.fread.statusui.img_banner_background\nimport org.jetbrains.compose.resources.painterResource\nimport org.jetbrains.compose.resources.stringResource\n\n@Composable\nfun BlogPlatformCard(\n    modifier: Modifier,\n    platform: BlogPlatform,\n    onLoginClick: (() -> Unit)?,\n) {\n    Box(modifier = modifier) {\n        Card(\n            modifier = Modifier.fillMaxWidth(),\n        ) {\n            Column(\n                modifier = Modifier.fillMaxWidth(),\n            ) {\n                Box(\n                    modifier = Modifier.fillMaxWidth()\n                        .aspectRatio(2F)\n                ) {\n                    Image(\n                        modifier = Modifier.fillMaxSize(),\n                        painter = painterResource(Res.drawable.img_banner_background),\n                        contentDescription = null,\n                        contentScale = ContentScale.Crop,\n                    )\n                    AutoSizeImage(\n                        url = platform.thumbnail.orEmpty(),\n                        modifier = Modifier.fillMaxSize(),\n                        contentScale = ContentScale.Crop,\n                        contentDescription = \"banner\",\n                    )\n                }\n                Row(\n                    modifier = Modifier.padding(start = 16.dp, top = 6.dp, end = 16.dp),\n                    verticalAlignment = Alignment.CenterVertically,\n                ) {\n                    Text(\n                        text = platform.name,\n                        maxLines = 1,\n                        textAlign = TextAlign.Start,\n                        overflow = TextOverflow.Ellipsis,\n                        fontWeight = FontWeight.SemiBold,\n                        style = MaterialTheme.typography.titleLarge,\n                    )\n                    Spacer(modifier = Modifier.width(4.dp))\n                    Image(\n                        modifier = Modifier.size(12.dp),\n                        imageVector = platform.protocol.logo,\n                        contentDescription = null,\n                    )\n                }\n                Text(\n                    modifier = Modifier.padding(\n                        start = 16.dp,\n                        top = 2.dp,\n                    ),\n                    text = platform.baseUrl.host,\n                    maxLines = 1,\n                    textAlign = TextAlign.Start,\n                    overflow = TextOverflow.Ellipsis,\n                    color = MaterialTheme.colorScheme.onSurfaceVariant,\n                    style = MaterialTheme.typography.labelMedium,\n                )\n                FreadRichText(\n                    modifier = Modifier.fillMaxWidth()\n                        .padding(\n                            start = 16.dp,\n                            top = 2.dp,\n                            end = 16.dp,\n                        ),\n                    content = platform.description,\n                    maxLines = 3,\n                )\n                Spacer(modifier = Modifier.height(16.dp))\n                if (onLoginClick != null) {\n                    Button(\n                        modifier = Modifier.padding(horizontal = 16.dp)\n                            .fillMaxWidth(),\n                        onClick = onLoginClick,\n                    ) {\n                        Text(\n                            text = stringResource(LocalizedString.login),\n                        )\n                    }\n                    Spacer(modifier = Modifier.height(16.dp))\n                }\n            }\n        }\n    }\n}\n\n@Composable\nfun BlogPlatformUi(\n    modifier: Modifier,\n    platform: BlogPlatform,\n    showDivider: Boolean = true,\n) {\n    SourceCommonUi(\n        modifier = modifier,\n        thumbnail = platform.thumbnail.orEmpty(),\n        title = platform.name,\n        subtitle = platform.baseUrl.host,\n        description = platform.description,\n        protocolLogo = platform.protocol.logo,\n        showDivider = showDivider,\n    )\n}\n\n@Composable\nfun BlogPlatformSnapshotUi(\n    modifier: Modifier,\n    platform: PlatformSnapshot,\n) {\n    SourceCommonUi(\n        modifier = modifier,\n        thumbnail = platform.thumbnail,\n        title = platform.domain,\n        subtitle = platform.domain,\n        description = platform.description,\n        protocolLogo = platform.protocol.logo,\n    )\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/source/SearchPlatformResultUi.kt",
    "content": "package com.zhangke.fread.status.ui.source\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport com.zhangke.fread.status.search.SearchedPlatform\n\n@Composable\nfun SearchPlatformResultUi(\n    searchedResult: SearchedPlatform,\n    onContentClick: (SearchedPlatform) -> Unit,\n) {\n    when (searchedResult) {\n        is SearchedPlatform.Platform -> BlogPlatformUi(\n            modifier = Modifier\n                .fillMaxWidth()\n                .clickable {\n                    onContentClick(searchedResult)\n                },\n            platform = searchedResult.platform,\n        )\n\n        is SearchedPlatform.Snapshot -> BlogPlatformSnapshotUi(\n            modifier = Modifier\n                .fillMaxWidth()\n                .clickable { onContentClick(searchedResult) },\n            platform = searchedResult.snapshot,\n        )\n    }\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/source/SourceCommonUi.kt",
    "content": "package com.zhangke.fread.status.ui.source\n\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.material3.HorizontalDivider\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport com.seiko.imageloader.model.ImageAction\nimport com.seiko.imageloader.rememberImageActionPainter\nimport com.seiko.imageloader.ui.AutoSizeBox\nimport com.zhangke.framework.composable.freadPlaceholder\nimport com.zhangke.fread.common.browser.LocalActivityBrowserLauncher\nimport com.zhangke.fread.common.browser.launchWebTabInApp\nimport com.zhangke.fread.status.ui.richtext.FreadRichText\n\n@Composable\nfun SourceCommonUi(\n    thumbnail: String,\n    title: String,\n    subtitle: String?,\n    description: String,\n    protocolLogo: ImageVector?,\n    modifier: Modifier = Modifier,\n    showDivider: Boolean = true,\n) {\n    val browserLauncher = LocalActivityBrowserLauncher.current\n    val coroutineScope = rememberCoroutineScope()\n    Column(modifier = modifier) {\n        Spacer(Modifier.height(8.dp))\n        Row(\n            modifier = Modifier.fillMaxWidth(),\n        ) {\n            Spacer(Modifier.width(16.dp))\n            Box(\n                modifier = Modifier.size(48.dp),\n            ) {\n                AutoSizeBox(\n                    url = thumbnail,\n                    modifier = Modifier.fillMaxSize(),\n                ) { action ->\n                    Image(\n                        modifier = Modifier\n                            .freadPlaceholder(\n                                visible = action !is ImageAction.Success,\n                                shape = CircleShape,\n                            )\n                            .matchParentSize()\n                            .clip(CircleShape),\n                        painter = rememberImageActionPainter(action),\n                        contentDescription = null,\n                        contentScale = ContentScale.Crop,\n                    )\n                }\n            }\n            Spacer(modifier = Modifier.width(8.dp))\n            Column {\n                Row(\n                    verticalAlignment = Alignment.CenterVertically,\n                ) {\n                    Text(\n                        text = title,\n                        maxLines = 1,\n                        textAlign = TextAlign.Start,\n                        overflow = TextOverflow.Ellipsis,\n                        style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold),\n                    )\n                    if (protocolLogo != null) {\n                        Spacer(modifier = Modifier.width(4.dp))\n                        Image(\n                            modifier = Modifier.size(12.dp),\n                            imageVector = protocolLogo,\n                            contentDescription = null,\n                        )\n                    }\n                }\n                if (subtitle.isNullOrEmpty()) {\n                    Spacer(modifier = Modifier.height(0.5.dp))\n                } else {\n                    Spacer(modifier = Modifier.height(2.dp))\n                    Text(\n                        text = subtitle,\n                        maxLines = 1,\n                        textAlign = TextAlign.Start,\n                        overflow = TextOverflow.Ellipsis,\n                        color = MaterialTheme.colorScheme.onSurfaceVariant,\n                        style = MaterialTheme.typography.labelSmall,\n                    )\n                }\n                Spacer(Modifier.height(2.dp))\n                FreadRichText(\n                    content = description,\n                    maxLines = 3,\n                    emojis = emptyList(),\n                    mentions = emptyList(),\n                    tags = emptyList(),\n                    onMentionClick = {},\n                    onHashtagClick = {},\n                    onUrlClick = {\n                        browserLauncher.launchWebTabInApp(coroutineScope, it)\n                    },\n                )\n                Spacer(modifier = Modifier.width(16.dp))\n            }\n        }\n        Spacer(Modifier.height(8.dp))\n        if (showDivider) {\n            HorizontalDivider()\n        }\n    }\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/source/StatusSourceUi.kt",
    "content": "package com.zhangke.fread.status.ui.source\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport com.zhangke.fread.common.resources.logo\nimport com.zhangke.fread.status.source.StatusSource\n\n@Composable\nfun StatusSourceUi(\n    source: StatusSource,\n    modifier: Modifier = Modifier,\n) {\n    SourceCommonUi(\n        modifier = modifier,\n        thumbnail = source.thumbnail.orEmpty(),\n        title = source.name,\n        subtitle = source.handle,\n        description = source.description,\n        protocolLogo = source.protocol.logo,\n    )\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/style/LocalStatusStyle.kt",
    "content": "package com.zhangke.fread.status.ui.style\n\nimport androidx.compose.material3.DividerDefaults\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\n\nobject StatusStyles {\n\n    @Composable\n    fun small(): StatusStyle {\n        return StatusStyle(\n            containerStartPadding = 16.dp,\n            containerTopPadding = 10.dp,\n            containerEndPadding = 16.dp,\n            containerBottomPadding = 10.dp,\n            infolineToTopLabelPadding = 4.dp,\n            topLabelStyle = smallTopLabelStyle(),\n            infoLineStyle = smallInfoStyle(),\n            contentStyle = smallContentStyle(),\n            bottomPanelStyle = smallBottomPanelStyle(),\n            threadsStyle = smallThreadsStyle(),\n            cardStyle = smallCardStyle(),\n            bottomLabelStyle = smallBottomLabelStyle(),\n        )\n    }\n\n    @Composable\n    fun medium(): StatusStyle {\n        return StatusStyle(\n            containerStartPadding = 16.dp,\n            containerTopPadding = 12.dp,\n            containerEndPadding = 16.dp,\n            containerBottomPadding = 12.dp,\n            infolineToTopLabelPadding = 4.dp,\n            topLabelStyle = mediumTopLabelStyle(),\n            infoLineStyle = mediumInfoStyle(),\n            contentStyle = mediumContentStyle(),\n            bottomPanelStyle = mediumBottomPanelStyle(),\n            threadsStyle = mediumThreadsStyle(),\n            cardStyle = mediumCardStyle(),\n            bottomLabelStyle = mediumBottomLabelStyle(),\n        )\n    }\n\n    @Composable\n    fun large(): StatusStyle {\n        return StatusStyle(\n            containerStartPadding = 16.dp,\n            containerTopPadding = 14.dp,\n            containerEndPadding = 16.dp,\n            containerBottomPadding = 14.dp,\n            infolineToTopLabelPadding = 6.dp,\n            topLabelStyle = largeTopLabelStyle(),\n            infoLineStyle = largeInfoStyle(),\n            contentStyle = largeContentStyle(),\n            bottomPanelStyle = largeBottomPanelStyle(),\n            threadsStyle = largeThreadsStyle(),\n            cardStyle = largeCardStyle(),\n            bottomLabelStyle = largeBottomLabelStyle(),\n        )\n    }\n\n    @Composable\n    private fun smallThreadsStyle(): StatusStyle.ThreadsStyle {\n        return mediumThreadsStyle()\n    }\n\n    @Composable\n    private fun mediumThreadsStyle(): StatusStyle.ThreadsStyle {\n        return StatusStyle.ThreadsStyle(\n            lineWidth = 1.5.dp,\n            color = DividerDefaults.color,\n        )\n    }\n\n    @Composable\n    private fun largeThreadsStyle(): StatusStyle.ThreadsStyle {\n        return mediumThreadsStyle()\n    }\n\n    @Composable\n    private fun smallTopLabelStyle(): StatusStyle.TopLabelStyle {\n        return StatusStyle.TopLabelStyle(\n            iconSize = 12.dp,\n            textSize = 11.sp,\n        )\n    }\n\n    @Composable\n    private fun mediumTopLabelStyle(): StatusStyle.TopLabelStyle {\n        return StatusStyle.TopLabelStyle(\n            iconSize = 14.dp,\n            textSize = 12.sp,\n        )\n    }\n\n    @Composable\n    private fun largeTopLabelStyle(): StatusStyle.TopLabelStyle {\n        return StatusStyle.TopLabelStyle(\n            iconSize = 16.dp,\n            textSize = 14.sp,\n        )\n    }\n\n    @Composable\n    private fun smallInfoStyle(): StatusStyle.InfoLineStyle {\n        return StatusStyle.InfoLineStyle(\n            nameSize = 14.sp,\n            avatarSize = 38.dp,\n            nameToAvatarSpacing = 6.dp,\n            descStyle = MaterialTheme.typography.bodySmall\n                .copy(fontWeight = FontWeight.Light)\n        )\n    }\n\n    @Composable\n    private fun mediumInfoStyle(): StatusStyle.InfoLineStyle {\n        return StatusStyle.InfoLineStyle(\n            nameSize = 16.sp,\n            avatarSize = 42.dp,\n            nameToAvatarSpacing = 8.dp,\n            descStyle = MaterialTheme.typography.bodySmall\n                .copy(fontWeight = FontWeight.Normal)\n        )\n    }\n\n    @Composable\n    private fun largeInfoStyle(): StatusStyle.InfoLineStyle {\n        return StatusStyle.InfoLineStyle(\n            nameSize = 16.sp,\n            avatarSize = 46.dp,\n            nameToAvatarSpacing = 8.dp,\n            descStyle = MaterialTheme.typography.bodyMedium\n                .copy(fontWeight = FontWeight.Medium)\n        )\n    }\n\n    @Composable\n    private fun smallContentStyle(): StatusStyle.ContentStyle {\n        return StatusStyle.ContentStyle(\n            maxLine = 10,\n            titleSize = 14.sp,\n            contentSize = 12.sp,\n            startPadding = 0.dp,\n            contentVerticalSpacing = 2.dp,\n        )\n    }\n\n    @Composable\n    private fun mediumContentStyle(): StatusStyle.ContentStyle {\n        return StatusStyle.ContentStyle(\n            maxLine = 10,\n            titleSize = 16.sp,\n            contentSize = 14.sp,\n            startPadding = 0.dp,\n            contentVerticalSpacing = 4.dp,\n        )\n    }\n\n    @Composable\n    private fun largeContentStyle(): StatusStyle.ContentStyle {\n        return StatusStyle.ContentStyle(\n            maxLine = 10,\n            titleSize = 18.sp,\n            contentSize = 16.sp,\n            startPadding = 0.dp,\n            contentVerticalSpacing = 6.dp,\n        )\n    }\n\n    @Composable\n    private fun smallBottomPanelStyle(): StatusStyle.BottomPanelStyle {\n        return StatusStyle.BottomPanelStyle(\n            iconSize = 26.dp,\n            startPadding = 0.dp\n        )\n    }\n\n    @Composable\n    private fun mediumBottomPanelStyle(): StatusStyle.BottomPanelStyle {\n        return StatusStyle.BottomPanelStyle(\n            iconSize = 28.dp,\n            startPadding = 0.dp\n        )\n    }\n\n    @Composable\n    private fun largeBottomPanelStyle(): StatusStyle.BottomPanelStyle {\n        return StatusStyle.BottomPanelStyle(\n            iconSize = 30.dp,\n            startPadding = 0.dp\n        )\n    }\n\n    @Composable\n    private fun smallCardStyle(): StatusStyle.CardStyle {\n        return StatusStyle.CardStyle(\n            titleStyle = MaterialTheme.typography.titleSmall,\n            descStyle = MaterialTheme.typography.bodySmall,\n            imageBottomPadding = 6.dp,\n            contentVerticalPadding = 6.dp\n        )\n    }\n\n    @Composable\n    private fun mediumCardStyle(): StatusStyle.CardStyle {\n        return StatusStyle.CardStyle(\n            titleStyle = MaterialTheme.typography.titleMedium,\n            descStyle = MaterialTheme.typography.bodyMedium,\n            imageBottomPadding = 8.dp,\n            contentVerticalPadding = 8.dp\n        )\n    }\n\n    @Composable\n    private fun largeCardStyle(): StatusStyle.CardStyle {\n        return StatusStyle.CardStyle(\n            titleStyle = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold),\n            descStyle = MaterialTheme.typography.bodyLarge,\n            imageBottomPadding = 10.dp,\n            contentVerticalPadding = 10.dp\n        )\n    }\n\n    @Composable\n    private fun smallBottomLabelStyle(): StatusStyle.BottomLabelStyle {\n        return StatusStyle.BottomLabelStyle(\n            textStyle = MaterialTheme.typography.bodySmall\n                .copy(fontSize = 11.sp),\n        )\n    }\n\n    @Composable\n    private fun mediumBottomLabelStyle(): StatusStyle.BottomLabelStyle {\n        return StatusStyle.BottomLabelStyle(\n            textStyle = MaterialTheme.typography.bodyMedium\n                .copy(fontSize = 12.sp),\n        )\n    }\n\n    @Composable\n    private fun largeBottomLabelStyle(): StatusStyle.BottomLabelStyle {\n        return StatusStyle.BottomLabelStyle(\n            textStyle = MaterialTheme.typography.bodyLarge\n                .copy(fontSize = 14.sp),\n        )\n    }\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/style/StatusInfoStyle.kt",
    "content": "package com.zhangke.fread.status.ui.style\n\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.text.TextStyle\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\n\ndata class StatusInfoStyle(\n    val avatarSize: Dp,\n    val avatarToNamePadding: Dp,\n    val nameToInfoLineSpacing: Dp,\n    val descStyle: TextStyle,\n)\n\nobject StatusInfoStyleDefaults {\n\n    val avatarSize = 40.dp\n\n    val avatarToNamePadding = 8.dp\n\n    val nameToInfoLineSpacing = 2.dp\n\n    val descStyle: TextStyle @Composable get() = MaterialTheme.typography.bodySmall\n}\n\n@Composable\nfun defaultStatusInfoStyle() = StatusInfoStyle(\n    avatarSize = StatusInfoStyleDefaults.avatarSize,\n    avatarToNamePadding = StatusInfoStyleDefaults.avatarToNamePadding,\n    nameToInfoLineSpacing = StatusInfoStyleDefaults.nameToInfoLineSpacing,\n    descStyle = StatusInfoStyleDefaults.descStyle,\n)\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/style/StatusStyle.kt",
    "content": "package com.zhangke.fread.status.ui.style\n\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.text.TextStyle\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.TextUnit\n\n// 垂直方向的 padding 外面统一加\ndata class StatusStyle(\n    val containerStartPadding: Dp,\n    val containerTopPadding: Dp,\n    val containerEndPadding: Dp,\n    val containerBottomPadding: Dp,\n    val topLabelStyle: TopLabelStyle,\n    val infolineToTopLabelPadding: Dp,\n    val infoLineStyle: InfoLineStyle,\n    val contentStyle: ContentStyle,\n    val bottomPanelStyle: BottomPanelStyle,\n    val threadsStyle: ThreadsStyle,\n    val cardStyle: CardStyle,\n    val bottomLabelStyle: BottomLabelStyle,\n) {\n\n    val secondaryFontColor: Color\n        @Composable get() = MaterialTheme.colorScheme.onSurfaceVariant\n\n    fun contentIndentStyle(): StatusStyle {\n        val contentStartPadding = infoLineStyle.avatarSize + infoLineStyle.nameToAvatarSpacing\n        return copy(\n            contentStyle = contentStyle.copy(startPadding = contentStartPadding),\n            bottomPanelStyle = bottomPanelStyle.copy(startPadding = contentStartPadding),\n        )\n    }\n\n    data class TopLabelStyle(\n        val iconSize: Dp,\n        val textSize: TextUnit,\n    )\n\n    data class ContentStyle(\n        val maxLine: Int,\n        val titleSize: TextUnit,\n        val contentSize: TextUnit,\n        val startPadding: Dp,\n        /**\n         * 内容部分竖向的间距，不包含顶部和底部，只包含info line to content\n         * content to bottom panel 等。\n         */\n        val contentVerticalSpacing: Dp,\n    )\n\n    data class InfoLineStyle(\n        val nameSize: TextUnit,\n        val avatarSize: Dp,\n        val nameToAvatarSpacing: Dp,\n        val descStyle: TextStyle,\n    )\n\n    data class BottomPanelStyle(\n        val iconSize: Dp,\n        val startPadding: Dp,\n    )\n\n    data class ThreadsStyle(\n        val lineWidth: Dp,\n        val color: Color,\n    )\n\n    data class CardStyle(\n        val titleStyle: TextStyle,\n        val descStyle: TextStyle,\n        val imageBottomPadding: Dp,\n        val contentVerticalPadding: Dp,\n    )\n\n    // for bottom label, like edited time, post time, application\n    // liked count, reblog count, etc.\n    data class BottomLabelStyle(\n        val textStyle: TextStyle,\n    )\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/style/StatusUiConfig.kt",
    "content": "package com.zhangke.fread.status.ui.style\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.ProvidableCompositionLocal\nimport androidx.compose.runtime.compositionLocalOf\nimport com.zhangke.fread.common.config.StatusConfig\nimport com.zhangke.fread.common.config.StatusContentSize\n\nval LocalStatusUiConfig: ProvidableCompositionLocal<StatusUiConfig> =\n    compositionLocalOf { error(\"LocalStatusUiConfig not init!\") }\n\ndata class StatusUiConfig(\n    val alwaysShowSensitiveContent: Boolean,\n    val contentStyle: StatusStyle,\n    val immersiveNavBar: Boolean,\n) {\n\n    companion object {\n\n        @Composable\n        fun create(\n            config: StatusConfig,\n        ): StatusUiConfig {\n            return StatusUiConfig(\n                alwaysShowSensitiveContent = config.alwaysShowSensitiveContent,\n                contentStyle = config.contentSize.toStyle(),\n                immersiveNavBar = config.immersiveNavBar,\n            )\n        }\n\n        @Composable\n        private fun StatusContentSize.toStyle(): StatusStyle {\n            return when (this) {\n                StatusContentSize.SMALL -> StatusStyles.small()\n                StatusContentSize.MEDIUM -> StatusStyles.medium()\n                StatusContentSize.LARGE -> StatusStyles.large()\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/threads/Threads.kt",
    "content": "package com.zhangke.fread.status.ui.threads\n\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.drawBehind\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.graphics.Path\nimport androidx.compose.ui.graphics.PathEffect\nimport androidx.compose.ui.graphics.StrokeCap\nimport androidx.compose.ui.graphics.drawscope.Stroke\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport com.zhangke.fread.status.ui.publish.PublishBlogStyle\nimport com.zhangke.fread.status.ui.style.StatusStyle\n\nfun Modifier.threads(\n    threadsType: ThreadsType,\n    infoToTopSpacing: Float?,\n    threadStyle: StatusStyle.ThreadsStyle,\n    avatarSize: Dp,\n    continueThreadLabelHeight: Int?,\n    containerStartPadding: Dp,\n    containerTopPadding: Dp,\n    nameToAvatarSpacing: Dp,\n): Modifier {\n    if (threadsType == ThreadsType.NONE || threadsType == ThreadsType.UNSPECIFIED) return this\n    infoToTopSpacing ?: return this\n    return this.drawBehind {\n        val containerTopPaddingPx = containerTopPadding.toPx()\n        val strokeWidthPx = threadStyle.lineWidth.toPx()\n        val threadPadding = 4.dp.toPx()\n        if (threadsType == ThreadsType.CONTINUED_THREAD) {\n            if (continueThreadLabelHeight != null) {\n                val path = Path().apply {\n                    moveTo(\n                        x = containerStartPadding.toPx() + avatarSize.toPx() + nameToAvatarSpacing.toPx() - threadPadding,\n                        y = containerTopPaddingPx / 2 + continueThreadLabelHeight / 2,\n                    )\n                    lineTo(\n                        x = containerStartPadding.toPx() + avatarSize.toPx() / 2,\n                        y = containerTopPaddingPx / 2 + continueThreadLabelHeight / 2,\n                    )\n                    lineTo(\n                        x = containerStartPadding.toPx() + avatarSize.toPx() / 2,\n                        y = infoToTopSpacing - threadPadding,\n                    )\n                }\n                drawPath(\n                    path = path,\n                    color = threadStyle.color,\n                    style = Stroke(\n                        width = strokeWidthPx,\n                        cap = StrokeCap.Round,\n                        pathEffect = PathEffect.cornerPathEffect(6.dp.toPx()),\n                    ),\n                )\n            }\n        } else {\n            val startMargin = containerStartPadding + avatarSize / 2\n            val startMarginPx = startMargin.toPx()\n            val firstLineY = infoToTopSpacing - threadPadding\n            if (threadsType.drawTopOfAvatarLine) {\n                drawLine(\n                    color = threadStyle.color,\n                    strokeWidth = strokeWidthPx,\n                    start = Offset(x = startMarginPx, y = 0F),\n                    end = Offset(\n                        x = startMarginPx,\n                        y = firstLineY,\n                    ),\n                    cap = StrokeCap.Round,\n                )\n            }\n            if (threadsType.drawBottomOfAvatarLine) {\n                val lineOnBottomOfAvatarY =\n                    infoToTopSpacing + avatarSize.toPx() + threadPadding\n                drawLine(\n                    color = threadStyle.color,\n                    start = Offset(x = startMarginPx, y = lineOnBottomOfAvatarY),\n                    end = Offset(x = startMarginPx, y = size.height),\n                    strokeWidth = strokeWidthPx,\n                    cap = StrokeCap.Round,\n                )\n            }\n        }\n    }\n}\n\nfun Modifier.threads(\n    threadsType: ThreadsType,\n    infoToTopSpacing: Float?,\n    style: StatusStyle,\n    continueThreadLabelHeight: Int?,\n): Modifier {\n    return this.threads(\n        threadsType = threadsType,\n        infoToTopSpacing = infoToTopSpacing,\n        avatarSize = style.infoLineStyle.avatarSize,\n        threadStyle = style.threadsStyle,\n        continueThreadLabelHeight = continueThreadLabelHeight,\n        containerStartPadding = style.containerStartPadding,\n        containerTopPadding = style.containerTopPadding,\n        nameToAvatarSpacing = style.infoLineStyle.nameToAvatarSpacing,\n    )\n}\n\nfun Modifier.blogBeReplyThreads(\n    threadsType: ThreadsType,\n    publishBlogStyle: PublishBlogStyle,\n): Modifier {\n    return this.threads(\n        threadsType = threadsType,\n        infoToTopSpacing = 0F,\n        threadStyle = publishBlogStyle.statusStyle.threadsStyle,\n        containerTopPadding = 0.dp,\n        continueThreadLabelHeight = null,\n        containerStartPadding = publishBlogStyle.startPadding,\n        avatarSize = publishBlogStyle.statusStyle.infoLineStyle.avatarSize,\n        nameToAvatarSpacing = publishBlogStyle.statusStyle.infoLineStyle.nameToAvatarSpacing,\n    )\n}\n\nfun Modifier.blogInReplyingThreads(\n    threadsType: ThreadsType,\n    infoToTopSpacing: Float,\n    publishBlogStyle: PublishBlogStyle,\n): Modifier {\n    return this.threads(\n        threadsType = threadsType,\n        infoToTopSpacing = infoToTopSpacing,\n        continueThreadLabelHeight = null,\n        threadStyle = publishBlogStyle.statusStyle.threadsStyle,\n        containerTopPadding = publishBlogStyle.topPadding,\n        containerStartPadding = publishBlogStyle.startPadding,\n        avatarSize = publishBlogStyle.statusStyle.infoLineStyle.avatarSize,\n        nameToAvatarSpacing = publishBlogStyle.statusStyle.infoLineStyle.nameToAvatarSpacing,\n    )\n}\n\ninternal val ThreadsType.drawTopOfAvatarLine: Boolean\n    get() = this == ThreadsType.ANCHOR || this == ThreadsType.ANCESTOR\n\nprivate val ThreadsType.drawBottomOfAvatarLine: Boolean\n    get() = this == ThreadsType.FIRST_ANCESTOR || this == ThreadsType.ANCESTOR\n\ninternal val ThreadsType.contentIndent: Boolean\n    get() = this == ThreadsType.FIRST_ANCESTOR || this == ThreadsType.ANCESTOR\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/threads/ThreadsType.kt",
    "content": "package com.zhangke.fread.status.ui.threads\n\nenum class ThreadsType {\n\n    UNSPECIFIED,\n\n    NONE,\n\n    /**\n     * 第一个评论\n     */\n    FIRST_ANCESTOR,\n\n    /**\n     * 帖子上级评论\n     */\n    ANCESTOR,\n\n    /**\n     * 锚点帖子，且没有父级\n     */\n    ANCHOR_FIRST,\n\n    /**\n     * 锚点帖子\n     */\n    ANCHOR,\n\n    /**\n     * 在 Feeds 中的有父级的帖子\n     */\n    CONTINUED_THREAD,\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/update/AppUpdateDialog.kt",
    "content": "package com.zhangke.fread.status.ui.update\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.window.DialogProperties\nimport com.zhangke.framework.composable.FreadDialog\nimport com.zhangke.fread.common.update.AppReleaseInfo\nimport com.zhangke.fread.localization.LocalizedString\nimport org.jetbrains.compose.resources.stringResource\n\n@Composable\nfun AppUpdateDialog(\n    appReleaseInfo: AppReleaseInfo,\n    onCancel: (AppReleaseInfo) -> Unit,\n    onUpdateClick: (AppReleaseInfo) -> Unit,\n) {\n    FreadDialog(\n        onDismissRequest = {},\n        properties = DialogProperties(\n            dismissOnBackPress = false,\n            dismissOnClickOutside = false,\n        ),\n        title = stringResource(LocalizedString.statusUiUpdateDialogTitle),\n        contentText = stringResource(\n            LocalizedString.statusUiUpdateDialogReleaseNote,\n            appReleaseInfo.versionName,\n            appReleaseInfo.releaseNote,\n        ),\n        onNegativeClick = { onCancel(appReleaseInfo) },\n        onPositiveClick = { onUpdateClick(appReleaseInfo) },\n    )\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/user/CommonUserUi.kt",
    "content": "package com.zhangke.fread.status.ui.user\n\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.RowScope\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.material3.HorizontalDivider\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.vector.rememberVectorPainter\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport com.zhangke.framework.composable.freadPlaceholder\nimport com.zhangke.fread.common.resources.logo\nimport com.zhangke.fread.status.account.LoggedAccount\nimport com.zhangke.fread.status.author.BlogAuthor\nimport com.zhangke.fread.status.model.StatusProviderProtocol\nimport com.zhangke.fread.status.richtext.RichText\nimport com.zhangke.fread.status.ui.BlogAuthorAvatar\nimport com.zhangke.fread.status.ui.richtext.FreadRichText\n\n@Composable\nfun CommonAccountUi(\n    modifier: Modifier,\n    account: LoggedAccount,\n    showDivider: Boolean,\n) {\n    CommonProfileUi(\n        modifier = modifier,\n        avatar = account.avatar.orEmpty(),\n        displayName = account.humanizedName,\n        handle = account.prettyHandle,\n        description = account.humanizedDescription,\n        showDivider = showDivider,\n        protocol = account.platform.protocol,\n        showProtocolLabel = true,\n    )\n}\n\n@Composable\nfun BasicAccountUi(\n    modifier: Modifier,\n    account: LoggedAccount,\n) {\n    Row(\n        modifier = modifier.fillMaxWidth()\n            .padding(16.dp),\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        BlogAuthorAvatar(\n            modifier = Modifier.size(42.dp),\n            imageUrl = account.avatar,\n        )\n        Column(\n            modifier = Modifier.weight(1F)\n                .padding(start = 8.dp),\n        ) {\n            Row(\n                modifier = Modifier.fillMaxWidth(),\n                verticalAlignment = Alignment.CenterVertically,\n            ) {\n                FreadRichText(\n                    modifier = Modifier,\n                    richText = account.humanizedName,\n                    maxLines = 1,\n                    overflow = TextOverflow.Ellipsis,\n                    fontSize = 16.sp,\n                )\n                Image(\n                    modifier = Modifier.padding(start = 4.dp).size(16.dp),\n                    painter = rememberVectorPainter(account.platform.protocol.logo),\n                    contentDescription = null,\n                )\n            }\n            Spacer(modifier = Modifier.height(2.dp))\n            Text(\n                text = account.prettyHandle,\n                maxLines = 1,\n                overflow = TextOverflow.Ellipsis,\n                style = MaterialTheme.typography.labelMedium,\n                color = MaterialTheme.colorScheme.onSurfaceVariant,\n            )\n        }\n    }\n}\n\n@Composable\nfun CommonUserUi(\n    user: BlogAuthor,\n    modifier: Modifier = Modifier,\n    actionButton: @Composable RowScope.() -> Unit = {},\n    showDivider: Boolean = true,\n) {\n    CommonProfileUi(\n        modifier = modifier,\n        avatar = user.avatar.orEmpty(),\n        displayName = user.humanizedName,\n        handle = user.prettyHandle,\n        description = user.humanizedDescription,\n        protocol = null,\n        showProtocolLabel = false,\n        showDivider = showDivider,\n        actionButton = actionButton,\n    )\n}\n\n@Composable\nprivate fun CommonProfileUi(\n    modifier: Modifier,\n    avatar: String,\n    displayName: RichText,\n    handle: String,\n    description: RichText,\n    protocol: StatusProviderProtocol?,\n    showProtocolLabel: Boolean,\n    showDivider: Boolean,\n    actionButton: @Composable RowScope.() -> Unit = {},\n) {\n    Column(modifier = modifier.fillMaxWidth()) {\n        Row(\n            modifier = Modifier\n                .fillMaxWidth()\n                .padding(16.dp),\n        ) {\n            BlogAuthorAvatar(\n                modifier = Modifier.size(48.dp),\n                imageUrl = avatar,\n            )\n            Column(\n                modifier = Modifier\n                    .padding(start = 16.dp)\n                    .align(Alignment.CenterVertically)\n                    .weight(1F),\n                horizontalAlignment = Alignment.Start,\n            ) {\n                Row(\n                    modifier = Modifier.fillMaxWidth(),\n                    verticalAlignment = Alignment.CenterVertically,\n                ) {\n                    FreadRichText(\n                        modifier = Modifier,\n                        richText = displayName,\n                        maxLines = 1,\n                        overflow = TextOverflow.Ellipsis,\n                        fontSize = 16.sp,\n                    )\n                    if (showProtocolLabel && protocol != null) {\n                        Image(\n                            modifier = Modifier.padding(start = 4.dp).size(16.dp),\n                            painter = rememberVectorPainter(protocol.logo),\n                            contentDescription = null,\n                        )\n                    }\n                }\n                Spacer(modifier = Modifier.height(4.dp))\n                Text(\n                    text = handle,\n                    maxLines = 1,\n                    overflow = TextOverflow.Ellipsis,\n                    style = MaterialTheme.typography.labelMedium,\n                    color = MaterialTheme.colorScheme.onSurfaceVariant,\n                )\n                Spacer(modifier = Modifier.height(4.dp))\n                FreadRichText(\n                    modifier = Modifier.fillMaxWidth(),\n                    richText = description,\n                    maxLines = 6,\n                )\n            }\n            actionButton()\n        }\n        if (showDivider) {\n            HorizontalDivider(\n                thickness = 0.5.dp,\n            )\n        }\n    }\n}\n\n@Composable\nfun CommonUserPlaceHolder() {\n    Row(\n        modifier = Modifier\n            .fillMaxWidth()\n            .padding(16.dp),\n    ) {\n        BlogAuthorAvatar(\n            modifier = Modifier.size(48.dp),\n            imageUrl = null,\n        )\n        Column(\n            modifier = Modifier\n                .padding(start = 16.dp)\n                .align(Alignment.CenterVertically)\n                .weight(1F),\n            horizontalAlignment = Alignment.Start,\n        ) {\n            Box(\n                modifier = Modifier\n                    .width(100.dp)\n                    .height(18.dp)\n                    .freadPlaceholder(true)\n            )\n            Spacer(modifier = Modifier.height(4.dp))\n            Box(\n                modifier = Modifier\n                    .width(80.dp)\n                    .height(14.dp)\n                    .freadPlaceholder(true)\n            )\n            Spacer(modifier = Modifier.height(4.dp))\n            Box(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .height(16.dp)\n                    .freadPlaceholder(true)\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/user/UserHandleLine.kt",
    "content": "package com.zhangke.fread.status.ui.user\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.foundation.text.selection.SelectionContainer\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.outlined.SmartToy\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport com.zhangke.fread.localization.LocalizedString\nimport org.jetbrains.compose.resources.stringResource\n\n@Composable\nfun UserHandleLine(\n    modifier: Modifier,\n    handle: String,\n    bot: Boolean,\n    followedBy: Boolean,\n) {\n    Row(\n        modifier = modifier,\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        if (bot) {\n            Icon(\n                modifier = Modifier\n                    .padding(end = 4.dp)\n                    .size(16.dp),\n                imageVector = Icons.Outlined.SmartToy,\n                contentDescription = \"Bot\",\n                tint = MaterialTheme.colorScheme.primary,\n            )\n        }\n        SelectionContainer {\n            Text(\n                modifier = Modifier,\n                text = handle,\n                maxLines = 1,\n                color = MaterialTheme.colorScheme.onSurfaceVariant,\n                overflow = TextOverflow.Ellipsis,\n                style = MaterialTheme.typography.labelMedium\n                    .copy(fontWeight = FontWeight.Normal),\n            )\n        }\n        if (followedBy) {\n            Text(\n                modifier = Modifier\n                    .padding(start = 4.dp)\n                    .background(\n                        color = MaterialTheme.colorScheme.surfaceContainer,\n                        shape = RoundedCornerShape(2.dp),\n                    )\n                    .padding(horizontal = 4.dp),\n                color = MaterialTheme.colorScheme.onSurfaceVariant,\n                text = stringResource(LocalizedString.statusUiUserDetailFollowsYou),\n                style = MaterialTheme.typography.bodySmall\n                    .copy(fontWeight = FontWeight.Normal),\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/utils/CardInfoSection.kt",
    "content": "package com.zhangke.fread.status.ui.utils\n\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.RowScope\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.material3.Card\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.painter.Painter\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport com.zhangke.fread.status.ui.BlogAuthorAvatar\nimport com.zhangke.fread.status.ui.richtext.FreadRichText\n\n@Composable\nfun CardInfoSection(\n    modifier: Modifier,\n    avatar: String?,\n    title: String,\n    handle: String,\n    description: String?,\n    logo: Painter? = null,\n    onClick: () -> Unit,\n    onUrlClick: (String) -> Unit,\n    actions: (@Composable RowScope.() -> Unit)? = null\n) {\n    Card(\n        modifier = modifier,\n        onClick = onClick,\n    ) {\n        Row(\n            modifier = Modifier.fillMaxWidth(),\n        ) {\n            BlogAuthorAvatar(\n                modifier = Modifier\n                    .padding(start = 16.dp, top = 16.dp, bottom = 16.dp)\n                    .size(40.dp),\n                imageUrl = avatar,\n            )\n            Column(\n                modifier = Modifier\n                    .weight(1F)\n                    .padding(start = 8.dp, top = 16.dp, bottom = 16.dp),\n                horizontalAlignment = Alignment.Start,\n            ) {\n                Row(\n                    modifier = Modifier.fillMaxWidth(),\n                    verticalAlignment = Alignment.CenterVertically,\n                ) {\n                    Text(\n                        maxLines = 1,\n                        text = title,\n                        fontWeight = FontWeight.SemiBold,\n                        style = MaterialTheme.typography.titleMedium,\n                        overflow = TextOverflow.Ellipsis,\n                    )\n                    if (logo != null) {\n                        Image(\n                            modifier = Modifier.padding(start = 4.dp).size(16.dp),\n                            painter = logo,\n                            contentDescription = null,\n                        )\n                    }\n                }\n                Text(\n                    maxLines = 1,\n                    text = handle,\n                    color = MaterialTheme.colorScheme.onSurfaceVariant,\n                    style = MaterialTheme.typography.labelMedium,\n                )\n                if (description.isNullOrEmpty().not()) {\n                    FreadRichText(\n                        modifier = Modifier.padding(top = 2.dp),\n                        content = description,\n                        mentions = emptyList(),\n                        emojis = emptyList(),\n                        tags = emptyList(),\n                        onHashtagClick = {},\n                        onMentionClick = {},\n                        onUrlClick = onUrlClick,\n                        maxLines = 3,\n                    )\n                }\n            }\n            Box(modifier = Modifier.align(Alignment.CenterVertically)) {\n                if (actions != null) {\n                    Row(\n                        modifier = Modifier.padding(start = 8.dp, end = 8.dp),\n                        verticalAlignment = Alignment.CenterVertically,\n                    ) {\n                        actions()\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/utils/ScreenSize.kt",
    "content": "package com.zhangke.fread.status.ui.utils\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.ReadOnlyComposable\nimport androidx.compose.ui.unit.Dp\n\n@Composable\n@ReadOnlyComposable\nexpect fun getScreenWidth(): Dp\n\n@Composable\n@ReadOnlyComposable\nexpect fun getScreenHeight(): Dp"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/video/BlogVideos.kt",
    "content": "package com.zhangke.fread.status.ui.video\n\nimport androidx.compose.runtime.Composable\nimport com.zhangke.fread.status.blog.BlogMedia\nimport com.zhangke.fread.status.ui.image.OnBlogMediaClick\n\n@Composable\nexpect fun BlogVideos(\n    mediaList: List<BlogMedia>,\n    hideContent: Boolean,\n    indexInList: Int,\n    onMediaClick: OnBlogMediaClick,\n)"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/video/VideoDurationFormatter.kt",
    "content": "package com.zhangke.fread.status.ui.video\n\nimport kotlin.time.Duration\nimport kotlin.time.Duration.Companion.hours\nimport kotlin.time.Duration.Companion.milliseconds\nimport kotlin.time.Duration.Companion.minutes\nimport kotlin.time.Duration.Companion.seconds\nimport kotlin.time.DurationUnit\n\nobject VideoDurationFormatter {\n\n    fun formatVideoProgressDesc(playerPosition: Long, durationMs: Long): String {\n        val currentDurationDesc = formatVideoDuration(playerPosition)\n        val durationDesc = formatVideoDuration(durationMs)\n        return \"$currentDurationDesc / $durationDesc\"\n    }\n\n    private fun formatVideoDuration(durationMs: Long): String {\n        val builder = StringBuilder()\n        val duration = durationMs.milliseconds\n        if (duration.isInfinite()) return \"\"\n        val hours = duration.inWholeHours.hours\n        if (hours > 0.hours) {\n            builder.append(hours.toFormatString(DurationUnit.HOURS))\n            builder.append(\":\")\n        }\n        val minutes = (duration - hours).inWholeMinutes.minutes\n        builder.append(minutes.toFormatString(DurationUnit.MINUTES))\n        builder.append(\":\")\n        val seconds = (duration - hours - minutes).inWholeSeconds.seconds\n        builder.append(seconds.toFormatString(DurationUnit.SECONDS))\n        return builder.toString()\n    }\n\n    private fun Duration.toFormatString(unit: DurationUnit): String {\n        return toInt(unit).toString().padStart(2, '0')\n    }\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/com/zhangke/fread/status/ui/video/full/FullScreenVideoPlayer.kt",
    "content": "package com.zhangke.fread.status.ui.video.full\n\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.fadeIn\nimport androidx.compose.animation.fadeOut\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.navigationBarsPadding\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.statusBarsPadding\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.filled.VolumeOff\nimport androidx.compose.material.icons.automirrored.filled.VolumeUp\nimport androidx.compose.material.icons.filled.Pause\nimport androidx.compose.material.icons.filled.PlayArrow\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Slider\nimport androidx.compose.material3.SliderDefaults\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableFloatStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.vector.rememberVectorPainter\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport com.zhangke.framework.composable.SimpleIconButton\nimport com.zhangke.framework.composable.Toolbar\nimport com.zhangke.framework.composable.ToolbarTokens\nimport com.zhangke.framework.composable.noRippleClick\nimport com.zhangke.framework.composable.video.VideoPlayer\nimport com.zhangke.framework.composable.video.rememberVideoPlayerController\nimport com.zhangke.framework.permission.RequireLocalStoragePermission\nimport com.zhangke.framework.utils.PlatformUri\nimport com.zhangke.fread.common.utils.LocalMediaFileHelper\nimport com.zhangke.fread.status.ui.video.VideoDurationFormatter\nimport kotlin.math.roundToInt\n\n@Composable\nfun FullScreenVideoPlayer(\n    uri: PlatformUri,\n    onBackClick: () -> Unit,\n    modifier: Modifier = Modifier,\n) {\n    var panelVisible by remember {\n        mutableStateOf(true)\n    }\n    Box(\n        modifier = modifier\n            .fillMaxSize()\n            .noRippleClick {\n                panelVisible = !panelVisible\n            }\n            .background(color = Color.Black)\n    ) {\n        val videoController = rememberVideoPlayerController(\n            mediaUrl = uri.toString(),\n            initialContentScale = ContentScale.Fit,\n        )\n        VideoPlayer(\n            modifier = Modifier,\n            controller = videoController,\n        )\n        AnimatedVisibility(\n            modifier = Modifier.fillMaxSize(),\n            visible = panelVisible,\n            enter = fadeIn(),\n            exit = fadeOut(),\n        ) {\n            Box(modifier = Modifier.fillMaxSize()) {\n                FullScreenVideoPlayerPanel(\n                    modifier = Modifier\n                        .align(Alignment.BottomCenter)\n                        .navigationBarsPadding(),\n                    playing = videoController.isPlaying,\n                    mute = videoController.isMuted,\n                    onPlayClick = {\n                        videoController.play()\n                    },\n                    onPauseClick = {\n                        videoController.pause()\n                    },\n                    playerPosition = videoController.currentTimeInSeconds.roundToInt() * 1000L,\n                    duration = videoController.totalDurationInSeconds.roundToInt() * 1000L,\n                    onPositionChangeRequest = {\n                        videoController.seekTo(it / 1000F)\n                    },\n                    onMuteClick = {\n                        videoController.mute()\n                    },\n                    onUnmuteClick = {\n                        videoController.unmute()\n                    },\n                )\n                FullScreenPlayerToolBar(\n                    modifier = Modifier.align(Alignment.TopStart),\n                    videoUrl = uri.toString(),\n                    onBackClick = onBackClick,\n                )\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun FullScreenPlayerToolBar(\n    modifier: Modifier,\n    videoUrl: String,\n    onBackClick: () -> Unit,\n) {\n    Box(\n        modifier = modifier\n            .fillMaxWidth()\n            .statusBarsPadding()\n            .padding(horizontal = ToolbarTokens.TopAppBarHorizontalPadding)\n            .height(ToolbarTokens.ContainerHeight),\n    ) {\n        Toolbar.BackButton(\n            onBackClick = onBackClick,\n            tint = Color.White,\n        )\n\n        var needSaveImage by remember { mutableStateOf(false) }\n        if (needSaveImage) {\n            val mediaFileHelper = LocalMediaFileHelper.current\n            RequireLocalStoragePermission(\n                onPermissionGranted = {\n                    mediaFileHelper.saveVideoToGallery(videoUrl)\n                    needSaveImage = false\n                },\n                onPermissionDenied = {\n                    needSaveImage = false\n                },\n            )\n        }\n        Toolbar.DownloadButton(\n            modifier = Modifier.align(Alignment.CenterEnd),\n            onClick = {\n                needSaveImage = true\n            },\n            tint = MaterialTheme.colorScheme.inverseOnSurface,\n        )\n    }\n}\n\n@Composable\nprivate fun FullScreenVideoPlayerPanel(\n    modifier: Modifier = Modifier,\n    playing: Boolean,\n    playerPosition: Long,\n    duration: Long,\n    mute: Boolean,\n    onPauseClick: () -> Unit,\n    onPlayClick: () -> Unit,\n    onPositionChangeRequest: (position: Long) -> Unit,\n    onMuteClick: () -> Unit,\n    onUnmuteClick: () -> Unit,\n) {\n    val progress = playerPosition / duration.toFloat()\n    Column(\n        modifier = modifier\n            .fillMaxWidth()\n            .padding(bottom = 16.dp),\n    ) {\n        PlayerProgress(\n            modifier = Modifier\n                .fillMaxWidth()\n                .padding(horizontal = 16.dp),\n            progress = progress,\n            onProgressChange = { progress ->\n                onPositionChangeRequest((duration * progress).toLong())\n            },\n        )\n        Row(\n            modifier = Modifier\n                .fillMaxWidth(),\n            verticalAlignment = Alignment.CenterVertically,\n        ) {\n            PlayPauseIconButton(\n                modifier = Modifier.padding(start = 4.dp),\n                playing = playing,\n                onPauseClick = onPauseClick,\n                onPlayClick = onPlayClick,\n            )\n            Spacer(modifier = Modifier.weight(1F))\n            Text(\n                modifier = Modifier.padding(end = 4.dp),\n                fontSize = 12.sp,\n                text = VideoDurationFormatter.formatVideoProgressDesc(playerPosition, duration),\n                color = Color.White,\n            )\n            val icon = if (mute) {\n                Icons.AutoMirrored.Filled.VolumeOff\n            } else {\n                Icons.AutoMirrored.Filled.VolumeUp\n            }\n            SimpleIconButton(\n                onClick = {\n                    if (mute) {\n                        onUnmuteClick()\n                    } else {\n                        onMuteClick()\n                    }\n                },\n                tint = Color.White,\n                imageVector = icon,\n                contentDescription = if (mute) \"unmute\" else \"mute\",\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun PlayPauseIconButton(\n    modifier: Modifier,\n    playing: Boolean,\n    onPauseClick: () -> Unit,\n    onPlayClick: () -> Unit,\n) {\n    IconButton(\n        modifier = modifier,\n        onClick = {\n            if (playing) {\n                onPauseClick()\n            } else {\n                onPlayClick()\n            }\n        },\n    ) {\n        val icon = if (playing) {\n            Icons.Default.Pause\n        } else {\n            Icons.Default.PlayArrow\n        }\n        Icon(\n            painter = rememberVectorPainter(icon),\n            contentDescription = if (playing) \"pause\" else \"play\",\n            tint = Color.White,\n        )\n    }\n}\n\n@Composable\nprivate fun PlayerProgress(\n    modifier: Modifier,\n    progress: Float,\n    onProgressChange: (progress: Float) -> Unit,\n) {\n    var progressInChanging by remember {\n        mutableFloatStateOf(progress)\n    }\n    var sliding by remember {\n        mutableStateOf(false)\n    }\n    val displayProgress = if (sliding) {\n        progressInChanging\n    } else {\n        if (progress.isNaN()) {\n            0F\n        } else {\n            progress\n        }\n    }\n    Slider(\n        modifier = modifier,\n        value = displayProgress.coerceAtLeast(0F).coerceAtMost(1F),\n        onValueChange = {\n            progressInChanging = it\n            sliding = true\n        },\n        onValueChangeFinished = {\n            onProgressChange(progressInChanging)\n            sliding = false\n        },\n        colors = SliderDefaults.colors(\n            thumbColor = Color.White,\n            activeTrackColor = Color.White,\n            activeTickColor = Color.White,\n        )\n    )\n}\n"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/org/burnoutcrew/reorderable/DetectReorder.kt",
    "content": "/*\n * Copyright 2022 André Claßen\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage org.burnoutcrew.reorderable\n\nimport androidx.compose.foundation.gestures.awaitFirstDown\nimport androidx.compose.foundation.gestures.forEachGesture\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.input.pointer.PointerInputChange\nimport androidx.compose.ui.input.pointer.pointerInput\n\nfun Modifier.detectReorder(state: ReorderableState<*>) =\n    this.then(\n        Modifier.pointerInput(Unit) {\n            forEachGesture {\n                awaitPointerEventScope {\n                    val down = awaitFirstDown(requireUnconsumed = false)\n                    var drag: PointerInputChange?\n                    var overSlop = Offset.Zero\n                    do {\n                        drag = awaitPointerSlopOrCancellation(down.id, down.type) { change, over ->\n                            change.consume()\n                            overSlop = over\n                        }\n                    } while (drag != null && !drag.isConsumed)\n                    if (drag != null) {\n                        state.interactions.trySend(StartDrag(down.id, overSlop))\n                    }\n                }\n            }\n        }\n    )\n\n\nfun Modifier.detectReorderAfterLongPress(state: ReorderableState<*>) =\n    this.then(\n        Modifier.pointerInput(Unit) {\n            forEachGesture {\n                val down = awaitPointerEventScope {\n                    awaitFirstDown(requireUnconsumed = false)\n                }\n                awaitLongPressOrCancellation(down)?.also {\n                    state.interactions.trySend(StartDrag(down.id))\n                }\n            }\n        }\n    )"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/org/burnoutcrew/reorderable/DragCancelledAnimation.kt",
    "content": "/*\n * Copyright 2022 André Claßen\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage org.burnoutcrew.reorderable\n\nimport androidx.compose.animation.core.Animatable\nimport androidx.compose.animation.core.Spring\nimport androidx.compose.animation.core.VectorConverter\nimport androidx.compose.animation.core.VisibilityThreshold\nimport androidx.compose.animation.core.spring\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.geometry.Offset\n\ninterface DragCancelledAnimation {\n    suspend fun dragCancelled(position: ItemPosition, offset: Offset)\n    val position: ItemPosition?\n    val offset: Offset\n}\n\nclass NoDragCancelledAnimation : DragCancelledAnimation {\n    override suspend fun dragCancelled(position: ItemPosition, offset: Offset) {}\n    override val position: ItemPosition? = null\n    override val offset: Offset = Offset.Zero\n}\n\nclass SpringDragCancelledAnimation(private val stiffness: Float = Spring.StiffnessMediumLow) : DragCancelledAnimation {\n    private val animatable = Animatable(Offset.Zero, Offset.VectorConverter)\n    override val offset: Offset\n        get() = animatable.value\n\n    override var position by mutableStateOf<ItemPosition?>(null)\n        private set\n\n    override suspend fun dragCancelled(position: ItemPosition, offset: Offset) {\n        this.position = position\n        animatable.snapTo(offset)\n        animatable.animateTo(\n            Offset.Zero,\n            spring(stiffness = stiffness, visibilityThreshold = Offset.VisibilityThreshold)\n        )\n        this.position = null\n    }\n}"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/org/burnoutcrew/reorderable/DragGesture.kt",
    "content": "/*\n * Copyright 2020 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage org.burnoutcrew.reorderable\n\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.input.pointer.AwaitPointerEventScope\nimport androidx.compose.ui.input.pointer.PointerEvent\nimport androidx.compose.ui.input.pointer.PointerEventPass\nimport androidx.compose.ui.input.pointer.PointerId\nimport androidx.compose.ui.input.pointer.PointerInputChange\nimport androidx.compose.ui.input.pointer.PointerInputScope\nimport androidx.compose.ui.input.pointer.PointerType\nimport androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed\nimport androidx.compose.ui.input.pointer.isOutOfBounds\nimport androidx.compose.ui.input.pointer.positionChange\nimport androidx.compose.ui.platform.ViewConfiguration\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.util.fastAll\nimport androidx.compose.ui.util.fastAny\nimport androidx.compose.ui.util.fastFirstOrNull\nimport kotlinx.coroutines.TimeoutCancellationException\nimport kotlinx.coroutines.withTimeout\n\n// Copied from DragGestureDetector , as long the pointer api isn`t ready.\n\ninternal suspend fun AwaitPointerEventScope.awaitPointerSlopOrCancellation(\n    pointerId: PointerId,\n    pointerType: PointerType,\n    onPointerSlopReached: (change: PointerInputChange, overSlop: Offset) -> Unit\n): PointerInputChange? {\n    if (currentEvent.isPointerUp(pointerId)) {\n        return null // The pointer has already been lifted, so the gesture is canceled\n    }\n    var offset = Offset.Zero\n    val touchSlop = viewConfiguration.pointerSlop(pointerType)\n\n    var pointer = pointerId\n\n    while (true) {\n        val event = awaitPointerEvent()\n        val dragEvent = event.changes.fastFirstOrNull { it.id == pointer } ?: return null\n        if (dragEvent.isConsumed) {\n            return null\n        } else if (dragEvent.changedToUpIgnoreConsumed()) {\n            val otherDown = event.changes.fastFirstOrNull { it.pressed }\n            if (otherDown == null) {\n                // This is the last \"up\"\n                return null\n            } else {\n                pointer = otherDown.id\n            }\n        } else {\n            offset += dragEvent.positionChange()\n            val distance = offset.getDistance()\n            var acceptedDrag = false\n            if (distance >= touchSlop) {\n                val touchSlopOffset = offset / distance * touchSlop\n                onPointerSlopReached(dragEvent, offset - touchSlopOffset)\n                if (dragEvent.isConsumed) {\n                    acceptedDrag = true\n                } else {\n                    offset = Offset.Zero\n                }\n            }\n\n            if (acceptedDrag) {\n                return dragEvent\n            } else {\n                awaitPointerEvent(PointerEventPass.Final)\n                if (dragEvent.isConsumed) {\n                    return null\n                }\n            }\n        }\n    }\n}\n\ninternal suspend fun PointerInputScope.awaitLongPressOrCancellation(\n    initialDown: PointerInputChange\n): PointerInputChange? {\n    var longPress: PointerInputChange? = null\n    var currentDown = initialDown\n    val longPressTimeout = viewConfiguration.longPressTimeoutMillis\n    return try {\n        // wait for first tap up or long press\n        withTimeout(longPressTimeout) {\n            awaitPointerEventScope {\n                var finished = false\n                while (!finished) {\n                    val event = awaitPointerEvent(PointerEventPass.Main)\n                    if (event.changes.fastAll { it.changedToUpIgnoreConsumed() }) {\n                        // All pointers are up\n                        finished = true\n                    }\n\n                    if (\n                        event.changes.fastAny {\n                            it.isConsumed || it.isOutOfBounds(size, extendedTouchPadding)\n                        }\n                    ) {\n                        finished = true // Canceled\n                    }\n\n                    // Check for cancel by position consumption. We can look on the Final pass of\n                    // the existing pointer event because it comes after the Main pass we checked\n                    // above.\n                    val consumeCheck = awaitPointerEvent(PointerEventPass.Final)\n                    if (consumeCheck.changes.fastAny { it.isConsumed }) {\n                        finished = true\n                    }\n                    if (!event.isPointerUp(currentDown.id)) {\n                        longPress = event.changes.fastFirstOrNull { it.id == currentDown.id }\n                    } else {\n                        val newPressed = event.changes.fastFirstOrNull { it.pressed }\n                        if (newPressed != null) {\n                            currentDown = newPressed\n                            longPress = currentDown\n                        } else {\n                            // should technically never happen as we checked it above\n                            finished = true\n                        }\n                    }\n                }\n            }\n        }\n        null\n    } catch (_: TimeoutCancellationException) {\n        longPress ?: initialDown\n    }\n}\n\nprivate fun PointerEvent.isPointerUp(pointerId: PointerId): Boolean =\n    changes.fastFirstOrNull { it.id == pointerId }?.pressed != true\n\n// This value was determined using experiments and common sense.\n// We can't use zero slop, because some hypothetical desktop/mobile devices can send\n// pointer events with a very high precision (but I haven't encountered any that send\n// events with less than 1px precision)\nprivate val mouseSlop = 0.125.dp\nprivate val defaultTouchSlop = 18.dp // The default touch slop on Android devices\nprivate val mouseToTouchSlopRatio = mouseSlop / defaultTouchSlop\n\n// TODO(demin): consider this as part of ViewConfiguration class after we make *PointerSlop*\n//  functions public (see the comment at the top of the file).\n//  After it will be a public API, we should get rid of `touchSlop / 144` and return absolute\n//  value 0.125.dp.toPx(). It is not possible right now, because we can't access density.\nprivate fun ViewConfiguration.pointerSlop(pointerType: PointerType): Float {\n    return when (pointerType) {\n        PointerType.Mouse -> touchSlop * mouseToTouchSlopRatio\n        else -> touchSlop\n    }\n}"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/org/burnoutcrew/reorderable/ItemPosition.kt",
    "content": "package org.burnoutcrew.reorderable\n\ndata class ItemPosition(val index: Int, val key: Any?)"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/org/burnoutcrew/reorderable/Reorderable.kt",
    "content": "/*\n * Copyright 2022 André Claßen\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage org.burnoutcrew.reorderable\n\nimport androidx.compose.foundation.gestures.drag\nimport androidx.compose.foundation.gestures.forEachGesture\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.input.pointer.PointerId\nimport androidx.compose.ui.input.pointer.PointerInputChange\nimport androidx.compose.ui.input.pointer.PointerInputScope\nimport androidx.compose.ui.input.pointer.changedToUp\nimport androidx.compose.ui.input.pointer.pointerInput\nimport androidx.compose.ui.input.pointer.positionChange\nimport androidx.compose.ui.util.fastFirstOrNull\n\nfun Modifier.reorderable(\n    state: ReorderableState<*>\n) = then(\n    Modifier.pointerInput(Unit) {\n        forEachGesture {\n            val dragStart = state.interactions.receive()\n            val down = awaitPointerEventScope {\n                currentEvent.changes.fastFirstOrNull { it.id == dragStart.id }\n            }\n            if (down != null && state.onDragStart(down.position.x.toInt(), down.position.y.toInt())) {\n                dragStart.offset?.apply {\n                    state.onDrag(x.toInt(), y.toInt())\n                }\n                detectDrag(\n                    down.id,\n                    onDragEnd = {\n                        state.onDragCanceled()\n                    },\n                    onDragCancel = {\n                        state.onDragCanceled()\n                    },\n                    onDrag = { change, dragAmount ->\n                        change.consume()\n                        state.onDrag(dragAmount.x.toInt(), dragAmount.y.toInt())\n                    })\n            }\n        }\n    })\n\ninternal suspend fun PointerInputScope.detectDrag(\n    down: PointerId,\n    onDragEnd: () -> Unit = { },\n    onDragCancel: () -> Unit = { },\n    onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit,\n) {\n    awaitPointerEventScope {\n        if (\n            drag(down) {\n                onDrag(it, it.positionChange())\n                it.consume()\n            }\n        ) {\n            // consume up if we quit drag gracefully with the up\n            currentEvent.changes.forEach {\n                if (it.changedToUp()) it.consume()\n            }\n            onDragEnd()\n        } else {\n            onDragCancel()\n        }\n    }\n}\n\ninternal data class StartDrag(val id: PointerId, val offset: Offset? = null)"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/org/burnoutcrew/reorderable/ReorderableItem.kt",
    "content": "/*\n * Copyright 2022 André Claßen\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage org.burnoutcrew.reorderable\n\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.BoxScope\nimport androidx.compose.foundation.lazy.LazyItemScope\nimport androidx.compose.foundation.lazy.grid.LazyGridItemScope\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.graphicsLayer\nimport androidx.compose.ui.zIndex\n\n@Composable\nfun LazyItemScope.ReorderableItem(\n    reorderableState: ReorderableState<*>,\n    key: Any?,\n    modifier: Modifier = Modifier,\n    index: Int? = null,\n    orientationLocked: Boolean = true,\n    content: @Composable BoxScope.(isDragging: Boolean) -> Unit\n) = ReorderableItem(\n    reorderableState,\n    key,\n    modifier,\n    Modifier.animateItem(),\n    orientationLocked,\n    index,\n    content\n)\n\n@Composable\nfun LazyGridItemScope.ReorderableItem(\n    reorderableState: ReorderableState<*>,\n    key: Any?,\n    modifier: Modifier = Modifier,\n    index: Int? = null,\n    content: @Composable BoxScope.(isDragging: Boolean) -> Unit\n) = ReorderableItem(reorderableState, key, modifier, Modifier.animateItem(), false, index, content)\n\n@Composable\nfun ReorderableItem(\n    state: ReorderableState<*>,\n    key: Any?,\n    modifier: Modifier = Modifier,\n    defaultDraggingModifier: Modifier = Modifier,\n    orientationLocked: Boolean = true,\n    index: Int? = null,\n    content: @Composable BoxScope.(isDragging: Boolean) -> Unit\n) {\n    val isDragging = if (index != null) {\n        index == state.draggingItemIndex\n    } else {\n        key == state.draggingItemKey\n    }\n    val draggingModifier =\n        if (isDragging) {\n            Modifier\n                .zIndex(1f)\n                .graphicsLayer {\n                    translationX =\n                        if (!orientationLocked || !state.isVerticalScroll) state.draggingItemLeft else 0f\n                    translationY =\n                        if (!orientationLocked || state.isVerticalScroll) state.draggingItemTop else 0f\n                }\n        } else {\n            val cancel = if (index != null) {\n                index == state.dragCancelledAnimation.position?.index\n            } else {\n                key == state.dragCancelledAnimation.position?.key\n            }\n            if (cancel) {\n                Modifier.zIndex(1f)\n                    .graphicsLayer {\n                        translationX =\n                            if (!orientationLocked || !state.isVerticalScroll) state.dragCancelledAnimation.offset.x else 0f\n                        translationY =\n                            if (!orientationLocked || state.isVerticalScroll) state.dragCancelledAnimation.offset.y else 0f\n                    }\n            } else {\n                defaultDraggingModifier\n            }\n        }\n    Box(modifier = modifier.then(draggingModifier)) {\n        content(isDragging)\n    }\n}"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/org/burnoutcrew/reorderable/ReorderableLazyGridState.kt",
    "content": "/*\n * Copyright 2022 André Claßen\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage org.burnoutcrew.reorderable\n\nimport androidx.compose.foundation.gestures.Orientation\nimport androidx.compose.foundation.gestures.scrollBy\nimport androidx.compose.foundation.lazy.grid.LazyGridItemInfo\nimport androidx.compose.foundation.lazy.grid.LazyGridState\nimport androidx.compose.foundation.lazy.grid.rememberLazyGridState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport kotlinx.coroutines.CoroutineScope\n\n@Composable\nfun rememberReorderableLazyGridState(\n    onMove: (ItemPosition, ItemPosition) -> Unit,\n    gridState: LazyGridState = rememberLazyGridState(),\n    canDragOver: ((draggedOver: ItemPosition, dragging: ItemPosition) -> Boolean)? = null,\n    onDragEnd: ((startIndex: Int, endIndex: Int) -> (Unit))? = null,\n    maxScrollPerFrame: Dp = 20.dp,\n    dragCancelledAnimation: DragCancelledAnimation = SpringDragCancelledAnimation()\n): ReorderableLazyGridState {\n    val maxScroll = with(LocalDensity.current) { maxScrollPerFrame.toPx() }\n    val scope = rememberCoroutineScope()\n    val state = remember(gridState) {\n        ReorderableLazyGridState(gridState, scope, maxScroll, onMove, canDragOver, onDragEnd, dragCancelledAnimation)\n    }\n    LaunchedEffect(state) {\n        state.visibleItemsChanged()\n            .collect { state.onDrag(0, 0) }\n    }\n\n    LaunchedEffect(state) {\n        while (true) {\n            val diff = state.scrollChannel.receive()\n            gridState.scrollBy(diff)\n        }\n    }\n    return state\n}\n\nclass ReorderableLazyGridState(\n    val gridState: LazyGridState,\n    scope: CoroutineScope,\n    maxScrollPerFrame: Float,\n    onMove: (fromIndex: ItemPosition, toIndex: ItemPosition) -> (Unit),\n    canDragOver: ((draggedOver: ItemPosition, dragging: ItemPosition) -> Boolean)? = null,\n    onDragEnd: ((startIndex: Int, endIndex: Int) -> (Unit))? = null,\n    dragCancelledAnimation: DragCancelledAnimation = SpringDragCancelledAnimation()\n) : ReorderableState<LazyGridItemInfo>(scope, maxScrollPerFrame, onMove, canDragOver, onDragEnd, dragCancelledAnimation) {\n    override val isVerticalScroll: Boolean\n        get() = gridState.layoutInfo.orientation == Orientation.Vertical\n    override val LazyGridItemInfo.left: Int\n        get() = offset.x\n    override val LazyGridItemInfo.right: Int\n        get() = offset.x + size.width\n    override val LazyGridItemInfo.top: Int\n        get() = offset.y\n    override val LazyGridItemInfo.bottom: Int\n        get() = offset.y + size.height\n    override val LazyGridItemInfo.width: Int\n        get() = size.width\n    override val LazyGridItemInfo.height: Int\n        get() = size.height\n    override val LazyGridItemInfo.itemIndex: Int\n        get() = index\n    override val LazyGridItemInfo.itemKey: Any\n        get() = key\n    override val visibleItemsInfo: List<LazyGridItemInfo>\n        get() = gridState.layoutInfo.visibleItemsInfo\n    override val viewportStartOffset: Int\n        get() = gridState.layoutInfo.viewportStartOffset\n    override val viewportEndOffset: Int\n        get() = gridState.layoutInfo.viewportEndOffset\n    override val firstVisibleItemIndex: Int\n        get() = gridState.firstVisibleItemIndex\n    override val firstVisibleItemScrollOffset: Int\n        get() = gridState.firstVisibleItemScrollOffset\n\n    override suspend fun scrollToItem(index: Int, offset: Int) {\n        gridState.scrollToItem(index, offset)\n    }\n}"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/org/burnoutcrew/reorderable/ReorderableLazyListState.kt",
    "content": "/*\n * Copyright 2022 André Claßen\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage org.burnoutcrew.reorderable\n\nimport androidx.compose.foundation.gestures.Orientation\nimport androidx.compose.foundation.gestures.ScrollableState\nimport androidx.compose.foundation.gestures.scrollBy\nimport androidx.compose.foundation.lazy.LazyListItemInfo\nimport androidx.compose.foundation.lazy.LazyListState\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.platform.LocalLayoutDirection\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.LayoutDirection\nimport androidx.compose.ui.unit.dp\nimport kotlinx.coroutines.CoroutineScope\n\n\n@Composable\nfun rememberReorderableLazyListState(\n    onMove: (ItemPosition, ItemPosition) -> Unit,\n    listState: LazyListState = rememberLazyListState(),\n    canDragOver: ((draggedOver: ItemPosition, dragging: ItemPosition) -> Boolean)? = null,\n    onDragEnd: ((startIndex: Int, endIndex: Int) -> (Unit))? = null,\n    maxScrollPerFrame: Dp = 20.dp,\n    dragCancelledAnimation: DragCancelledAnimation = SpringDragCancelledAnimation()\n): ReorderableLazyListState {\n    val maxScroll = with(LocalDensity.current) { maxScrollPerFrame.toPx() }\n    val scope = rememberCoroutineScope()\n    val state = remember(listState) {\n        ReorderableLazyListState(listState, scope, maxScroll, onMove, canDragOver, onDragEnd, dragCancelledAnimation)\n    }\n    val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl\n    LaunchedEffect(state) {\n        state.visibleItemsChanged()\n            .collect { state.onDrag(0, 0) }\n    }\n\n    LaunchedEffect(state) {\n        var reverseDirection = !listState.layoutInfo.reverseLayout\n        if (isRtl && listState.layoutInfo.orientation != Orientation.Vertical) {\n            reverseDirection = !reverseDirection\n        }\n        val direction = if (reverseDirection) 1f else -1f\n        while (true) {\n            val diff = state.scrollChannel.receive()\n            listState.scrollBy(diff * direction)\n        }\n    }\n    return state\n}\n\nclass ReorderableLazyListState(\n    val listState: LazyListState,\n    scope: CoroutineScope,\n    maxScrollPerFrame: Float,\n    onMove: (fromIndex: ItemPosition, toIndex: ItemPosition) -> (Unit),\n    canDragOver: ((draggedOver: ItemPosition, dragging: ItemPosition) -> Boolean)? = null,\n    onDragEnd: ((startIndex: Int, endIndex: Int) -> (Unit))? = null,\n    dragCancelledAnimation: DragCancelledAnimation = SpringDragCancelledAnimation()\n) : ReorderableState<LazyListItemInfo>(\n    scope,\n    maxScrollPerFrame,\n    onMove,\n    canDragOver,\n    onDragEnd,\n    dragCancelledAnimation\n) {\n    override val isVerticalScroll: Boolean\n        get() = listState.layoutInfo.orientation == Orientation.Vertical\n    override val LazyListItemInfo.left: Int\n        get() = when {\n            isVerticalScroll -> 0\n            listState.layoutInfo.reverseLayout -> listState.layoutInfo.viewportSize.width - offset - size\n            else -> offset\n        }\n    override val LazyListItemInfo.top: Int\n        get() = when {\n            !isVerticalScroll -> 0\n            listState.layoutInfo.reverseLayout -> listState.layoutInfo.viewportSize.height - offset - size\n            else -> offset\n        }\n    override val LazyListItemInfo.right: Int\n        get() = when {\n            isVerticalScroll -> 0\n            listState.layoutInfo.reverseLayout -> listState.layoutInfo.viewportSize.width - offset\n            else -> offset + size\n        }\n    override val LazyListItemInfo.bottom: Int\n        get() = when {\n            !isVerticalScroll -> 0\n            listState.layoutInfo.reverseLayout -> listState.layoutInfo.viewportSize.height - offset\n            else -> offset + size\n        }\n    override val LazyListItemInfo.width: Int\n        get() = if (isVerticalScroll) 0 else size\n    override val LazyListItemInfo.height: Int\n        get() = if (isVerticalScroll) size else 0\n    override val LazyListItemInfo.itemIndex: Int\n        get() = index\n    override val LazyListItemInfo.itemKey: Any\n        get() = key\n    override val visibleItemsInfo: List<LazyListItemInfo>\n        get() = listState.layoutInfo.visibleItemsInfo\n    override val viewportStartOffset: Int\n        get() = listState.layoutInfo.viewportStartOffset\n    override val viewportEndOffset: Int\n        get() = listState.layoutInfo.viewportEndOffset\n    override val firstVisibleItemIndex: Int\n        get() = listState.firstVisibleItemIndex\n    override val firstVisibleItemScrollOffset: Int\n        get() = listState.firstVisibleItemScrollOffset\n\n    override suspend fun scrollToItem(index: Int, offset: Int) {\n        listState.scrollToItem(index, offset)\n    }\n\n    override fun onDragStart(offsetX: Int, offsetY: Int): Boolean =\n        if (isVerticalScroll) {\n            super.onDragStart(0, offsetY)\n        } else {\n            super.onDragStart(offsetX, 0)\n        }\n\n    override fun findTargets(x: Int, y: Int, selected: LazyListItemInfo) =\n        if (isVerticalScroll) {\n            super.findTargets(0, y, selected)\n        } else {\n            super.findTargets(x, 0, selected)\n        }\n\n    override fun chooseDropItem(\n        draggedItemInfo: LazyListItemInfo?,\n        items: List<LazyListItemInfo>,\n        curX: Int,\n        curY: Int\n    ) =\n        if (isVerticalScroll) {\n            super.chooseDropItem(draggedItemInfo, items, 0, curY)\n        } else {\n            super.chooseDropItem(draggedItemInfo, items, curX, 0)\n        }\n}"
  },
  {
    "path": "commonbiz/status-ui/src/commonMain/kotlin/org/burnoutcrew/reorderable/ReorderableState.kt",
    "content": "/*\n * Copyright 2022 André Claßen\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage org.burnoutcrew.reorderable\n\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport androidx.compose.runtime.snapshotFlow\nimport androidx.compose.runtime.withFrameMillis\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.util.fastForEach\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.ExperimentalCoroutinesApi\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.channels.Channel\nimport kotlinx.coroutines.flow.distinctUntilChanged\nimport kotlinx.coroutines.flow.filterNotNull\nimport kotlinx.coroutines.flow.flatMapLatest\nimport kotlinx.coroutines.flow.flowOf\nimport kotlinx.coroutines.launch\nimport kotlin.math.absoluteValue\nimport kotlin.math.min\nimport kotlin.math.sign\n\n\nabstract class ReorderableState<T>(\n    private val scope: CoroutineScope,\n    private val maxScrollPerFrame: Float,\n    private val onMove: (fromIndex: ItemPosition, toIndex: ItemPosition) -> (Unit),\n    private val canDragOver: ((draggedOver: ItemPosition, dragging: ItemPosition) -> Boolean)?,\n    private val onDragEnd: ((startIndex: Int, endIndex: Int) -> (Unit))?,\n    val dragCancelledAnimation: DragCancelledAnimation\n) {\n    var draggingItemIndex by mutableStateOf<Int?>(null)\n        private set\n    val draggingItemKey: Any?\n        get() = selected?.itemKey\n    protected abstract val T.left: Int\n    protected abstract val T.top: Int\n    protected abstract val T.right: Int\n    protected abstract val T.bottom: Int\n    protected abstract val T.width: Int\n    protected abstract val T.height: Int\n    protected abstract val T.itemIndex: Int\n    protected abstract val T.itemKey: Any\n    protected abstract val visibleItemsInfo: List<T>\n    protected abstract val firstVisibleItemIndex: Int\n    protected abstract val firstVisibleItemScrollOffset: Int\n    protected abstract val viewportStartOffset: Int\n    protected abstract val viewportEndOffset: Int\n    internal val interactions = Channel<StartDrag>()\n    internal val scrollChannel = Channel<Float>()\n    val draggingItemLeft: Float\n        get() = draggingLayoutInfo?.let { item ->\n            (selected?.left ?: 0) + draggingDelta.x - item.left\n        } ?: 0f\n    val draggingItemTop: Float\n        get() = draggingLayoutInfo?.let { item ->\n            (selected?.top ?: 0) + draggingDelta.y - item.top\n        } ?: 0f\n    abstract val isVerticalScroll: Boolean\n    private val draggingLayoutInfo: T?\n        get() = visibleItemsInfo\n            .firstOrNull { it.itemIndex == draggingItemIndex }\n    private var draggingDelta by mutableStateOf(Offset.Zero)\n    private var selected by mutableStateOf<T?>(null)\n    private var autoscroller: Job? = null\n    private val targets = mutableListOf<T>()\n    private val distances = mutableListOf<Int>()\n\n    protected abstract suspend fun scrollToItem(index: Int, offset: Int)\n\n    @OptIn(ExperimentalCoroutinesApi::class)\n    internal fun visibleItemsChanged() =\n        snapshotFlow { draggingItemIndex != null }\n            .flatMapLatest { if (it) snapshotFlow { visibleItemsInfo } else flowOf(null) }\n            .filterNotNull()\n            .distinctUntilChanged { old, new -> old.firstOrNull()?.itemIndex == new.firstOrNull()?.itemIndex && old.count() == new.count() }\n\n    internal open fun onDragStart(offsetX: Int, offsetY: Int): Boolean {\n        val x: Int\n        val y: Int\n        if (isVerticalScroll) {\n            x = offsetX\n            y = offsetY + viewportStartOffset\n        } else {\n            x = offsetX + viewportStartOffset\n            y = offsetY\n        }\n        return visibleItemsInfo\n            .firstOrNull { x in it.left..it.right && y in it.top..it.bottom }\n            ?.also {\n                selected = it\n                draggingItemIndex = it.itemIndex\n            } != null\n    }\n\n    internal fun onDragCanceled() {\n        val dragIdx = draggingItemIndex\n        if (dragIdx != null) {\n            val position = ItemPosition(dragIdx, selected?.itemKey)\n            val offset = Offset(draggingItemLeft, draggingItemTop)\n            scope.launch {\n                dragCancelledAnimation.dragCancelled(position, offset)\n            }\n        }\n        val startIndex = selected?.itemIndex\n        val endIndex = draggingItemIndex\n        selected = null\n        draggingDelta = Offset.Zero\n        draggingItemIndex = null\n        cancelAutoScroll()\n        onDragEnd?.apply {\n            if (startIndex != null && endIndex != null) {\n                invoke(startIndex, endIndex)\n            }\n        }\n    }\n\n    internal fun onDrag(offsetX: Int, offsetY: Int) {\n        val selected = selected ?: return\n        draggingDelta = Offset(draggingDelta.x + offsetX, draggingDelta.y + offsetY)\n        val draggingItem = draggingLayoutInfo ?: return\n        val startOffset = draggingItem.top + draggingItemTop\n        val startOffsetX = draggingItem.left + draggingItemLeft\n        chooseDropItem(\n            draggingItem,\n            findTargets(draggingDelta.x.toInt(), draggingDelta.y.toInt(), selected),\n            startOffsetX.toInt(),\n            startOffset.toInt()\n        )?.also { targetItem ->\n            if (targetItem.itemIndex == firstVisibleItemIndex || draggingItem.itemIndex == firstVisibleItemIndex) {\n                scope.launch {\n                    onMove.invoke(\n                        ItemPosition(draggingItem.itemIndex, draggingItem.itemKey),\n                        ItemPosition(targetItem.itemIndex, targetItem.itemKey)\n                    )\n                    scrollToItem(firstVisibleItemIndex, firstVisibleItemScrollOffset)\n                }\n            } else {\n                onMove.invoke(\n                    ItemPosition(draggingItem.itemIndex, draggingItem.itemKey),\n                    ItemPosition(targetItem.itemIndex, targetItem.itemKey)\n                )\n            }\n            draggingItemIndex = targetItem.itemIndex\n        }\n\n        with(calcAutoScrollOffset(0, maxScrollPerFrame)) {\n            if (this != 0f) autoscroll(this)\n        }\n    }\n\n    private fun autoscroll(scrollOffset: Float) {\n        if (scrollOffset != 0f) {\n            if (autoscroller?.isActive == true) {\n                return\n            }\n            autoscroller = scope.launch {\n                var scroll = scrollOffset\n                var start = 0L\n                while (scroll != 0f && autoscroller?.isActive == true) {\n                    withFrameMillis {\n                        if (start == 0L) {\n                            start = it\n                        } else {\n                            scroll = calcAutoScrollOffset(it - start, maxScrollPerFrame)\n                        }\n                    }\n                    scrollChannel.trySend(scroll)\n                }\n            }\n        } else {\n            cancelAutoScroll()\n        }\n    }\n\n    private fun cancelAutoScroll() {\n        autoscroller?.cancel()\n        autoscroller = null\n    }\n\n    protected open fun findTargets(x: Int, y: Int, selected: T): List<T> {\n        targets.clear()\n        distances.clear()\n        val left = x + selected.left\n        val right = x + selected.right\n        val top = y + selected.top\n        val bottom = y + selected.bottom\n        val centerX = (left + right) / 2\n        val centerY = (top + bottom) / 2\n        visibleItemsInfo.fastForEach { item ->\n            if (\n                item.itemIndex == draggingItemIndex\n                || item.bottom < top\n                || item.top > bottom\n                || item.right < left\n                || item.left > right\n            ) {\n                return@fastForEach\n            }\n            if (canDragOver?.invoke(\n                    ItemPosition(item.itemIndex, item.itemKey),\n                    ItemPosition(selected.itemIndex, selected.itemKey)\n                ) != false\n            ) {\n                val dx = (centerX - (item.left + item.right) / 2).absoluteValue\n                val dy = (centerY - (item.top + item.bottom) / 2).absoluteValue\n                val dist = dx * dx + dy * dy\n                var pos = 0\n                for (j in targets.indices) {\n                    if (dist > distances[j]) {\n                        pos++\n                    } else {\n                        break\n                    }\n                }\n                targets.add(pos, item)\n                distances.add(pos, dist)\n            }\n        }\n        return targets\n    }\n\n    protected open fun chooseDropItem(draggedItemInfo: T?, items: List<T>, curX: Int, curY: Int): T? {\n        if (draggedItemInfo == null) {\n            return if (draggingItemIndex != null) items.lastOrNull() else null\n        }\n        var target: T? = null\n        var highScore = -1\n        val right = curX + draggedItemInfo.width\n        val bottom = curY + draggedItemInfo.height\n        val dx = curX - draggedItemInfo.left\n        val dy = curY - draggedItemInfo.top\n\n        items.fastForEach { item ->\n            if (dx > 0) {\n                val diff = item.right - right\n                if (diff < 0 && item.right > draggedItemInfo.right) {\n                    val score = diff.absoluteValue\n                    if (score > highScore) {\n                        highScore = score\n                        target = item\n                    }\n                }\n            }\n            if (dx < 0) {\n                val diff = item.left - curX\n                if (diff > 0 && item.left < draggedItemInfo.left) {\n                    val score = diff.absoluteValue\n                    if (score > highScore) {\n                        highScore = score\n                        target = item\n                    }\n                }\n            }\n            if (dy < 0) {\n                val diff = item.top - curY\n                if (diff > 0 && item.top < draggedItemInfo.top) {\n                    val score = diff.absoluteValue\n                    if (score > highScore) {\n                        highScore = score\n                        target = item\n                    }\n                }\n            }\n            if (dy > 0) {\n                val diff = item.bottom - bottom\n                if (diff < 0 && item.bottom > draggedItemInfo.bottom) {\n                    val score = diff.absoluteValue\n                    if (score > highScore) {\n                        highScore = score\n                        target = item\n                    }\n                }\n            }\n        }\n        return target\n    }\n\n    private fun calcAutoScrollOffset(time: Long, maxScroll: Float): Float {\n        val draggingItem = draggingLayoutInfo ?: return 0f\n        val startOffset: Float\n        val endOffset: Float\n        val delta: Float\n        if (isVerticalScroll) {\n            startOffset = draggingItem.top + draggingItemTop\n            endOffset = startOffset + draggingItem.height\n            delta = draggingDelta.y\n        } else {\n            startOffset = draggingItem.left + draggingItemLeft\n            endOffset = startOffset + draggingItem.width\n            delta = draggingDelta.x\n        }\n        return when {\n            delta > 0 ->\n                (endOffset - viewportEndOffset).coerceAtLeast(0f)\n            delta < 0 ->\n                (startOffset - viewportStartOffset).coerceAtMost(0f)\n            else -> 0f\n        }\n            .let { interpolateOutOfBoundsScroll((endOffset - startOffset).toInt(), it, time, maxScroll) }\n    }\n\n\n    companion object {\n        private const val ACCELERATION_LIMIT_TIME_MS: Long = 1500\n        private val EaseOutQuadInterpolator: (Float) -> (Float) = {\n            val t = 1 - it\n            1 - t * t * t * t\n        }\n        private val EaseInQuintInterpolator: (Float) -> (Float) = {\n            it * it * it * it * it\n        }\n\n        private fun interpolateOutOfBoundsScroll(\n            viewSize: Int,\n            viewSizeOutOfBounds: Float,\n            time: Long,\n            maxScroll: Float,\n        ): Float {\n            if (viewSizeOutOfBounds == 0f) return 0f\n            val outOfBoundsRatio = min(1f, 1f * viewSizeOutOfBounds.absoluteValue / viewSize)\n            val cappedScroll = sign(viewSizeOutOfBounds) * maxScroll * EaseOutQuadInterpolator(outOfBoundsRatio)\n            val timeRatio = if (time > ACCELERATION_LIMIT_TIME_MS) 1f else time.toFloat() / ACCELERATION_LIMIT_TIME_MS\n            return (cappedScroll * EaseInQuintInterpolator(timeRatio)).let {\n                if (it == 0f) {\n                    if (viewSizeOutOfBounds > 0) 1f else -1f\n                } else {\n                    it\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "commonbiz/status-ui/src/iosMain/kotlin/com/zhangke/fread/status/ui/utils/ScreenSize.ios.kt",
    "content": "package com.zhangke.fread.status.ui.utils\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.ReadOnlyComposable\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport kotlinx.cinterop.ExperimentalForeignApi\nimport kotlinx.cinterop.useContents\nimport platform.UIKit.UIScreen\n\n@OptIn(ExperimentalForeignApi::class)\n@ReadOnlyComposable\n@Composable\nactual fun getScreenWidth(): Dp {\n    return with(LocalDensity.current) {\n        // TODO: This is not the correct way to get the screen width\n        UIScreen.mainScreen().bounds().useContents {\n            size.width.toInt().dp\n        }\n    }\n}\n\n@OptIn(ExperimentalForeignApi::class)\n@ReadOnlyComposable\n@Composable\nactual fun getScreenHeight(): Dp {\n    return with(LocalDensity.current) {\n        // TODO: This is not the correct way to get the screen width\n        UIScreen.mainScreen().bounds().useContents {\n            size.height.toInt().dp\n        }\n    }\n}"
  },
  {
    "path": "commonbiz/status-ui/src/iosMain/kotlin/com/zhangke/fread/status/ui/video/BlogVideos.ios.kt",
    "content": "package com.zhangke.fread.status.ui.video\n\nimport androidx.compose.runtime.Composable\nimport com.zhangke.fread.status.blog.BlogMedia\nimport com.zhangke.fread.status.ui.image.OnBlogMediaClick\n\n@Composable\nactual fun BlogVideos(\n    mediaList: List<BlogMedia>,\n    hideContent: Boolean,\n    indexInList: Int,\n    onMediaClick: OnBlogMediaClick,\n) {\n    // TODO: Not implemented yet\n}"
  },
  {
    "path": "deleteuserdata.html",
    "content": "<p>This document will tell you how to delete personal data in Fread.</p>\n<p>1. Open the homepage of the app</p>\n<p>2. Switch the bottom TAB to Profile</p>\n<p>3. Click the more button of the account you logged in to</p>\n<p>4. Click Log out</p>\n<p>This way you can clear your login history.</p>\n<p>If you want to clear all data, you can open the system settings page, select Fread and clear all data.</p>\n<p>If you have any questions or suggestions about your personal data, do not hesitate to contact me at zhangkeport@gmail.com.</p>\n<p>We do not save any of your personal information on the server, and your login information is all on the local disk of your device.</p>"
  },
  {
    "path": "di-dependencis.md",
    "content": "# Kotlin-Inject Dependency Graphs\n\nNotes:\n- Sources: Kotlin files with `@Inject`, `@Provides`, or `@Component` annotations across all modules and source sets.\n- Arrow direction: dependency -> dependent (top to bottom).\n- `External:` nodes represent assisted inputs or values not bound in the same source set graph.\n\n## androidMain\n\nRoots (no dependencies):\n- Activity\n- ApplicationContext\n- RssParser\n\n```mermaid\nflowchart TB\n  Necfc2dffe5[\"Activity\"]\n  Nb642dc2e53[\"ActivityPubDatabases\"]\n  Nd6c6a6bb97[\"ActivityPubLoggedAccountDatabase\"]\n  Ne582c54210[\"ActivityPubStatusDatabases\"]\n  N4b0697a4f6[\"ActivityPubStatusReadStateDatabases\"]\n  N8d5906e226[\"ApplicationContext\"]\n  N9be163fa87[\"BlueskyLoggedAccountDatabase\"]\n  Nebba756115[\"ContentConfigDatabases\"]\n  Ne1d90545f7[\"External: ActivityPubClientManager\"]\n  Na5181cbba3[\"External: ActivityPubLoggedAccountRepo\"]\n  N1a7b1cc6f8[\"External: AndroidApplicationComponent\"]\n  N4394529ff6[\"External: AndroidSystemBrowserLauncher\"]\n  N0cfc9f656b[\"External: Application\"]\n  N5e8baaa3bb[\"External: ApplicationCoroutineScope\"]\n  Nc3ea95cd13[\"External: ComponentActivity\"]\n  N7d16a1d3d9[\"External: DayNightHelper\"]\n  Ne47295ccce[\"External: FeedsRepoModuleStartup\"]\n  Nc1a3aa7b35[\"External: FreadConfigManager\"]\n  N2edcc08008[\"External: LanguageHelper\"]\n  Ne5a922a653[\"External: LanguageModuleStartup\"]\n  N956e808f53[\"External: LocalConfigManager\"]\n  N3f61db1662[\"External: StorageHelper\"]\n  Nade7b46a17[\"External: TextHandler\"]\n  N0c2f18938e[\"FlowSettings\"]\n  Nbeb46cbb74[\"FreadContentDatabase\"]\n  Nbee06c41c2[\"ImageLoader\"]\n  N316a667bb3[\"MixedStatusDatabases\"]\n  Nded7da349d[\"ModuleStartup\"]\n  Na3291e7bfa[\"NotificationsDatabase\"]\n  Ne5814d179d[\"OldFreadContentDatabase\"]\n  N010a4e8c92[\"PushInfoDatabase\"]\n  N9671344cf0[\"PushInfoRepo\"]\n  N88f2708bac[\"RssDatabases\"]\n  N33e9b1425c[\"RssParser\"]\n  N18af4a5dbc[\"SelectedAccountPublishingDatabase\"]\n  N6bb8cf23c8[\"SystemBrowserLauncher\"]\n  Na773e1b613[\"com.zhangke.fread.activitypub.app.internal.push.ActivityPubPushManager\"]\n  Ncd90060920[\"com.zhangke.fread.activitypub.app.internal.push.notification.PushNotificationManager\"]\n  N1c6b36a398[\"com.zhangke.fread.common.browser.AndroidSystemBrowserLauncher\"]\n  Nad842ac95b[\"com.zhangke.fread.common.browser.BrowserBridgeDialogActivityComponent\"]\n  N227f594a4d[\"com.zhangke.fread.common.browser.OAuthHandler\"]\n  N05a4c3e9fe[\"com.zhangke.fread.common.daynight.ActivityDayNightHelper\"]\n  N005b37105d[\"com.zhangke.fread.common.handler.ActivityTextHandler\"]\n  N039314d0f5[\"com.zhangke.fread.common.handler.TextHandler\"]\n  N13715080e6[\"com.zhangke.fread.common.language.ActivityLanguageHelper\"]\n  N53c4ecbc13[\"com.zhangke.fread.common.language.LanguageHelper\"]\n  Na893363c04[\"com.zhangke.fread.common.startup.LanguageModuleStartup\"]\n  N399ea0e365[\"com.zhangke.fread.common.utils.MediaFileHelper\"]\n  N7e131d7774[\"com.zhangke.fread.common.utils.PlatformUriHelper\"]\n  N82c109ec61[\"com.zhangke.fread.common.utils.StorageHelper\"]\n  N21e10f71ca[\"com.zhangke.fread.common.utils.ThumbnailHelper\"]\n  Nef22c2fc33[\"com.zhangke.fread.common.utils.ToastHelper\"]\n  N6d0399996e[\"com.zhangke.fread.di.AndroidActivityComponent\"]\n  N7a66505a85[\"com.zhangke.fread.di.AndroidApplicationComponent\"]\n  N1e131c0677[\"com.zhangke.fread.utils.ActivityHelper\"]\n  Necfc2dffe5 --> N1e131c0677\n  N3f61db1662 --> Nbee06c41c2\n  N0cfc9f656b --> N7a66505a85\n  N1a7b1cc6f8 --> N6d0399996e\n  Nc3ea95cd13 --> N6d0399996e\n  Nc1a3aa7b35 --> Na773e1b613\n  Ne1d90545f7 --> Na773e1b613\n  N9671344cf0 --> Na773e1b613\n  Na5181cbba3 --> Ncd90060920\n  N8d5906e226 --> Nb642dc2e53\n  N8d5906e226 --> Nd6c6a6bb97\n  N8d5906e226 --> Ne582c54210\n  N8d5906e226 --> N4b0697a4f6\n  N8d5906e226 --> N010a4e8c92\n  N010a4e8c92 --> N9671344cf0\n  N8d5906e226 --> Na3291e7bfa\n  N8d5906e226 --> N9be163fa87\n  N4394529ff6 --> N6bb8cf23c8\n  N4394529ff6 --> N6bb8cf23c8\n  N8d5906e226 --> N039314d0f5\n  Nade7b46a17 --> N005b37105d\n  Necfc2dffe5 --> N005b37105d\n  N2edcc08008 --> Na893363c04\n  N8d5906e226 --> N0c2f18938e\n  N8d5906e226 --> Nebba756115\n  N8d5906e226 --> Nbeb46cbb74\n  N8d5906e226 --> Ne5814d179d\n  N8d5906e226 --> N316a667bb3\n  Ne47295ccce --> Nded7da349d\n  Ne5a922a653 --> Nded7da349d\n  Nc3ea95cd13 --> Nad842ac95b\n  N8d5906e226 --> N227f594a4d\n  Necfc2dffe5 --> N1c6b36a398\n  N7d16a1d3d9 --> N05a4c3e9fe\n  Nc3ea95cd13 --> N05a4c3e9fe\n  N956e808f53 --> N53c4ecbc13\n  N0cfc9f656b --> N53c4ecbc13\n  N2edcc08008 --> N13715080e6\n  Nc3ea95cd13 --> N13715080e6\n  N8d5906e226 --> N21e10f71ca\n  N8d5906e226 --> N399ea0e365\n  N5e8baaa3bb --> N399ea0e365\n  N8d5906e226 --> N7e131d7774\n  N8d5906e226 --> N82c109ec61\n  Necfc2dffe5 --> Nef22c2fc33\n  N8d5906e226 --> N88f2708bac\n  N8d5906e226 --> N18af4a5dbc\n```\n\n## commonMain\n\nRoots (no dependencies):\n- ApplicationCoroutineScope\n- IFeedsScreenVisitor\n- IProfileScreenVisitor\n- com.zhangke.fread.activitypub.app.ActivityPubContentManager\n- com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubApplicationEntityAdapter\n- com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubBlogMetaAdapter\n- com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubContentAdapter\n- com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubCustomEmojiEntityAdapter\n- com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubPollAdapter\n- com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubTagAdapter\n- com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubTranslationEntityAdapter\n- com.zhangke.fread.activitypub.app.internal.adapter.PostStatusAttachmentAdapter\n- com.zhangke.fread.activitypub.app.internal.adapter.RegisterApplicationEntryAdapter\n- com.zhangke.fread.activitypub.app.internal.auth.LoggedAccountProvider\n- com.zhangke.fread.activitypub.app.internal.repo.platform.MastodonInstanceRepo\n- com.zhangke.fread.activitypub.app.internal.screen.status.post.adapter.CustomEmojiAdapter\n- com.zhangke.fread.activitypub.app.internal.uri.PlatformUriTransformer\n- com.zhangke.fread.activitypub.app.internal.uri.UserUriTransformer\n- com.zhangke.fread.activitypub.app.internal.usecase.emoji.MapCustomEmojiUseCase\n- com.zhangke.fread.bluesky.internal.adapter.BlueskyProfileAdapter\n- com.zhangke.fread.bluesky.internal.content.BlueskyContentManager\n- com.zhangke.fread.bluesky.internal.uri.platform.PlatformUriTransformer\n- com.zhangke.fread.bluesky.internal.uri.user.UserUriTransformer\n- com.zhangke.fread.bluesky.internal.usecase.GetAtIdentifierUseCase\n- com.zhangke.fread.bluesky.internal.usecase.UpdateProfileRecordUseCase\n- com.zhangke.fread.common.adapter.StatusUiStateAdapter\n- com.zhangke.fread.common.bubble.BubbleManager\n- com.zhangke.fread.common.deeplink.SelectedContentSwitcher\n- com.zhangke.fread.common.onboarding.OnboardingComponent\n- com.zhangke.fread.common.publish.PublishPostManager\n- com.zhangke.fread.common.startup.FeedsRepoModuleStartup\n- com.zhangke.fread.common.status.StatusIdGenerator\n- com.zhangke.fread.common.status.StatusUpdater\n- com.zhangke.fread.common.status.adapter.ContentConfigAdapter\n- com.zhangke.fread.common.status.usecase.FormatStatusDisplayTimeUseCase\n- com.zhangke.fread.commonbiz.shared.usecase.RefactorToNewBlogUseCase\n- com.zhangke.fread.explore.screens.search.SearchViewModel\n- com.zhangke.fread.explore.usecase.BuildSearchResultUiStateUseCase\n- com.zhangke.fread.feeds.FeedsScreenVisitor\n- com.zhangke.fread.rss.RssAccountManager\n- com.zhangke.fread.rss.RssContentManager\n- com.zhangke.fread.rss.RssNotificationResolver\n- com.zhangke.fread.rss.RssPlatformResolver\n- com.zhangke.fread.rss.RssPublishManager\n- com.zhangke.fread.rss.internal.platform.RssPlatformTransformer\n- com.zhangke.fread.rss.internal.source.RssSourceTransformer\n- com.zhangke.fread.rss.internal.uri.RssUriTransformer\n- com.zhangke.fread.rss.internal.webfinger.RssSourceWebFingerTransformer\n\n```mermaid\nflowchart TB\n  N028b64f105[\"ApplicationCoroutineScope\"]\n  N2a8e209e43[\"BrowserInterceptor\"]\n  Nc7b67d9d2b[\"External: () -> AboutViewModel\"]\n  N8e5132e28c[\"External: () -> ActivityPubContentViewModel\"]\n  Nbd7f0dd8e9[\"External: () -> ActivityPubTimelineContainerViewModel\"]\n  N539851c83f[\"External: () -> BlueskyHomeContainerViewModel\"]\n  N93812a316c[\"External: () -> ContentHomeViewModel\"]\n  N7d66c5f8f4[\"External: () -> ExplorerContainerViewModel\"]\n  Ne12ccda2a8[\"External: () -> ExplorerHomeViewModel\"]\n  Nf04a3d7220[\"External: () -> HashtagTimelineContainerViewModel\"]\n  Ne9cd17762c[\"External: () -> HomeFeedsContainerViewModel\"]\n  Nca6fb5baac[\"External: () -> ImportFeedsViewModel\"]\n  Nbc470bf63e[\"External: () -> MainDrawerViewModel\"]\n  Na83d469aab[\"External: () -> MainViewModel\"]\n  N8ad85967a5[\"External: () -> MixedContentViewModel\"]\n  N47bc9f8990[\"External: () -> NotificationContainerViewModel\"]\n  Ncd1624647b[\"External: () -> NotificationsHomeViewModel\"]\n  Nc3119e5bf1[\"External: () -> ProfileHomeViewModel\"]\n  N3151de3431[\"External: () -> RssBlogDetailViewModel\"]\n  Nadad2fb90b[\"External: () -> SearchBarViewModel\"]\n  Nbf2f4aef51[\"External: () -> SearchSourceForAddViewModel\"]\n  Nbb771e20d1[\"External: () -> SearchViewModel\"]\n  Ne01b9b9f2a[\"External: () -> SelectContentTypeViewModel\"]\n  N45ba9694f3[\"External: () -> SelectPlatformViewModel\"]\n  Nbd1b619127[\"External: () -> ServerAboutViewModel\"]\n  Nafe62137ba[\"External: () -> ServerTrendsTagsViewModel\"]\n  N8377f0741c[\"External: () -> SettingScreenModel\"]\n  N8622fecfa8[\"External: () -> StatusContextViewModel\"]\n  N2cd9f1d3f1[\"External: () -> StatusListContainerViewModel\"]\n  N68eeaa5a05[\"External: () -> TrendingStatusViewModel\"]\n  N4fff844501[\"External: () -> UserAboutContainerViewModel\"]\n  N001ea94182[\"External: () -> UserDetailContainerViewModel\"]\n  N0642b33e0f[\"External: () -> UserTimelineContainerViewModel\"]\n  N31d4d14732[\"External: (BlogPlatform) -> AddActivityPubContentViewModel\"]\n  N57ed876353[\"External: (FormalBaseUrl) -> InstanceDetailViewModel\"]\n  N19fb61e6c8[\"External: (FormalBaseUrl, FormalUri) -> EditAccountInfoViewModel\"]\n  N53c634cf7b[\"External: (FormalBaseUrl?, Boolean, String?, String?, String?) -> AddBlueskyContentViewModel\"]\n  N21b95cbace[\"External: (List<String>) -> MultiAccountPublishingViewModel\"]\n  N8c8c79030d[\"External: (PlatformLocator) -> AddListViewModel\"]\n  N501999f437[\"External: (PlatformLocator) -> CreatedListsViewModel\"]\n  N58fefad36d[\"External: (PlatformLocator) -> EditProfileViewModel\"]\n  N88e1e3a250[\"External: (PlatformLocator) -> ExplorerFeedsViewModel\"]\n  N07e54212e3[\"External: (PlatformLocator) -> FiltersListViewModel\"]\n  N9caacd2f72[\"External: (PlatformLocator) -> SearchAuthorViewModel\"]\n  Nd126f08d3c[\"External: (PlatformLocator) -> SearchHashtagViewModel\"]\n  Ne2a88c124a[\"External: (PlatformLocator) -> SearchStatusViewModel\"]\n  Nebb604cfe3[\"External: (PlatformLocator) -> TagListViewModel\"]\n  N67ba4edacc[\"External: (PlatformLocator, BlueskyFeeds) -> FeedsDetailViewModel\"]\n  Nda853e75a6[\"External: (PlatformLocator, Boolean) -> SearchUserViewModel\"]\n  N104d9467e2[\"External: (PlatformLocator, String) -> BskyUserDetailViewModel\"]\n  N75378e1d2e[\"External: (PlatformLocator, String) -> EditListViewModel\"]\n  Nc3405bb7fb[\"External: (PlatformLocator, String) -> SearchPlatformViewModel\"]\n  Nedd7048ddd[\"External: (PlatformLocator, String) -> SearchStatusViewModel\"]\n  Nb72392fcac[\"External: (PlatformLocator, String?) -> EditFilterViewModel\"]\n  N676803adc9[\"External: (PlatformLocator, String?, String?, String?) -> PublishPostViewModel\"]\n  N1e28617dd6[\"External: (PlatformLocator, UserListType, String?) -> UserListViewModel\"]\n  Nfa680b36d7[\"External: (PlatformLocator, UserListType, String?, FormalUri?, String?) -> UserListViewModel\"]\n  Nc6378cb626[\"External: (PostStatusScreenParams) -> PostStatusViewModel\"]\n  N8d922c0725[\"External: (StatusSource?) -> AddMixedFeedsViewModel\"]\n  N8ab1ebfa12[\"External: (String) -> EditContentConfigViewModel\"]\n  N27777dbf81[\"External: (String) -> EditMixedContentViewModel\"]\n  Ne704a6b3a2[\"External: (String) -> RssSourceViewModel\"]\n  N405cba47a3[\"External: (String) -> SelectAccountForPublishViewModel\"]\n  Nb3f3c43702[\"External: (String, PlatformLocator?, Boolean) -> UrlRedirectViewModel\"]\n  Ned074bd7ac[\"External: (String, String, PlatformLocator, StatusProviderProtocol) -> SelectAccountOpenStatusViewModel\"]\n  N6fc97c9b92[\"External: (String?, PlatformLocator?) -> BskyFollowingFeedsViewModel\"]\n  Nf925d9cef8[\"External: ActiveAccountsSynchronizer\"]\n  N8a212358d8[\"External: ActivityPubAccountEntityAdapter\"]\n  Nfa1084f8e1[\"External: ActivityPubAccountLogoutUseCase\"]\n  N0213a37765[\"External: ActivityPubAccountManager\"]\n  Ncd1f5b63de[\"External: ActivityPubApplicationEntityAdapter\"]\n  N763a9a135a[\"External: ActivityPubApplicationRepo\"]\n  Nc8e1a4e364[\"External: ActivityPubBlogMetaAdapter\"]\n  Ne1d90545f7[\"External: ActivityPubClientManager\"]\n  N99947e629d[\"External: ActivityPubContentAdapter\"]\n  N0f64956567[\"External: ActivityPubContentManager\"]\n  Nad6d79a615[\"External: ActivityPubContentMigrator\"]\n  N6344d7e729[\"External: ActivityPubCustomEmojiEntityAdapter\"]\n  N039bf3472c[\"External: ActivityPubDatabases\"]\n  Ne0d9d411b9[\"External: ActivityPubInstanceAdapter\"]\n  Ncdb4c0cbf8[\"External: ActivityPubLoggedAccountAdapter\"]\n  Nb76ef8157e[\"External: ActivityPubLoggedAccountDatabase\"]\n  Na5181cbba3[\"External: ActivityPubLoggedAccountRepo\"]\n  N69f9eb5086[\"External: ActivityPubNotificationResolver\"]\n  Na38935cd08[\"External: ActivityPubOAuthor\"]\n  Nddddaa04f9[\"External: ActivityPubPlatformEntityAdapter\"]\n  N31eb146107[\"External: ActivityPubPlatformRepo\"]\n  N3167de5c45[\"External: ActivityPubPlatformResolver\"]\n  N627bd14f2c[\"External: ActivityPubPollAdapter\"]\n  Na83ede2cf3[\"External: ActivityPubProvider\"]\n  Ne98830554d[\"External: ActivityPubPublishManager\"]\n  N42c7e0d519[\"External: ActivityPubPushManager\"]\n  N34b6c0ecf8[\"External: ActivityPubScreenProvider\"]\n  Ndd6fc86978[\"External: ActivityPubSearchAdapter\"]\n  N5f9390e388[\"External: ActivityPubSearchEngine\"]\n  Nf4928cbc67[\"External: ActivityPubSourceResolver\"]\n  N6b94985842[\"External: ActivityPubStartup\"]\n  N8a5de8d122[\"External: ActivityPubStatusAdapter\"]\n  N5a2a2e0c5a[\"External: ActivityPubStatusDatabases\"]\n  N99c776fc0d[\"External: ActivityPubStatusReadStateDatabases\"]\n  N63649e8010[\"External: ActivityPubStatusReadStateRepo\"]\n  N13f7294241[\"External: ActivityPubStatusResolver\"]\n  Nc46e325e61[\"External: ActivityPubTagAdapter\"]\n  N330df1c7c3[\"External: ActivityPubTimelineStatusRepo\"]\n  N31ad4ebc3e[\"External: ActivityPubTranslationEntityAdapter\"]\n  N682f9b093e[\"External: ActivityPubUrlInterceptor\"]\n  N764e0c2541[\"External: AppUpdateManager\"]\n  N5f6db2df23[\"External: BlogAuthorAdapter\"]\n  N50dddbe9d7[\"External: BlogPlatform\"]\n  Nff835f4d54[\"External: BlogPlatformResourceLoader\"]\n  Nfe8e0ad46e[\"External: BlueskyAccountAdapter\"]\n  Nbc589c4e22[\"External: BlueskyAccountManager\"]\n  N2a892e6731[\"External: BlueskyClientManager\"]\n  N6f03b95a32[\"External: BlueskyContentManager\"]\n  N9cf75ab12f[\"External: BlueskyContentMigrator\"]\n  N6c4085f9e6[\"External: BlueskyFeeds\"]\n  Nddf4738cc6[\"External: BlueskyFeedsAdapter\"]\n  N8a21a3c96d[\"External: BlueskyLoggedAccountDatabase\"]\n  N1d2b497a2d[\"External: BlueskyLoggedAccountManager\"]\n  Nf4707f5a66[\"External: BlueskyLoggedAccountRepo\"]\n  N4987b0cc45[\"External: BlueskyNotificationAdapter\"]\n  N441fc16448[\"External: BlueskyNotificationResolver\"]\n  N899a6a5d72[\"External: BlueskyPlatformRepo\"]\n  N02144e4562[\"External: BlueskyPlatformResolver\"]\n  Nc27915e91f[\"External: BlueskyProfileAdapter\"]\n  Nfbead4488e[\"External: BlueskyProvider\"]\n  Nd62fd06788[\"External: BlueskyPublishManager\"]\n  N4567427c74[\"External: BlueskyScreenProvider\"]\n  N5b4170cec6[\"External: BlueskySearchEngine\"]\n  N3eaf2103f9[\"External: BlueskyStatusAdapter\"]\n  N1309b38634[\"External: BlueskyStatusResolver\"]\n  Na61c85bfce[\"External: BlueskyStatusSourceResolver\"]\n  N47f3ffd6bb[\"External: Boolean\"]\n  N7c9f6610f5[\"External: BrowserLauncher\"]\n  Nadb2fdf057[\"External: BskyStartup\"]\n  N5e46761521[\"External: BskyStatusInteractiveUseCase\"]\n  N081b94b5b4[\"External: BskyUrlInterceptor\"]\n  Ne9b8efd0a2[\"External: BuildSearchResultUiStateUseCase\"]\n  Ne9d909c972[\"External: CommonStartup\"]\n  Ne857bdea8f[\"External: ContentConfigAdapter\"]\n  N97d9454d21[\"External: ContentConfigDatabases\"]\n  N90274c6fd3[\"External: CreateRecordUseCase\"]\n  N55ea83885d[\"External: CustomEmojiAdapter\"]\n  N7d16a1d3d9[\"External: DayNightHelper\"]\n  Nbfcb071d01[\"External: DeleteRecordUseCase\"]\n  Nd09094fac2[\"External: FormalBaseUrl\"]\n  Nd1f4ce6c75[\"External: FormalBaseUrl?\"]\n  Nc99a834813[\"External: FormalUri\"]\n  N92888aa436[\"External: FormalUri?\"]\n  Nc1a3aa7b35[\"External: FreadConfigManager\"]\n  Na7a1f228b2[\"External: FreadConfigModuleStartup\"]\n  Na2e59385e1[\"External: FreadContentDatabase\"]\n  Nce41f4af94[\"External: FreadContentDbMigrateManager\"]\n  N8b57d73a24[\"External: FreadContentRepo\"]\n  N09817aab3f[\"External: GenerateInitPostStatusUiStateUseCase\"]\n  N0d0f4fdfe4[\"External: GetAllListsUseCase\"]\n  Nec83350cf0[\"External: GetAtIdentifierUseCase\"]\n  Nf3ef21420d[\"External: GetCompletedNotificationUseCase\"]\n  N42538c992a[\"External: GetCustomEmojiUseCase\"]\n  Nab5171e538[\"External: GetDefaultBaseUrlUseCase\"]\n  Na6f7ba048e[\"External: GetFeedsStatusUseCase\"]\n  Nd6f3ec0423[\"External: GetFollowingFeedsUseCase\"]\n  N70c4ac54ad[\"External: GetInstanceAnnouncementUseCase\"]\n  Na603c25104[\"External: GetInstancePostStatusRulesUseCase\"]\n  Nb5aaf4d43a[\"External: GetServerTrendTagsUseCase\"]\n  N2bb5d05d8c[\"External: GetStatusContextUseCase\"]\n  Nb8ecde7600[\"External: GetTimelineStatusUseCase\"]\n  N0602ca7339[\"External: GetUserCreatedListUseCase\"]\n  N74f0cdf0b1[\"External: GetUserStatusUseCase\"]\n  N4e9026ec18[\"External: Lazy<FlowSettings>\"]\n  Nb11d90f398[\"External: Lazy<FreadConfigManager>\"]\n  N9c1fd3b6bd[\"External: List<String>\"]\n  N956e808f53[\"External: LocalConfigManager\"]\n  N518c8572e4[\"External: LoggedAccountProvider\"]\n  N41dc86ed2f[\"External: LoginToBskyUseCase\"]\n  Nad83422d07[\"External: Map<ViewModelKey, ViewModelCreator>\"]\n  Nc7c563b2fe[\"External: Map<ViewModelKey, ViewModelFactory>\"]\n  N9d3d48dd27[\"External: MapCustomEmojiUseCase\"]\n  Ndc8255602a[\"External: MastodonHelper\"]\n  Nfad7dfebbc[\"External: MastodonInstanceRepo\"]\n  N6e341f5a7e[\"External: MixedStatusDatabases\"]\n  N62787d2759[\"External: MixedStatusRepo\"]\n  N10307617d7[\"External: NotificationsDatabase\"]\n  N197c4d966b[\"External: NotificationsRepo\"]\n  N2c5e13e7ef[\"External: OAuthHandler\"]\n  N9e91cb11f0[\"External: OldFreadContentDatabase\"]\n  N95905d8f88[\"External: OnboardingComponent\"]\n  Nfddd23677f[\"External: PinFeedsUseCase\"]\n  N0a28ac2206[\"External: PlatformLocator\"]\n  Nc9967a40ec[\"External: PlatformLocator?\"]\n  N32852bb49c[\"External: PlatformUriHelper\"]\n  Neac054df51[\"External: PlatformUriTransformer\"]\n  N1783552df5[\"External: PostStatusAttachmentAdapter\"]\n  N49e7f8879f[\"External: PostStatusScreenParams\"]\n  N80815d1c2a[\"External: PublishPostOnMultiAccountUseCase\"]\n  Nda7a54e5ba[\"External: PublishPostUseCase\"]\n  N469bf9f2a3[\"External: PublishingPostUseCase\"]\n  N2938d7ec78[\"External: RefactorToNewBlogUseCase\"]\n  N6d142895b2[\"External: RefactorToNewStatusUseCase\"]\n  N2890eb47aa[\"External: RefreshSessionUseCase\"]\n  N55690dfeac[\"External: RegisterApplicationEntryAdapter\"]\n  Nb4e8996b51[\"External: ReorderActivityPubTabUseCase\"]\n  Nd6a79489d8[\"External: RssAccountManager\"]\n  N6f090cefe2[\"External: RssContentManager\"]\n  N91552eee08[\"External: RssDatabases\"]\n  Nc70e828b1b[\"External: RssFetcher\"]\n  N0b062c0693[\"External: RssNotificationResolver\"]\n  N5654bbd309[\"External: RssParser\"]\n  N97256a9fdd[\"External: RssParserWrapper\"]\n  Nfa6e174e82[\"External: RssPlatformResolver\"]\n  N7cf470ad30[\"External: RssPlatformTransformer\"]\n  N590d5cdc23[\"External: RssPublishManager\"]\n  N842ce557d0[\"External: RssRepo\"]\n  N5dcd3839b5[\"External: RssScreenProvider\"]\n  Nb2629703b0[\"External: RssSearchEngine\"]\n  N72e9d0ab5f[\"External: RssSourceTransformer\"]\n  N267d44c00b[\"External: RssSourceWebFingerTransformer\"]\n  Na271be4ceb[\"External: RssStatusAdapter\"]\n  N8e0b95dcc8[\"External: RssStatusProvider\"]\n  Nc14a15894f[\"External: RssStatusRepo\"]\n  Nade6277f9a[\"External: RssStatusResolver\"]\n  N10bf2daf93[\"External: RssStatusSourceResolver\"]\n  N8d327df2c9[\"External: RssUriTransformer\"]\n  N02dc009f8d[\"External: SearchUserSourceNoTokenUseCase\"]\n  Nb25c31e711[\"External: SelectedAccountPublishingDatabase\"]\n  Nb029431978[\"External: SelectedAccountPublishingRepo\"]\n  Na9c5830ade[\"External: SelectedContentSwitcher\"]\n  N6be4ab7a3f[\"External: Set< IStatusProvider>\"]\n  Nec0f4747d1[\"External: Set<BrowserInterceptor>\"]\n  N57f14f2301[\"External: Set<ModuleStartup>\"]\n  N64da6866c5[\"External: StatusInteractiveUseCase\"]\n  N5cca16c063[\"External: StatusProviderProtocol\"]\n  Ncabe0092a5[\"External: StatusSource?\"]\n  N9c9d85dbd1[\"External: StatusUiStateAdapter\"]\n  N24985a671f[\"External: StatusUpdater\"]\n  N3f61db1662[\"External: StorageHelper\"]\n  N67372312b4[\"External: String\"]\n  N138e502bb4[\"External: String?\"]\n  Nc1297b1253[\"External: SystemBrowserLauncher\"]\n  Nade7b46a17[\"External: TextHandler\"]\n  Nb18c480f32[\"External: UnblockUserWithoutUriUseCase\"]\n  N6d0f1dfbc1[\"External: UnpinFeedsUseCase\"]\n  N45e019f62d[\"External: UpdateActivityPubUserListUseCase\"]\n  N7059866b06[\"External: UpdateBlockUseCase\"]\n  Nd05cf8618d[\"External: UpdateHomeTabUseCase\"]\n  N7d1aae4546[\"External: UpdatePinnedFeedsOrderUseCase\"]\n  N81a220d953[\"External: UpdatePreferencesUseCase\"]\n  N0ce0954aff[\"External: UpdateProfileRecordUseCase\"]\n  N46e9002ac7[\"External: UpdateRelationshipUseCase\"]\n  Nd9b7ca9e32[\"External: UploadBlobUseCase\"]\n  Nc8bc48368c[\"External: UploadMediaAttachmentUseCase\"]\n  N95ab25169e[\"External: UserListType\"]\n  Ndd50d64a83[\"External: UserRepo\"]\n  N6761ba2285[\"External: UserSourceTransformer\"]\n  N99c4d3861d[\"External: UserUriTransformer\"]\n  Na0e184b0f2[\"External: VotePollUseCase\"]\n  Nc01d179ed0[\"External: WebFingerBaseUrlToUserIdRepo\"]\n  N0e870bb90c[\"IFeedsScreenVisitor\"]\n  N14bea58fc8[\"IProfileScreenVisitor\"]\n  N03de987c55[\"IStatusProvider\"]\n  N4e74262a4b[\"ModuleScreenVisitor\"]\n  Nded7da349d[\"ModuleStartup\"]\n  N670240c0b4[\"Pair<ViewModelKey, ViewModelCreator>\"]\n  N600e1b9c05[\"Pair<ViewModelKey, ViewModelFactory>\"]\n  N938d7e8361[\"StatusProvider\"]\n  Na1a88a4190[\"ViewModelProvider.Factory\"]\n  N983c37f247[\"com.zhangke.fread.activitypub.app.ActivityPubAccountManager\"]\n  N5ffb317e4a[\"com.zhangke.fread.activitypub.app.ActivityPubContentManager\"]\n  N7599b19614[\"com.zhangke.fread.activitypub.app.ActivityPubNotificationResolver\"]\n  N89c600306b[\"com.zhangke.fread.activitypub.app.ActivityPubPlatformResolver\"]\n  N83a1cc9594[\"com.zhangke.fread.activitypub.app.ActivityPubProvider\"]\n  N2835833403[\"com.zhangke.fread.activitypub.app.ActivityPubPublishManager\"]\n  N4f3f62043e[\"com.zhangke.fread.activitypub.app.ActivityPubScreenProvider\"]\n  Nf146960e53[\"com.zhangke.fread.activitypub.app.ActivityPubSearchEngine\"]\n  N9d07219951[\"com.zhangke.fread.activitypub.app.ActivityPubSourceResolver\"]\n  Ndaf5bc8fd2[\"com.zhangke.fread.activitypub.app.ActivityPubStartup\"]\n  Nb208df30c6[\"com.zhangke.fread.activitypub.app.ActivityPubStatusResolver\"]\n  Ne75ab179a9[\"com.zhangke.fread.activitypub.app.ActivityPubUrlInterceptor\"]\n  N0a105fd185[\"com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubAccountEntityAdapter\"]\n  N35e77148e3[\"com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubApplicationEntityAdapter\"]\n  Na16b42f88a[\"com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubBlogMetaAdapter\"]\n  Na813625355[\"com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubContentAdapter\"]\n  N544ed29e84[\"com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubCustomEmojiEntityAdapter\"]\n  N34b4365f58[\"com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubInstanceAdapter\"]\n  N76cca85cc9[\"com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubLoggedAccountAdapter\"]\n  Neb04cc57d3[\"com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubPlatformEntityAdapter\"]\n  Nbb06f0148e[\"com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubPollAdapter\"]\n  N2dec340afe[\"com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubSearchAdapter\"]\n  N861b6e835c[\"com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubStatusAdapter\"]\n  Na569151165[\"com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubTagAdapter\"]\n  N1679f20192[\"com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubTranslationEntityAdapter\"]\n  Nb44b380f31[\"com.zhangke.fread.activitypub.app.internal.adapter.PostStatusAttachmentAdapter\"]\n  Nefcac13e53[\"com.zhangke.fread.activitypub.app.internal.adapter.RegisterApplicationEntryAdapter\"]\n  Nfc93284358[\"com.zhangke.fread.activitypub.app.internal.auth.ActivityPubClientManager\"]\n  N2134b618db[\"com.zhangke.fread.activitypub.app.internal.auth.ActivityPubOAuthor\"]\n  Ncc9c430dcc[\"com.zhangke.fread.activitypub.app.internal.auth.LoggedAccountProvider\"]\n  Ncb9eccea48[\"com.zhangke.fread.activitypub.app.internal.migrate.ActivityPubContentMigrator\"]\n  N9d1106ba83[\"com.zhangke.fread.activitypub.app.internal.repo.WebFingerBaseUrlToUserIdRepo\"]\n  Nfe43780815[\"com.zhangke.fread.activitypub.app.internal.repo.account.ActivityPubLoggedAccountRepo\"]\n  N6ed815bee9[\"com.zhangke.fread.activitypub.app.internal.repo.application.ActivityPubApplicationRepo\"]\n  N5410010b86[\"com.zhangke.fread.activitypub.app.internal.repo.platform.ActivityPubPlatformRepo\"]\n  N2a59a5e095[\"com.zhangke.fread.activitypub.app.internal.repo.platform.BlogPlatformResourceLoader\"]\n  N9bdf8300e8[\"com.zhangke.fread.activitypub.app.internal.repo.platform.MastodonInstanceRepo\"]\n  Ne3f62b2909[\"com.zhangke.fread.activitypub.app.internal.repo.status.ActivityPubStatusReadStateRepo\"]\n  N3b9c39302d[\"com.zhangke.fread.activitypub.app.internal.repo.status.ActivityPubTimelineStatusRepo\"]\n  Nf7bf75d177[\"com.zhangke.fread.activitypub.app.internal.repo.user.UserRepo\"]\n  Na38fd2af9d[\"com.zhangke.fread.activitypub.app.internal.screen.account.EditAccountInfoViewModel\"]\n  Nfaa9c1fa63[\"com.zhangke.fread.activitypub.app.internal.screen.add.AddActivityPubContentViewModel\"]\n  N6198d8071e[\"com.zhangke.fread.activitypub.app.internal.screen.add.select.SelectPlatformViewModel\"]\n  N612cea2370[\"com.zhangke.fread.activitypub.app.internal.screen.content.ActivityPubContentViewModel\"]\n  Nd4ace750cf[\"com.zhangke.fread.activitypub.app.internal.screen.content.edit.EditContentConfigViewModel\"]\n  N7812ab16ff[\"com.zhangke.fread.activitypub.app.internal.screen.content.timeline.ActivityPubTimelineContainerViewModel\"]\n  N44d3ea7b14[\"com.zhangke.fread.activitypub.app.internal.screen.explorer.ExplorerContainerViewModel\"]\n  N30a6f4c6fa[\"com.zhangke.fread.activitypub.app.internal.screen.filters.edit.EditFilterViewModel\"]\n  N5135846fa4[\"com.zhangke.fread.activitypub.app.internal.screen.filters.list.FiltersListViewModel\"]\n  N180df54f96[\"com.zhangke.fread.activitypub.app.internal.screen.hashtag.HashtagTimelineContainerViewModel\"]\n  N93d36dda72[\"com.zhangke.fread.activitypub.app.internal.screen.instance.InstanceDetailViewModel\"]\n  N8be5538ade[\"com.zhangke.fread.activitypub.app.internal.screen.instance.about.ServerAboutViewModel\"]\n  N0b26d8c2a7[\"com.zhangke.fread.activitypub.app.internal.screen.instance.tags.ServerTrendsTagsViewModel\"]\n  N6194ba3b2c[\"com.zhangke.fread.activitypub.app.internal.screen.list.CreatedListsViewModel\"]\n  Nd497fddb90[\"com.zhangke.fread.activitypub.app.internal.screen.list.add.AddListViewModel\"]\n  N5fe718218e[\"com.zhangke.fread.activitypub.app.internal.screen.list.edit.EditListViewModel\"]\n  N650474fcdb[\"com.zhangke.fread.activitypub.app.internal.screen.search.SearchStatusViewModel\"]\n  N192d71f0fe[\"com.zhangke.fread.activitypub.app.internal.screen.status.post.PostStatusViewModel\"]\n  N056d1822fe[\"com.zhangke.fread.activitypub.app.internal.screen.status.post.adapter.CustomEmojiAdapter\"]\n  Ne82168dd10[\"com.zhangke.fread.activitypub.app.internal.screen.status.post.usecase.GenerateInitPostStatusUiStateUseCase\"]\n  N4be37fe41d[\"com.zhangke.fread.activitypub.app.internal.screen.status.post.usecase.PublishPostUseCase\"]\n  N7635d8534f[\"com.zhangke.fread.activitypub.app.internal.screen.trending.TrendingStatusViewModel\"]\n  N715833dc58[\"com.zhangke.fread.activitypub.app.internal.screen.user.UserDetailContainerViewModel\"]\n  N989cdc6ac7[\"com.zhangke.fread.activitypub.app.internal.screen.user.about.UserAboutContainerViewModel\"]\n  Nf969754b6d[\"com.zhangke.fread.activitypub.app.internal.screen.user.list.UserListViewModel\"]\n  N223d04494e[\"com.zhangke.fread.activitypub.app.internal.screen.user.search.SearchUserViewModel\"]\n  N4a1af0534a[\"com.zhangke.fread.activitypub.app.internal.screen.user.status.StatusListContainerViewModel\"]\n  Nb04c20d695[\"com.zhangke.fread.activitypub.app.internal.screen.user.tags.TagListViewModel\"]\n  N4e57c358d5[\"com.zhangke.fread.activitypub.app.internal.screen.user.timeline.UserTimelineContainerViewModel\"]\n  Ncdcbb200e6[\"com.zhangke.fread.activitypub.app.internal.source.UserSourceTransformer\"]\n  N7a3a9d45dd[\"com.zhangke.fread.activitypub.app.internal.uri.PlatformUriTransformer\"]\n  N43f5eba7da[\"com.zhangke.fread.activitypub.app.internal.uri.UserUriTransformer\"]\n  Nb2e79fc2a4[\"com.zhangke.fread.activitypub.app.internal.usecase.ActivityPubAccountLogoutUseCase\"]\n  N836bf814dd[\"com.zhangke.fread.activitypub.app.internal.usecase.GetDefaultBaseUrlUseCase\"]\n  N0f2bcb0a97[\"com.zhangke.fread.activitypub.app.internal.usecase.GetInstanceAnnouncementUseCase\"]\n  Nafcad36339[\"com.zhangke.fread.activitypub.app.internal.usecase.GetServerTrendTagsUseCase\"]\n  Na8cd3aecea[\"com.zhangke.fread.activitypub.app.internal.usecase.UpdateActivityPubUserListUseCase\"]\n  Na6e95d2b4f[\"com.zhangke.fread.activitypub.app.internal.usecase.content.GetUserCreatedListUseCase\"]\n  N016044c115[\"com.zhangke.fread.activitypub.app.internal.usecase.content.ReorderActivityPubTabUseCase\"]\n  N5bbbb2a4e1[\"com.zhangke.fread.activitypub.app.internal.usecase.emoji.GetCustomEmojiUseCase\"]\n  N675ecab856[\"com.zhangke.fread.activitypub.app.internal.usecase.emoji.MapCustomEmojiUseCase\"]\n  N9691d9d391[\"com.zhangke.fread.activitypub.app.internal.usecase.media.UploadMediaAttachmentUseCase\"]\n  N5ad886c66c[\"com.zhangke.fread.activitypub.app.internal.usecase.platform.GetInstancePostStatusRulesUseCase\"]\n  N181f4c5848[\"com.zhangke.fread.activitypub.app.internal.usecase.source.user.SearchUserSourceNoTokenUseCase\"]\n  N358b0dfb7f[\"com.zhangke.fread.activitypub.app.internal.usecase.status.GetStatusContextUseCase\"]\n  N4d4cff5def[\"com.zhangke.fread.activitypub.app.internal.usecase.status.GetTimelineStatusUseCase\"]\n  N50cbb0fb75[\"com.zhangke.fread.activitypub.app.internal.usecase.status.GetUserStatusUseCase\"]\n  Ncb5bd399a8[\"com.zhangke.fread.activitypub.app.internal.usecase.status.StatusInteractiveUseCase\"]\n  Nbd8bdfc245[\"com.zhangke.fread.activitypub.app.internal.usecase.status.VotePollUseCase\"]\n  Nd25d4a2627[\"com.zhangke.fread.activitypub.app.internal.utils.MastodonHelper\"]\n  N92f4bfcb88[\"com.zhangke.fread.bluesky.BlueskyAccountManager\"]\n  N1dd24803fc[\"com.zhangke.fread.bluesky.BlueskyNotificationResolver\"]\n  N25db25751b[\"com.zhangke.fread.bluesky.BlueskyPlatformResolver\"]\n  N87085a1143[\"com.zhangke.fread.bluesky.BlueskyProvider\"]\n  Nb046baaf15[\"com.zhangke.fread.bluesky.BlueskyPublishManager\"]\n  Nb605bb1880[\"com.zhangke.fread.bluesky.BlueskyScreenProvider\"]\n  N5ddf6b757c[\"com.zhangke.fread.bluesky.BlueskySearchEngine\"]\n  Na441222c7c[\"com.zhangke.fread.bluesky.BlueskyStatusResolver\"]\n  N1dfc34f4e2[\"com.zhangke.fread.bluesky.BlueskyStatusSourceResolver\"]\n  N474c7183d7[\"com.zhangke.fread.bluesky.BskyStartup\"]\n  N08c08fff2f[\"com.zhangke.fread.bluesky.BskyUrlInterceptor\"]\n  Ncdcbec942e[\"com.zhangke.fread.bluesky.internal.account.BlueskyLoggedAccountManager\"]\n  N1f97cb5ed0[\"com.zhangke.fread.bluesky.internal.adapter.BlueskyAccountAdapter\"]\n  Naedc4712fe[\"com.zhangke.fread.bluesky.internal.adapter.BlueskyFeedsAdapter\"]\n  N41c71b6508[\"com.zhangke.fread.bluesky.internal.adapter.BlueskyNotificationAdapter\"]\n  N4a631eb113[\"com.zhangke.fread.bluesky.internal.adapter.BlueskyProfileAdapter\"]\n  N903dda79b9[\"com.zhangke.fread.bluesky.internal.adapter.BlueskyStatusAdapter\"]\n  Nb11e833c56[\"com.zhangke.fread.bluesky.internal.client.BlueskyClientManager\"]\n  N707513bc08[\"com.zhangke.fread.bluesky.internal.content.BlueskyContentManager\"]\n  Nd9d1761e06[\"com.zhangke.fread.bluesky.internal.migrate.BlueskyContentMigrator\"]\n  N1d38cc12fa[\"com.zhangke.fread.bluesky.internal.repo.BlueskyLoggedAccountRepo\"]\n  Nce8bbc5e3f[\"com.zhangke.fread.bluesky.internal.repo.BlueskyPlatformRepo\"]\n  Nd564c70d15[\"com.zhangke.fread.bluesky.internal.screen.add.AddBlueskyContentViewModel\"]\n  N172376f55d[\"com.zhangke.fread.bluesky.internal.screen.feeds.detail.FeedsDetailViewModel\"]\n  Nbf36bf8b4b[\"com.zhangke.fread.bluesky.internal.screen.feeds.explorer.ExplorerFeedsViewModel\"]\n  N9eda8650e6[\"com.zhangke.fread.bluesky.internal.screen.feeds.following.BskyFollowingFeedsViewModel\"]\n  N8a2fd8e592[\"com.zhangke.fread.bluesky.internal.screen.feeds.home.HomeFeedsContainerViewModel\"]\n  Nda5aa01e7b[\"com.zhangke.fread.bluesky.internal.screen.home.BlueskyHomeContainerViewModel\"]\n  N29e74314a8[\"com.zhangke.fread.bluesky.internal.screen.publish.PublishPostViewModel\"]\n  Nab40c1ba63[\"com.zhangke.fread.bluesky.internal.screen.search.SearchStatusViewModel\"]\n  Ncdacb19798[\"com.zhangke.fread.bluesky.internal.screen.user.detail.BskyUserDetailViewModel\"]\n  N1583773318[\"com.zhangke.fread.bluesky.internal.screen.user.edit.EditProfileViewModel\"]\n  Nc7c6b8dcad[\"com.zhangke.fread.bluesky.internal.screen.user.list.UserListViewModel\"]\n  N74000278ab[\"com.zhangke.fread.bluesky.internal.uri.platform.PlatformUriTransformer\"]\n  N81dee9c36e[\"com.zhangke.fread.bluesky.internal.uri.user.UserUriTransformer\"]\n  N029b3040d5[\"com.zhangke.fread.bluesky.internal.usecase.BskyStatusInteractiveUseCase\"]\n  N17da903f13[\"com.zhangke.fread.bluesky.internal.usecase.CreateRecordUseCase\"]\n  Ne683d65bf3[\"com.zhangke.fread.bluesky.internal.usecase.DeleteRecordUseCase\"]\n  N7363a346ce[\"com.zhangke.fread.bluesky.internal.usecase.GetAllListsUseCase\"]\n  Nba2b3d5f0a[\"com.zhangke.fread.bluesky.internal.usecase.GetAtIdentifierUseCase\"]\n  N889caf6c37[\"com.zhangke.fread.bluesky.internal.usecase.GetCompletedNotificationUseCase\"]\n  N228667e0fc[\"com.zhangke.fread.bluesky.internal.usecase.GetFeedsStatusUseCase\"]\n  Ndf1c7623c1[\"com.zhangke.fread.bluesky.internal.usecase.GetFollowingFeedsUseCase\"]\n  N5f37dbc6b4[\"com.zhangke.fread.bluesky.internal.usecase.GetStatusContextUseCase\"]\n  N665d0d39c0[\"com.zhangke.fread.bluesky.internal.usecase.LoginToBskyUseCase\"]\n  N685f01219b[\"com.zhangke.fread.bluesky.internal.usecase.PinFeedsUseCase\"]\n  N31f70f1ec8[\"com.zhangke.fread.bluesky.internal.usecase.PublishingPostUseCase\"]\n  N8fd33a8f34[\"com.zhangke.fread.bluesky.internal.usecase.RefreshSessionUseCase\"]\n  Nd89d3b24ef[\"com.zhangke.fread.bluesky.internal.usecase.UnblockUserWithoutUriUseCase\"]\n  N9cb819add0[\"com.zhangke.fread.bluesky.internal.usecase.UnpinFeedsUseCase\"]\n  Nff66e76bcd[\"com.zhangke.fread.bluesky.internal.usecase.UpdateBlockUseCase\"]\n  N594d88b966[\"com.zhangke.fread.bluesky.internal.usecase.UpdateHomeTabUseCase\"]\n  Naab06a2778[\"com.zhangke.fread.bluesky.internal.usecase.UpdatePinnedFeedsOrderUseCase\"]\n  N6b410ec35a[\"com.zhangke.fread.bluesky.internal.usecase.UpdatePreferencesUseCase\"]\n  Na1830f3db0[\"com.zhangke.fread.bluesky.internal.usecase.UpdateProfileRecordUseCase\"]\n  Nea78d24b39[\"com.zhangke.fread.bluesky.internal.usecase.UpdateRelationshipUseCase\"]\n  Nde323b227d[\"com.zhangke.fread.bluesky.internal.usecase.UploadBlobUseCase\"]\n  N8aaa81f108[\"com.zhangke.fread.common.CommonStartup\"]\n  N9df3abce6b[\"com.zhangke.fread.common.account.ActiveAccountsSynchronizer\"]\n  N395a854fc3[\"com.zhangke.fread.common.adapter.StatusUiStateAdapter\"]\n  N62d87a6a32[\"com.zhangke.fread.common.browser.BrowserLauncher\"]\n  Nc337a1e77c[\"com.zhangke.fread.common.browser.SelectAccount\"]\n  Nc6c2890ff7[\"com.zhangke.fread.common.bubble.BubbleManager\"]\n  N48d67cd949[\"com.zhangke.fread.common.config.FreadConfigManager\"]\n  N563775d5a8[\"com.zhangke.fread.common.config.LocalConfigManager\"]\n  N6f1eea881c[\"com.zhangke.fread.common.content.FreadContentDbMigrateManager\"]\n  Nbb3d22f17a[\"com.zhangke.fread.common.content.FreadContentRepo\"]\n  N380013ad7c[\"com.zhangke.fread.common.daynight.DayNightHelper\"]\n  Ndd799373e4[\"com.zhangke.fread.common.deeplink.SelectAccountForPublishViewModel\"]\n  N947480c46d[\"com.zhangke.fread.common.deeplink.SelectedContentSwitcher\"]\n  Nc69166447f[\"com.zhangke.fread.common.mixed.MixedStatusRepo\"]\n  N5dc007a633[\"com.zhangke.fread.common.onboarding.OnboardingComponent\"]\n  Nb807debe6e[\"com.zhangke.fread.common.publish.PublishPostManager\"]\n  N1adbb66287[\"com.zhangke.fread.common.review.FreadReviewManager\"]\n  Nb125055b98[\"com.zhangke.fread.common.startup.FeedsRepoModuleStartup\"]\n  N5d0b7117a1[\"com.zhangke.fread.common.startup.FreadConfigModuleStartup\"]\n  Nc40baa57c9[\"com.zhangke.fread.common.startup.StartupManager\"]\n  N718fa7cc7e[\"com.zhangke.fread.common.status.StatusIdGenerator\"]\n  N7da3998e6c[\"com.zhangke.fread.common.status.StatusUpdater\"]\n  Nfc9327537a[\"com.zhangke.fread.common.status.adapter.ContentConfigAdapter\"]\n  Nc40f3870c1[\"com.zhangke.fread.common.status.usecase.FormatStatusDisplayTimeUseCase\"]\n  N7666e419d6[\"com.zhangke.fread.common.update.AppUpdateManager\"]\n  Nf01dc9f97b[\"com.zhangke.fread.commonbiz.shared.blog.detail.RssBlogDetailViewModel\"]\n  Naba11c2cb4[\"com.zhangke.fread.commonbiz.shared.repo.SelectedAccountPublishingRepo\"]\n  N2147ec9332[\"com.zhangke.fread.commonbiz.shared.screen.publish.multi.MultiAccountPublishingViewModel\"]\n  N897a9829c9[\"com.zhangke.fread.commonbiz.shared.screen.status.account.SelectAccountOpenStatusViewModel\"]\n  N255f535a0d[\"com.zhangke.fread.commonbiz.shared.screen.status.context.StatusContextViewModel\"]\n  Nfd5998257b[\"com.zhangke.fread.commonbiz.shared.usecase.PublishPostOnMultiAccountUseCase\"]\n  N74f86e7350[\"com.zhangke.fread.commonbiz.shared.usecase.RefactorToNewBlogUseCase\"]\n  Nc39e8692db[\"com.zhangke.fread.commonbiz.shared.usecase.RefactorToNewStatusUseCase\"]\n  Nce4eda4a4f[\"com.zhangke.fread.explore.screens.home.ExplorerHomeViewModel\"]\n  Ndcfb257eea[\"com.zhangke.fread.explore.screens.search.SearchViewModel\"]\n  Neb2dc71608[\"com.zhangke.fread.explore.screens.search.author.SearchAuthorViewModel\"]\n  N1b09b9b75d[\"com.zhangke.fread.explore.screens.search.bar.SearchBarViewModel\"]\n  N446af303e4[\"com.zhangke.fread.explore.screens.search.hashtag.SearchHashtagViewModel\"]\n  Ne52d757fc0[\"com.zhangke.fread.explore.screens.search.platform.SearchPlatformViewModel\"]\n  N5ae2eec09c[\"com.zhangke.fread.explore.screens.search.status.SearchStatusViewModel\"]\n  Ncd0dc85346[\"com.zhangke.fread.explore.usecase.BuildSearchResultUiStateUseCase\"]\n  N74364fb5a2[\"com.zhangke.fread.feature.message.repo.notification.NotificationsRepo\"]\n  N3740ccb747[\"com.zhangke.fread.feature.message.screens.home.NotificationsHomeViewModel\"]\n  N721ec2aeb7[\"com.zhangke.fread.feature.message.screens.notification.NotificationContainerViewModel\"]\n  Ndcf9a87edd[\"com.zhangke.fread.feeds.FeedsScreenVisitor\"]\n  N0d49daa8e1[\"com.zhangke.fread.feeds.pages.home.ContentHomeViewModel\"]\n  N98bfa34fa6[\"com.zhangke.fread.feeds.pages.home.feeds.MixedContentViewModel\"]\n  N5ee8323aae[\"com.zhangke.fread.feeds.pages.manager.add.mixed.AddMixedFeedsViewModel\"]\n  N17d22a06ae[\"com.zhangke.fread.feeds.pages.manager.add.type.SelectContentTypeViewModel\"]\n  Nf338fca47d[\"com.zhangke.fread.feeds.pages.manager.edit.EditMixedContentViewModel\"]\n  N4316f19e6b[\"com.zhangke.fread.feeds.pages.manager.importing.ImportFeedsViewModel\"]\n  N2c544d5e49[\"com.zhangke.fread.feeds.pages.manager.search.SearchSourceForAddViewModel\"]\n  N1d4374eee9[\"com.zhangke.fread.profile.screen.home.ProfileHomeViewModel\"]\n  Ncffa15e1ec[\"com.zhangke.fread.profile.screen.setting.SettingScreenModel\"]\n  N0f6ad4ad06[\"com.zhangke.fread.profile.screen.setting.about.AboutViewModel\"]\n  Nd37614cb3f[\"com.zhangke.fread.rss.RssAccountManager\"]\n  N031a96d2a3[\"com.zhangke.fread.rss.RssContentManager\"]\n  Ne85ec7d1d7[\"com.zhangke.fread.rss.RssNotificationResolver\"]\n  N7a6b1c837a[\"com.zhangke.fread.rss.RssPlatformResolver\"]\n  N597ca23223[\"com.zhangke.fread.rss.RssPublishManager\"]\n  N32d14f14d2[\"com.zhangke.fread.rss.RssScreenProvider\"]\n  N57fae43ca5[\"com.zhangke.fread.rss.RssSearchEngine\"]\n  N01d70e70ba[\"com.zhangke.fread.rss.RssStatusProvider\"]\n  N579f6837ac[\"com.zhangke.fread.rss.RssStatusResolver\"]\n  Nd239f94637[\"com.zhangke.fread.rss.RssStatusSourceResolver\"]\n  N48a15302a0[\"com.zhangke.fread.rss.internal.adapter.BlogAuthorAdapter\"]\n  Ne814135223[\"com.zhangke.fread.rss.internal.adapter.RssStatusAdapter\"]\n  Nb4ed30afb8[\"com.zhangke.fread.rss.internal.platform.RssPlatformTransformer\"]\n  Nfb562b9ab9[\"com.zhangke.fread.rss.internal.repo.RssRepo\"]\n  Na4cb7492f5[\"com.zhangke.fread.rss.internal.repo.RssStatusRepo\"]\n  Nbe677a0500[\"com.zhangke.fread.rss.internal.rss.RssFetcher\"]\n  N764c4a5430[\"com.zhangke.fread.rss.internal.rss.RssParserWrapper\"]\n  Nbcc71b73a5[\"com.zhangke.fread.rss.internal.screen.source.RssSourceViewModel\"]\n  Nc9048b2b23[\"com.zhangke.fread.rss.internal.source.RssSourceTransformer\"]\n  N676c5f88ec[\"com.zhangke.fread.rss.internal.uri.RssUriTransformer\"]\n  Nd73a3cbe21[\"com.zhangke.fread.rss.internal.webfinger.RssSourceWebFingerTransformer\"]\n  Nfd2d45eb2d[\"com.zhangke.fread.screen.main.MainViewModel\"]\n  N3b721c25d3[\"com.zhangke.fread.screen.main.drawer.MainDrawerViewModel\"]\n  N8b57d73a24 --> N3b721c25d3\n  N938d7e8361 --> N3b721c25d3\n  N764e0c2541 --> Nfd2d45eb2d\n  Ne1d90545f7 --> N7599b19614\n  N31eb146107 --> N7599b19614\n  N518c8572e4 --> N7599b19614\n  N8a212358d8 --> N7599b19614\n  N8a5de8d122 --> N7599b19614\n  Na38935cd08 --> N983c37f247\n  Ne1d90545f7 --> N983c37f247\n  N518c8572e4 --> N983c37f247\n  Na5181cbba3 --> N983c37f247\n  N99c4d3861d --> N983c37f247\n  Ncdb4c0cbf8 --> N983c37f247\n  N42c7e0d519 --> N983c37f247\n  N028b64f105 --> N983c37f247\n  N8a212358d8 --> N983c37f247\n  Nfa1084f8e1 --> N983c37f247\n  N31eb146107 --> N89c600306b\n  Ne1d90545f7 --> N2835833403\n  Nda7a54e5ba --> N2835833403\n  N99c4d3861d --> N4f3f62043e\n  N518c8572e4 --> N4f3f62043e\n  N31eb146107 --> Ne75ab179a9\n  Ne1d90545f7 --> Ne75ab179a9\n  N518c8572e4 --> Ne75ab179a9\n  N8a212358d8 --> Ne75ab179a9\n  N8a5de8d122 --> Ne75ab179a9\n  N8b57d73a24 --> Ne75ab179a9\n  Ne1d90545f7 --> Nb208df30c6\n  N74f0cdf0b1 --> Nb208df30c6\n  N99c4d3861d --> Nb208df30c6\n  N64da6866c5 --> Nb208df30c6\n  N8a5de8d122 --> Nb208df30c6\n  N2bb5d05d8c --> Nb208df30c6\n  Na0e184b0f2 --> Nb208df30c6\n  Nc01d179ed0 --> Nb208df30c6\n  N518c8572e4 --> Nb208df30c6\n  N31ad4ebc3e --> Nb208df30c6\n  N8b57d73a24 --> Ndaf5bc8fd2\n  Na5181cbba3 --> Ndaf5bc8fd2\n  N99947e629d --> Ndaf5bc8fd2\n  Nad6d79a615 --> Ndaf5bc8fd2\n  Na5181cbba3 --> Ndaf5bc8fd2\n  N6be4ab7a3f --> N938d7e8361\n  Nbc470bf63e --> N670240c0b4\n  Na83d469aab --> N670240c0b4\n  Nc3119e5bf1 --> N670240c0b4\n  N8377f0741c --> N670240c0b4\n  Nc7b67d9d2b --> N670240c0b4\n  Ne12ccda2a8 --> N670240c0b4\n  Nadad2fb90b --> N670240c0b4\n  Nbb771e20d1 --> N670240c0b4\n  N93812a316c --> N670240c0b4\n  N8ad85967a5 --> N670240c0b4\n  Nca6fb5baac --> N670240c0b4\n  Nbf2f4aef51 --> N670240c0b4\n  Ne01b9b9f2a --> N670240c0b4\n  Nbd7f0dd8e9 --> N670240c0b4\n  N8e5132e28c --> N670240c0b4\n  Nf04a3d7220 --> N670240c0b4\n  N7d66c5f8f4 --> N670240c0b4\n  Nbd1b619127 --> N670240c0b4\n  Nafe62137ba --> N670240c0b4\n  N68eeaa5a05 --> N670240c0b4\n  N4fff844501 --> N670240c0b4\n  N2cd9f1d3f1 --> N670240c0b4\n  N0642b33e0f --> N670240c0b4\n  N001ea94182 --> N670240c0b4\n  N45ba9694f3 --> N670240c0b4\n  Ncd1624647b --> N670240c0b4\n  N47bc9f8990 --> N670240c0b4\n  N539851c83f --> N670240c0b4\n  Ne9cd17762c --> N670240c0b4\n  N8622fecfa8 --> N670240c0b4\n  N3151de3431 --> N670240c0b4\n  N039bf3472c --> Nfe43780815\n  Nb76ef8157e --> Nfe43780815\n  N039bf3472c --> N6ed815bee9\n  Ne1d90545f7 --> N6ed815bee9\n  N55690dfeac --> N6ed815bee9\n  Ncd1f5b63de --> N6ed815bee9\n  N99c776fc0d --> Ne3f62b2909\n  N5a2a2e0c5a --> N3b9c39302d\n  Nb8ecde7600 --> N3b9c39302d\n  Ne1d90545f7 --> Nf7bf75d177\n  Nc01d179ed0 --> Nf7bf75d177\n  N6761ba2285 --> Nf7bf75d177\n  N039bf3472c --> N5410010b86\n  Ne1d90545f7 --> N5410010b86\n  Nddddaa04f9 --> N5410010b86\n  Ne0d9d411b9 --> N5410010b86\n  Nff835f4d54 --> N5410010b86\n  Nfad7dfebbc --> N5410010b86\n  N518c8572e4 --> N5410010b86\n  Ndc8255602a --> N2a59a5e095\n  N039bf3472c --> N9d1106ba83\n  Ne1d90545f7 --> N9d1106ba83\n  Ne1d90545f7 --> N9691d9d391\n  Ne1d90545f7 --> N0f2bcb0a97\n  Ne1d90545f7 --> Nafcad36339\n  Nc46e325e61 --> Nafcad36339\n  N8b57d73a24 --> Na8cd3aecea\n  Ne1d90545f7 --> Nbd8bdfc245\n  N627bd14f2c --> Nbd8bdfc245\n  Ne1d90545f7 --> N358b0dfb7f\n  N8a5de8d122 --> N358b0dfb7f\n  N518c8572e4 --> N358b0dfb7f\n  Ne1d90545f7 --> N50cbb0fb75\n  Nc01d179ed0 --> N50cbb0fb75\n  N8a5de8d122 --> N50cbb0fb75\n  N31eb146107 --> N50cbb0fb75\n  N518c8572e4 --> N50cbb0fb75\n  Ne1d90545f7 --> N4d4cff5def\n  N31eb146107 --> N4d4cff5def\n  N8a5de8d122 --> N4d4cff5def\n  Ne1d90545f7 --> Ncb5bd399a8\n  N8a5de8d122 --> Ncb5bd399a8\n  N938d7e8361 --> N1b09b9b75d\n  N24985a671f --> N1b09b9b75d\n  Ne9b8efd0a2 --> N1b09b9b75d\n  N9c9d85dbd1 --> N1b09b9b75d\n  N6d142895b2 --> N1b09b9b75d\n  Ne1d90545f7 --> Na6e95d2b4f\n  N8b57d73a24 --> N016044c115\n  N938d7e8361 --> N5ae2eec09c\n  N24985a671f --> N5ae2eec09c\n  N9c9d85dbd1 --> N5ae2eec09c\n  N6d142895b2 --> N5ae2eec09c\n  N0a28ac2206 --> N5ae2eec09c\n  Ne1d90545f7 --> N5bbbb2a4e1\n  N6344d7e729 --> N5bbbb2a4e1\n  N55ea83885d --> N5bbbb2a4e1\n  Ne1d90545f7 --> N181f4c5848\n  Ndd50d64a83 --> N181f4c5848\n  N99c4d3861d --> N181f4c5848\n  N6761ba2285 --> N181f4c5848\n  Nab5171e538 --> N181f4c5848\n  N938d7e8361 --> Ne52d757fc0\n  N0a28ac2206 --> Ne52d757fc0\n  N67372312b4 --> Ne52d757fc0\n  N764e0c2541 --> N0f6ad4ad06\n  Nade7b46a17 --> Ncffa15e1ec\n  Nc1a3aa7b35 --> Ncffa15e1ec\n  N7d16a1d3d9 --> Ncffa15e1ec\n  N764e0c2541 --> Ncffa15e1ec\n  Ne1d90545f7 --> N5ad886c66c\n  N42c7e0d519 --> Nb2e79fc2a4\n  Na5181cbba3 --> Nb2e79fc2a4\n  N518c8572e4 --> Nb2e79fc2a4\n  N518c8572e4 --> N836bf814dd\n  N938d7e8361 --> Neb2dc71608\n  N0a28ac2206 --> Neb2dc71608\n  N938d7e8361 --> N446af303e4\n  N0a28ac2206 --> N446af303e4\n  N938d7e8361 --> N1d4374eee9\n  Nf925d9cef8 --> N1d4374eee9\n  N938d7e8361 --> Nce4eda4a4f\n  Nf925d9cef8 --> Nce4eda4a4f\n  N9caacd2f72 --> N600e1b9c05\n  Nd126f08d3c --> N600e1b9c05\n  Nc3405bb7fb --> N600e1b9c05\n  Ne2a88c124a --> N600e1b9c05\n  N8d922c0725 --> N600e1b9c05\n  N27777dbf81 --> N600e1b9c05\n  N19fb61e6c8 --> N600e1b9c05\n  N8ab1ebfa12 --> N600e1b9c05\n  Nb72392fcac --> N600e1b9c05\n  N07e54212e3 --> N600e1b9c05\n  N57ed876353 --> N600e1b9c05\n  Nc6378cb626 --> N600e1b9c05\n  Nfa680b36d7 --> N600e1b9c05\n  Nebb604cfe3 --> N600e1b9c05\n  N31d4d14732 --> N600e1b9c05\n  N501999f437 --> N600e1b9c05\n  Nda853e75a6 --> N600e1b9c05\n  Nedd7048ddd --> N600e1b9c05\n  N75378e1d2e --> N600e1b9c05\n  N8c8c79030d --> N600e1b9c05\n  N53c634cf7b --> N600e1b9c05\n  N6fc97c9b92 --> N600e1b9c05\n  N104d9467e2 --> N600e1b9c05\n  N58fefad36d --> N600e1b9c05\n  N1e28617dd6 --> N600e1b9c05\n  N676803adc9 --> N600e1b9c05\n  Nedd7048ddd --> N600e1b9c05\n  N88e1e3a250 --> N600e1b9c05\n  N67ba4edacc --> N600e1b9c05\n  Ne704a6b3a2 --> N600e1b9c05\n  Nb3f3c43702 --> N600e1b9c05\n  N405cba47a3 --> N600e1b9c05\n  N21b95cbace --> N600e1b9c05\n  Ned074bd7ac --> N600e1b9c05\n  Ne1d90545f7 --> Na38fd2af9d\n  N32852bb49c --> Na38fd2af9d\n  Nd09094fac2 --> Na38fd2af9d\n  Nc99a834813 --> Na38fd2af9d\n  N938d7e8361 --> N7635d8534f\n  Ne1d90545f7 --> N7635d8534f\n  N24985a671f --> N7635d8534f\n  N8a5de8d122 --> N7635d8534f\n  N9c9d85dbd1 --> N7635d8534f\n  N31eb146107 --> N7635d8534f\n  N6d142895b2 --> N7635d8534f\n  N518c8572e4 --> N7635d8534f\n  N31eb146107 --> N93d36dda72\n  N8a212358d8 --> N93d36dda72\n  Nd09094fac2 --> N93d36dda72\n  Nb5aaf4d43a --> N0b26d8c2a7\n  Na5181cbba3 --> N8be5538ade\n  N70c4ac54ad --> N8be5538ade\n  N938d7e8361 --> N4316f19e6b\n  N8b57d73a24 --> N4316f19e6b\n  N32852bb49c --> N4316f19e6b\n  N0602ca7339 --> N6194ba3b2c\n  N0a28ac2206 --> N6194ba3b2c\n  Ne1d90545f7 --> Nd497fddb90\n  N0a28ac2206 --> Nd497fddb90\n  Ne1d90545f7 --> N5fe718218e\n  N0a28ac2206 --> N5fe718218e\n  N67372312b4 --> N5fe718218e\n  N95905d8f88 --> N17d22a06ae\n  N938d7e8361 --> N17d22a06ae\n  N938d7e8361 --> N5ee8323aae\n  N8b57d73a24 --> N5ee8323aae\n  N95905d8f88 --> N5ee8323aae\n  Ncabe0092a5 --> N5ee8323aae\n  N99c4d3861d --> Ncdcbb200e6\n  N8a212358d8 --> Ncdcbb200e6\n  N9d3d48dd27 --> Ncdcbb200e6\n  N6344d7e729 --> Ncdcbb200e6\n  N8b57d73a24 --> Nf338fca47d\n  N938d7e8361 --> Nf338fca47d\n  N67372312b4 --> Nf338fca47d\n  N518c8572e4 --> Nfc93284358\n  Na5181cbba3 --> N2134b618db\n  N763a9a135a --> N2134b618db\n  Ne1d90545f7 --> N2134b618db\n  Ncdb4c0cbf8 --> N2134b618db\n  Nddddaa04f9 --> N2134b618db\n  N039bf3472c --> N2134b618db\n  N028b64f105 --> N2134b618db\n  N2c5e13e7ef --> N2134b618db\n  N8b57d73a24 --> N2134b618db\n  Ndd50d64a83 --> N9d07219951\n  N99c4d3861d --> N9d07219951\n  N0f64956567 --> N83a1cc9594\n  N34b6c0ecf8 --> N83a1cc9594\n  N3167de5c45 --> N83a1cc9594\n  N5f9390e388 --> N83a1cc9594\n  N13f7294241 --> N83a1cc9594\n  Nf4928cbc67 --> N83a1cc9594\n  N0213a37765 --> N83a1cc9594\n  N69f9eb5086 --> N83a1cc9594\n  Ne98830554d --> N83a1cc9594\n  N938d7e8361 --> N2c544d5e49\n  Na83ede2cf3 --> N03de987c55\n  Nfbead4488e --> N03de987c55\n  N8e0b95dcc8 --> N03de987c55\n  N682f9b093e --> N2a8e209e43\n  N081b94b5b4 --> N2a8e209e43\n  N6b94985842 --> Nded7da349d\n  Nadb2fdf057 --> Nded7da349d\n  Ne9d909c972 --> Nded7da349d\n  Na7a1f228b2 --> Nded7da349d\n  N02dc009f8d --> Nf146960e53\n  Ne1d90545f7 --> Nf146960e53\n  N31eb146107 --> Nf146960e53\n  Ndd6fc86978 --> Nf146960e53\n  N8a5de8d122 --> Nf146960e53\n  Nc46e325e61 --> Nf146960e53\n  N8a212358d8 --> Nf146960e53\n  N518c8572e4 --> Nf146960e53\n  N8b57d73a24 --> N0d49daa8e1\n  N938d7e8361 --> N0d49daa8e1\n  Nf925d9cef8 --> N0d49daa8e1\n  Na9c5830ade --> N0d49daa8e1\n  N8b57d73a24 --> N98bfa34fa6\n  N62787d2759 --> N98bfa34fa6\n  N24985a671f --> N98bfa34fa6\n  N9c9d85dbd1 --> N98bfa34fa6\n  N938d7e8361 --> N98bfa34fa6\n  N6d142895b2 --> N98bfa34fa6\n  N31eb146107 --> N6198d8071e\n  N95905d8f88 --> N6198d8071e\n  N8b57d73a24 --> Nfaa9c1fa63\n  Na38935cd08 --> Nfaa9c1fa63\n  N99947e629d --> Nfaa9c1fa63\n  N95905d8f88 --> Nfaa9c1fa63\n  N50dddbe9d7 --> Nfaa9c1fa63\n  Ne1d90545f7 --> N650474fcdb\n  N938d7e8361 --> N650474fcdb\n  N31eb146107 --> N650474fcdb\n  N518c8572e4 --> N650474fcdb\n  N9c9d85dbd1 --> N650474fcdb\n  N8a5de8d122 --> N650474fcdb\n  N6d142895b2 --> N650474fcdb\n  N24985a671f --> N650474fcdb\n  N0a28ac2206 --> N650474fcdb\n  N67372312b4 --> N650474fcdb\n  N0213a37765 --> Ne82168dd10\n  Ne1d90545f7 --> N4be37fe41d\n  Nc8bc48368c --> N4be37fe41d\n  N1783552df5 --> N4be37fe41d\n  N24985a671f --> N4be37fe41d\n  N8a5de8d122 --> N4be37fe41d\n  N42538c992a --> N192d71f0fe\n  Na603c25104 --> N192d71f0fe\n  N09817aab3f --> N192d71f0fe\n  Ne1d90545f7 --> N192d71f0fe\n  Nda7a54e5ba --> N192d71f0fe\n  N49e7f8879f --> N192d71f0fe\n  N32852bb49c --> N192d71f0fe\n  N938d7e8361 --> N4e57c358d5\n  Nc01d179ed0 --> N4e57c358d5\n  N24985a671f --> N4e57c358d5\n  N9c9d85dbd1 --> N4e57c358d5\n  N31eb146107 --> N4e57c358d5\n  N8a5de8d122 --> N4e57c358d5\n  Ne1d90545f7 --> N4e57c358d5\n  N6d142895b2 --> N4e57c358d5\n  N518c8572e4 --> N4e57c358d5\n  N938d7e8361 --> N721ec2aeb7\n  N197c4d966b --> N721ec2aeb7\n  N9c9d85dbd1 --> N721ec2aeb7\n  N6d142895b2 --> N721ec2aeb7\n  N24985a671f --> N721ec2aeb7\n  N938d7e8361 --> N3740ccb747\n  Nf925d9cef8 --> N3740ccb747\n  Ne1d90545f7 --> Nf969754b6d\n  N99c4d3861d --> Nf969754b6d\n  Nc01d179ed0 --> Nf969754b6d\n  N8a212358d8 --> Nf969754b6d\n  N0a28ac2206 --> Nf969754b6d\n  N95ab25169e --> Nf969754b6d\n  N138e502bb4 --> Nf969754b6d\n  N92888aa436 --> Nf969754b6d\n  N138e502bb4 --> Nf969754b6d\n  Ne1d90545f7 --> N223d04494e\n  N0a28ac2206 --> N223d04494e\n  N47f3ffd6bb --> N223d04494e\n  Ne1d90545f7 --> Nb04c20d695\n  Nc46e325e61 --> Nb04c20d695\n  N0a28ac2206 --> Nb04c20d695\n  Ne1d90545f7 --> N4a1af0534a\n  N8a5de8d122 --> N4a1af0534a\n  N938d7e8361 --> N4a1af0534a\n  N24985a671f --> N4a1af0534a\n  N31eb146107 --> N4a1af0534a\n  N9c9d85dbd1 --> N4a1af0534a\n  N6d142895b2 --> N4a1af0534a\n  N518c8572e4 --> N4a1af0534a\n  N10307617d7 --> N74364fb5a2\n  Ne1d90545f7 --> N989cdc6ac7\n  Nc01d179ed0 --> N989cdc6ac7\n  N6344d7e729 --> N989cdc6ac7\n  N0213a37765 --> N715833dc58\n  N99c4d3861d --> N715833dc58\n  Ne1d90545f7 --> N715833dc58\n  N8a212358d8 --> N715833dc58\n  N6344d7e729 --> N715833dc58\n  Nfa1084f8e1 --> N715833dc58\n  N1d2b497a2d --> N92f4bfcb88\n  Nb18c480f32 --> N92f4bfcb88\n  N469bf9f2a3 --> Nb046baaf15\n  N899a6a5d72 --> N25db25751b\n  N99c4d3861d --> Nb605bb1880\n  N6f03b95a32 --> N87085a1143\n  N4567427c74 --> N87085a1143\n  N02144e4562 --> N87085a1143\n  N5b4170cec6 --> N87085a1143\n  N1309b38634 --> N87085a1143\n  Na61c85bfce --> N87085a1143\n  Nbc589c4e22 --> N87085a1143\n  N441fc16448 --> N87085a1143\n  Nd62fd06788 --> N87085a1143\n  N2a892e6731 --> N5ddf6b757c\n  Nfe8e0ad46e --> N5ddf6b757c\n  Nec83350cf0 --> N5ddf6b757c\n  N899a6a5d72 --> N5ddf6b757c\n  N3eaf2103f9 --> N5ddf6b757c\n  N899a6a5d72 --> N5ddf6b757c\n  N1d2b497a2d --> N5ddf6b757c\n  N2890eb47aa --> N474c7183d7\n  N9cf75ab12f --> N474c7183d7\n  Ne1d90545f7 --> N44d3ea7b14\n  N518c8572e4 --> N44d3ea7b14\n  N8a5de8d122 --> N44d3ea7b14\n  N8a212358d8 --> N44d3ea7b14\n  Nc46e325e61 --> N44d3ea7b14\n  N938d7e8361 --> N44d3ea7b14\n  N24985a671f --> N44d3ea7b14\n  N9c9d85dbd1 --> N44d3ea7b14\n  N6d142895b2 --> N44d3ea7b14\n  N8a212358d8 --> N861b6e835c\n  Nc8e1a4e364 --> N861b6e835c\n  N627bd14f2c --> N861b6e835c\n  N6344d7e729 --> N861b6e835c\n  N99c4d3861d --> N0a105fd185\n  N6344d7e729 --> N0a105fd185\n  N8a212358d8 --> N2dec340afe\n  Nc46e325e61 --> N2dec340afe\n  N8a5de8d122 --> N2dec340afe\n  Neac054df51 --> N34b4365f58\n  Neac054df51 --> Neb04cc57d3\n  Ne0d9d411b9 --> N76cca85cc9\n  N99c4d3861d --> N76cca85cc9\n  N6344d7e729 --> N76cca85cc9\n  N8a21a3c96d --> N1d38cc12fa\n  Neac054df51 --> Nce8bbc5e3f\n  N3f61db1662 --> Nd25d4a2627\n  Nf4707f5a66 --> Nb11e833c56\n  Nfe8e0ad46e --> Nb11e833c56\n  N1d2b497a2d --> N665d0d39c0\n  N2a892e6731 --> N889caf6c37\n  N2a892e6731 --> N7363a346ce\n  N2a892e6731 --> N8fd33a8f34\n  Nbc589c4e22 --> N8fd33a8f34\n  N81a220d953 --> Naab06a2778\n  N81a220d953 --> N9cb819add0\n  N2a892e6731 --> Nde323b227d\n  N32852bb49c --> Nde323b227d\n  N2a892e6731 --> N228667e0fc\n  N3eaf2103f9 --> N228667e0fc\n  N899a6a5d72 --> N228667e0fc\n  Nd6f3ec0423 --> N594d88b966\n  N8b57d73a24 --> N594d88b966\n  N81a220d953 --> N685f01219b\n  N2a892e6731 --> N5f37dbc6b4\n  N899a6a5d72 --> N5f37dbc6b4\n  N3eaf2103f9 --> N5f37dbc6b4\n  N2a892e6731 --> N17da903f13\n  N2a892e6731 --> Ne683d65bf3\n  N2a892e6731 --> N029b3040d5\n  N0ce0954aff --> N029b3040d5\n  N2a892e6731 --> Nea78d24b39\n  N90274c6fd3 --> Nea78d24b39\n  Nbfcb071d01 --> Nea78d24b39\n  N2a892e6731 --> Ndf1c7623c1\n  Nddf4738cc6 --> Ndf1c7623c1\n  N2a892e6731 --> N6b410ec35a\n  N90274c6fd3 --> Nff66e76bcd\n  Nbfcb071d01 --> Nff66e76bcd\n  N2a892e6731 --> N31f70f1ec8\n  Nd9b7ca9e32 --> N31f70f1ec8\n  N2a892e6731 --> Nd89d3b24ef\n  N7059866b06 --> Nd89d3b24ef\n  N2a892e6731 --> Ncdcbec942e\n  Nfe8e0ad46e --> Ncdcbec942e\n  N899a6a5d72 --> Ncdcbec942e\n  Nf4707f5a66 --> Ncdcbec942e\n  N8b57d73a24 --> Ncb9eccea48\n  Na5181cbba3 --> Ncb9eccea48\n  Ne1d90545f7 --> N180df54f96\n  N938d7e8361 --> N180df54f96\n  N24985a671f --> N180df54f96\n  N8a5de8d122 --> N180df54f96\n  N31eb146107 --> N180df54f96\n  N9c9d85dbd1 --> N180df54f96\n  N6d142895b2 --> N180df54f96\n  N518c8572e4 --> N180df54f96\n  N41dc86ed2f --> Nd564c70d15\n  N8b57d73a24 --> Nd564c70d15\n  N899a6a5d72 --> Nd564c70d15\n  N95905d8f88 --> Nd564c70d15\n  Nd1f4ce6c75 --> Nd564c70d15\n  N47f3ffd6bb --> Nd564c70d15\n  N138e502bb4 --> Nd564c70d15\n  N138e502bb4 --> Nd564c70d15\n  N138e502bb4 --> Nd564c70d15\n  N2a892e6731 --> N29e74314a8\n  N0d0f4fdfe4 --> N29e74314a8\n  N32852bb49c --> N29e74314a8\n  Nc1a3aa7b35 --> N29e74314a8\n  N469bf9f2a3 --> N29e74314a8\n  N0a28ac2206 --> N29e74314a8\n  N138e502bb4 --> N29e74314a8\n  N138e502bb4 --> N29e74314a8\n  N138e502bb4 --> N29e74314a8\n  N938d7e8361 --> N7812ab16ff\n  N24985a671f --> N7812ab16ff\n  N8a5de8d122 --> N7812ab16ff\n  N9c9d85dbd1 --> N7812ab16ff\n  N6d142895b2 --> N7812ab16ff\n  N518c8572e4 --> N7812ab16ff\n  N330df1c7c3 --> N7812ab16ff\n  N0213a37765 --> N7812ab16ff\n  N63649e8010 --> N7812ab16ff\n  Nc1a3aa7b35 --> N7812ab16ff\n  Nd6f3ec0423 --> N9eda8650e6\n  N8b57d73a24 --> N9eda8650e6\n  N7d1aae4546 --> N9eda8650e6\n  N1d2b497a2d --> N9eda8650e6\n  N138e502bb4 --> N9eda8650e6\n  Nc9967a40ec --> N9eda8650e6\n  N8b57d73a24 --> Nd4ace750cf\n  Nb4e8996b51 --> Nd4ace750cf\n  N67372312b4 --> Nd4ace750cf\n  N8b57d73a24 --> N612cea2370\n  N0213a37765 --> N612cea2370\n  N0602ca7339 --> N612cea2370\n  N45e019f62d --> N612cea2370\n  N2a892e6731 --> N172376f55d\n  Nddf4738cc6 --> N172376f55d\n  N90274c6fd3 --> N172376f55d\n  Nbfcb071d01 --> N172376f55d\n  Nfddd23677f --> N172376f55d\n  N6d0f1dfbc1 --> N172376f55d\n  N0a28ac2206 --> N172376f55d\n  N6c4085f9e6 --> N172376f55d\n  N2a892e6731 --> Nbf36bf8b4b\n  Nddf4738cc6 --> Nbf36bf8b4b\n  Nfddd23677f --> Nbf36bf8b4b\n  N0a28ac2206 --> Nbf36bf8b4b\n  N9c9d85dbd1 --> N8a2fd8e592\n  N24985a671f --> N8a2fd8e592\n  N938d7e8361 --> N8a2fd8e592\n  N6d142895b2 --> N8a2fd8e592\n  Na6f7ba048e --> N8a2fd8e592\n  N2a892e6731 --> Nab40c1ba63\n  N938d7e8361 --> Nab40c1ba63\n  N9c9d85dbd1 --> Nab40c1ba63\n  N24985a671f --> Nab40c1ba63\n  N6d142895b2 --> Nab40c1ba63\n  N899a6a5d72 --> Nab40c1ba63\n  N3eaf2103f9 --> Nab40c1ba63\n  N0a28ac2206 --> Nab40c1ba63\n  N67372312b4 --> Nab40c1ba63\n  N2a892e6731 --> Nc7c6b8dcad\n  Nfe8e0ad46e --> Nc7c6b8dcad\n  N46e9002ac7 --> Nc7c6b8dcad\n  N7059866b06 --> Nc7c6b8dcad\n  N0a28ac2206 --> Nc7c6b8dcad\n  N95ab25169e --> Nc7c6b8dcad\n  N138e502bb4 --> Nc7c6b8dcad\n  N2a892e6731 --> Ncdacb19798\n  Nfe8e0ad46e --> Ncdacb19798\n  N46e9002ac7 --> Ncdacb19798\n  N7059866b06 --> Ncdacb19798\n  N1d2b497a2d --> Ncdacb19798\n  N2890eb47aa --> Ncdacb19798\n  N99c4d3861d --> Ncdacb19798\n  N0a28ac2206 --> Ncdacb19798\n  N67372312b4 --> Ncdacb19798\n  N1d2b497a2d --> N1583773318\n  N2a892e6731 --> N1583773318\n  Nd9b7ca9e32 --> N1583773318\n  N0a28ac2206 --> N1583773318\n  N8b57d73a24 --> Nda5aa01e7b\n  N1d2b497a2d --> Nda5aa01e7b\n  Nd05cf8618d --> Nda5aa01e7b\n  N8b57d73a24 --> Nd9d1761e06\n  N1d2b497a2d --> Nd9d1761e06\n  Nfe8e0ad46e --> N903dda79b9\n  Nfe8e0ad46e --> N41c71b6508\n  N3eaf2103f9 --> N41c71b6508\n  Nc27915e91f --> Naedc4712fe\n  N99c4d3861d --> N1f97cb5ed0\n  N2a892e6731 --> N1dfc34f4e2\n  N899a6a5d72 --> N1dfc34f4e2\n  Nfe8e0ad46e --> N1dfc34f4e2\n  N99c4d3861d --> N1dfc34f4e2\n  N2a892e6731 --> Na441222c7c\n  N3eaf2103f9 --> Na441222c7c\n  N899a6a5d72 --> Na441222c7c\n  N99c4d3861d --> Na441222c7c\n  N46e9002ac7 --> Na441222c7c\n  N5e46761521 --> Na441222c7c\n  N2bb5d05d8c --> Na441222c7c\n  N99c4d3861d --> Na441222c7c\n  N1d2b497a2d --> N08c08fff2f\n  N2a892e6731 --> N08c08fff2f\n  N3eaf2103f9 --> N08c08fff2f\n  N899a6a5d72 --> N08c08fff2f\n  N8b57d73a24 --> N08c08fff2f\n  N2a892e6731 --> N1dd24803fc\n  Nf3ef21420d --> N1dd24803fc\n  N4987b0cc45 --> N1dd24803fc\n  Nfe8e0ad46e --> N1dd24803fc\n  Ne1d90545f7 --> N30a6f4c6fa\n  N0a28ac2206 --> N30a6f4c6fa\n  N138e502bb4 --> N30a6f4c6fa\n  N8d327df2c9 --> N579f6837ac\n  Nc14a15894f --> N579f6837ac\n  N7cf470ad30 --> N57fae43ca5\n  N72e9d0ab5f --> N57fae43ca5\n  N842ce557d0 --> N57fae43ca5\n  N5f6db2df23 --> N57fae43ca5\n  N8d327df2c9 --> N57fae43ca5\n  N6f090cefe2 --> N01d70e70ba\n  N5dcd3839b5 --> N01d70e70ba\n  Nb2629703b0 --> N01d70e70ba\n  Nd6a79489d8 --> N01d70e70ba\n  Nade6277f9a --> N01d70e70ba\n  N10bf2daf93 --> N01d70e70ba\n  Nfa6e174e82 --> N01d70e70ba\n  N0b062c0693 --> N01d70e70ba\n  N590d5cdc23 --> N01d70e70ba\n  Ne1d90545f7 --> N5135846fa4\n  N0a28ac2206 --> N5135846fa4\n  Na271be4ceb --> Na4cb7492f5\n  N842ce557d0 --> Na4cb7492f5\n  N91552eee08 --> Nfb562b9ab9\n  N5f6db2df23 --> Nfb562b9ab9\n  N8d327df2c9 --> Nfb562b9ab9\n  Nc70e828b1b --> Nfb562b9ab9\n  N842ce557d0 --> Nbcc71b73a5\n  N67372312b4 --> Nbcc71b73a5\n  N5f6db2df23 --> Ne814135223\n  N7cf470ad30 --> Ne814135223\n  N267d44c00b --> N48a15302a0\n  N5654bbd309 --> N764c4a5430\n  N97256a9fdd --> Nbe677a0500\n  N938d7e8361 --> Ndd799373e4\n  N67372312b4 --> Ndd799373e4\n  N028b64f105 --> N5d0b7117a1\n  Nb11d90f398 --> N5d0b7117a1\n  N57f14f2301 --> Nc40baa57c9\n  Nc1a3aa7b35 --> N9df3abce6b\n  N8d327df2c9 --> N32d14f14d2\n  N8d327df2c9 --> Nd239f94637\n  N72e9d0ab5f --> Nd239f94637\n  N842ce557d0 --> Nd239f94637\n  Nc70e828b1b --> Nd239f94637\n  N956e808f53 --> N48d67cd949\n  N4e9026ec18 --> N563775d5a8\n  N8b57d73a24 --> N6f1eea881c\n  N938d7e8361 --> N6f1eea881c\n  N97d9454d21 --> N6f1eea881c\n  Ne857bdea8f --> N6f1eea881c\n  Na2e59385e1 --> Nbb3d22f17a\n  N9e91cb11f0 --> Nbb3d22f17a\n  Nc1a3aa7b35 --> N7666e419d6\n  Nce41f4af94 --> N8aaa81f108\n  Nf925d9cef8 --> N8aaa81f108\n  N938d7e8361 --> Nc69166447f\n  N6e341f5a7e --> Nc69166447f\n  Nad83422d07 --> Na1a88a4190\n  Nc7c563b2fe --> Na1a88a4190\n  N956e808f53 --> N380013ad7c\n  Nb25c31e711 --> Naba11c2cb4\n  N956e808f53 --> N1adbb66287\n  N028b64f105 --> N1adbb66287\n  N0e870bb90c --> N4e74262a4b\n  N14bea58fc8 --> N4e74262a4b\n  Nc1297b1253 --> N62d87a6a32\n  Nec0f4747d1 --> Nc337a1e77c\n  N7c9f6610f5 --> Nc337a1e77c\n  N938d7e8361 --> Nc337a1e77c\n  Na9c5830ade --> Nc337a1e77c\n  N67372312b4 --> Nc337a1e77c\n  Nc9967a40ec --> Nc337a1e77c\n  N47f3ffd6bb --> Nc337a1e77c\n  N938d7e8361 --> Nf01dc9f97b\n  N938d7e8361 --> Nfd5998257b\n  N2938d7ec78 --> Nc39e8692db\n  N62787d2759 --> N255f535a0d\n  N938d7e8361 --> N255f535a0d\n  N24985a671f --> N255f535a0d\n  N9c9d85dbd1 --> N255f535a0d\n  N6d142895b2 --> N255f535a0d\n  N938d7e8361 --> N2147ec9332\n  N32852bb49c --> N2147ec9332\n  N80815d1c2a --> N2147ec9332\n  Nb029431978 --> N2147ec9332\n  N9c1fd3b6bd --> N2147ec9332\n  N938d7e8361 --> N897a9829c9\n  N67372312b4 --> N897a9829c9\n  N67372312b4 --> N897a9829c9\n  N0a28ac2206 --> N897a9829c9\n  N5cca16c063 --> N897a9829c9\n```\n\n## iosMain\n\nRoots (no dependencies):\n- ActivityPubDatabases\n- ActivityPubLoggedAccountDatabase\n- ActivityPubStatusDatabases\n- ActivityPubStatusReadStateDatabases\n- BlueskyLoggedAccountDatabase\n- ContentConfigDatabases\n- FreadContentDatabase\n- MixedStatusDatabases\n- NSUserDefaults\n- NotificationsDatabase\n- OldFreadContentDatabase\n- RssDatabases\n- RssParser\n- SelectedAccountPublishingDatabase\n- UIApplication\n- com.zhangke.fread.activitypub.app.internal.push.ActivityPubPushManager\n- com.zhangke.fread.common.handler.TextHandler\n- com.zhangke.fread.common.utils.MediaFileHelper\n- com.zhangke.fread.common.utils.PlatformUriHelper\n- com.zhangke.fread.common.utils.StorageHelper\n- com.zhangke.fread.common.utils.ThumbnailHelper\n- com.zhangke.fread.common.utils.ToastHelper\n- com.zhangke.fread.startup.KRouterStartup\n\n```mermaid\nflowchart TB\n  Nb642dc2e53[\"ActivityPubDatabases\"]\n  Nd6c6a6bb97[\"ActivityPubLoggedAccountDatabase\"]\n  Ne582c54210[\"ActivityPubStatusDatabases\"]\n  N4b0697a4f6[\"ActivityPubStatusReadStateDatabases\"]\n  N9be163fa87[\"BlueskyLoggedAccountDatabase\"]\n  Nebba756115[\"ContentConfigDatabases\"]\n  N7d16a1d3d9[\"External: DayNightHelper\"]\n  N6c9049d7ad[\"External: FreadViewController\"]\n  N6a87135bd7[\"External: IosApplicationComponent\"]\n  N9bc311411d[\"External: IosSystemBrowserLauncher\"]\n  N7cdf6808bb[\"External: KRouterStartup\"]\n  N2edcc08008[\"External: LanguageHelper\"]\n  N6a3d55c4c9[\"External: Lazy<UIViewController>\"]\n  N956e808f53[\"External: LocalConfigManager\"]\n  N3f61db1662[\"External: StorageHelper\"]\n  Nade7b46a17[\"External: TextHandler\"]\n  Ncc8d86a7b8[\"External: UIApplicationDelegateProtocol\"]\n  N0c2f18938e[\"FlowSettings\"]\n  Nbeb46cbb74[\"FreadContentDatabase\"]\n  Nbee06c41c2[\"ImageLoader\"]\n  N316a667bb3[\"MixedStatusDatabases\"]\n  Nded7da349d[\"ModuleStartup\"]\n  N1abd7ba261[\"NSUserDefaults\"]\n  Na3291e7bfa[\"NotificationsDatabase\"]\n  Ne5814d179d[\"OldFreadContentDatabase\"]\n  N88f2708bac[\"RssDatabases\"]\n  N33e9b1425c[\"RssParser\"]\n  N18af4a5dbc[\"SelectedAccountPublishingDatabase\"]\n  N6bb8cf23c8[\"SystemBrowserLauncher\"]\n  Nabaabf7d26[\"UIApplication\"]\n  N1559c35b03[\"UIViewController\"]\n  Na773e1b613[\"com.zhangke.fread.activitypub.app.internal.push.ActivityPubPushManager\"]\n  N12ab4280a1[\"com.zhangke.fread.common.browser.IosSystemBrowserLauncher\"]\n  N227f594a4d[\"com.zhangke.fread.common.browser.OAuthHandler\"]\n  N05a4c3e9fe[\"com.zhangke.fread.common.daynight.ActivityDayNightHelper\"]\n  N005b37105d[\"com.zhangke.fread.common.handler.ActivityTextHandler\"]\n  N039314d0f5[\"com.zhangke.fread.common.handler.TextHandler\"]\n  N13715080e6[\"com.zhangke.fread.common.language.ActivityLanguageHelper\"]\n  N53c4ecbc13[\"com.zhangke.fread.common.language.LanguageHelper\"]\n  N399ea0e365[\"com.zhangke.fread.common.utils.MediaFileHelper\"]\n  N7e131d7774[\"com.zhangke.fread.common.utils.PlatformUriHelper\"]\n  N82c109ec61[\"com.zhangke.fread.common.utils.StorageHelper\"]\n  N21e10f71ca[\"com.zhangke.fread.common.utils.ThumbnailHelper\"]\n  Nef22c2fc33[\"com.zhangke.fread.common.utils.ToastHelper\"]\n  Na3719264fb[\"com.zhangke.fread.di.IosActivityComponent\"]\n  N6ef831e5f5[\"com.zhangke.fread.di.IosApplicationComponent\"]\n  N3222557576[\"com.zhangke.fread.startup.KRouterStartup\"]\n  N1e131c0677[\"com.zhangke.fread.utils.ActivityHelper\"]\n  N6a3d55c4c9 --> N1e131c0677\n  N3f61db1662 --> Nbee06c41c2\n  N7cdf6808bb --> Nded7da349d\n  Ncc8d86a7b8 --> N6ef831e5f5\n  N6c9049d7ad --> N1559c35b03\n  N6a87135bd7 --> Na3719264fb\n  N9bc311411d --> N6bb8cf23c8\n  N6a3d55c4c9 --> N12ab4280a1\n  Nabaabf7d26 --> N12ab4280a1\n  Nabaabf7d26 --> N227f594a4d\n  N7d16a1d3d9 --> N05a4c3e9fe\n  N1abd7ba261 --> N0c2f18938e\n  N956e808f53 --> N53c4ecbc13\n  N2edcc08008 --> N13715080e6\n  Nade7b46a17 --> N005b37105d\n```\n\n"
  },
  {
    "path": "documents/UserSource.drawio",
    "content": "<mxfile host=\"app.diagrams.net\" modified=\"2023-11-06T14:10:11.568Z\" agent=\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36\" etag=\"oHMNEfnzzkNUcWHEqwCc\" version=\"22.0.8\" type=\"device\">\n  <diagram name=\"第 1 页\" id=\"4grBjr5hehTKnHqGOY0P\">\n    <mxGraphModel dx=\"2405\" dy=\"2154\" grid=\"1\" gridSize=\"10\" guides=\"1\" tooltips=\"1\" connect=\"1\" arrows=\"1\" fold=\"1\" page=\"1\" pageScale=\"1\" pageWidth=\"827\" pageHeight=\"1169\" math=\"0\" shadow=\"0\">\n      <root>\n        <mxCell id=\"0\" />\n        <mxCell id=\"1\" parent=\"0\" />\n        <mxCell id=\"tVMkS5m_2Bp1rili5dJo-1\" value=\"&lt;div style=&quot;background-color:#282c34;color:#bbbbbb;font-family:&#39;JetBrains Mono&#39;,monospace;font-size:9.8pt;&quot;&gt;&lt;pre&gt;&lt;br&gt;&lt;/pre&gt;&lt;/div&gt;\" style=\"rounded=0;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;\" vertex=\"1\" parent=\"1\">\n          <mxGeometry x=\"14\" y=\"215\" width=\"800\" height=\"370\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"tVMkS5m_2Bp1rili5dJo-2\" value=\"&lt;font style=&quot;font-size: 20px;&quot;&gt;ActivityPub&lt;/font&gt;\" style=\"text;html=1;strokeColor=#d6b656;fillColor=#fff2cc;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;\" vertex=\"1\" parent=\"1\">\n          <mxGeometry x=\"340\" y=\"200\" width=\"130\" height=\"30\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"tVMkS5m_2Bp1rili5dJo-8\" value=\"\" style=\"edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;\" edge=\"1\" parent=\"1\" source=\"tVMkS5m_2Bp1rili5dJo-3\" target=\"tVMkS5m_2Bp1rili5dJo-6\">\n          <mxGeometry relative=\"1\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"tVMkS5m_2Bp1rili5dJo-3\" value=\"&lt;font style=&quot;font-size: 17px;&quot;&gt;ActivityPubUser&lt;/font&gt;\" style=\"rounded=0;whiteSpace=wrap;html=1;\" vertex=\"1\" parent=\"1\">\n          <mxGeometry x=\"80\" y=\"430\" width=\"140\" height=\"50\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"tVMkS5m_2Bp1rili5dJo-15\" style=\"edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=1;entryDx=0;entryDy=0;\" edge=\"1\" parent=\"1\" source=\"tVMkS5m_2Bp1rili5dJo-4\" target=\"tVMkS5m_2Bp1rili5dJo-13\">\n          <mxGeometry relative=\"1\" as=\"geometry\">\n            <mxPoint x=\"400\" y=\"100\" as=\"targetPoint\" />\n            <Array as=\"points\">\n              <mxPoint x=\"660\" y=\"160\" />\n              <mxPoint x=\"405\" y=\"160\" />\n            </Array>\n          </mxGeometry>\n        </mxCell>\n        <mxCell id=\"tVMkS5m_2Bp1rili5dJo-4\" value=\"&lt;span style=&quot;font-size: 17px;&quot;&gt;TimelineSource&lt;/span&gt;\" style=\"rounded=0;whiteSpace=wrap;html=1;\" vertex=\"1\" parent=\"1\">\n          <mxGeometry x=\"570\" y=\"350\" width=\"180\" height=\"50\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"tVMkS5m_2Bp1rili5dJo-7\" style=\"edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;\" edge=\"1\" parent=\"1\" source=\"tVMkS5m_2Bp1rili5dJo-5\" target=\"tVMkS5m_2Bp1rili5dJo-3\">\n          <mxGeometry relative=\"1\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"tVMkS5m_2Bp1rili5dJo-5\" value=\"&lt;span style=&quot;font-size: 17px;&quot;&gt;ActivityPubAccountEntity&lt;/span&gt;\" style=\"rounded=0;whiteSpace=wrap;html=1;\" vertex=\"1\" parent=\"1\">\n          <mxGeometry x=\"40\" y=\"520\" width=\"220\" height=\"50\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"tVMkS5m_2Bp1rili5dJo-14\" style=\"edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=1;entryDx=0;entryDy=0;\" edge=\"1\" parent=\"1\" source=\"tVMkS5m_2Bp1rili5dJo-6\" target=\"tVMkS5m_2Bp1rili5dJo-13\">\n          <mxGeometry relative=\"1\" as=\"geometry\">\n            <Array as=\"points\">\n              <mxPoint x=\"150\" y=\"160\" />\n              <mxPoint x=\"405\" y=\"160\" />\n            </Array>\n          </mxGeometry>\n        </mxCell>\n        <mxCell id=\"tVMkS5m_2Bp1rili5dJo-6\" value=\"&lt;font style=&quot;font-size: 17px;&quot;&gt;UserSource&lt;/font&gt;\" style=\"rounded=0;whiteSpace=wrap;html=1;\" vertex=\"1\" parent=\"1\">\n          <mxGeometry x=\"80\" y=\"340\" width=\"140\" height=\"50\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"tVMkS5m_2Bp1rili5dJo-11\" value=\"\" style=\"edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;\" edge=\"1\" parent=\"1\" source=\"tVMkS5m_2Bp1rili5dJo-9\" target=\"tVMkS5m_2Bp1rili5dJo-10\">\n          <mxGeometry relative=\"1\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"tVMkS5m_2Bp1rili5dJo-9\" value=\"&lt;span style=&quot;font-size: 17px;&quot;&gt;ActivityPubInstanceEntity&lt;/span&gt;\" style=\"rounded=0;whiteSpace=wrap;html=1;\" vertex=\"1\" parent=\"1\">\n          <mxGeometry x=\"550\" y=\"520\" width=\"220\" height=\"50\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"tVMkS5m_2Bp1rili5dJo-12\" value=\"\" style=\"edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;\" edge=\"1\" parent=\"1\" source=\"tVMkS5m_2Bp1rili5dJo-10\" target=\"tVMkS5m_2Bp1rili5dJo-4\">\n          <mxGeometry relative=\"1\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"tVMkS5m_2Bp1rili5dJo-10\" value=\"&lt;span style=&quot;font-size: 17px;&quot;&gt;StatusProviderServer&lt;/span&gt;\" style=\"rounded=0;whiteSpace=wrap;html=1;\" vertex=\"1\" parent=\"1\">\n          <mxGeometry x=\"570\" y=\"440\" width=\"180\" height=\"50\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"tVMkS5m_2Bp1rili5dJo-13\" value=\"&lt;font style=&quot;font-size: 24px;&quot;&gt;StatusSource&lt;/font&gt;\" style=\"whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;\" vertex=\"1\" parent=\"1\">\n          <mxGeometry x=\"325\" y=\"20\" width=\"160\" height=\"60\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"tVMkS5m_2Bp1rili5dJo-32\" style=\"edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;\" edge=\"1\" parent=\"1\" source=\"tVMkS5m_2Bp1rili5dJo-16\" target=\"tVMkS5m_2Bp1rili5dJo-26\">\n          <mxGeometry relative=\"1\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"tVMkS5m_2Bp1rili5dJo-16\" value=\"&lt;font style=&quot;font-size: 19px;&quot;&gt;StatusSourceUri: String&lt;/font&gt;\" style=\"whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;\" vertex=\"1\" parent=\"1\">\n          <mxGeometry x=\"1170\" y=\"20\" width=\"220\" height=\"70\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"tVMkS5m_2Bp1rili5dJo-21\" value=\"\" style=\"endArrow=none;html=1;rounded=0;strokeWidth=5;fillColor=#1ba1e2;strokeColor=#006EAF;\" edge=\"1\" parent=\"1\">\n          <mxGeometry width=\"50\" height=\"50\" relative=\"1\" as=\"geometry\">\n            <mxPoint x=\"-290\" y=\"120\" as=\"sourcePoint\" />\n            <mxPoint x=\"1720\" y=\"120\" as=\"targetPoint\" />\n          </mxGeometry>\n        </mxCell>\n        <mxCell id=\"tVMkS5m_2Bp1rili5dJo-22\" value=\"&lt;font style=&quot;font-size: 34px;&quot;&gt;Application&lt;/font&gt;\" style=\"whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;\" vertex=\"1\" parent=\"1\">\n          <mxGeometry x=\"-220\" y=\"-70\" width=\"220\" height=\"70\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"tVMkS5m_2Bp1rili5dJo-23\" value=\"&lt;font style=&quot;font-size: 34px;&quot;&gt;SubModule&lt;/font&gt;\" style=\"whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;\" vertex=\"1\" parent=\"1\">\n          <mxGeometry x=\"-220\" y=\"150\" width=\"220\" height=\"70\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"tVMkS5m_2Bp1rili5dJo-24\" value=\"&lt;div style=&quot;background-color:#282c34;color:#bbbbbb;font-family:&#39;JetBrains Mono&#39;,monospace;font-size:9.8pt;&quot;&gt;&lt;pre&gt;&lt;br&gt;&lt;/pre&gt;&lt;/div&gt;\" style=\"rounded=0;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;\" vertex=\"1\" parent=\"1\">\n          <mxGeometry x=\"880\" y=\"220\" width=\"800\" height=\"370\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"tVMkS5m_2Bp1rili5dJo-25\" value=\"&lt;font style=&quot;font-size: 20px;&quot;&gt;ActivityPub&lt;/font&gt;\" style=\"text;html=1;strokeColor=#d6b656;fillColor=#fff2cc;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;\" vertex=\"1\" parent=\"1\">\n          <mxGeometry x=\"930\" y=\"200\" width=\"130\" height=\"30\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"tVMkS5m_2Bp1rili5dJo-35\" style=\"edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;\" edge=\"1\" parent=\"1\" source=\"tVMkS5m_2Bp1rili5dJo-26\" target=\"tVMkS5m_2Bp1rili5dJo-28\">\n          <mxGeometry relative=\"1\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"tVMkS5m_2Bp1rili5dJo-36\" style=\"edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;\" edge=\"1\" parent=\"1\" source=\"tVMkS5m_2Bp1rili5dJo-26\" target=\"tVMkS5m_2Bp1rili5dJo-29\">\n          <mxGeometry relative=\"1\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"tVMkS5m_2Bp1rili5dJo-26\" value=\"&lt;font style=&quot;font-size: 19px;&quot;&gt;StatusResolver&lt;/font&gt;\" style=\"whiteSpace=wrap;html=1;\" vertex=\"1\" parent=\"1\">\n          <mxGeometry x=\"1202.5\" y=\"260\" width=\"155\" height=\"60\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"tVMkS5m_2Bp1rili5dJo-37\" style=\"edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;\" edge=\"1\" parent=\"1\" source=\"tVMkS5m_2Bp1rili5dJo-28\" target=\"tVMkS5m_2Bp1rili5dJo-30\">\n          <mxGeometry relative=\"1\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"tVMkS5m_2Bp1rili5dJo-28\" value=\"&lt;font style=&quot;font-size: 19px;&quot;&gt;UserStatusResolver&lt;/font&gt;\" style=\"whiteSpace=wrap;html=1;\" vertex=\"1\" parent=\"1\">\n          <mxGeometry x=\"970\" y=\"390\" width=\"210\" height=\"60\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"tVMkS5m_2Bp1rili5dJo-38\" style=\"edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;\" edge=\"1\" parent=\"1\" source=\"tVMkS5m_2Bp1rili5dJo-29\" target=\"tVMkS5m_2Bp1rili5dJo-31\">\n          <mxGeometry relative=\"1\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"tVMkS5m_2Bp1rili5dJo-29\" value=\"&lt;font style=&quot;font-size: 19px;&quot;&gt;TimelineStatusResolver&lt;/font&gt;\" style=\"whiteSpace=wrap;html=1;\" vertex=\"1\" parent=\"1\">\n          <mxGeometry x=\"1390\" y=\"390\" width=\"230\" height=\"60\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"tVMkS5m_2Bp1rili5dJo-30\" value=\"&lt;font style=&quot;font-size: 19px;&quot;&gt;Status&lt;/font&gt;\" style=\"whiteSpace=wrap;html=1;\" vertex=\"1\" parent=\"1\">\n          <mxGeometry x=\"1012.5\" y=\"500\" width=\"125\" height=\"60\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"tVMkS5m_2Bp1rili5dJo-31\" value=\"&lt;font style=&quot;font-size: 19px;&quot;&gt;Status&lt;/font&gt;\" style=\"whiteSpace=wrap;html=1;\" vertex=\"1\" parent=\"1\">\n          <mxGeometry x=\"1442.5\" y=\"500\" width=\"125\" height=\"60\" as=\"geometry\" />\n        </mxCell>\n      </root>\n    </mxGraphModel>\n  </diagram>\n</mxfile>\n"
  },
  {
    "path": "fastlane/Fastfile",
    "content": "default_platform(:android)\n\nplatform :android do\n  desc \"Build a release bundle (AAB)\"\n  lane :build do\n    gradle(\n      task: \"bundle\",\n      build_type: \"release\"\n    )\n  end\n\nend\n"
  },
  {
    "path": "fastlane/metadata/android/en-US/full_description.txt",
    "content": "Fread is a fediverse microblogging client that supports Mastodon, Bluesky, and RSS.\nWith Fread, you can seamlessly access all three platforms within a single app.\nIt delivers a consistent microblogging experience while preserving the unique features of each network.\nMost importantly, Fread allows you to create unified feeds that blend content across different protocols, breaking down barriers and strengthening decentralization.\nOn top of that, Fread is designed with a focus on beautiful, comfortable UI/UX, offering a smooth and enjoyable experience."
  },
  {
    "path": "fastlane/metadata/android/en-US/short_description.txt",
    "content": "A fediverse microblogging client supporting Mastodon, Bluesky, and RSS."
  },
  {
    "path": "fastlane/metadata/android/zh-CN/full_description.txt",
    "content": "Fread 是一个联邦宇宙 Micro blogging 社交客户端，目前已经支持了 Mastodon、Bluesky、RSS 三种社交平台，这意味着你可以在同一个 App 中同时使用这三种社交平台。\nFread 不仅提供了 Micro blogging 社交的一致性，也保持了不同平台的特色功能。\n更重要的是，Fread 支持创建一个同时包含了三种来自不同平台的 Feeds 流，这打破了协议之间的壁垒，进一步增强了去中心化的能力。\n另外 Fread 也专注于提供漂亮舒适的 UI/UX。"
  },
  {
    "path": "fastlane/metadata/android/zh-CN/short_description.txt",
    "content": "同时支持 Mastodon、Bluesky、RSS 的联邦宇宙客户端"
  },
  {
    "path": "feature/explore/.gitignore",
    "content": "/build"
  },
  {
    "path": "feature/explore/build.gradle.kts",
    "content": "plugins {\n    id(\"fread.project.feature.kmp\")\n    id(\"com.google.devtools.ksp\")\n}\n\nandroid {\n    namespace = \"com.zhangke.fread.explore\"\n}\n\nkotlin {\n    sourceSets {\n        commonMain {\n            dependencies {\n                implementation(project(\":framework\"))\n                implementation(project(\":bizframework:status-provider\"))\n                implementation(project(\":commonbiz:common\"))\n                implementation(project(\":commonbiz:analytics\"))\n                implementation(project(\":commonbiz:sharedscreen\"))\n                implementation(project(\":commonbiz:status-ui\"))\n\n                implementation(compose.components.resources)\n\n                implementation(libs.jetbrains.lifecycle.viewmodel)\n\n                implementation(libs.krouter.runtime)\n            }\n        }\n        commonTest {\n            dependencies {\n                implementation(kotlin(\"test\"))\n            }\n        }\n        androidMain {\n            dependencies {\n                implementation(libs.androidx.core.ktx)\n            }\n        }\n    }\n}\n\ndependencies {\n    kspAll(libs.krouter.collecting.compiler)\n}\n\ncompose {\n    resources {\n        publicResClass = false\n        packageOfResClass = \"com.zhangke.fread.explore\"\n        generateResClass = always\n    }\n}\n"
  },
  {
    "path": "feature/explore/consumer-rules.pro",
    "content": ""
  },
  {
    "path": "feature/explore/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguardFiles setting in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n\n# Uncomment this to preserve the line number information for\n# debugging stack traces.\n#-keepattributes SourceFile,LineNumberTable\n\n# If you keep the line number information, uncomment this to\n# hide the original source file name.\n#-renamesourcefileattribute SourceFile"
  },
  {
    "path": "feature/explore/src/commonMain/kotlin/com/zhangke/fread/explore/ExploreNavEntryProvider.kt",
    "content": "package com.zhangke.fread.explore\n\nimport androidx.navigation3.runtime.EntryProviderScope\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.nav.NavEntryProvider\nimport com.zhangke.fread.explore.screens.search.SearchScreen\nimport com.zhangke.fread.explore.screens.search.SearchScreenNavKey\nimport kotlinx.serialization.modules.PolymorphicModuleBuilder\nimport kotlinx.serialization.modules.subclass\n\nclass ExploreNavEntryProvider : NavEntryProvider {\n\n    override fun EntryProviderScope<NavKey>.build() {\n        entry<SearchScreenNavKey> { key ->\n            SearchScreen(\n                locator = key.locator,\n                protocol = key.protocol,\n                query = key.query,\n            )\n        }\n    }\n\n    override fun PolymorphicModuleBuilder<NavKey>.polymorph() {\n        subclass(SearchScreenNavKey::class)\n    }\n}\n"
  },
  {
    "path": "feature/explore/src/commonMain/kotlin/com/zhangke/fread/explore/ExplorerElements.kt",
    "content": "package com.zhangke.fread.explore\n\nobject ExplorerElements {\n\n    const val SEARCH = \"explorerSearch\"\n\n    const val SWITCH_ACCOUNT = \"explorerSwitchAccount\"\n}\n"
  },
  {
    "path": "feature/explore/src/commonMain/kotlin/com/zhangke/fread/explore/di/ExploreModule.kt",
    "content": "package com.zhangke.fread.explore.di\n\nimport com.zhangke.framework.nav.NavEntryProvider\nimport com.zhangke.fread.explore.ExploreNavEntryProvider\nimport com.zhangke.fread.explore.screens.home.ExplorerHomeViewModel\nimport com.zhangke.fread.explore.screens.search.SearchViewModel\nimport com.zhangke.fread.explore.screens.search.author.SearchAuthorViewModel\nimport com.zhangke.fread.explore.screens.search.bar.SearchBarViewModel\nimport com.zhangke.fread.explore.screens.search.hashtag.SearchHashtagViewModel\nimport com.zhangke.fread.explore.screens.search.platform.SearchPlatformViewModel\nimport com.zhangke.fread.explore.screens.search.status.SearchStatusViewModel\nimport com.zhangke.fread.explore.usecase.BuildSearchResultUiStateUseCase\nimport org.koin.core.module.dsl.factoryOf\nimport org.koin.core.module.dsl.viewModelOf\nimport org.koin.dsl.bind\nimport org.koin.dsl.module\n\nval exploreModule = module {\n\n    factoryOf(::ExploreNavEntryProvider) bind NavEntryProvider::class\n\n    factoryOf(::BuildSearchResultUiStateUseCase)\n\n    viewModelOf(::ExplorerHomeViewModel)\n    viewModelOf(::SearchViewModel)\n    viewModelOf(::SearchBarViewModel)\n    viewModelOf(::SearchAuthorViewModel)\n    viewModelOf(::SearchHashtagViewModel)\n    viewModelOf(::SearchPlatformViewModel)\n    viewModelOf(::SearchStatusViewModel)\n}\n"
  },
  {
    "path": "feature/explore/src/commonMain/kotlin/com/zhangke/fread/explore/model/ExplorerItem.kt",
    "content": "package com.zhangke.fread.explore.model\n\nimport com.zhangke.fread.status.model.StatusUiState\nimport com.zhangke.fread.status.author.BlogAuthor\nimport com.zhangke.fread.status.model.Hashtag\n\nsealed interface ExplorerItem {\n\n    val id: String\n\n    data class ExplorerStatus(val status: StatusUiState) :\n        ExplorerItem {\n        override val id: String\n            get() = status.status.id\n    }\n\n    data class ExplorerHashtag(val hashtag: Hashtag) :\n        ExplorerItem {\n        override val id: String\n            get() = hashtag.name\n    }\n\n    data class ExplorerUser(val user: BlogAuthor, val following: Boolean) :\n        ExplorerItem {\n        override val id: String\n            get() = user.uri.toString()\n    }\n}\n"
  },
  {
    "path": "feature/explore/src/commonMain/kotlin/com/zhangke/fread/explore/screens/home/ExploreTab.kt",
    "content": "package com.zhangke.fread.explore.screens.home\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport com.zhangke.framework.nav.BaseTab\nimport com.zhangke.framework.nav.TabOptions\nimport com.zhangke.fread.commonbiz.Res\nimport com.zhangke.fread.commonbiz.ic_explorer\nimport org.jetbrains.compose.resources.painterResource\n\nclass ExploreTab() : BaseTab() {\n\n    override val options: TabOptions\n        @Composable get() {\n            val icon = painterResource(Res.drawable.ic_explorer)\n            return remember {\n                TabOptions(\n                    title = \"Explore\",\n                    icon = icon,\n                )\n            }\n        }\n\n    @Composable\n    override fun Content() {\n        super.Content()\n        ExplorerScreen()\n    }\n}\n"
  },
  {
    "path": "feature/explore/src/commonMain/kotlin/com/zhangke/fread/explore/screens/home/ExplorerHomeUiState.kt",
    "content": "package com.zhangke.fread.explore.screens.home\n\nimport com.zhangke.framework.nav.Tab\nimport com.zhangke.fread.status.account.LoggedAccount\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.platform.BlogPlatform\n\ndata class ExplorerHomeUiState(\n    val selectedAccount: LoggedAccount?,\n    val accountWithTabList: List<Pair<LoggedAccount, Tab>>,\n) {\n\n    val locator: PlatformLocator?\n        get() {\n            if (selectedAccount == null) return null\n            return PlatformLocator(\n                baseUrl = selectedAccount.platform.baseUrl,\n                accountUri = selectedAccount.uri,\n            )\n        }\n\n    val platform: BlogPlatform? get() = selectedAccount?.platform\n\n    val tab: Tab?\n        get() {\n            return accountWithTabList.firstOrNull { it.first.uri == selectedAccount?.uri }?.second\n        }\n\n    companion object {\n\n        fun default(): ExplorerHomeUiState {\n            return ExplorerHomeUiState(\n                selectedAccount = null,\n                accountWithTabList = emptyList(),\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "feature/explore/src/commonMain/kotlin/com/zhangke/fread/explore/screens/home/ExplorerHomeViewModel.kt",
    "content": "package com.zhangke.fread.explore.screens.home\n\nimport androidx.lifecycle.ViewModel\nimport com.zhangke.framework.ktx.launchInViewModel\nimport com.zhangke.framework.nav.Tab\nimport com.zhangke.fread.common.account.ActiveAccountsSynchronizer\nimport com.zhangke.fread.status.StatusProvider\nimport com.zhangke.fread.status.account.LoggedAccount\nimport com.zhangke.fread.status.model.PlatformLocator\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.mapNotNull\nimport kotlinx.coroutines.flow.update\n\nclass ExplorerHomeViewModel(\n    private val statusProvider: StatusProvider,\n    private val activeAccountsSynchronizer: ActiveAccountsSynchronizer,\n) : ViewModel() {\n\n    private val _uiState = MutableStateFlow(ExplorerHomeUiState.default())\n    val uiState = _uiState.asStateFlow()\n\n    init {\n        launchInViewModel {\n            statusProvider.accountManager\n                .getAllAccountFlow()\n                .collect { accountsList ->\n                    val currentUiState = _uiState.value\n                    var selectedAccount = currentUiState.selectedAccount\n                    if (selectedAccount == null) {\n                        val latestSelectedAccount =\n                            activeAccountsSynchronizer.activeAccountUriFlow.value\n                        selectedAccount =\n                            accountsList.firstOrNull { it.uri.toString() == latestSelectedAccount }\n                    }\n                    if (selectedAccount == null) {\n                        selectedAccount = accountsList.firstOrNull()\n                    }\n                    val newUiState = currentUiState.copy(\n                        accountWithTabList = convertContentsToWithTab(accountsList),\n                        selectedAccount = selectedAccount,\n                    )\n                    _uiState.value = newUiState\n                }\n        }\n        launchInViewModel {\n            activeAccountsSynchronizer.activeAccountUriFlow\n                .mapNotNull { it?.takeIf { it.isNotEmpty() } }\n                .collect { lastActiveAccountUri ->\n                    val accounts = uiState.value.accountWithTabList.map { it.first }\n                    val selectedAccount =\n                        accounts.firstOrNull { it.uri.toString() == lastActiveAccountUri }\n                    if (selectedAccount != null && selectedAccount.uri != uiState.value.selectedAccount?.uri) {\n                        _uiState.update { it.copy(selectedAccount = selectedAccount) }\n                    }\n                }\n        }\n    }\n\n    fun onAccountSelected(account: LoggedAccount) {\n        if (account.uri == uiState.value.selectedAccount?.uri) return\n        launchInViewModel {\n            _uiState.update { it.copy(selectedAccount = account) }\n            activeAccountsSynchronizer.onAccountSelected(account.uri.toString())\n        }\n    }\n\n    private fun convertContentsToWithTab(accounts: List<LoggedAccount>): List<Pair<LoggedAccount, Tab>> {\n        return accounts.mapNotNull { account ->\n            statusProvider.screenProvider.getExplorerTab(\n                locator = PlatformLocator(\n                    baseUrl = account.platform.baseUrl,\n                    accountUri = account.uri,\n                ),\n                platform = account.platform,\n            )?.let { account to it }\n        }\n    }\n}\n"
  },
  {
    "path": "feature/explore/src/commonMain/kotlin/com/zhangke/fread/explore/screens/home/ExplorerScreen.kt",
    "content": "package com.zhangke.fread.explore.screens.home\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.SnackbarHost\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.key\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport com.zhangke.framework.composable.LocalContentPadding\nimport com.zhangke.framework.composable.LocalSnackbarHostState\nimport com.zhangke.framework.composable.currentOrThrow\nimport com.zhangke.framework.composable.rememberSnackbarHostState\nimport com.zhangke.framework.composable.updateTopPadding\nimport com.zhangke.framework.nav.LocalNavBackStack\nimport com.zhangke.fread.common.composable.EmptyContent\nimport com.zhangke.fread.commonbiz.shared.LocalModuleScreenVisitor\nimport com.zhangke.fread.explore.screens.search.bar.ExplorerSearchBar\nimport com.zhangke.fread.status.account.LoggedAccount\nimport com.zhangke.fread.status.ui.common.LocalNestedTabConnection\nimport com.zhangke.fread.status.ui.common.NestedTabConnection\nimport org.koin.compose.viewmodel.koinViewModel\n\n@Composable\nfun ExplorerScreen() {\n    val backstack = LocalNavBackStack.currentOrThrow\n    val feedsScreenVisitor = LocalModuleScreenVisitor.current.feedsScreenVisitor\n    val viewModel = koinViewModel<ExplorerHomeViewModel>()\n    val uiState by viewModel.uiState.collectAsState()\n    ExplorerHomeContent(\n        uiState = uiState,\n        onAccountSelected = viewModel::onAccountSelected,\n        onAccContentClick = {\n            backstack.add(feedsScreenVisitor.getAddContentScreen())\n        },\n    )\n}\n\n@Composable\nprivate fun ExplorerHomeContent(\n    uiState: ExplorerHomeUiState,\n    onAccountSelected: (LoggedAccount) -> Unit,\n    onAccContentClick: () -> Unit,\n) {\n    val snackbarHostState = rememberSnackbarHostState()\n    var topBarHeight: Dp by remember { mutableStateOf(40.dp) }\n    Box(\n        modifier = Modifier.fillMaxSize()\n            .background(MaterialTheme.colorScheme.background)\n    ) {\n        var searchBarActive by remember { mutableStateOf(false) }\n        CompositionLocalProvider(\n            LocalContentPadding provides updateTopPadding(topBarHeight)\n        ) {\n            if (!searchBarActive) {\n                Box(modifier = Modifier.fillMaxSize()) {\n                    key(uiState.tab) {\n                        val tab = uiState.tab\n                        if (tab != null) {\n                            val nestedTabConnection = remember { NestedTabConnection() }\n                            CompositionLocalProvider(\n                                LocalSnackbarHostState provides snackbarHostState,\n                                LocalNestedTabConnection provides nestedTabConnection,\n                            ) {\n                                tab.Content()\n                            }\n                        } else {\n                            EmptyContent(\n                                modifier = Modifier.fillMaxSize(),\n                                onClick = onAccContentClick,\n                            )\n                        }\n                    }\n                }\n            }\n        }\n        ExplorerSearchBar(\n            selectedAccount = uiState.selectedAccount,\n            accountList = uiState.accountWithTabList.map { it.first },\n            onAccountSelected = onAccountSelected,\n            onHeightChanged = { topBarHeight = it },\n            onActiveChanged = { searchBarActive = it },\n        )\n        SnackbarHost(\n            hostState = snackbarHostState,\n            modifier = Modifier.align(Alignment.BottomCenter)\n                .padding(bottom = LocalContentPadding.current.calculateBottomPadding() + 16.dp),\n        )\n    }\n}\n"
  },
  {
    "path": "feature/explore/src/commonMain/kotlin/com/zhangke/fread/explore/screens/search/SearchScreen.kt",
    "content": "package com.zhangke.fread.explore.screens.search\n\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.SnackbarHost\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.composable.currentOrThrow\nimport com.zhangke.framework.composable.LocalSnackbarHostState\nimport com.zhangke.framework.composable.Toolbar\nimport com.zhangke.framework.composable.rememberSnackbarHostState\nimport com.zhangke.framework.nav.HorizontalPagerWithTab\nimport com.zhangke.framework.nav.LocalNavBackStack\nimport com.zhangke.fread.explore.screens.search.author.SearchedAuthorTab\nimport com.zhangke.fread.explore.screens.search.hashtag.SearchedHashtagTab\nimport com.zhangke.fread.explore.screens.search.platform.SearchedPlatformTab\nimport com.zhangke.fread.explore.screens.search.status.SearchedStatusTab\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.model.StatusProviderProtocol\nimport com.zhangke.fread.status.model.isActivityPub\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class SearchScreenNavKey(\n    val locator: PlatformLocator,\n    val protocol: StatusProviderProtocol,\n    val query: String,\n) : NavKey\n\n@Composable\nfun SearchScreen(\n    locator: PlatformLocator,\n    protocol: StatusProviderProtocol,\n    query: String,\n) {\n    val backStack = LocalNavBackStack.currentOrThrow\n    SearchScreenContent(\n        locator = locator,\n        protocol = protocol,\n        query = query,\n        onBackClick = backStack::removeLastOrNull,\n    )\n}\n\n@Composable\nprivate fun SearchScreenContent(\n    locator: PlatformLocator,\n    protocol: StatusProviderProtocol,\n    query: String,\n    onBackClick: () -> Unit,\n) {\n    val snackbarHostState = rememberSnackbarHostState()\n    Scaffold(\n        modifier = Modifier.fillMaxSize(),\n        topBar = {\n            Toolbar(\n                onBackClick = onBackClick,\n                title = query,\n            )\n        },\n        snackbarHost = {\n            SnackbarHost(snackbarHostState)\n        },\n    ) { paddingValues ->\n        val tabs = remember(locator, protocol, query) {\n            buildList {\n                add(SearchedAuthorTab(locator, query))\n                add(SearchedStatusTab(locator, query))\n                if (protocol.isActivityPub) {\n                    add(SearchedPlatformTab(locator, query))\n                }\n                add(SearchedHashtagTab(locator, query))\n            }\n        }\n        Column(\n            modifier = Modifier\n                .fillMaxWidth()\n                .padding(paddingValues)\n        ) {\n            CompositionLocalProvider(\n                LocalSnackbarHostState provides snackbarHostState\n            ) {\n                HorizontalPagerWithTab(tabList = tabs)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "feature/explore/src/commonMain/kotlin/com/zhangke/fread/explore/screens/search/SearchUiState.kt",
    "content": "package com.zhangke.fread.explore.screens.search\n\nclass SearchUiState {\n}"
  },
  {
    "path": "feature/explore/src/commonMain/kotlin/com/zhangke/fread/explore/screens/search/SearchViewModel.kt",
    "content": "package com.zhangke.fread.explore.screens.search\n\nimport androidx.lifecycle.ViewModel\n\nclass SearchViewModel(): ViewModel() {\n\n}"
  },
  {
    "path": "feature/explore/src/commonMain/kotlin/com/zhangke/fread/explore/screens/search/author/SearchAuthorViewModel.kt",
    "content": "package com.zhangke.fread.explore.screens.search.author\n\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.composable.TextString\nimport com.zhangke.framework.controller.CommonLoadableController\nimport com.zhangke.framework.controller.CommonLoadableUiState\nimport com.zhangke.framework.ktx.launchInViewModel\nimport com.zhangke.fread.status.StatusProvider\nimport com.zhangke.fread.status.author.BlogAuthor\nimport com.zhangke.fread.status.model.PlatformLocator\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.SharedFlow\nimport kotlinx.coroutines.flow.StateFlow\nopen class SearchAuthorViewModel(\n    private val statusProvider: StatusProvider,\n    val locator: PlatformLocator,\n) : ViewModel() {\n\n    private val _snackMessageFlow = MutableSharedFlow<TextString>()\n    val snackMessageFlow: SharedFlow<TextString> get() = _snackMessageFlow\n\n    private val loadableController = CommonLoadableController<BlogAuthor>(\n        viewModelScope,\n        onPostSnackMessage = {\n            launchInViewModel {\n                _snackMessageFlow.emit(it)\n            }\n        })\n    val uiState: StateFlow<CommonLoadableUiState<BlogAuthor>> get() = loadableController.uiState\n\n    private val _openScreenFlow = MutableSharedFlow<NavKey>()\n    val openScreenFlow: SharedFlow<NavKey> get() = _openScreenFlow\n\n    fun initQuery(query: String) {\n        if (loadableController.uiState.value.dataList.isNotEmpty()) return\n        onRefresh(query)\n    }\n\n    fun onRefresh(query: String) {\n        loadableController.onRefresh {\n            statusProvider.searchEngine.searchAuthor(locator, query, null)\n        }\n    }\n\n    fun onLoadMore(query: String) {\n        val offset = uiState.value.dataList.size\n        if (offset == 0) return\n        loadableController.onLoadMore {\n            statusProvider.searchEngine.searchAuthor(locator, query, offset)\n        }\n    }\n\n    fun onUserInfoClick(blogAuthor: BlogAuthor) {\n        launchInViewModel {\n            statusProvider.screenProvider\n                .getUserDetailScreen(locator, blogAuthor.uri, blogAuthor.userId)\n                ?.let { _openScreenFlow.emit(it) }\n        }\n    }\n}"
  },
  {
    "path": "feature/explore/src/commonMain/kotlin/com/zhangke/fread/explore/screens/search/author/SearchedAuthorTab.kt",
    "content": "package com.zhangke.fread.explore.screens.search.author\n\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.material.ExperimentalMaterialApi\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.input.nestedscroll.NestedScrollConnection\nimport com.zhangke.framework.composable.ConsumeFlow\nimport com.zhangke.framework.composable.ConsumeSnackbarFlow\nimport com.zhangke.framework.composable.LocalSnackbarHostState\nimport com.zhangke.framework.composable.applyNestedScrollConnection\nimport com.zhangke.framework.composable.currentOrThrow\nimport com.zhangke.framework.controller.CommonLoadableUiState\nimport com.zhangke.framework.loadable.lazycolumn.LoadableInlineVideoLazyColumn\nimport com.zhangke.framework.loadable.lazycolumn.rememberLoadableInlineVideoLazyColumnState\nimport com.zhangke.framework.nav.BaseTab\nimport com.zhangke.framework.nav.LocalNavBackStack\nimport com.zhangke.framework.nav.TabOptions\nimport com.zhangke.fread.common.browser.LocalActivityBrowserLauncher\nimport com.zhangke.fread.common.browser.launchWebTabInApp\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.author.BlogAuthor\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.ui.BlogAuthorUi\nimport org.jetbrains.compose.resources.stringResource\nimport org.koin.compose.viewmodel.koinViewModel\nimport org.koin.core.parameter.parametersOf\n\ninternal class SearchedAuthorTab(\n    private val locator: PlatformLocator,\n    private val query: String,\n) : BaseTab() {\n\n    override val options: TabOptions\n        @Composable get() = TabOptions(\n            title = stringResource(LocalizedString.explorerSearchTabTitleAuthor),\n        )\n\n    @Composable\n    override fun Content() {\n        super.Content()\n        val backStack = LocalNavBackStack.currentOrThrow\n        val viewModel = koinViewModel<SearchAuthorViewModel> { parametersOf(locator) }\n        val uiState by viewModel.uiState.collectAsState()\n\n        val snackbarHostState = LocalSnackbarHostState.current\n        LaunchedEffect(query) {\n            viewModel.initQuery(query)\n        }\n\n        SearchedAuthorContent(\n            uiState = uiState,\n            onRefresh = {\n                viewModel.onRefresh(query)\n            },\n            onLoadMore = {\n                viewModel.onLoadMore(query)\n            },\n            onUserInfoClick = viewModel::onUserInfoClick,\n            nestedScrollConnection = null,\n        )\n        ConsumeFlow(viewModel.openScreenFlow) {\n            backStack.add(it)\n        }\n        ConsumeSnackbarFlow(\n            hostState = snackbarHostState,\n            messageTextFlow = viewModel.snackMessageFlow\n        )\n    }\n\n    @OptIn(ExperimentalMaterialApi::class)\n    @Composable\n    private fun SearchedAuthorContent(\n        uiState: CommonLoadableUiState<BlogAuthor>,\n        onRefresh: () -> Unit,\n        onLoadMore: () -> Unit,\n        onUserInfoClick: (BlogAuthor) -> Unit,\n        nestedScrollConnection: NestedScrollConnection?,\n    ) {\n        val browserLauncher = LocalActivityBrowserLauncher.current\n        val coroutineScope = rememberCoroutineScope()\n        val state = rememberLoadableInlineVideoLazyColumnState(\n            onRefresh = onRefresh,\n            onLoadMore = onLoadMore,\n        )\n        LoadableInlineVideoLazyColumn(\n            modifier = Modifier\n                .fillMaxSize()\n                .applyNestedScrollConnection(nestedScrollConnection),\n            state = state,\n            refreshing = uiState.refreshing,\n            loadState = uiState.loadMoreState,\n        ) {\n            itemsIndexed(uiState.dataList) { _, item ->\n                BlogAuthorUi(\n                    modifier = Modifier.fillMaxWidth(),\n                    author = item,\n                    onClick = onUserInfoClick,\n                    onUrlClick = {\n                        browserLauncher.launchWebTabInApp(coroutineScope, it, locator)\n                    },\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "feature/explore/src/commonMain/kotlin/com/zhangke/fread/explore/screens/search/bar/ExplorerSearchBar.kt",
    "content": "package com.zhangke.fread.explore.screens.search.bar\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.ArrowDropDown\nimport androidx.compose.material.icons.filled.Clear\nimport androidx.compose.material.icons.filled.Search\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.SearchBarDefaults\nimport androidx.compose.material3.SnackbarHost\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.ExperimentalComposeUiApi\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.focus.onFocusChanged\nimport androidx.compose.ui.layout.onSizeChanged\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport com.zhangke.framework.blur.applyBlurEffect\nimport com.zhangke.framework.blur.blurEffectContainerColor\nimport com.zhangke.framework.composable.BackHandler\nimport com.zhangke.framework.composable.ConsumeFlow\nimport com.zhangke.framework.composable.ConsumeSnackbarFlow\nimport com.zhangke.framework.composable.InsetAwareSearchBar\nimport com.zhangke.framework.composable.SimpleIconButton\nimport com.zhangke.framework.composable.TextString\nimport com.zhangke.framework.composable.Toolbar\nimport com.zhangke.framework.composable.currentOrThrow\nimport com.zhangke.framework.composable.inline.InlineVideoLazyColumn\nimport com.zhangke.framework.composable.noRippleClick\nimport com.zhangke.framework.composable.rememberSnackbarHostState\nimport com.zhangke.framework.nav.LocalNavBackStack\nimport com.zhangke.framework.utils.pxToDp\nimport com.zhangke.fread.commonbiz.shared.composable.SearchResultUi\nimport com.zhangke.fread.explore.screens.search.SearchScreenNavKey\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.account.LoggedAccount\nimport com.zhangke.fread.status.ui.BlogAuthorAvatar\nimport com.zhangke.fread.status.ui.ComposedStatusInteraction\nimport com.zhangke.fread.status.ui.common.SelectAccountDialog\nimport kotlinx.coroutines.flow.Flow\nimport org.jetbrains.compose.resources.stringResource\nimport org.koin.compose.viewmodel.koinViewModel\n\n@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)\n@Composable\nfun ExplorerSearchBar(\n    selectedAccount: LoggedAccount?,\n    accountList: List<LoggedAccount>,\n    onAccountSelected: (LoggedAccount) -> Unit,\n    onHeightChanged: (Dp) -> Unit,\n    onActiveChanged: (Boolean) -> Unit,\n) {\n    val density = LocalDensity.current\n    val backStack = LocalNavBackStack.currentOrThrow\n    var active by rememberSaveable { mutableStateOf(false) }\n    val viewModel = koinViewModel<SearchBarViewModel>()\n    LaunchedEffect(selectedAccount) {\n        viewModel.selectedAccount = selectedAccount\n    }\n    val uiState by viewModel.uiState.collectAsState()\n    LaunchedEffect(active) {\n        onActiveChanged(active)\n        if (!active) {\n            viewModel.onSearchQueryChanged(\"\")\n        }\n    }\n    val containerColor = MaterialTheme.colorScheme.surface\n    InsetAwareSearchBar(\n        modifier = Modifier\n            .fillMaxWidth()\n            .applyBlurEffect(enabled = !active, containerColor = containerColor)\n            .onSizeChanged {\n                if (!active) {\n                    onHeightChanged(it.height.pxToDp(density))\n                }\n            }\n            .padding(horizontal = 16.dp),\n        insetContainerColor = blurEffectContainerColor(!active, containerColor),\n        colors = SearchBarDefaults.colors(\n            containerColor = blurEffectContainerColor(!active, containerColor),\n        ),\n        inputField = {\n            SearchBarDefaults.InputField(\n                modifier = Modifier.onFocusChanged {\n                    if (it.hasFocus && !active) {\n                        active = true\n                    }\n                },\n                query = uiState.query,\n                onQueryChange = viewModel::onSearchQueryChanged,\n                onSearch = {\n                    val locator = uiState.locator\n                    val protocol = uiState.account?.platform?.protocol\n                    if (locator != null && protocol != null) {\n                        backStack.add(\n                            SearchScreenNavKey(\n                                locator = locator,\n                                protocol = protocol,\n                                query = uiState.query,\n                            )\n                        )\n                    }\n                },\n                expanded = active,\n                onExpandedChange = { active = it },\n                placeholder = {\n                    if (selectedAccount != null && accountList.size > 1) {\n                        Text(\n                            modifier = Modifier,\n                            text = stringResource(\n                                LocalizedString.explorerSearchBarHintSpecializePlatform,\n                                selectedAccount.platform.baseUrl.host,\n                            ),\n                            overflow = TextOverflow.Ellipsis,\n                        )\n                    } else {\n                        Text(\n                            modifier = Modifier,\n                            text = stringResource(LocalizedString.explorerSearchBarHint),\n                        )\n                    }\n                },\n                leadingIcon = {\n                    if (active) {\n                        Toolbar.BackButton(onBackClick = { active = false })\n                    } else {\n                        SimpleIconButton(\n                            onClick = { active = true },\n                            imageVector = Icons.Default.Search,\n                            contentDescription = \"Back\",\n                        )\n                    }\n                },\n                trailingIcon = {\n                    SearchBarTrailing(\n                        active = active,\n                        selectedAccount = selectedAccount,\n                        onClearClick = { viewModel.onSearchQueryChanged(\"\") },\n                        accountList = accountList,\n                        onAccountSelected = onAccountSelected,\n                    )\n                },\n            )\n        },\n        expanded = active,\n        onExpandedChange = { active = it },\n        content = {\n            if (active) {\n                BackHandler(true) {\n                    active = false\n                }\n                SearchContent(\n                    uiState = uiState,\n                    snackbarMessageFlow = viewModel.errorMessageFlow,\n                    composedStatusInteraction = viewModel.composedStatusInteraction,\n                )\n            }\n        },\n    )\n    ConsumeFlow(viewModel.openScreenFlow) {\n        backStack.add(it)\n    }\n}\n\n@Composable\nprivate fun SearchContent(\n    uiState: SearchBarUiState,\n    snackbarMessageFlow: Flow<TextString>,\n    composedStatusInteraction: ComposedStatusInteraction,\n) {\n    Box(modifier = Modifier.fillMaxSize()) {\n        val state = rememberLazyListState()\n        InlineVideoLazyColumn(\n            modifier = Modifier.fillMaxSize(),\n            state = state,\n        ) {\n            itemsIndexed(uiState.resultList) { index, item ->\n                SearchResultUi(\n                    modifier = Modifier.fillMaxWidth(),\n                    searchResult = item,\n                    indexInList = index,\n                    composedStatusInteraction = composedStatusInteraction,\n                )\n            }\n        }\n\n        val snackbarHostState = rememberSnackbarHostState()\n        SnackbarHost(\n            hostState = snackbarHostState,\n            modifier = Modifier.align(Alignment.Center),\n        )\n        ConsumeSnackbarFlow(snackbarHostState, snackbarMessageFlow)\n    }\n}\n\n@Composable\nprivate fun SearchBarTrailing(\n    active: Boolean,\n    selectedAccount: LoggedAccount?,\n    onClearClick: () -> Unit,\n    accountList: List<LoggedAccount>,\n    onAccountSelected: (LoggedAccount) -> Unit,\n) {\n    if (active) {\n        SimpleIconButton(\n            onClick = onClearClick,\n            imageVector = Icons.Default.Clear,\n            contentDescription = \"Clear Query\",\n        )\n    } else {\n        if (accountList.size > 1) {\n            var showSelectAccountPopup by remember {\n                mutableStateOf(false)\n            }\n            Row(\n                modifier = Modifier\n                    .padding(end = 8.dp)\n                    .background(\n                        color = MaterialTheme.colorScheme.secondaryContainer,\n                        shape = RoundedCornerShape(23.dp),\n                    )\n                    .noRippleClick { showSelectAccountPopup = true },\n                verticalAlignment = Alignment.CenterVertically,\n            ) {\n                Spacer(modifier = Modifier.width(8.dp))\n                Icon(\n                    imageVector = Icons.Default.ArrowDropDown,\n                    contentDescription = \"Select Account\",\n                )\n                Spacer(modifier = Modifier.width(4.dp))\n                BlogAuthorAvatar(\n                    modifier = Modifier.size(40.dp),\n                    imageUrl = selectedAccount?.avatar,\n                )\n            }\n            if (showSelectAccountPopup) {\n                SelectAccountDialog(\n                    accountList = accountList,\n                    selectedAccounts = selectedAccount?.let { listOf(it) } ?: emptyList(),\n                    onDismissRequest = { showSelectAccountPopup = false },\n                    onAccountClicked = onAccountSelected,\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "feature/explore/src/commonMain/kotlin/com/zhangke/fread/explore/screens/search/bar/SearchBarUiState.kt",
    "content": "package com.zhangke.fread.explore.screens.search.bar\n\nimport com.zhangke.fread.common.status.model.SearchResultUiState\nimport com.zhangke.fread.status.account.LoggedAccount\nimport com.zhangke.fread.status.model.PlatformLocator\n\ndata class SearchBarUiState(\n    val locator: PlatformLocator?,\n    val account: LoggedAccount?,\n    val query: String,\n    val resultList: List<SearchResultUiState>,\n)\n"
  },
  {
    "path": "feature/explore/src/commonMain/kotlin/com/zhangke/fread/explore/screens/search/bar/SearchBarViewModel.kt",
    "content": "package com.zhangke.fread.explore.screens.search.bar\n\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.zhangke.framework.ktx.launchInViewModel\nimport com.zhangke.fread.common.adapter.StatusUiStateAdapter\nimport com.zhangke.fread.common.status.StatusUpdater\nimport com.zhangke.fread.common.status.model.SearchResultUiState\nimport com.zhangke.fread.commonbiz.shared.feeds.IInteractiveHandler\nimport com.zhangke.fread.commonbiz.shared.feeds.InteractiveHandleResult\nimport com.zhangke.fread.commonbiz.shared.feeds.InteractiveHandler\nimport com.zhangke.fread.commonbiz.shared.feeds.handle\nimport com.zhangke.fread.commonbiz.shared.usecase.RefactorToNewStatusUseCase\nimport com.zhangke.fread.explore.usecase.BuildSearchResultUiStateUseCase\nimport com.zhangke.fread.status.StatusProvider\nimport com.zhangke.fread.status.account.LoggedAccount\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.richtext.preParse\nimport com.zhangke.fread.status.search.SearchResult\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.update\n\nclass SearchBarViewModel(\n    private val statusProvider: StatusProvider,\n    statusUpdater: StatusUpdater,\n    private val buildSearchResultUiState: BuildSearchResultUiStateUseCase,\n    statusUiStateAdapter: StatusUiStateAdapter,\n    refactorToNewStatus: RefactorToNewStatusUseCase,\n) : ViewModel(), IInteractiveHandler by InteractiveHandler(\n    statusProvider = statusProvider,\n    statusUpdater = statusUpdater,\n    statusUiStateAdapter = statusUiStateAdapter,\n    refactorToNewStatus = refactorToNewStatus,\n) {\n\n    var selectedAccount: LoggedAccount? = null\n        set(value) {\n            if (field == value) return\n            field = value\n            _uiState.update {\n                it.copy(\n                    locator = locator,\n                    query = \"\",\n                    account = value,\n                    resultList = emptyList(),\n                )\n            }\n        }\n\n    private val locator: PlatformLocator?\n        get() {\n            val account = selectedAccount ?: return null\n            return PlatformLocator(\n                baseUrl = account.platform.baseUrl,\n                accountUri = account.uri,\n            )\n        }\n\n    private var searchJob: Job? = null\n\n    private val _uiState = MutableStateFlow(\n        SearchBarUiState(\n            locator = locator,\n            query = \"\",\n            account = null,\n            resultList = emptyList(),\n        )\n    )\n    val uiState = _uiState.asStateFlow()\n\n    init {\n        initInteractiveHandler(\n            coroutineScope = viewModelScope,\n            onInteractiveHandleResult = { it.handleResult() },\n        )\n    }\n\n    fun onSearchQueryChanged(query: String) {\n        if (query.isEmpty()) {\n            searchJob?.cancel()\n            _uiState.update { it.copy(query = \"\", resultList = emptyList()) }\n            return\n        }\n        val locator = locator ?: return\n        if (query == _uiState.value.query) return\n        _uiState.update { it.copy(query = query) }\n        searchJob?.cancel()\n        searchJob = launchInViewModel {\n            statusProvider.searchEngine\n                .search(locator, query)\n                .map { list ->\n                    list.filterIsInstance<SearchResult.SearchedStatus>()\n                        .map { it.status }\n                        .preParse()\n                    list\n                }.map { list ->\n                    list.map { buildSearchResultUiState(locator, it) }\n                }.onSuccess { searchResult ->\n                    _uiState.update {\n                        it.copy(resultList = searchResult)\n                    }\n                }\n        }\n    }\n\n    private suspend fun InteractiveHandleResult.handleResult() {\n        handle(\n            uiStatusUpdater = { newUiState ->\n                _uiState.update { currentUiState ->\n                    currentUiState.copy(\n                        resultList = currentUiState.resultList.map {\n                            if (it !is SearchResultUiState.SearchedStatus) return@map it\n                            if (it.status.status.intrinsicBlog.id != newUiState.status.intrinsicBlog.id) return@map it\n                            return@map it.copy(status = newUiState)\n                        }\n                    )\n                }\n            },\n            deleteStatus = { statusId ->\n                _uiState.update { state ->\n                    state.copy(\n                        resultList = state.resultList.filter {\n                            when (it) {\n                                is SearchResultUiState.SearchedStatus -> {\n                                    it.status.status.id != statusId\n                                }\n\n                                else -> true\n                            }\n                        }\n                    )\n                }\n            },\n            followStateUpdater = { _, _ ->\n\n            }\n        )\n    }\n}\n"
  },
  {
    "path": "feature/explore/src/commonMain/kotlin/com/zhangke/fread/explore/screens/search/hashtag/SearchHashtagViewModel.kt",
    "content": "package com.zhangke.fread.explore.screens.search.hashtag\n\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.composable.TextString\nimport com.zhangke.framework.controller.CommonLoadableController\nimport com.zhangke.framework.controller.CommonLoadableUiState\nimport com.zhangke.framework.ktx.launchInViewModel\nimport com.zhangke.fread.status.StatusProvider\nimport com.zhangke.fread.status.model.Hashtag\nimport com.zhangke.fread.status.model.PlatformLocator\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.SharedFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.asSharedFlow\nopen class SearchHashtagViewModel(\n    private val statusProvider: StatusProvider,\n    private val locator: PlatformLocator,\n) : ViewModel() {\n\n    private val _snackMessageFlow = MutableSharedFlow<TextString>()\n    val snackMessageFlow: SharedFlow<TextString> get() = _snackMessageFlow\n\n    private val loadableController = CommonLoadableController<Hashtag>(\n        viewModelScope,\n        onPostSnackMessage = {\n            launchInViewModel {\n                _snackMessageFlow.emit(it)\n            }\n        },\n    )\n\n    val uiState: StateFlow<CommonLoadableUiState<Hashtag>> get() = loadableController.uiState\n\n    private val _openScreenFlow = MutableSharedFlow<NavKey>()\n    val openScreenFlow = _openScreenFlow.asSharedFlow()\n\n    fun initQuery(query: String) {\n        if (uiState.value.dataList.isNotEmpty()) return\n        onRefresh(query)\n    }\n\n    fun onRefresh(query: String) {\n        loadableController.onRefresh {\n            statusProvider.searchEngine.searchHashtag(locator, query, null)\n        }\n    }\n\n    fun onLoadMore(query: String) {\n        val offset = uiState.value.dataList.size\n        if (offset == 0) return\n        loadableController.onLoadMore {\n            statusProvider.searchEngine.searchHashtag(locator, query, offset)\n        }\n    }\n\n    fun onHashtagClick(hashtag: Hashtag) {\n        launchInViewModel {\n            val screen = statusProvider.screenProvider.getTagTimelineScreen(\n                locator,\n                hashtag.name,\n                hashtag.protocol\n            ) ?: return@launchInViewModel\n            _openScreenFlow.emit(screen)\n        }\n    }\n}"
  },
  {
    "path": "feature/explore/src/commonMain/kotlin/com/zhangke/fread/explore/screens/search/hashtag/SearchedHashtagTab.kt",
    "content": "package com.zhangke.fread.explore.screens.search.hashtag\n\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.material.ExperimentalMaterialApi\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.input.nestedscroll.NestedScrollConnection\nimport com.zhangke.framework.composable.currentOrThrow\nimport com.zhangke.framework.composable.ConsumeFlow\nimport com.zhangke.framework.composable.ConsumeSnackbarFlow\nimport com.zhangke.framework.composable.LocalSnackbarHostState\nimport com.zhangke.framework.composable.applyNestedScrollConnection\nimport com.zhangke.framework.controller.CommonLoadableUiState\nimport com.zhangke.framework.loadable.lazycolumn.LoadableInlineVideoLazyColumn\nimport com.zhangke.framework.loadable.lazycolumn.rememberLoadableInlineVideoLazyColumnState\nimport com.zhangke.framework.nav.BaseTab\nimport com.zhangke.framework.nav.LocalNavBackStack\nimport com.zhangke.framework.nav.TabOptions\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.model.Hashtag\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.ui.hashtag.HashtagUi\nimport org.jetbrains.compose.resources.stringResource\nimport org.koin.compose.viewmodel.koinViewModel\nimport org.koin.core.parameter.parametersOf\n\ninternal class SearchedHashtagTab(\n    private val locator: PlatformLocator,\n    private val query: String\n) : BaseTab() {\n\n    override val options: TabOptions\n        @Composable get() = TabOptions(\n            title = stringResource(LocalizedString.explorerSearchTabTitleHashtag),\n        )\n\n    @Composable\n    override fun Content() {\n        super.Content()\n        val backStack = LocalNavBackStack.currentOrThrow\n        val viewModel = koinViewModel<SearchHashtagViewModel> { parametersOf(locator) }\n        val uiState by viewModel.uiState.collectAsState()\n\n        val snackbarHostState = LocalSnackbarHostState.current\n\n        LaunchedEffect(query) {\n            viewModel.initQuery(query)\n        }\n\n        SearchedHashtagContent(\n            uiState = uiState,\n            onRefresh = {\n                viewModel.onRefresh(query)\n            },\n            onLoadMore = {\n                viewModel.onLoadMore(query)\n            },\n            onHashtagClick = viewModel::onHashtagClick,\n            nestedScrollConnection = null,\n        )\n        ConsumeFlow(viewModel.openScreenFlow) {\n            backStack.add(it)\n        }\n        ConsumeSnackbarFlow(\n            hostState = snackbarHostState,\n            messageTextFlow = viewModel.snackMessageFlow\n        )\n    }\n\n    @OptIn(ExperimentalMaterialApi::class)\n    @Composable\n    private fun SearchedHashtagContent(\n        uiState: CommonLoadableUiState<Hashtag>,\n        onRefresh: () -> Unit,\n        onLoadMore: () -> Unit,\n        onHashtagClick: (Hashtag) -> Unit,\n        nestedScrollConnection: NestedScrollConnection?\n    ) {\n        val state = rememberLoadableInlineVideoLazyColumnState(\n            onRefresh = onRefresh,\n            onLoadMore = onLoadMore,\n        )\n        LoadableInlineVideoLazyColumn(\n            modifier = Modifier\n                .fillMaxSize()\n                .applyNestedScrollConnection(nestedScrollConnection),\n            state = state,\n            refreshing = uiState.refreshing,\n            loadState = uiState.loadMoreState,\n        ) {\n            itemsIndexed(uiState.dataList) { _, item ->\n                HashtagUi(\n                    modifier = Modifier.fillMaxWidth(),\n                    tag = item,\n                    onClick = onHashtagClick,\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "feature/explore/src/commonMain/kotlin/com/zhangke/fread/explore/screens/search/platform/SearchPlatformViewModel.kt",
    "content": "package com.zhangke.fread.explore.screens.search.platform\n\nimport androidx.lifecycle.ViewModel\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.composable.emitInViewModel\nimport com.zhangke.framework.ktx.launchInViewModel\nimport com.zhangke.framework.network.FormalBaseUrl\nimport com.zhangke.fread.status.StatusProvider\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.search.SearchedPlatform\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.SharedFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.update\nopen class SearchPlatformViewModel(\n    private val statusProvider: StatusProvider,\n    private val locator: PlatformLocator,\n    private val query: String,\n) : ViewModel() {\n\n    private val _uiState = MutableStateFlow(SearchedPlatformUiState.default())\n    val uiState: StateFlow<SearchedPlatformUiState> get() = _uiState\n\n    private val _openScreenFlow = MutableSharedFlow<NavKey>()\n    val openScreenFlow: SharedFlow<NavKey> get() = _openScreenFlow\n\n    init {\n        launchInViewModel {\n            _uiState.update { it.copy(searching = true) }\n            statusProvider.searchEngine\n                .searchPlatform(locator, query.trim())\n                .collect { results ->\n                    _uiState.update {\n                        it.copy(\n                            searching = false,\n                            searchedList = it.searchedList + results,\n                        )\n                    }\n                }\n        }\n    }\n\n    fun onContentClick(result: SearchedPlatform) {\n        val baseUrl = when (result) {\n            is SearchedPlatform.Platform -> result.platform.baseUrl\n\n            is SearchedPlatform.Snapshot -> {\n                FormalBaseUrl.parse(result.snapshot.domain) ?: return\n            }\n        }\n        val protocol = when (result) {\n            is SearchedPlatform.Platform -> result.platform.protocol\n            is SearchedPlatform.Snapshot -> result.snapshot.protocol\n        }\n        statusProvider.screenProvider.getInstanceDetailScreen(\n            baseUrl = baseUrl,\n            protocol = protocol,\n            locator = locator,\n        )?.let { _openScreenFlow.emitInViewModel(it) }\n    }\n}"
  },
  {
    "path": "feature/explore/src/commonMain/kotlin/com/zhangke/fread/explore/screens/search/platform/SearchedPlatformTab.kt",
    "content": "package com.zhangke.fread.explore.screens.search.platform\n\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.ui.Modifier\nimport com.zhangke.framework.composable.ConsumeOpenScreenFlow\nimport com.zhangke.framework.composable.DefaultLoading\nimport com.zhangke.framework.nav.BaseTab\nimport com.zhangke.framework.nav.TabOptions\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.search.SearchedPlatform\nimport com.zhangke.fread.status.ui.source.SearchPlatformResultUi\nimport org.jetbrains.compose.resources.stringResource\nimport org.koin.compose.viewmodel.koinViewModel\nimport org.koin.core.parameter.parametersOf\n\ninternal class SearchedPlatformTab(\n    private val locator: PlatformLocator,\n    private val query: String,\n) : BaseTab() {\n\n    override val options: TabOptions\n        @Composable get() = TabOptions(\n            title = stringResource(LocalizedString.explorerSearchTabTitleServer),\n        )\n\n    @Composable\n    override fun Content() {\n        super.Content()\n        val viewModel = koinViewModel<SearchPlatformViewModel> {\n            parametersOf(locator, query)\n        }\n        val uiState by viewModel.uiState.collectAsState()\n        SearchedSourcesContent(\n            uiState = uiState,\n            onContentClick = viewModel::onContentClick,\n        )\n        ConsumeOpenScreenFlow(viewModel.openScreenFlow)\n    }\n\n    @Composable\n    private fun SearchedSourcesContent(\n        uiState: SearchedPlatformUiState,\n        onContentClick: (SearchedPlatform) -> Unit,\n    ) {\n        Box(modifier = Modifier.fillMaxSize()) {\n            if (uiState.searching && uiState.searchedList.isEmpty()) {\n                DefaultLoading()\n            } else {\n                LazyColumn(modifier = Modifier.fillMaxSize()) {\n                    items(uiState.searchedList) {\n                        SearchPlatformResultUi(searchedResult = it, onContentClick = onContentClick)\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "feature/explore/src/commonMain/kotlin/com/zhangke/fread/explore/screens/search/platform/SearchedPlatformUiState.kt",
    "content": "package com.zhangke.fread.explore.screens.search.platform\n\nimport com.zhangke.fread.status.search.SearchedPlatform\n\ndata class SearchedPlatformUiState(\n    val searchedList: List<SearchedPlatform>,\n    val searching: Boolean,\n){\n\n    companion object{\n\n        fun default() = SearchedPlatformUiState(\n            searchedList = emptyList(),\n            searching = false,\n        )\n    }\n}"
  },
  {
    "path": "feature/explore/src/commonMain/kotlin/com/zhangke/fread/explore/screens/search/status/SearchStatusViewModel.kt",
    "content": "package com.zhangke.fread.explore.screens.search.status\n\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.zhangke.framework.controller.CommonLoadableUiState\nimport com.zhangke.fread.common.adapter.StatusUiStateAdapter\nimport com.zhangke.fread.common.status.StatusUpdater\nimport com.zhangke.fread.commonbiz.shared.feeds.IInteractiveHandler\nimport com.zhangke.fread.commonbiz.shared.feeds.InteractiveHandleResult\nimport com.zhangke.fread.commonbiz.shared.feeds.InteractiveHandler\nimport com.zhangke.fread.commonbiz.shared.usecase.RefactorToNewStatusUseCase\nimport com.zhangke.fread.commonbiz.shared.utils.LoadableStatusController\nimport com.zhangke.fread.status.StatusProvider\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.model.StatusUiState\nimport com.zhangke.fread.status.model.updateStatus\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.update\nclass SearchStatusViewModel(\n    private val statusProvider: StatusProvider,\n    statusUpdater: StatusUpdater,\n    statusUiStateAdapter: StatusUiStateAdapter,\n    refactorToNewStatus: RefactorToNewStatusUseCase,\n    val locator: PlatformLocator,\n) : ViewModel(), IInteractiveHandler by InteractiveHandler(\n    statusProvider = statusProvider,\n    statusUpdater = statusUpdater,\n    statusUiStateAdapter = statusUiStateAdapter,\n    refactorToNewStatus = refactorToNewStatus,\n) {\n\n    private val loadStatusController = LoadableStatusController(viewModelScope)\n\n    val uiState: StateFlow<CommonLoadableUiState<StatusUiState>> get() = loadStatusController.uiState\n\n    init {\n        initInteractiveHandler(\n            coroutineScope = viewModelScope,\n            onInteractiveHandleResult = { result ->\n                when (result) {\n                    is InteractiveHandleResult.UpdateStatus -> {\n                        loadStatusController.mutableUiState.update { state ->\n                            state.copy(\n                                dataList = state.dataList.updateStatus(result.status),\n                            )\n                        }\n                    }\n\n                    is InteractiveHandleResult.DeleteStatus -> {\n                        loadStatusController.mutableUiState.update { state ->\n                            state.copy(\n                                dataList = state.dataList.filter { it.status.id != result.statusId },\n                            )\n                        }\n                    }\n\n                    is InteractiveHandleResult.UpdateFollowState -> {\n                        // no-op\n                    }\n                }\n            }\n        )\n    }\n\n    fun initQuery(query: String) {\n        if (uiState.value.dataList.isNotEmpty()) return\n        onRefresh(query)\n    }\n\n    fun onRefresh(query: String) {\n        loadStatusController.onRefresh(locator) {\n            statusProvider.searchEngine\n                .searchStatus(locator, query, null)\n        }\n    }\n\n    fun onLoadMore(query: String) {\n        loadStatusController.onLoadMore(locator) {\n            statusProvider.searchEngine.searchStatus(locator, query, it)\n        }\n    }\n}\n"
  },
  {
    "path": "feature/explore/src/commonMain/kotlin/com/zhangke/fread/explore/screens/search/status/SearchedStatusTab.kt",
    "content": "package com.zhangke.fread.explore.screens.search.status\n\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.material.ExperimentalMaterialApi\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.input.nestedscroll.NestedScrollConnection\nimport com.zhangke.framework.composable.currentOrThrow\nimport com.zhangke.framework.composable.ConsumeFlow\nimport com.zhangke.framework.composable.ConsumeSnackbarFlow\nimport com.zhangke.framework.composable.LocalSnackbarHostState\nimport com.zhangke.framework.composable.applyNestedScrollConnection\nimport com.zhangke.framework.controller.CommonLoadableUiState\nimport com.zhangke.framework.loadable.lazycolumn.LoadableInlineVideoLazyColumn\nimport com.zhangke.framework.loadable.lazycolumn.rememberLoadableInlineVideoLazyColumnState\nimport com.zhangke.framework.nav.BaseTab\nimport com.zhangke.framework.nav.LocalNavBackStack\nimport com.zhangke.framework.nav.TabOptions\nimport com.zhangke.fread.commonbiz.shared.composable.FeedsStatusNode\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.model.StatusUiState\nimport com.zhangke.fread.status.ui.ComposedStatusInteraction\nimport org.jetbrains.compose.resources.stringResource\nimport org.koin.compose.viewmodel.koinViewModel\nimport org.koin.core.parameter.parametersOf\n\ninternal class SearchedStatusTab(\n    private val locator: PlatformLocator,\n    private val query: String,\n) : BaseTab() {\n\n    override val options: TabOptions\n        @Composable get() = TabOptions(\n            title = stringResource(LocalizedString.explorerSearchTabTitleStatus),\n        )\n\n    @Composable\n    override fun Content() {\n        super.Content()\n        val backStack = LocalNavBackStack.currentOrThrow\n        val viewModel = koinViewModel<SearchStatusViewModel> { parametersOf(locator) }\n        val uiState by viewModel.uiState.collectAsState()\n\n        LaunchedEffect(query) {\n            viewModel.initQuery(query)\n        }\n\n        SearchStatusTabContent(\n            uiState = uiState,\n            composedStatusInteraction = viewModel.composedStatusInteraction,\n            onRefresh = {\n                viewModel.onRefresh(query)\n            },\n            onLoadMore = {\n                viewModel.onLoadMore(query)\n            },\n            nestedScrollConnection = null,\n        )\n        ConsumeFlow(viewModel.openScreenFlow) {\n            backStack.add(it)\n        }\n        val snackbarHostState = LocalSnackbarHostState.currentOrThrow\n        ConsumeSnackbarFlow(snackbarHostState, viewModel.errorMessageFlow)\n    }\n\n    @OptIn(ExperimentalMaterialApi::class)\n    @Composable\n    private fun SearchStatusTabContent(\n        uiState: CommonLoadableUiState<StatusUiState>,\n        composedStatusInteraction: ComposedStatusInteraction,\n        onRefresh: () -> Unit,\n        onLoadMore: () -> Unit,\n        nestedScrollConnection: NestedScrollConnection?,\n    ) {\n        val state = rememberLoadableInlineVideoLazyColumnState(\n            onRefresh = onRefresh,\n            onLoadMore = onLoadMore,\n        )\n        LoadableInlineVideoLazyColumn(\n            modifier = Modifier\n                .fillMaxSize()\n                .applyNestedScrollConnection(nestedScrollConnection),\n            state = state,\n            refreshing = uiState.refreshing,\n            loadState = uiState.loadMoreState,\n        ) {\n            itemsIndexed(uiState.dataList) { index, item ->\n                FeedsStatusNode(\n                    modifier = Modifier.fillMaxWidth(),\n                    status = item,\n                    indexInList = index,\n                    composedStatusInteraction = composedStatusInteraction,\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "feature/explore/src/commonMain/kotlin/com/zhangke/fread/explore/usecase/BuildSearchResultUiStateUseCase.kt",
    "content": "package com.zhangke.fread.explore.usecase\n\nimport com.zhangke.fread.common.status.model.SearchResultUiState\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.search.SearchResult\n\nclass BuildSearchResultUiStateUseCase() {\n\n    suspend operator fun invoke(locator: PlatformLocator, result: SearchResult): SearchResultUiState {\n        return when (result) {\n            is SearchResult.Author -> {\n                SearchResultUiState.Author(locator, result.user)\n            }\n\n            is SearchResult.Platform -> {\n                SearchResultUiState.Platform(locator, result.platform)\n            }\n\n            is SearchResult.SearchedStatus -> {\n                SearchResultUiState.SearchedStatus(result.status)\n            }\n\n            is SearchResult.SearchedHashtag -> {\n                SearchResultUiState.SearchedHashtag(locator, result.hashtag)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "feature/feeds/.gitignore",
    "content": "/build"
  },
  {
    "path": "feature/feeds/build.gradle.kts",
    "content": "plugins {\n    id(\"fread.project.feature.kmp\")\n    id(\"com.google.devtools.ksp\")\n    id(\"kotlin-parcelize\")\n}\n\nandroid {\n    namespace = \"com.zhangke.fread.feeds\"\n}\n\nkotlin {\n    sourceSets {\n        commonMain {\n            dependencies {\n                implementation(project(\":framework\"))\n                implementation(project(\":bizframework:status-provider\"))\n                implementation(project(\":commonbiz:common\"))\n                implementation(project(\":commonbiz:analytics\"))\n                implementation(project(\":commonbiz:sharedscreen\"))\n                implementation(project(\":commonbiz:status-ui\"))\n\n                implementation(compose.components.resources)\n\n                implementation(libs.kotlinx.serialization.core)\n                implementation(libs.kotlinx.serialization.json)\n\n                implementation(libs.arrow.core)\n\n                implementation(libs.jetbrains.lifecycle.viewmodel)\n\n                implementation(libs.haze)\n\n                implementation(libs.krouter.runtime)\n                implementation(libs.androidx.room)\n                implementation(libs.androidx.constraintlayout.compose.kmp)\n                implementation(libs.auto.service.annotations)\n                implementation(libs.uri.kmp)\n\n                implementation(libs.androidx.paging.common)\n            }\n        }\n        commonTest {\n            dependencies {\n                implementation(kotlin(\"test\"))\n            }\n        }\n        androidMain {\n            dependencies {\n                implementation(libs.androidx.core.ktx)\n                implementation(libs.bundles.androidx.activity)\n                implementation(libs.androidx.browser)\n            }\n        }\n    }\n    configureCommonMainKsp()\n}\n\ndependencies {\n    kspAll(libs.androidx.room.compiler)\n    kspAll(libs.auto.service.ksp)\n    kspAll(libs.krouter.collecting.compiler)\n}\n\ncompose {\n    resources {\n        publicResClass = false\n        packageOfResClass = \"com.zhangke.fread.feeds\"\n        generateResClass = always\n    }\n}\n"
  },
  {
    "path": "feature/feeds/consumer-rules.pro",
    "content": ""
  },
  {
    "path": "feature/feeds/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguardFiles setting in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n\n# Uncomment this to preserve the line number information for\n# debugging stack traces.\n#-keepattributes SourceFile,LineNumberTable\n\n# If you keep the line number information, uncomment this to\n# hide the original source file name.\n#-renamesourcefileattribute SourceFile"
  },
  {
    "path": "feature/feeds/src/androidMain/kotlin/com/zhangke/fread/feeds/pages/manager/importing/OpenDocumentContainer.android.kt",
    "content": "package com.zhangke.fread.feeds.pages.manager.importing\n\nimport android.net.Uri\nimport androidx.activity.compose.ManagedActivityResultLauncher\nimport androidx.activity.compose.rememberLauncherForActivityResult\nimport androidx.activity.result.contract.ActivityResultContracts\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport com.zhangke.framework.utils.PlatformUri\nimport com.zhangke.framework.utils.toPlatformUri\n\n@Composable\nactual fun OpenDocumentContainer(\n    onResult: (PlatformUri) -> Unit,\n    content: @Composable OpenDocumentContainerScope.() -> Unit,\n) {\n    val selectedFileLauncher =\n        rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->\n            uri?.toPlatformUri()?.let { onResult(it) }\n        }\n    val scope = remember {\n        OpenDocumentContainerScope(selectedFileLauncher)\n    }\n    with(scope) {\n        content()\n    }\n}\n\nactual class OpenDocumentContainerScope(\n    private val launcher: ManagedActivityResultLauncher<Array<String>, Uri?>\n) {\n    actual fun launch() {\n        launcher.launch(arrayOf(\"*/*\"))\n    }\n}"
  },
  {
    "path": "feature/feeds/src/commonMain/composeResources/drawable/ic_home.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n  <path\n      android:pathData=\"M12.6,2.2C12.244,1.933 11.756,1.933 11.4,2.2L3.4,8.2C3.148,8.389 3,8.685 3,9V18.718C3,19.275 3.065,19.61 3.252,19.96C3.432,20.297 3.703,20.568 4.04,20.748C4.39,20.935 4.725,21 5.282,21H18.718C19.275,21 19.61,20.935 19.96,20.748C20.297,20.568 20.568,20.297 20.748,19.96C20.935,19.61 21,19.275 21,18.718V9C21,8.685 20.852,8.389 20.6,8.2L12.6,2.2ZM5,9.5L12,4.25L19,9.5V18.718L19,18.803C18.999,18.904 18.997,18.953 18.994,18.981L18.992,18.992L18.981,18.994C18.947,18.998 18.878,19 18.718,19H5.282L5.197,19C5.096,18.999 5.047,18.997 5.019,18.994L5.007,18.992L5.006,18.981C5.003,18.96 5.002,18.927 5.001,18.87L5,9.5Z\"\n      android:fillColor=\"#828FB0\"\n      android:fillType=\"evenOdd\"/>\n  <path\n      android:pathData=\"M10,14L14,14A1,1 0,0 1,15 15L15,15A1,1 0,0 1,14 16L10,16A1,1 0,0 1,9 15L9,15A1,1 0,0 1,10 14z\"\n      android:fillColor=\"#828FB0\"/>\n</vector>\n"
  },
  {
    "path": "feature/feeds/src/commonMain/composeResources/drawable/ic_import.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n  <path\n      android:pathData=\"M20,5H12L10,3H4C2.9,3 2.01,3.9 2.01,5L2,19C2,20.1 2.9,21 4,21H20C21.1,21 22,20.1 22,19V7C22,5.9 21.1,5 20,5ZM20,19H4V5H9.17L11.17,7H20V19ZM9.41,14.42L11,12.84V17H13V12.84L14.59,14.43L16,13.01L12.01,9L8,13.01L9.41,14.42Z\"\n      android:fillColor=\"#000000\"/>\n</vector>\n"
  },
  {
    "path": "feature/feeds/src/commonMain/kotlin/com/zhangke/fread/feeds/FeedsNavEntryProvider.kt",
    "content": "package com.zhangke.fread.feeds\n\nimport androidx.navigation3.runtime.EntryProviderScope\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.nav.NavEntryProvider\nimport com.zhangke.fread.feeds.pages.manager.add.mixed.AddMixedFeedsScreen\nimport com.zhangke.fread.feeds.pages.manager.add.mixed.AddMixedFeedsScreenNavKey\nimport com.zhangke.fread.feeds.pages.manager.add.type.SelectContentTypeScreen\nimport com.zhangke.fread.feeds.pages.manager.add.type.SelectContentTypeScreenNavKey\nimport com.zhangke.fread.feeds.pages.manager.edit.EditMixedContentScreen\nimport com.zhangke.fread.feeds.pages.manager.edit.EditMixedContentScreenNavKey\nimport com.zhangke.fread.feeds.pages.manager.importing.ImportFeedsScreen\nimport com.zhangke.fread.feeds.pages.manager.importing.ImportFeedsScreenNavKey\nimport com.zhangke.fread.feeds.pages.manager.search.SearchSourceForAddScreen\nimport com.zhangke.fread.feeds.pages.manager.search.SearchSourceForAddScreenNavKey\nimport kotlinx.serialization.modules.PolymorphicModuleBuilder\nimport kotlinx.serialization.modules.subclass\nimport org.koin.compose.viewmodel.koinViewModel\nimport org.koin.core.parameter.parametersOf\n\nclass FeedsNavEntryProvider : NavEntryProvider {\n\n    override fun EntryProviderScope<NavKey>.build() {\n        entry<SelectContentTypeScreenNavKey> {\n            SelectContentTypeScreen(koinViewModel())\n        }\n        entry<AddMixedFeedsScreenNavKey> {\n            AddMixedFeedsScreen(koinViewModel { parametersOf(null) })\n        }\n        entry<ImportFeedsScreenNavKey> {\n            ImportFeedsScreen(koinViewModel())\n        }\n        entry<SearchSourceForAddScreenNavKey> {\n            SearchSourceForAddScreen(koinViewModel())\n        }\n        entry<EditMixedContentScreenNavKey> { key ->\n            EditMixedContentScreen(\n                koinViewModel { parametersOf(key.contentId) }\n            )\n        }\n    }\n\n    override fun PolymorphicModuleBuilder<NavKey>.polymorph() {\n        subclass(SelectContentTypeScreenNavKey::class)\n        subclass(AddMixedFeedsScreenNavKey::class)\n        subclass(ImportFeedsScreenNavKey::class)\n        subclass(SearchSourceForAddScreenNavKey::class)\n        subclass(EditMixedContentScreenNavKey::class)\n    }\n}\n"
  },
  {
    "path": "feature/feeds/src/commonMain/kotlin/com/zhangke/fread/feeds/FeedsScreenVisitor.kt",
    "content": "package com.zhangke.fread.feeds\n\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.fread.commonbiz.shared.IFeedsScreenVisitor\nimport com.zhangke.fread.feeds.pages.manager.add.type.SelectContentTypeScreenNavKey\n\nclass FeedsScreenVisitor : IFeedsScreenVisitor {\n\n    override fun getAddContentScreen(): NavKey {\n        return SelectContentTypeScreenNavKey\n    }\n}\n"
  },
  {
    "path": "feature/feeds/src/commonMain/kotlin/com/zhangke/fread/feeds/composable/StatusSource.kt",
    "content": "package com.zhangke.fread.feeds.composable\n\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Delete\nimport androidx.compose.material.icons.rounded.AddCircleOutline\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.vector.rememberVectorPainter\nimport androidx.compose.ui.unit.dp\nimport com.zhangke.framework.composable.FreadDialog\nimport com.zhangke.fread.common.browser.LocalActivityBrowserLauncher\nimport com.zhangke.fread.common.browser.launchWebTabInApp\nimport com.zhangke.fread.common.resources.logo\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.source.StatusSource\nimport com.zhangke.fread.status.ui.utils.CardInfoSection\nimport org.jetbrains.compose.resources.stringResource\n\ndata class StatusSourceUiState(\n    val source: StatusSource,\n    val addEnabled: Boolean,\n    val removeEnabled: Boolean,\n)\n\n@Composable\ninternal fun RemovableStatusSource(\n    modifier: Modifier = Modifier,\n    source: StatusSourceUiState,\n    onClick: () -> Unit,\n    onRemoveClick: () -> Unit,\n) {\n    StatusSourceNode(\n        modifier = modifier,\n        source = source,\n        onClick = onClick,\n        onRemoveClick = onRemoveClick,\n    )\n}\n\n@Composable\ninternal fun StatusSourceNode(\n    modifier: Modifier = Modifier,\n    source: StatusSourceUiState,\n    onClick: () -> Unit,\n    onAddClick: (() -> Unit)? = null,\n    onRemoveClick: (() -> Unit)? = null,\n) {\n    val browserLauncher = LocalActivityBrowserLauncher.current\n    val coroutineScope = rememberCoroutineScope()\n    CardInfoSection(\n        modifier = modifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp),\n        avatar = source.source.thumbnail,\n        title = source.source.name,\n        handle = source.source.handle,\n        description = source.source.description,\n        logo = rememberVectorPainter(source.source.protocol.logo),\n        onClick = onClick,\n        onUrlClick = { browserLauncher.launchWebTabInApp(coroutineScope, it) },\n        actions = {\n            if (source.addEnabled) {\n                IconButton(\n                    modifier = Modifier\n                        .size(24.dp)\n                        .padding(end = 8.dp),\n                    onClick = { onAddClick?.invoke() },\n                    enabled = onAddClick != null,\n                ) {\n                    Icon(\n                        painter = rememberVectorPainter(image = Icons.Rounded.AddCircleOutline),\n                        contentDescription = \"Add\",\n                    )\n                }\n            }\n            if (source.removeEnabled) {\n                var showDeleteConfirmDialog by remember {\n                    mutableStateOf(false)\n                }\n                IconButton(\n                    modifier = Modifier.size(24.dp),\n                    onClick = { showDeleteConfirmDialog = true },\n                    enabled = onRemoveClick != null,\n                ) {\n                    Icon(\n                        painter = rememberVectorPainter(image = Icons.Default.Delete),\n                        contentDescription = \"Remove\",\n                    )\n                }\n                if (showDeleteConfirmDialog) {\n                    FreadDialog(\n                        onDismissRequest = { showDeleteConfirmDialog = false },\n                        contentText = stringResource(LocalizedString.feedsDeleteConfirmContent),\n                        onNegativeClick = {\n                            showDeleteConfirmDialog = false\n                        },\n                        onPositiveClick = {\n                            showDeleteConfirmDialog = false\n                            onRemoveClick?.invoke()\n                        },\n                    )\n                }\n            }\n        }\n    )\n}\n"
  },
  {
    "path": "feature/feeds/src/commonMain/kotlin/com/zhangke/fread/feeds/di/FeedsModule.kt",
    "content": "package com.zhangke.fread.feeds.di\n\nimport com.zhangke.framework.nav.NavEntryProvider\nimport com.zhangke.fread.commonbiz.shared.IFeedsScreenVisitor\nimport com.zhangke.fread.feeds.FeedsNavEntryProvider\nimport com.zhangke.fread.feeds.FeedsScreenVisitor\nimport com.zhangke.fread.feeds.pages.home.ContentHomeViewModel\nimport com.zhangke.fread.feeds.pages.home.feeds.MixedContentViewModel\nimport com.zhangke.fread.feeds.pages.manager.add.mixed.AddMixedFeedsViewModel\nimport com.zhangke.fread.feeds.pages.manager.add.type.SelectContentTypeViewModel\nimport com.zhangke.fread.feeds.pages.manager.edit.EditMixedContentViewModel\nimport com.zhangke.fread.feeds.pages.manager.importing.ImportFeedsViewModel\nimport com.zhangke.fread.feeds.pages.manager.search.SearchSourceForAddViewModel\nimport com.zhangke.fread.status.source.StatusSource\nimport org.koin.core.module.dsl.factoryOf\nimport org.koin.core.module.dsl.singleOf\nimport org.koin.core.module.dsl.viewModel\nimport org.koin.core.module.dsl.viewModelOf\nimport org.koin.dsl.bind\nimport org.koin.dsl.module\n\nval feedsModule = module {\n\n    factoryOf(::FeedsNavEntryProvider) bind NavEntryProvider::class\n\n    viewModelOf(::ContentHomeViewModel)\n    viewModelOf(::MixedContentViewModel)\n    viewModel { params ->\n        AddMixedFeedsViewModel(\n            statusProvider = get(),\n            contentRepo = get(),\n            onboardingComponent = get(),\n            statusSource = params.getOrNull<StatusSource>(),\n        )\n    }\n    viewModelOf(::EditMixedContentViewModel)\n    viewModelOf(::ImportFeedsViewModel)\n    viewModelOf(::SearchSourceForAddViewModel)\n    viewModelOf(::SelectContentTypeViewModel)\n\n    singleOf(::FeedsScreenVisitor) bind IFeedsScreenVisitor::class\n}\n"
  },
  {
    "path": "feature/feeds/src/commonMain/kotlin/com/zhangke/fread/feeds/pages/home/ContentHomeUiState.kt",
    "content": "package com.zhangke.fread.feeds.pages.home\n\nimport com.zhangke.framework.nav.Tab\nimport com.zhangke.fread.status.model.FreadContent\n\ndata class ContentHomeUiState(\n    val currentPageIndex: Int,\n    val loading: Boolean,\n    val contentAndTabList: List<Pair<FreadContent, Tab>>,\n) {\n\n    companion object {\n\n        val default = ContentHomeUiState(\n            currentPageIndex = 0,\n            loading = true,\n            contentAndTabList = emptyList(),\n        )\n    }\n}\n"
  },
  {
    "path": "feature/feeds/src/commonMain/kotlin/com/zhangke/fread/feeds/pages/home/ContentHomeViewModel.kt",
    "content": "package com.zhangke.fread.feeds.pages.home\n\nimport androidx.lifecycle.ViewModel\nimport com.zhangke.framework.ktx.launchInViewModel\nimport com.zhangke.framework.nav.Tab\nimport com.zhangke.fread.common.account.ActiveAccountsSynchronizer\nimport com.zhangke.fread.common.content.FreadContentRepo\nimport com.zhangke.fread.common.deeplink.SelectedContentSwitcher\nimport com.zhangke.fread.feeds.pages.home.feeds.MixedContentTab\nimport com.zhangke.fread.status.StatusProvider\nimport com.zhangke.fread.status.content.MixedContent\nimport com.zhangke.fread.status.model.FreadContent\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.SharedFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.drop\nimport kotlinx.coroutines.flow.map\nimport kotlinx.coroutines.flow.mapNotNull\nimport kotlinx.coroutines.flow.update\n\nclass ContentHomeViewModel(\n    private val contentRepo: FreadContentRepo,\n    private val statusProvider: StatusProvider,\n    private val activeAccountsSynchronizer: ActiveAccountsSynchronizer,\n    private val selectedContentSwitcher: SelectedContentSwitcher,\n) : ViewModel() {\n\n    private val _uiState = MutableStateFlow(ContentHomeUiState.default)\n    val uiState: StateFlow<ContentHomeUiState> = _uiState\n\n    private val _switchPageFlow = MutableSharedFlow<Int>(1)\n    val switchPageFlow: SharedFlow<Int> = _switchPageFlow\n\n    init {\n        launchInViewModel {\n            _uiState.update { it.copy(loading = true) }\n            val allContent = contentRepo.getAllContent()\n            var currentPageIndex = 0\n            val lastActiveAccount = activeAccountsSynchronizer.activeAccountUriFlow.value\n            if (!lastActiveAccount.isNullOrEmpty()) {\n                allContent.indexOfFirst { it.accountUri?.toString() == lastActiveAccount }\n                    .takeIf { it >= 0 }\n                    ?.let { currentPageIndex = it }\n            }\n            _uiState.update { currentState ->\n                currentState.copy(\n                    currentPageIndex = currentPageIndex,\n                    contentAndTabList = convertContentsToWithTab(allContent),\n                    loading = false,\n                )\n            }\n            activeAccountsSynchronizer.activeAccountUriFlow\n                .mapNotNull { it?.takeIf { it.isNotEmpty() } }\n                .collect { uri ->\n                    val activeIndex = _uiState.value.contentAndTabList.indexOfFirst { config ->\n                        config.first.accountUri?.toString() == uri\n                    }\n                    if (activeIndex >= 0 && activeIndex != _uiState.value.currentPageIndex) {\n                        _switchPageFlow.emit(activeIndex)\n                    }\n                }\n        }\n        launchInViewModel {\n            contentRepo.getAllContentFlow()\n                .drop(1)\n                .map { convertContentsToWithTab(it) }\n                .collect {\n                    _uiState.update { currentState ->\n                        currentState.copy(\n                            currentPageIndex = currentState.currentPageIndex.coerceAtMost(it.size - 1),\n                            contentAndTabList = it,\n                        )\n                    }\n                }\n        }\n        launchInViewModel {\n            selectedContentSwitcher.selectedContentFlow.collect {\n                val targetIndex = _uiState.value.contentAndTabList.indexOfFirst { pair ->\n                    pair.first.id == it.id\n                }\n                if (targetIndex >= 0 && targetIndex != _uiState.value.currentPageIndex) {\n                    _switchPageFlow.emit(targetIndex)\n                }\n            }\n        }\n    }\n\n    fun onCurrentPageChanged(currentPage: Int) {\n        val currentState = _uiState.value\n        if (currentPage == currentState.currentPageIndex) return\n        _uiState.update { it.copy(currentPageIndex = currentPage) }\n        launchInViewModel {\n            _uiState.value\n                .contentAndTabList\n                .getOrNull(currentPage)\n                ?.first\n                ?.accountUri\n                ?.toString()\n                ?.let { activeAccountsSynchronizer.onAccountSelected(it) }\n        }\n    }\n\n    private fun convertContentsToWithTab(contents: List<FreadContent>): List<Pair<FreadContent, Tab>> {\n        return contents.mapIndexed { index, content ->\n            content.convertToWithTab(index == contents.lastIndex)\n        }\n    }\n\n    private fun FreadContent.convertToWithTab(isLatestTab: Boolean): Pair<FreadContent, Tab> {\n        if (this is MixedContent) {\n            return this to MixedContentTab(\n                configId = id,\n                isLatestTab = isLatestTab,\n            )\n        }\n        return this to statusProvider.screenProvider.getContentScreen(this, isLatestTab)\n    }\n\n    fun onSwitchPageFlowUsed() {\n        _switchPageFlow.resetReplayCache()\n        selectedContentSwitcher.resetReplayCache()\n    }\n}\n"
  },
  {
    "path": "feature/feeds/src/commonMain/kotlin/com/zhangke/fread/feeds/pages/home/FeedsContentHomeTab.kt",
    "content": "package com.zhangke.fread.feeds.pages.home\n\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.pager.HorizontalPager\nimport androidx.compose.foundation.pager.rememberPagerState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.ui.Modifier\nimport com.zhangke.framework.composable.ConsumeFlow\nimport com.zhangke.framework.composable.currentOrThrow\nimport com.zhangke.framework.nav.LocalNavBackStack\nimport com.zhangke.fread.common.composable.EmptyContent\nimport com.zhangke.fread.feeds.pages.manager.add.type.SelectContentTypeScreenNavKey\nimport com.zhangke.fread.status.ui.common.LocalNestedTabConnection\nimport kotlinx.coroutines.launch\nimport org.koin.compose.viewmodel.koinViewModel\n\n@Composable\nfun FeedsContentHomeScreen() {\n    val backStack = LocalNavBackStack.currentOrThrow\n    val coroutineScope = rememberCoroutineScope()\n    val viewModel: ContentHomeViewModel = koinViewModel()\n    val uiState by viewModel.uiState.collectAsState()\n    if (uiState.contentAndTabList.isEmpty()) {\n        if (uiState.loading) {\n            Box(modifier = Modifier.fillMaxSize())\n        } else {\n            EmptyContent(modifier = Modifier.fillMaxSize()) {\n                backStack.add(SelectContentTypeScreenNavKey)\n            }\n        }\n    } else {\n        val mainTabConnection = LocalNestedTabConnection.current\n        val pagerState = rememberPagerState(\n            initialPage = uiState.currentPageIndex.coerceAtLeast(0),\n            pageCount = { uiState.contentAndTabList.size },\n        )\n        ConsumeFlow(mainTabConnection.switchToNextTabFlow) {\n            if (pagerState.currentPage < pagerState.pageCount - 1) {\n                coroutineScope.launch {\n                    pagerState.animateScrollToPage(pagerState.currentPage + 1)\n                }\n            }\n        }\n        ConsumeFlow(mainTabConnection.scrollToContentTabFlow) { content ->\n            val index = uiState.contentAndTabList.indexOfFirst { it.first == content }\n            if (index in 0 until pagerState.pageCount) {\n                coroutineScope.launch {\n                    pagerState.animateScrollToPage(index)\n                }\n            }\n        }\n        ConsumeFlow(viewModel.switchPageFlow) {\n            coroutineScope.launch {\n                viewModel.onSwitchPageFlowUsed()\n                pagerState.animateScrollToPage(it)\n            }\n        }\n        val targetPage = pagerState.targetPage\n        LaunchedEffect(targetPage) {\n            viewModel.onCurrentPageChanged(targetPage)\n        }\n        val contentScrollInProgress by mainTabConnection.contentScrollInpProgress.collectAsState()\n        HorizontalPager(\n            state = pagerState,\n            userScrollEnabled = !contentScrollInProgress,\n        ) { pageIndex ->\n            val currentScreen = uiState.contentAndTabList[pageIndex].second\n            with(currentScreen) {\n                Content()\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "feature/feeds/src/commonMain/kotlin/com/zhangke/fread/feeds/pages/home/FeedsHomeTab.kt",
    "content": "package com.zhangke.fread.feeds.pages.home\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport com.zhangke.framework.nav.Tab\nimport com.zhangke.framework.nav.TabOptions\nimport com.zhangke.fread.feeds.Res\nimport com.zhangke.fread.feeds.ic_home\nimport org.jetbrains.compose.resources.painterResource\n\nclass FeedsHomeTab : Tab {\n\n    override val options: TabOptions\n        @Composable get() {\n            val icon = painterResource(Res.drawable.ic_home)\n            return remember {\n                TabOptions(\n                    title = \"Home\",\n                    icon = icon,\n                )\n            }\n        }\n\n    @Composable\n    override fun Content() {\n        FeedsContentHomeScreen()\n    }\n}\n"
  },
  {
    "path": "feature/feeds/src/commonMain/kotlin/com/zhangke/fread/feeds/pages/home/feeds/MixedContentSubViewModel.kt",
    "content": "package com.zhangke.fread.feeds.pages.home.feeds\n\nimport com.zhangke.framework.collections.updateItem\nimport com.zhangke.framework.composable.emitTextMessageFromThrowable\nimport com.zhangke.framework.composable.toTextStringOrNull\nimport com.zhangke.framework.ktx.launchInViewModel\nimport com.zhangke.framework.lifecycle.SubViewModel\nimport com.zhangke.framework.utils.LoadState\nimport com.zhangke.fread.common.adapter.StatusUiStateAdapter\nimport com.zhangke.fread.common.config.FreadConfigManager\nimport com.zhangke.fread.common.content.FreadContentRepo\nimport com.zhangke.fread.common.mixed.MixedStatusRepo\nimport com.zhangke.fread.common.status.StatusUpdater\nimport com.zhangke.fread.commonbiz.shared.feeds.IInteractiveHandler\nimport com.zhangke.fread.commonbiz.shared.feeds.InteractiveHandleResult\nimport com.zhangke.fread.commonbiz.shared.feeds.InteractiveHandler\nimport com.zhangke.fread.commonbiz.shared.usecase.RefactorToNewStatusUseCase\nimport com.zhangke.fread.feeds.pages.manager.edit.EditMixedContentScreenNavKey\nimport com.zhangke.fread.status.StatusProvider\nimport com.zhangke.fread.status.author.updateFollowingState\nimport com.zhangke.fread.status.content.MixedContent\nimport com.zhangke.fread.status.model.StatusUiState\nimport com.zhangke.fread.status.model.updateBlogAuthor\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.drop\nimport kotlinx.coroutines.flow.mapNotNull\nimport kotlinx.coroutines.flow.update\nimport kotlinx.coroutines.launch\n\nclass MixedContentSubViewModel(\n    private val contentRepo: FreadContentRepo,\n    private val mixedRepo: MixedStatusRepo,\n    statusUpdater: StatusUpdater,\n    private val freadConfigManager: FreadConfigManager,\n    private val statusUiStateAdapter: StatusUiStateAdapter,\n    private val statusProvider: StatusProvider,\n    private val refactorToNewStatus: RefactorToNewStatusUseCase,\n    private val configId: String,\n) : SubViewModel(), IInteractiveHandler by InteractiveHandler(\n    statusProvider = statusProvider,\n    statusUpdater = statusUpdater,\n    statusUiStateAdapter = statusUiStateAdapter,\n    refactorToNewStatus = refactorToNewStatus,\n) {\n\n    private val _uiState = MutableStateFlow(MixedContentUiState.default())\n    val uiState = _uiState.asStateFlow()\n\n    private var refreshJob: Job? = null\n    private var loadMoreJob: Job? = null\n\n    init {\n        initInteractiveHandler(\n            coroutineScope = viewModelScope,\n            onInteractiveHandleResult = { interaction ->\n                when (interaction) {\n                    is InteractiveHandleResult.UpdateStatus -> {\n                        _uiState.update { state ->\n                            state.copy(\n                                dataList = state.dataList\n                                    .updateItem(interaction.status) { interaction.status }\n                            )\n                        }\n                        mixedRepo.updateStatus(interaction.status)\n                    }\n\n                    is InteractiveHandleResult.DeleteStatus -> {\n                        _uiState.update { state ->\n                            state.copy(\n                                dataList = state.dataList\n                                    .filter { it.status.id != interaction.statusId }\n                            )\n                        }\n                        mixedRepo.deleteStatus(interaction.statusId)\n                    }\n\n                    is InteractiveHandleResult.UpdateFollowState -> {\n                        var updatedStatus: StatusUiState? = null\n                        _uiState.update { state ->\n                            state.copy(\n                                dataList = state.dataList.map { status ->\n                                    if (status.status.intrinsicBlog.author.uri == interaction.userUri) {\n                                        status.updateBlogAuthor {\n                                            it.updateFollowingState(interaction.following)\n                                        }.also {\n                                            updatedStatus = it\n                                        }\n                                    } else {\n                                        status\n                                    }\n                                }\n                            )\n                        }\n                        updatedStatus?.let { mixedRepo.updateStatus(it) }\n                    }\n                }\n            },\n        )\n        launchInViewModel {\n            _uiState.update { it.copy(initializing = true) }\n            val mixedContent = contentRepo.getContent(configId) as? MixedContent\n            if (mixedContent == null) {\n                _uiState.update {\n                    it.copy(\n                        pageError = IllegalStateException(\"Content($configId) does not exists!\"),\n                        initializing = false,\n                    )\n                }\n            } else {\n                _uiState.update { it.copy(content = mixedContent) }\n                launch { mixedRepo.refresh(mixedContent) }\n                mixedRepo.getLocalStatusFlow(mixedContent)\n                    .collect { data ->\n                        _uiState.update { it.copy(dataList = data, initializing = false) }\n                    }\n            }\n        }\n\n        launchInViewModel {\n            contentRepo.getContentFlow(configId)\n                .drop(1)\n                .mapNotNull { it as? MixedContent }\n                .collect { content ->\n                    delay(50)\n                    _uiState.update { it.copy(content = content) }\n                    mixedRepo.refresh(content)\n                }\n        }\n        launchInViewModel {\n            freadConfigManager.homeTabRefreshButtonVisibleFlow\n                .collect { visible ->\n                    _uiState.update { it.copy(showRefreshButton = visible) }\n                }\n        }\n        launchInViewModel {\n            freadConfigManager.homeTabNextButtonVisibleFlow\n                .collect { visible ->\n                    _uiState.update { it.copy(showNextButton = visible) }\n                }\n        }\n    }\n\n    fun onContentTitleClick() {\n        val mixedContent = uiState.value.content ?: return\n        launchInViewModel {\n            mutableOpenScreenFlow.emit(EditMixedContentScreenNavKey(mixedContent.id))\n        }\n    }\n\n    fun onRefresh() {\n        val content = uiState.value.content ?: return\n        if (refreshJob?.isActive == true || loadMoreJob?.isActive == true) return\n        refreshJob?.cancel()\n        loadMoreJob?.cancel()\n        refreshJob = launchInViewModel {\n            _uiState.update { it.copy(refreshing = true) }\n            mixedRepo.refresh(content)\n                .onSuccess {\n                    _uiState.update { it.copy(refreshing = false) }\n                }.onFailure { t ->\n                    _uiState.update { it.copy(refreshing = false) }\n                    mutableErrorMessageFlow.emitTextMessageFromThrowable(t)\n                }\n        }\n    }\n\n    fun onLoadMore() {\n        val content = uiState.value.content ?: return\n        if (refreshJob?.isActive == true || loadMoreJob?.isActive == true) return\n        refreshJob?.cancel()\n        loadMoreJob?.cancel()\n        loadMoreJob = launchInViewModel {\n            _uiState.update { it.copy(loadMoreState = LoadState.Loading) }\n            mixedRepo.loadMoreStatus(content)\n                .onSuccess {\n                    _uiState.update { it.copy(loadMoreState = LoadState.Idle) }\n                }.onFailure { t ->\n                    _uiState.update { it.copy(loadMoreState = LoadState.Failed(t.toTextStringOrNull())) }\n                    mutableErrorMessageFlow.emitTextMessageFromThrowable(t)\n                }\n        }\n    }\n}\n"
  },
  {
    "path": "feature/feeds/src/commonMain/kotlin/com/zhangke/fread/feeds/pages/home/feeds/MixedContentTab.kt",
    "content": "package com.zhangke.fread.feeds.pages.home.feeds\n\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.SnackbarHost\nimport androidx.compose.material3.SnackbarHostState\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBarDefaults\nimport androidx.compose.material3.rememberTopAppBarState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.input.nestedscroll.nestedScroll\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport com.zhangke.framework.composable.ConsumeOpenScreenFlow\nimport com.zhangke.framework.composable.ConsumeSnackbarFlow\nimport com.zhangke.framework.composable.LocalContentPadding\nimport com.zhangke.framework.composable.ToolbarTokens\nimport com.zhangke.framework.composable.noRippleClick\nimport com.zhangke.framework.composable.rememberSnackbarHostState\nimport com.zhangke.framework.nav.Tab\nimport com.zhangke.framework.nav.TabOptions\nimport com.zhangke.fread.commonbiz.shared.composable.FeedsContent\nimport com.zhangke.fread.status.ui.ComposedStatusInteraction\nimport com.zhangke.fread.status.ui.common.ContentToolbar\nimport com.zhangke.fread.status.ui.common.LocalNestedTabConnection\nimport com.zhangke.fread.status.ui.common.doubleTapToScrollTop\nimport kotlinx.coroutines.launch\nimport org.koin.compose.viewmodel.koinViewModel\n\ninternal class MixedContentTab(\n    private val configId: String,\n    private val isLatestTab: Boolean,\n) : Tab {\n\n    override val options: TabOptions?\n        @Composable get() = null\n\n    @Composable\n    override fun Content() {\n        val snackBarHostState = rememberSnackbarHostState()\n        val viewModel = koinViewModel<MixedContentViewModel>().getSubViewModel(configId)\n        val uiState by viewModel.uiState.collectAsState()\n        ConsumeSnackbarFlow(snackBarHostState, viewModel.errorMessageFlow)\n        ConsumeOpenScreenFlow(viewModel.openScreenFlow)\n        MixedContentUi(\n            uiState = uiState,\n            snackBarHostState = snackBarHostState,\n            composedStatusInteraction = viewModel.composedStatusInteraction,\n            onTitleClick = viewModel::onContentTitleClick,\n            onRefresh = viewModel::onRefresh,\n            onLoadMore = viewModel::onLoadMore,\n        )\n    }\n\n    @OptIn(ExperimentalMaterial3Api::class)\n    @Composable\n    private fun MixedContentUi(\n        uiState: MixedContentUiState,\n        snackBarHostState: SnackbarHostState,\n        onTitleClick: () -> Unit,\n        onRefresh: () -> Unit,\n        onLoadMore: () -> Unit,\n        composedStatusInteraction: ComposedStatusInteraction,\n    ) {\n        val mainTabConnection = LocalNestedTabConnection.current\n        val coroutineScope = rememberCoroutineScope()\n        val topBarState = rememberTopAppBarState()\n        val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(topBarState)\n        Scaffold(\n            modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),\n            topBar = {\n                ContentToolbar(\n                    modifier = Modifier.doubleTapToScrollTop {\n                        coroutineScope.launch {\n                            mainTabConnection.scrollToTop()\n                        }\n                    },\n                    scrollBehavior = scrollBehavior,\n                    title = uiState.content?.name.orEmpty(),\n                    showNextIcon = !isLatestTab && uiState.showNextButton,\n                    showRefreshButton = uiState.showRefreshButton,\n                    account = null,\n                    showAccountInfo = false,\n                    onMenuClick = {\n                        coroutineScope.launch {\n                            mainTabConnection.openDrawer()\n                        }\n                    },\n                    onNextClick = {\n                        coroutineScope.launch {\n                            mainTabConnection.switchToNextTab()\n                        }\n                    },\n                    onRefreshClick = {\n                        coroutineScope.launch {\n                            mainTabConnection.scrollToTop()\n                            onRefresh()\n                        }\n                    },\n                    onTitleClick = onTitleClick,\n                    onDoubleClick = {\n                        coroutineScope.launch {\n                            mainTabConnection.scrollToTop()\n                        }\n                    }\n                )\n            },\n            snackbarHost = {\n                SnackbarHost(\n                    modifier = Modifier.padding(bottom = 68.dp),\n                    hostState = snackBarHostState,\n                )\n            },\n        ) { paddings ->\n            CompositionLocalProvider(\n                LocalContentPadding provides paddings\n            ) {\n                Box(modifier = Modifier.fillMaxSize()) {\n                    val nestedTabConnection = LocalNestedTabConnection.current\n                    FeedsContent(\n                        feeds = uiState.dataList,\n                        refreshing = uiState.refreshing,\n                        loadMoreState = uiState.loadMoreState,\n                        showPagingLoadingPlaceholder = uiState.initializing,\n                        pageErrorContent = uiState.pageError,\n                        newStatusNotifyFlow = null,\n                        onRefresh = onRefresh,\n                        onLoadMore = onLoadMore,\n                        composedStatusInteraction = composedStatusInteraction,\n                        observeScrollToTopEvent = true,\n                        onScrollToTopConsumed = {\n                            topBarState.contentOffset = 0F\n                            topBarState.heightOffset = 0F\n                        },\n                        onImmersiveEvent = {\n                            if (it) {\n                                mainTabConnection.openImmersiveMode(coroutineScope)\n                            } else {\n                                mainTabConnection.closeImmersiveMode(coroutineScope)\n                            }\n                        },\n                        onScrollInProgress = {\n                            nestedTabConnection.updateContentScrollInProgress(it)\n                        },\n                    )\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "feature/feeds/src/commonMain/kotlin/com/zhangke/fread/feeds/pages/home/feeds/MixedContentUiState.kt",
    "content": "package com.zhangke.fread.feeds.pages.home.feeds\n\nimport com.zhangke.framework.utils.LoadState\nimport com.zhangke.fread.status.content.MixedContent\nimport com.zhangke.fread.status.model.StatusUiState\n\ndata class MixedContentUiState(\n    val content: MixedContent?,\n    val dataList: List<StatusUiState>,\n    val initializing: Boolean,\n    val refreshing: Boolean,\n    val loadMoreState: LoadState,\n    val pageError: Throwable?,\n    val showRefreshButton: Boolean,\n    val showNextButton: Boolean,\n) {\n\n    companion object {\n\n        fun default(): MixedContentUiState {\n            return MixedContentUiState(\n                content = null,\n                dataList = emptyList(),\n                initializing = true,\n                refreshing = false,\n                loadMoreState = LoadState.Idle,\n                pageError = null,\n                showRefreshButton = false,\n                showNextButton = false,\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "feature/feeds/src/commonMain/kotlin/com/zhangke/fread/feeds/pages/home/feeds/MixedContentViewModel.kt",
    "content": "package com.zhangke.fread.feeds.pages.home.feeds\n\nimport com.zhangke.framework.lifecycle.ContainerViewModel\nimport com.zhangke.fread.common.adapter.StatusUiStateAdapter\nimport com.zhangke.fread.common.config.FreadConfigManager\nimport com.zhangke.fread.common.content.FreadContentRepo\nimport com.zhangke.fread.common.mixed.MixedStatusRepo\nimport com.zhangke.fread.common.status.StatusUpdater\nimport com.zhangke.fread.commonbiz.shared.usecase.RefactorToNewStatusUseCase\nimport com.zhangke.fread.status.StatusProvider\n\nclass MixedContentViewModel(\n    private val contentRepo: FreadContentRepo,\n    private val mixedRepo: MixedStatusRepo,\n    private val statusUpdater: StatusUpdater,\n    private val freadConfigManager: FreadConfigManager,\n    private val statusUiStateAdapter: StatusUiStateAdapter,\n    private val statusProvider: StatusProvider,\n    private val refactorToNewStatus: RefactorToNewStatusUseCase,\n) : ContainerViewModel<MixedContentSubViewModel, MixedContentViewModel.Params>() {\n\n    override fun createSubViewModel(params: Params): MixedContentSubViewModel {\n        return MixedContentSubViewModel(\n            contentRepo = contentRepo,\n            mixedRepo = mixedRepo,\n            statusUpdater = statusUpdater,\n            freadConfigManager = freadConfigManager,\n            statusUiStateAdapter = statusUiStateAdapter,\n            statusProvider = statusProvider,\n            configId = params.configId,\n            refactorToNewStatus = refactorToNewStatus,\n        )\n    }\n\n    fun getSubViewModel(configId: String): MixedContentSubViewModel {\n        val params = Params(configId)\n        return obtainSubViewModel(params)\n    }\n\n    class Params(val configId: String) : SubViewModelParams() {\n        override val key: String\n            get() = configId.toString()\n    }\n}\n"
  },
  {
    "path": "feature/feeds/src/commonMain/kotlin/com/zhangke/fread/feeds/pages/manager/add/mixed/AddMixedFeedsScreen.kt",
    "content": "package com.zhangke.fread.feeds.pages.manager.add.mixed\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Add\nimport androidx.compose.material.icons.filled.Check\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.OutlinedTextField\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.SnackbarHostState\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.rotate\nimport androidx.compose.ui.unit.dp\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.composable.currentOrThrow\nimport com.zhangke.framework.composable.ConsumeFlow\nimport com.zhangke.framework.composable.ConsumeSnackbarFlow\nimport com.zhangke.framework.composable.IconButtonStyle\nimport com.zhangke.framework.composable.SimpleIconButton\nimport com.zhangke.framework.composable.StyledIconButton\nimport com.zhangke.framework.composable.Toolbar\nimport com.zhangke.framework.composable.rememberSnackbarHostState\nimport com.zhangke.framework.composable.snackbarHost\nimport com.zhangke.framework.nav.LocalNavBackStack\nimport com.zhangke.fread.common.utils.LocalToastHelper\nimport com.zhangke.fread.feeds.Res\nimport com.zhangke.fread.feeds.composable.RemovableStatusSource\nimport com.zhangke.fread.feeds.composable.StatusSourceUiState\nimport com.zhangke.fread.feeds.ic_import\nimport com.zhangke.fread.feeds.pages.manager.importing.ImportFeedsScreenNavKey\nimport com.zhangke.fread.feeds.pages.manager.search.SearchSourceForAddScreenNavKey\nimport com.zhangke.fread.localization.LocalizedString\nimport kotlinx.serialization.Serializable\nimport org.jetbrains.compose.resources.getString\nimport org.jetbrains.compose.resources.stringResource\nimport org.jetbrains.compose.resources.vectorResource\n\n/**\n * 添加混合 Feeds 页面\n */\n@Serializable\nobject AddMixedFeedsScreenNavKey : NavKey\n\n@Composable\nfun AddMixedFeedsScreen(viewModel: AddMixedFeedsViewModel) {\n    val toastHelper = LocalToastHelper.current\n    val backStack = LocalNavBackStack.currentOrThrow\n    val snackbarHostState = rememberSnackbarHostState()\n    FeedsManager(\n        uiState = viewModel.uiState.collectAsState().value,\n        snackbarHostState = snackbarHostState,\n        onBackClick = backStack::removeLastOrNull,\n        onAddSourceClick = {\n            backStack.add(SearchSourceForAddScreenNavKey)\n        },\n        onImportClick = {\n            backStack.add(ImportFeedsScreenNavKey)\n        },\n        onConfirmClick = {\n            viewModel.onConfirmClick()\n        },\n        onNameInputValueChanged = viewModel::onSourceNameInput,\n        onRemoveSourceClick = {\n            viewModel.onRemoveSource(it)\n        },\n    )\n    ConsumeFlow(SearchSourceForAddScreenNavKey.sourceSelectedFlow.flow) {\n        viewModel.onAddSource(it)\n    }\n    ConsumeSnackbarFlow(snackbarHostState, viewModel.errorMessageFlow)\n    ConsumeFlow(viewModel.addContentSuccessFlow) {\n        toastHelper.showToast(getString(LocalizedString.addContentSuccessSnackbar))\n        backStack.removeLastOrNull()\n    }\n}\n\n@Composable\nprivate fun FeedsManager(\n    uiState: AddMixedFeedsUiState,\n    snackbarHostState: SnackbarHostState,\n    onBackClick: () -> Unit,\n    onAddSourceClick: () -> Unit,\n    onImportClick: () -> Unit,\n    onConfirmClick: () -> Unit,\n    onNameInputValueChanged: (String) -> Unit,\n    onRemoveSourceClick: (item: StatusSourceUiState) -> Unit,\n) {\n    Scaffold(\n        topBar = {\n            Toolbar(\n                title = stringResource(LocalizedString.addFeedsPageTitle),\n                onBackClick = onBackClick,\n                actions = {\n                    SimpleIconButton(\n                        modifier = Modifier.rotate(180F),\n                        onClick = onImportClick,\n                        imageVector = vectorResource(Res.drawable.ic_import),\n                        contentDescription = \"Import\",\n                    )\n                    SimpleIconButton(\n                        onClick = onConfirmClick,\n                        imageVector = Icons.Default.Check,\n                        contentDescription = \"Add\",\n                    )\n                }\n            )\n        },\n        snackbarHost = snackbarHost(snackbarHostState),\n    ) { paddings ->\n        Column(\n            modifier = Modifier\n                .fillMaxSize()\n                .padding(paddings)\n        ) {\n            Box(modifier = Modifier.height(32.dp))\n            Row(\n                modifier = Modifier.fillMaxWidth(),\n                verticalAlignment = Alignment.CenterVertically,\n            ) {\n                OutlinedTextField(\n                    modifier = Modifier\n                        .padding(start = 16.dp, end = 16.dp)\n                        .weight(1F),\n                    value = uiState.sourceName,\n                    maxLines = 1,\n                    label = {\n                        Text(text = stringResource(LocalizedString.addFeedsPageFeedsNameLabel))\n                    },\n                    placeholder = {\n                        Text(text = stringResource(LocalizedString.addFeedsPageFeedsNameHint))\n                    },\n                    onValueChange = {\n                        onNameInputValueChanged(it.take(uiState.maxNameLength))\n                    },\n                )\n\n                StyledIconButton(\n                    modifier = Modifier.padding(end = 8.dp),\n                    imageVector = Icons.Default.Add,\n                    style = IconButtonStyle.STANDARD,\n                    onClick = onAddSourceClick,\n                )\n            }\n\n            if (uiState.sourceList.isEmpty()) {\n                Text(\n                    modifier = Modifier\n                        .padding(top = 32.dp)\n                        .align(Alignment.CenterHorizontally)\n                        .clickable {\n                            onAddSourceClick()\n                        },\n                    text = stringResource(LocalizedString.addFeedsPageFeedsEmpty),\n                    style = MaterialTheme.typography.labelLarge,\n                )\n            } else {\n                LazyColumn(\n                    modifier = Modifier.padding(top = 32.dp),\n                    contentPadding = PaddingValues(bottom = 32.dp),\n                ) {\n                    items(uiState.sourceList) { item ->\n                        RemovableStatusSource(\n                            modifier = Modifier.fillMaxWidth(),\n                            source = item,\n                            onClick = {},\n                            onRemoveClick = {\n                                onRemoveSourceClick(item)\n                            },\n                        )\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "feature/feeds/src/commonMain/kotlin/com/zhangke/fread/feeds/pages/manager/add/mixed/AddMixedFeedsUiState.kt",
    "content": "package com.zhangke.fread.feeds.pages.manager.add.mixed\n\nimport com.zhangke.fread.feeds.composable.StatusSourceUiState\n\ndata class AddMixedFeedsUiState(\n    val maxNameLength: Int,\n    val sourceList: List<StatusSourceUiState>,\n    val sourceName: String,\n)\n"
  },
  {
    "path": "feature/feeds/src/commonMain/kotlin/com/zhangke/fread/feeds/pages/manager/add/mixed/AddMixedFeedsViewModel.kt",
    "content": "package com.zhangke.fread.feeds.pages.manager.add.mixed\n\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.zhangke.framework.collections.container\nimport com.zhangke.framework.composable.TextString\nimport com.zhangke.framework.composable.textOf\nimport com.zhangke.framework.ktx.launchInViewModel\nimport com.zhangke.framework.ktx.map\nimport com.zhangke.fread.common.content.FreadContentRepo\nimport com.zhangke.fread.common.onboarding.OnboardingComponent\nimport com.zhangke.fread.feeds.composable.StatusSourceUiState\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.StatusProvider\nimport com.zhangke.fread.status.content.MixedContent\nimport com.zhangke.fread.status.source.StatusSource\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.SharedFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.asSharedFlow\nimport kotlinx.coroutines.flow.update\nimport kotlin.uuid.ExperimentalUuidApi\nimport kotlin.uuid.Uuid\n\nclass AddMixedFeedsViewModel(\n    private val statusProvider: StatusProvider,\n    private val contentRepo: FreadContentRepo,\n    private val onboardingComponent: OnboardingComponent,\n    private val statusSource: StatusSource? = null\n) : ViewModel() {\n\n    private val viewModelState = MutableStateFlow(initialViewModelState())\n\n    val uiState: StateFlow<AddMixedFeedsUiState> =\n        viewModelState.map(viewModelScope) { it.toUiState() }\n\n    private val _errorMessageFlow = MutableSharedFlow<TextString>()\n    val errorMessageFlow: Flow<TextString> = _errorMessageFlow.asSharedFlow()\n\n    private val _addContentSuccessFlow = MutableSharedFlow<Unit>()\n    val addContentSuccessFlow: SharedFlow<Unit> get() = _addContentSuccessFlow\n\n    init {\n        onboardingComponent.clearState()\n        launchInViewModel {\n            val initAccountList = statusProvider.accountManager.getAllLoggedAccount()\n            statusProvider.accountManager\n                .getAllAccountFlow()\n                .collect { currentAccountList ->\n                    if (initAccountList.isNotEmpty()) return@collect\n                    if (currentAccountList.isEmpty()) return@collect\n                    onConfirmClick()\n                }\n        }\n    }\n\n    fun onAddSource(source: StatusSource) {\n        launchInViewModel {\n            val sourceList = mutableListOf<StatusSource>()\n            sourceList.addAll(viewModelState.value.sourceList)\n            if (!sourceList.container { it.uri == source.uri }) {\n                sourceList += source\n            }\n            viewModelState.update {\n                it.copy(sourceList = sourceList)\n            }\n        }\n    }\n\n    fun onRemoveSource(source: StatusSourceUiState) {\n        viewModelState.update { state ->\n            state.copy(\n                sourceList = state.sourceList.filter { it.uri != source.source.uri }\n            )\n        }\n    }\n\n    fun onSourceNameInput(name: String) {\n        viewModelState.update {\n            it.copy(sourceName = name)\n        }\n    }\n\n    fun onConfirmClick() {\n        launchInViewModel {\n            val currentState = viewModelState.value\n            if (currentState.sourceName.isEmpty()) {\n                _errorMessageFlow.emit(textOf(LocalizedString.addFeedsPageEmptyNameTips))\n                return@launchInViewModel\n            }\n            if (contentRepo.checkNameExist(currentState.sourceName)) {\n                _errorMessageFlow.emit(textOf(LocalizedString.addFeedsPageEmptyNameExist))\n                return@launchInViewModel\n            }\n            val sourceList = currentState.sourceList\n            if (sourceList.isEmpty()) {\n                _errorMessageFlow.emit(textOf(LocalizedString.addFeedsPageEmptySourceTips))\n                return@launchInViewModel\n            }\n            performAddContent()\n        }\n    }\n\n    @OptIn(ExperimentalUuidApi::class)\n    private fun performAddContent() {\n        val currentState = viewModelState.value\n        val sourceUriList = currentState.sourceList.map { it.uri }\n        val sourceName = currentState.sourceName\n        launchInViewModel {\n            val order = contentRepo.getMaxOrder() + 1\n            val contentConfig = MixedContent(\n                id = Uuid.random().toHexString(),\n                order = order,\n                name = sourceName,\n                sourceUriList = sourceUriList,\n            )\n            contentRepo.insertContent(contentConfig)\n            _addContentSuccessFlow.emit(Unit)\n            onboardingComponent.onboardingSuccess()\n        }\n    }\n\n    private fun initialViewModelState(): AddSourceViewModelState {\n        return AddSourceViewModelState(\n            sourceList = if (statusSource == null) emptyList() else listOf(statusSource),\n            sourceName = statusSource?.name.orEmpty(),\n        )\n    }\n\n    private fun AddSourceViewModelState.toUiState(): AddMixedFeedsUiState {\n        return AddMixedFeedsUiState(\n            sourceList = sourceList.map { it.toUiState() },\n            sourceName = sourceName,\n            maxNameLength = 8,\n        )\n    }\n\n    private fun StatusSource.toUiState(\n        addEnabled: Boolean = false,\n        removeEnabled: Boolean = true,\n    ): StatusSourceUiState {\n        return StatusSourceUiState(\n            source = this,\n            addEnabled = addEnabled,\n            removeEnabled = removeEnabled,\n        )\n    }\n}\n\ninternal data class AddSourceViewModelState(\n    val sourceList: List<StatusSource>,\n    val sourceName: String,\n)\n"
  },
  {
    "path": "feature/feeds/src/commonMain/kotlin/com/zhangke/fread/feeds/pages/manager/add/type/SelectContentTypeScreen.kt",
    "content": "package com.zhangke.fread.feeds.pages.manager.add.type\n\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.BoxScope\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material3.Card\nimport androidx.compose.material3.CardDefaults\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.drawBehind\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.unit.dp\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.composable.ConsumeFlow\nimport com.zhangke.framework.composable.ConsumeOpenScreenFlow\nimport com.zhangke.framework.composable.Toolbar\nimport com.zhangke.framework.composable.currentOrThrow\nimport com.zhangke.framework.nav.LocalNavBackStack\nimport com.zhangke.fread.common.resources.blueskyDescription\nimport com.zhangke.fread.common.resources.blueskyName\nimport com.zhangke.fread.common.resources.mastodonDescription\nimport com.zhangke.fread.common.resources.mastodonName\nimport com.zhangke.fread.common.resources.mixedDescription\nimport com.zhangke.fread.common.resources.mixedName\nimport com.zhangke.fread.feeds.Res\nimport com.zhangke.fread.feeds.img_add_content_bsky\nimport com.zhangke.fread.feeds.img_add_content_mastodon\nimport com.zhangke.fread.feeds.img_add_content_mixed\nimport com.zhangke.fread.feeds.pages.manager.add.mixed.AddMixedFeedsScreenNavKey\nimport com.zhangke.fread.localization.LocalizedString\nimport kotlinx.serialization.Serializable\nimport org.jetbrains.compose.resources.painterResource\nimport org.jetbrains.compose.resources.stringResource\n\n@Serializable\nobject SelectContentTypeScreenNavKey : NavKey\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun SelectContentTypeScreen(viewModel: SelectContentTypeViewModel) {\n    val backStack = LocalNavBackStack.currentOrThrow\n    val mastodonCardColor = Color(0xFFFFA600)\n    val blueskyCardColor = Color(0xFF0080FF)\n    val mixedCardColor = Color(0xFF3AD06B)\n    Scaffold(\n        topBar = {\n            Toolbar(\n                title = stringResource(LocalizedString.feedsSelectTypeScreenTitle),\n                onBackClick = backStack::removeLastOrNull,\n            )\n        },\n        modifier = Modifier.fillMaxSize(),\n    ) { innerPadding ->\n        Column(\n            modifier = Modifier\n                .fillMaxSize()\n                .padding(innerPadding)\n                .verticalScroll(rememberScrollState()),\n            verticalArrangement = Arrangement.spacedBy(16.dp),\n        ) {\n            ContentTypeCard(\n                modifier = Modifier.padding(top = 22.dp).fillMaxWidth(),\n                cardColor = mastodonCardColor,\n                title = mastodonName(),\n                description = mastodonDescription(),\n                onCardClick = viewModel::onMastodonClick,\n                contentImage = {\n                    Image(\n                        modifier = Modifier.fillMaxHeight()\n                            .align(Alignment.CenterEnd),\n                        painter = painterResource(Res.drawable.img_add_content_mastodon),\n                        contentDescription = null,\n                        contentScale = ContentScale.FillHeight,\n                    )\n                },\n            )\n            ContentTypeCard(\n                modifier = Modifier.fillMaxWidth(),\n                cardColor = blueskyCardColor,\n                title = blueskyName(),\n                description = blueskyDescription(),\n                onCardClick = viewModel::onBlueskyClick,\n                contentImage = {\n                    Image(\n                        modifier = Modifier.fillMaxHeight()\n                            .align(Alignment.CenterEnd),\n                        painter = painterResource(Res.drawable.img_add_content_bsky),\n                        contentDescription = null,\n                        contentScale = ContentScale.FillHeight,\n                    )\n                },\n            )\n            ContentTypeCard(\n                modifier = Modifier.fillMaxWidth(),\n                cardColor = mixedCardColor,\n                title = mixedName(),\n                description = mixedDescription(),\n                onCardClick = { backStack.add(AddMixedFeedsScreenNavKey) },\n                contentImage = {\n                    Image(\n                        modifier = Modifier.fillMaxHeight()\n                            .align(Alignment.CenterEnd),\n                        painter = painterResource(Res.drawable.img_add_content_mixed),\n                        contentDescription = null,\n                        contentScale = ContentScale.FillHeight,\n                    )\n                },\n            )\n        }\n    }\n    ConsumeOpenScreenFlow(viewModel.openScreenFlow)\n    ConsumeFlow(viewModel.finishPageFlow) { backStack.removeLastOrNull() }\n    LaunchedEffect(Unit) {\n        viewModel.onPageResumed(this)\n    }\n}\n\n@Composable\nprivate fun ContentTypeCard(\n    modifier: Modifier,\n    cardColor: Color,\n    contentImage: @Composable BoxScope.() -> Unit,\n    title: String,\n    description: String,\n    onCardClick: () -> Unit,\n) {\n    Card(\n        modifier = modifier.padding(horizontal = 16.dp)\n            .fillMaxWidth()\n            .height(180.dp),\n        onClick = onCardClick,\n        colors = CardDefaults.cardColors(\n            containerColor = cardColor,\n            contentColor = MaterialTheme.colorScheme.inverseOnSurface,\n        ),\n        shape = RoundedCornerShape(16.dp),\n    ) {\n        Box(modifier = Modifier.fillMaxSize()) {\n            contentImage()\n            Column(\n                modifier = Modifier.fillMaxWidth()\n                    .align(Alignment.BottomStart)\n                    .drawBehind {\n                        drawRect(\n                            brush = Brush.verticalGradient(\n                                colors = listOf(Color.Transparent, cardColor.copy(alpha = 0.9F))\n                            )\n                        )\n                    },\n            ) {\n                Text(\n                    modifier = Modifier.padding(start = 16.dp),\n                    text = title,\n                    style = MaterialTheme.typography.headlineMedium,\n                    fontWeight = FontWeight.Bold,\n                )\n                Text(\n                    modifier = Modifier.padding(start = 16.dp, top = 4.dp, bottom = 22.dp)\n                        .fillMaxWidth(fraction = 0.9F),\n                    textAlign = TextAlign.Start,\n                    text = description,\n                    style = MaterialTheme.typography.labelMedium,\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "feature/feeds/src/commonMain/kotlin/com/zhangke/fread/feeds/pages/manager/add/type/SelectContentTypeViewModel.kt",
    "content": "package com.zhangke.fread.feeds.pages.manager.add.type\n\nimport androidx.lifecycle.ViewModel\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.ktx.launchInViewModel\nimport com.zhangke.fread.common.onboarding.OnboardingComponent\nimport com.zhangke.fread.status.StatusProvider\nimport com.zhangke.fread.status.model.createActivityPubProtocol\nimport com.zhangke.fread.status.model.createBlueskyProtocol\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.asSharedFlow\nimport kotlinx.coroutines.launch\n\nclass SelectContentTypeViewModel(\n    private val onboardingComponent: OnboardingComponent,\n    private val statusProvider: StatusProvider,\n) : ViewModel() {\n\n    private val _openScreenFlow = MutableSharedFlow<NavKey>()\n    val openScreenFlow = _openScreenFlow.asSharedFlow()\n\n    private val _finishPageFlow = MutableSharedFlow<Unit>()\n    val finishPageFlow = _finishPageFlow.asSharedFlow()\n\n    init {\n        onboardingComponent.clearState()\n    }\n\n    fun onPageResumed(uiScope: CoroutineScope) {\n        uiScope.launch {\n            onboardingComponent.onboardingFinishedFlow.collect {\n                _finishPageFlow.emit(Unit)\n            }\n        }\n    }\n\n    fun onMastodonClick() {\n        statusProvider.screenProvider\n            .getAddContentScreen(createActivityPubProtocol())\n            .let {\n                launchInViewModel { _openScreenFlow.emit(it) }\n            }\n    }\n\n    fun onBlueskyClick() {\n        statusProvider.screenProvider\n            .getAddContentScreen(createBlueskyProtocol())\n            .let { launchInViewModel { _openScreenFlow.emit(it) } }\n    }\n}\n"
  },
  {
    "path": "feature/feeds/src/commonMain/kotlin/com/zhangke/fread/feeds/pages/manager/edit/EditMixedContentScreen.kt",
    "content": "package com.zhangke.fread.feeds.pages.manager.edit\n\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Add\nimport androidx.compose.material.icons.filled.Delete\nimport androidx.compose.material.icons.filled.Edit\nimport androidx.compose.material3.FloatingActionButton\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.OutlinedTextField\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.SnackbarHost\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.vector.rememberVectorPainter\nimport androidx.compose.ui.unit.dp\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.composable.AlertConfirmDialog\nimport com.zhangke.framework.composable.ConsumeFlow\nimport com.zhangke.framework.composable.FreadDialog\nimport com.zhangke.framework.composable.LoadableLayout\nimport com.zhangke.framework.composable.LoadableState\nimport com.zhangke.framework.composable.Toolbar\nimport com.zhangke.framework.composable.currentOrThrow\nimport com.zhangke.framework.composable.rememberSnackbarHostState\nimport com.zhangke.framework.composable.successDataOrNull\nimport com.zhangke.framework.nav.LocalNavBackStack\nimport com.zhangke.fread.feeds.composable.RemovableStatusSource\nimport com.zhangke.fread.feeds.composable.StatusSourceUiState\nimport com.zhangke.fread.feeds.pages.manager.search.SearchSourceForAddScreenNavKey\nimport com.zhangke.fread.localization.LocalizedString\nimport kotlinx.serialization.Serializable\nimport org.jetbrains.compose.resources.stringResource\n\n@Serializable\ndata class EditMixedContentScreenNavKey(val contentId: String) : NavKey\n\n@Composable\nfun EditMixedContentScreen(viewModel: EditMixedContentViewModel) {\n    val backStack = LocalNavBackStack.currentOrThrow\n    val uiState by viewModel.uiState.collectAsState()\n    EditFeedsScreenContent(\n        uiState = uiState,\n        onRemoveSourceClick = viewModel::onSourceDelete,\n        onEditNameClick = viewModel::onEditName,\n        onBackClick = backStack::removeLastOrNull,\n        onAddSourceClick = {\n            backStack.add(SearchSourceForAddScreenNavKey)\n        },\n        onDeleteClick = viewModel::onDeleteFeeds,\n    )\n    ConsumeFlow(SearchSourceForAddScreenNavKey.sourceSelectedFlow.flow) {\n        viewModel.onAddSource(it)\n    }\n    ConsumeFlow(viewModel.finishScreenFlow) {\n        backStack.removeLastOrNull()\n    }\n}\n\n@Composable\nprivate fun EditFeedsScreenContent(\n    uiState: LoadableState<EditMixedContentUiState>,\n    onRemoveSourceClick: (StatusSourceUiState) -> Unit,\n    onEditNameClick: (String) -> Unit,\n    onBackClick: () -> Unit,\n    onAddSourceClick: () -> Unit,\n    onDeleteClick: () -> Unit,\n) {\n    val snackbarHostState = rememberSnackbarHostState()\n    val errorMessage = uiState.successDataOrNull()?.errorMessage?.take(180)\n    if (errorMessage.isNullOrEmpty().not()) {\n        LaunchedEffect(errorMessage) {\n            snackbarHostState.showSnackbar(errorMessage.orEmpty())\n        }\n    }\n    Scaffold(\n        topBar = {\n            EditFeedsScreenTopBar(\n                uiState = uiState,\n                onEditNameClick = onEditNameClick,\n                onBackClick = onBackClick,\n                onDeleteClick = onDeleteClick,\n            )\n        },\n        snackbarHost = {\n            SnackbarHost(hostState = snackbarHostState)\n        },\n        floatingActionButton = {\n            if (uiState.isSuccess) {\n                FloatingActionButton(\n                    modifier = Modifier.padding(bottom = 32.dp),\n                    containerColor = MaterialTheme.colorScheme.surface,\n                    onClick = onAddSourceClick,\n                    shape = CircleShape,\n                ) {\n                    Icon(\n                        painter = rememberVectorPainter(image = Icons.Default.Add),\n                        contentDescription = \"Add Source\",\n                    )\n                }\n            }\n        },\n    ) { paddings ->\n        LoadableLayout(\n            modifier = Modifier\n                .fillMaxSize()\n                .padding(paddings),\n            state = uiState,\n        ) { uiState ->\n            LazyColumn(modifier = Modifier.fillMaxSize()) {\n                items(uiState.sourceList) { item ->\n                    RemovableStatusSource(\n                        modifier = Modifier.fillMaxWidth(),\n                        onClick = {},\n                        source = item,\n                        onRemoveClick = {\n                            onRemoveSourceClick(item)\n                        }\n                    )\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun EditFeedsScreenTopBar(\n    uiState: LoadableState<EditMixedContentUiState>,\n    onEditNameClick: (String) -> Unit,\n    onBackClick: () -> Unit,\n    onDeleteClick: () -> Unit,\n) {\n    var showEditNameDialog by remember {\n        mutableStateOf(false)\n    }\n    var showDeleteConfirmDialog by remember {\n        mutableStateOf(false)\n    }\n    val configName = uiState.successDataOrNull()?.name.orEmpty()\n    Toolbar(\n        title = configName,\n        onBackClick = onBackClick,\n        actions = {\n            IconButton(\n                onClick = {\n                    showEditNameDialog = true\n                },\n            ) {\n                Icon(\n                    painter = rememberVectorPainter(Icons.Default.Edit),\n                    contentDescription = \"Edit Name\",\n                )\n            }\n\n            IconButton(\n                onClick = {\n                    showDeleteConfirmDialog = true\n                },\n            ) {\n                Icon(\n                    painter = rememberVectorPainter(Icons.Default.Delete),\n                    contentDescription = \"Delete Feeds\",\n                )\n            }\n        },\n    )\n    val loadedUiState = uiState.successDataOrNull()\n    if (loadedUiState != null && showEditNameDialog) {\n        var inputtedText by remember {\n            mutableStateOf(configName)\n        }\n        FreadDialog(\n            title = stringResource(LocalizedString.feedsMixedConfigEditNewNameDialogTitle),\n            onDismissRequest = { showEditNameDialog = false },\n            onNegativeClick = { showEditNameDialog = false },\n            onPositiveClick = {\n                showEditNameDialog = false\n                onEditNameClick(inputtedText)\n            },\n            content = {\n                OutlinedTextField(\n                    modifier = Modifier\n                        .padding(16.dp)\n                        .fillMaxWidth(),\n                    value = inputtedText,\n                    onValueChange = {\n                        inputtedText = it\n                    },\n                    label = {\n                        Text(text = stringResource(LocalizedString.feedsMixedConfigEditNewNameDialogLabel))\n                    }\n                )\n            },\n        )\n    }\n    if (showDeleteConfirmDialog) {\n        AlertConfirmDialog(\n            content = stringResource(LocalizedString.feedsMixedConfigEditDeleteContentDialogMessage),\n            onDismissRequest = { showDeleteConfirmDialog = false },\n            onConfirm = onDeleteClick,\n        )\n    }\n}\n"
  },
  {
    "path": "feature/feeds/src/commonMain/kotlin/com/zhangke/fread/feeds/pages/manager/edit/EditMixedContentUiState.kt",
    "content": "package com.zhangke.fread.feeds.pages.manager.edit\n\nimport com.zhangke.fread.feeds.composable.StatusSourceUiState\n\ndata class EditMixedContentUiState(\n    val name: String,\n    val sourceList: List<StatusSourceUiState>,\n    val errorMessage: String? = null,\n)\n"
  },
  {
    "path": "feature/feeds/src/commonMain/kotlin/com/zhangke/fread/feeds/pages/manager/edit/EditMixedContentViewModel.kt",
    "content": "package com.zhangke.fread.feeds.pages.manager.edit\n\nimport androidx.lifecycle.ViewModel\nimport com.zhangke.framework.composable.LoadableState\nimport com.zhangke.framework.composable.requireSuccessData\nimport com.zhangke.framework.composable.successDataOrNull\nimport com.zhangke.framework.composable.updateOnSuccess\nimport com.zhangke.framework.ktx.launchInViewModel\nimport com.zhangke.fread.common.content.FreadContentRepo\nimport com.zhangke.fread.feeds.composable.StatusSourceUiState\nimport com.zhangke.fread.status.StatusProvider\nimport com.zhangke.fread.status.content.MixedContent\nimport com.zhangke.fread.status.source.StatusSource\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.SharedFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.asSharedFlow\nimport kotlinx.coroutines.flow.asStateFlow\n\nclass EditMixedContentViewModel(\n    private val configRepo: FreadContentRepo,\n    private val statusProvider: StatusProvider,\n    private val configId: String,\n) : ViewModel() {\n\n    private val _uiState = MutableStateFlow(LoadableState.loading<EditMixedContentUiState>())\n    val uiState: StateFlow<LoadableState<EditMixedContentUiState>> = _uiState.asStateFlow()\n\n    private val _finishScreenFlow = MutableSharedFlow<Unit>()\n    val finishScreenFlow: SharedFlow<Unit> = _finishScreenFlow.asSharedFlow()\n\n    init {\n        loadFeedsDetail()\n    }\n\n    fun onSourceDelete(source: StatusSourceUiState) {\n        launchInViewModel {\n            val newSourceList = _uiState.value\n                .requireSuccessData()\n                .sourceList\n                .filter { it != source }\n            updateSourceList(newSourceList)\n            _uiState.updateOnSuccess {\n                it.copy(sourceList = newSourceList)\n            }\n        }\n    }\n\n    fun onDeleteFeeds() {\n        launchInViewModel {\n            configRepo.delete(configId)\n            _finishScreenFlow.emit(Unit)\n        }\n    }\n\n    fun onAddSource(source: StatusSource) {\n        val sourceList = _uiState.value.successDataOrNull()?.sourceList?.toMutableList() ?: return\n        launchInViewModel {\n            if (sourceList.any { it.source.uri == source.uri }) return@launchInViewModel\n            sourceList += StatusSourceUiState(\n                source = source,\n                addEnabled = true,\n                removeEnabled = false\n            )\n            updateSourceList(sourceList)\n            loadFeedsDetail()\n        }\n    }\n\n    private suspend fun updateSourceList(sourceList: List<StatusSourceUiState>) {\n        getMixedContent()?.copy(sourceUriList = sourceList.map { it.source.uri })\n            ?.let { configRepo.insertContent(it) }\n    }\n\n    private fun loadFeedsDetail() {\n        launchInViewModel {\n            val contentConfig = configRepo.getContent(configId)\n            if (contentConfig == null) {\n                _uiState.emit(LoadableState.failed(IllegalArgumentException(\"Unknown Content of $configId\")))\n                return@launchInViewModel\n            }\n            if (contentConfig !is MixedContent) {\n                _uiState.emit(LoadableState.failed(IllegalArgumentException(\"Only for Mixed Content\")))\n                return@launchInViewModel\n            }\n            val sourceList = contentConfig.sourceUriList.mapNotNull {\n                statusProvider.statusSourceResolver.resolveSourceByUri(it).getOrNull()\n            }.map { source ->\n                StatusSourceUiState(\n                    source = source,\n                    addEnabled = false,\n                    removeEnabled = true,\n                )\n            }\n            _uiState.emit(\n                LoadableState.success(EditMixedContentUiState(contentConfig.name, sourceList))\n            )\n        }\n    }\n\n    fun onEditName(newName: String) {\n        if (newName == _uiState.value.requireSuccessData().name) return\n        launchInViewModel {\n            val exists = configRepo.checkNameExist(newName)\n            if (exists) {\n                _uiState.updateOnSuccess {\n                    it.copy(errorMessage = \"$newName exists!\")\n                }\n                return@launchInViewModel\n            }\n            getMixedContent()?.copy(name = newName)?.let { configRepo.insertContent(it) }\n            _uiState.updateOnSuccess {\n                it.copy(name = newName)\n            }\n        }\n    }\n\n    private suspend fun getMixedContent(): MixedContent? {\n        return configRepo.getContent(configId) as? MixedContent\n    }\n}\n"
  },
  {
    "path": "feature/feeds/src/commonMain/kotlin/com/zhangke/fread/feeds/pages/manager/importing/ImportFeedsScreen.kt",
    "content": "package com.zhangke.fread.feeds.pages.manager.importing\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Check\nimport androidx.compose.material.icons.filled.Delete\nimport androidx.compose.material.icons.filled.Refresh\nimport androidx.compose.material.icons.filled.Save\nimport androidx.compose.material3.Button\nimport androidx.compose.material3.Card\nimport androidx.compose.material3.CircularProgressIndicator\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.vector.rememberVectorPainter\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.text.style.TextOverflow\nimport com.zhangke.framework.composable.BackHandler\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.composable.currentOrThrow\nimport com.zhangke.framework.composable.ConsumeFlow\nimport com.zhangke.framework.composable.FreadDialog\nimport com.zhangke.framework.composable.SimpleIconButton\nimport com.zhangke.framework.composable.Toolbar\nimport com.zhangke.framework.nav.LocalNavBackStack\nimport com.zhangke.framework.utils.PlatformUri\nimport com.zhangke.fread.common.utils.LocalPlatformUriHelper\nimport com.zhangke.fread.common.utils.LocalToastHelper\nimport com.zhangke.fread.localization.LocalizedString\nimport kotlinx.serialization.Serializable\nimport org.jetbrains.compose.resources.getString\nimport org.jetbrains.compose.resources.stringResource\n\n@Serializable\nobject ImportFeedsScreenNavKey : NavKey\n\n@Composable\nfun ImportFeedsScreen(viewModel: ImportFeedsViewModel) {\n    val toastHelper = LocalToastHelper.current\n    val backStack = LocalNavBackStack.currentOrThrow\n    val uiState by viewModel.uiState.collectAsState()\n    var showBackDialog by remember {\n        mutableStateOf(false)\n    }\n\n    fun onBackRequest() {\n        if (uiState.selectedFileUri != null || uiState.sourceList.isNotEmpty()) {\n            showBackDialog = true\n        } else {\n            backStack.removeLastOrNull()\n        }\n    }\n    if (showBackDialog) {\n        FreadDialog(\n            onDismissRequest = {\n                showBackDialog = false\n            },\n            title = stringResource(LocalizedString.alert),\n            contentText = stringResource(LocalizedString.feedsImportBackDialogMessage),\n            onNegativeClick = {\n                showBackDialog = false\n            },\n            onPositiveClick = {\n                backStack.removeLastOrNull()\n            }\n        )\n    }\n    BackHandler(true) {\n        onBackRequest()\n    }\n    ImportFeedsContent(\n        uiState = uiState,\n        onBackClick = ::onBackRequest,\n        onFileSelected = viewModel::onFileSelected,\n        onImportClick = {\n            viewModel.onImportClick()\n        },\n        onGroupDelete = viewModel::onGroupDelete,\n        onSourceDelete = viewModel::onSourceDelete,\n        onSaveClick = viewModel::onSaveClick,\n        retryImportClick = viewModel::retryImportClick,\n    )\n    ConsumeFlow(viewModel.saveSuccessFlow) {\n        toastHelper.showToast(getString(LocalizedString.addContentSuccessSnackbar))\n        backStack.removeLastOrNull()\n    }\n}\n\n@Composable\nprivate fun ImportFeedsContent(\n    uiState: ImportFeedsUiState,\n    onFileSelected: (PlatformUri) -> Unit,\n    onBackClick: () -> Unit,\n    onImportClick: () -> Unit,\n    onGroupDelete: (ImportSourceGroup) -> Unit,\n    onSourceDelete: (ImportSourceGroup, ImportingSource) -> Unit,\n    retryImportClick: (ImportSourceGroup, ImportingSource) -> Unit,\n    onSaveClick: () -> Unit,\n) {\n    val platformUriHelper = LocalPlatformUriHelper.current\n    Scaffold(\n        topBar = {\n            Toolbar(\n                title = stringResource(LocalizedString.feedsImportPageTitle),\n                onBackClick = onBackClick,\n                actions = {\n                    SimpleIconButton(\n                        onClick = onSaveClick,\n                        imageVector = Icons.Default.Save,\n                        contentDescription = \"Save\",\n                    )\n                }\n            )\n        },\n    ) { innerPadding ->\n        Column(\n            modifier = Modifier.padding(innerPadding),\n        ) {\n            Row(\n                modifier = Modifier\n                    .padding(16.dp)\n                    .fillMaxWidth(),\n                verticalAlignment = Alignment.CenterVertically,\n            ) {\n                OpenDocumentContainer(\n                    onResult = onFileSelected,\n                ) {\n                    Card(\n                        modifier = Modifier\n                            .weight(1F)\n                            .clickable {\n                                launch()\n                            },\n                    ) {\n                        Box(\n                            modifier = Modifier\n                                .fillMaxWidth()\n                                .padding(vertical = 8.dp),\n                        ) {\n                            val prettyFileUri = remember(uiState.selectedFileUri) {\n                                uiState.selectedFileUri?.let {\n                                    platformUriHelper.queryFileName(it)\n                                }\n                            }\n                            Text(\n                                modifier = Modifier.align(Alignment.Center),\n                                text = prettyFileUri\n                                    ?: stringResource(LocalizedString.feedsImportPageHint),\n                                overflow = TextOverflow.Clip,\n                                maxLines = 1,\n                                fontSize = 12.sp,\n                            )\n                        }\n                    }\n                }\n                Button(\n                    modifier = Modifier.padding(start = 16.dp),\n                    onClick = onImportClick,\n                    enabled = uiState.selectedFileUri != null,\n                ) {\n                    Text(\n                        text = stringResource(LocalizedString.feedsImportButton)\n                    )\n                }\n            }\n\n            if (uiState.errorMessage.isNullOrEmpty().not()) {\n                Text(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .padding(start = 16.dp, end = 16.dp, bottom = 16.dp),\n                    text = uiState.errorMessage.orEmpty(),\n                    textAlign = TextAlign.Start,\n                    color = MaterialTheme.colorScheme.error,\n                )\n            }\n\n            ImportGroupList(\n                itemList = uiState.importingUiItems,\n                onGroupDelete = onGroupDelete,\n                onSourceDelete = onSourceDelete,\n                retryImportClick = retryImportClick,\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun ImportGroupList(\n    itemList: List<ImportingUiItem>,\n    onGroupDelete: (ImportSourceGroup) -> Unit,\n    onSourceDelete: (ImportSourceGroup, ImportingSource) -> Unit,\n    retryImportClick: (ImportSourceGroup, ImportingSource) -> Unit,\n) {\n    val lazyListState = rememberLazyListState()\n    LazyColumn(\n        modifier = Modifier.fillMaxSize(),\n        state = lazyListState,\n    ) {\n        items(itemList) { item ->\n            when (item) {\n                is ImportingUiItem.Group -> {\n                    ImportGroupItem(\n                        group = item.group,\n                        onGroupDelete = onGroupDelete,\n                    )\n                }\n\n                is ImportingUiItem.Source -> {\n                    ImportSourceItem(\n                        group = item.group,\n                        source = item.source,\n                        onSourceDelete = onSourceDelete,\n                        retryImportClick = retryImportClick,\n                    )\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun ImportGroupItem(\n    group: ImportSourceGroup,\n    onGroupDelete: (ImportSourceGroup) -> Unit,\n) {\n    Row(\n        modifier = Modifier\n            .fillMaxWidth()\n            .padding(start = 16.dp, top = 16.dp, bottom = 8.dp, end = 8.dp),\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        Text(\n            modifier = Modifier,\n            text = group.title,\n            maxLines = 1,\n            overflow = TextOverflow.Ellipsis,\n            style = MaterialTheme.typography.titleLarge,\n        )\n        Spacer(modifier = Modifier.weight(1F))\n        var showDeleteDialog by remember {\n            mutableStateOf(false)\n        }\n        Text(\n            text = \"${group.children.size} sources\",\n            style = MaterialTheme.typography.labelMedium,\n        )\n        Spacer(modifier = Modifier.width(8.dp))\n        SimpleIconButton(\n            onClick = { showDeleteDialog = true },\n            imageVector = Icons.Default.Delete,\n            contentDescription = \"Delete group\",\n        )\n        if (showDeleteDialog) {\n            FreadDialog(\n                onDismissRequest = { showDeleteDialog = false },\n                contentText = stringResource(LocalizedString.feedsDeleteConfirmContent),\n                onNegativeClick = { showDeleteDialog = false },\n                onPositiveClick = {\n                    showDeleteDialog = false\n                    onGroupDelete(group)\n                },\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun ImportSourceItem(\n    group: ImportSourceGroup,\n    source: ImportingSource,\n    onSourceDelete: (ImportSourceGroup, ImportingSource) -> Unit,\n    retryImportClick: (ImportSourceGroup, ImportingSource) -> Unit,\n) {\n    Row(\n        modifier = Modifier\n            .fillMaxWidth()\n            .padding(start = 32.dp, top = 4.dp, bottom = 4.dp, end = 8.dp),\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        Column(\n            modifier = Modifier.weight(1F),\n        ) {\n            Text(\n                modifier = Modifier,\n                text = source.title,\n                maxLines = 1,\n                overflow = TextOverflow.Ellipsis,\n                style = MaterialTheme.typography.bodyMedium,\n            )\n            if (source is ImportingSource.Failure) {\n                Text(\n                    text = source.errorMessage,\n                    color = MaterialTheme.colorScheme.error,\n                    style = MaterialTheme.typography.labelSmall,\n                    maxLines = 1,\n                )\n            }\n        }\n        Spacer(modifier = Modifier.width(6.dp))\n        when (source) {\n            is ImportingSource.Importing -> {\n                CircularProgressIndicator(\n                    modifier = Modifier.size(16.dp)\n                )\n            }\n\n            is ImportingSource.Success -> {\n                Icon(\n                    imageVector = Icons.Default.Check,\n                    tint = MaterialTheme.colorScheme.primary,\n                    contentDescription = null,\n                )\n            }\n\n            is ImportingSource.Pending -> {\n                Text(text = \"waiting...\")\n            }\n\n            is ImportingSource.Failure -> {\n                Icon(\n                    modifier = Modifier.clickable { retryImportClick(group, source) },\n                    painter = rememberVectorPainter(image = Icons.Default.Refresh),\n                    contentDescription = \"Retry\",\n                )\n            }\n        }\n        Spacer(modifier = Modifier.width(8.dp))\n        var showDeleteDialog by remember {\n            mutableStateOf(false)\n        }\n        SimpleIconButton(\n            onClick = { showDeleteDialog = true },\n            imageVector = Icons.Default.Delete,\n            contentDescription = \"Delete source\",\n        )\n        if (showDeleteDialog) {\n            FreadDialog(\n                onDismissRequest = { showDeleteDialog = false },\n                contentText = stringResource(LocalizedString.feedsDeleteConfirmContent),\n                onNegativeClick = { showDeleteDialog = false },\n                onPositiveClick = {\n                    showDeleteDialog = false\n                    onSourceDelete(group, source)\n                },\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "feature/feeds/src/commonMain/kotlin/com/zhangke/fread/feeds/pages/manager/importing/ImportFeedsUiState.kt",
    "content": "package com.zhangke.fread.feeds.pages.manager.importing\n\nimport com.zhangke.framework.utils.PlatformUri\nimport com.zhangke.fread.status.uri.FormalUri\n\ndata class ImportFeedsUiState(\n    val selectedFileUri: PlatformUri?,\n    val sourceList: List<ImportSourceGroup>,\n    val errorMessage: String? = null,\n) {\n\n    val importingUiItems: List<ImportingUiItem> by lazy {\n        createUiItems()\n    }\n\n    private fun createUiItems(): List<ImportingUiItem> {\n        return sourceList.flatMap { group ->\n            listOf(ImportingUiItem.Group(group)) + group.children.map { ImportingUiItem.Source(group, it) }\n        }\n    }\n\n    companion object {\n\n        val default = ImportFeedsUiState(\n            selectedFileUri = null,\n            sourceList = emptyList(),\n            errorMessage = null,\n        )\n    }\n}\n\nsealed interface ImportingUiItem {\n\n    data class Group(val group: ImportSourceGroup) : ImportingUiItem\n\n    data class Source(val group: ImportSourceGroup, val source: ImportingSource) : ImportingUiItem\n}\n\ndata class ImportSourceGroup(\n    val title: String,\n    val children: List<ImportingSource>,\n)\n\nsealed interface ImportingSource {\n\n    val title: String\n    val url: String\n\n    data class Importing(\n        override val title: String,\n        override val url: String,\n    ) : ImportingSource\n\n    data class Success(\n        override val title: String,\n        override val url: String,\n        val formalUri: FormalUri,\n    ) : ImportingSource\n\n    data class Failure(\n        override val title: String,\n        override val url: String,\n        val errorMessage: String,\n    ) : ImportingSource\n\n    data class Pending(\n        override val title: String,\n        override val url: String\n    ) : ImportingSource\n}\n"
  },
  {
    "path": "feature/feeds/src/commonMain/kotlin/com/zhangke/fread/feeds/pages/manager/importing/ImportFeedsViewModel.kt",
    "content": "package com.zhangke.fread.feeds.pages.manager.importing\n\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.zhangke.framework.collections.container\nimport com.zhangke.framework.collections.remove\nimport com.zhangke.framework.network.SimpleUri\nimport com.zhangke.framework.opml.OpmlOutline\nimport com.zhangke.framework.opml.OpmlParser\nimport com.zhangke.framework.utils.PlatformUri\nimport com.zhangke.fread.analytics.reportToLogger\nimport com.zhangke.fread.common.content.FreadContentRepo\nimport com.zhangke.fread.common.utils.PlatformUriHelper\nimport com.zhangke.fread.status.StatusProvider\nimport com.zhangke.fread.status.content.MixedContent\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.IO\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.async\nimport kotlinx.coroutines.awaitAll\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asSharedFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.update\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.supervisorScope\nimport kotlinx.coroutines.withContext\nimport kotlin.uuid.ExperimentalUuidApi\nimport kotlin.uuid.Uuid\n\nclass ImportFeedsViewModel(\n    private val statusProvider: StatusProvider,\n    private val contentRepo: FreadContentRepo,\n    private val platformUriHelper: PlatformUriHelper,\n) : ViewModel() {\n\n    private val _uiState = MutableStateFlow(ImportFeedsUiState.default)\n    val uiState = _uiState.asStateFlow()\n\n    private val _saveSuccessFlow = MutableSharedFlow<Unit>()\n    val saveSuccessFlow = _saveSuccessFlow.asSharedFlow()\n\n    private var importingJob: Job? = null\n\n    fun onFileSelected(uri: PlatformUri) {\n        _uiState.value = _uiState.value.copy(\n            selectedFileUri = uri,\n            sourceList = emptyList(),\n        )\n    }\n\n    fun onImportClick() {\n        if (importingJob?.isActive == true) return\n        if (uiState.value.sourceList.isNotEmpty()) return\n        _uiState.update { it.copy(errorMessage = null) }\n        importingJob = viewModelScope.launch {\n            val sourceGroup = parseOpmlToGroup(uiState.value.selectedFileUri!!)\n            _uiState.update { it.copy(sourceList = sourceGroup) }\n            sourceGroup.forEach {\n                importGroup(it)\n            }\n        }\n    }\n\n    private suspend fun importGroup(group: ImportSourceGroup) {\n        if (checkGroupDeleted(group)) return\n        supervisorScope {\n            group.children.map { source ->\n                async {\n                    importSource(group, source)\n                }\n            }.awaitAll()\n        }\n    }\n\n    private suspend fun importSource(group: ImportSourceGroup, source: ImportingSource) {\n        if (checkSourceDeleted(group, source)) return\n        if (source !is ImportingSource.Pending && source !is ImportingSource.Failure) return\n        updateSourceUiState(group, ImportingSource.Importing(source.title, source.url))\n        statusProvider.statusSourceResolver.resolveRssSource(source.url)\n            .onSuccess {\n                updateSourceUiState(\n                    group,\n                    ImportingSource.Success(source.title, source.url, it.uri)\n                )\n            }.onFailure {\n                updateSourceUiState(\n                    group,\n                    ImportingSource.Failure(source.title, source.url, it.message ?: \"Unknown error\")\n                )\n            }\n    }\n\n    private fun updateSourceUiState(group: ImportSourceGroup, source: ImportingSource) {\n        _uiState.update { state ->\n            state.copy(\n                sourceList = state.sourceList.map { item ->\n                    if (item.title == group.title) {\n                        item.copy(\n                            children = item.children.map { child ->\n                                if (child.url == source.url) {\n                                    source\n                                } else {\n                                    child\n                                }\n                            })\n                    } else {\n                        item\n                    }\n                }\n            )\n        }\n    }\n\n    fun onGroupDelete(group: ImportSourceGroup) {\n        _uiState.update { state ->\n            state.copy(\n                sourceList = state.sourceList.remove { it == group }\n            )\n        }\n    }\n\n    fun onSourceDelete(group: ImportSourceGroup, source: ImportingSource) {\n        _uiState.update { state ->\n            state.copy(\n                sourceList = state.sourceList.map { item ->\n                    if (item == group) {\n                        item.copy(children = item.children.remove { it == source })\n                    } else {\n                        item\n                    }\n                }\n            )\n        }\n    }\n\n    fun onSaveClick() {\n        if (importingJob?.isActive == true) return\n        viewModelScope.launch {\n            val mixedContentList = _uiState.value\n                .sourceList\n                .mapNotNull { it.toMixedContent() }\n            if (mixedContentList.isEmpty()) return@launch\n            contentRepo.insertAll(mixedContentList)\n            _saveSuccessFlow.emit(Unit)\n        }\n    }\n\n    fun retryImportClick(group: ImportSourceGroup, source: ImportingSource) {\n        viewModelScope.launch {\n            importSource(group, source)\n        }\n    }\n\n    private fun checkGroupDeleted(group: ImportSourceGroup): Boolean {\n        val currentList = _uiState.value.sourceList\n        return !currentList.container { it.title == group.title }\n    }\n\n    private fun checkSourceDeleted(group: ImportSourceGroup, source: ImportingSource): Boolean {\n        val currentList = _uiState.value.sourceList\n        val existGroup = currentList.firstOrNull { it.title == group.title } ?: return true\n        return !existGroup.children.container { it.url == source.url }\n    }\n\n    @OptIn(ExperimentalUuidApi::class)\n    private suspend fun ImportSourceGroup.toMixedContent(): MixedContent? {\n        val successChildren = this.children.filterIsInstance<ImportingSource.Success>()\n        if (successChildren.isEmpty()) return null\n        return MixedContent(\n            id = Uuid.random().toHexString(),\n            order = contentRepo.getMaxOrder() + 1,\n            name = this.title,\n            sourceUriList = successChildren.map { it.formalUri },\n        )\n    }\n\n    private suspend fun parseOpmlToGroup(uri: PlatformUri): List<ImportSourceGroup> {\n        return parseOpml(uri).groupBy { it.title.ifEmpty { \"Unknown\" } }\n            .map { entry ->\n                ImportSourceGroup(\n                    title = entry.key,\n                    children = entry.value.flatMap { it.allChildren() }.map { it.toSource() }\n                )\n            }\n    }\n\n    private fun OpmlOutline.toSource(): ImportingSource {\n        return ImportingSource.Pending(\n            title = title.ifEmpty { convertSimpleNameFromUrl(xmlUrl) },\n            url = xmlUrl\n        )\n    }\n\n    private fun convertSimpleNameFromUrl(url: String): String {\n        if (url.isBlank()) return \"Unknown\"\n        return SimpleUri.parse(url)?.host ?: \"Unknown\"\n    }\n\n    private fun OpmlOutline.allChildren(): List<OpmlOutline> {\n        val list = mutableListOf<OpmlOutline>()\n        if (xmlUrl.isNotBlank()) {\n            list.add(this)\n        }\n        children.forEach {\n            list.addAll(it.allChildren())\n        }\n        return list\n    }\n\n    private suspend fun parseOpml(\n        uri: PlatformUri,\n    ): List<OpmlOutline> = withContext(Dispatchers.IO) {\n        var xmlDocument = \"\"\n        return@withContext try {\n            xmlDocument = platformUriHelper.readBytes(uri)?.decodeToString() ?: \"\"\n            OpmlParser.parse(xmlDocument)\n        } catch (e: Throwable) {\n            _uiState.update { it.copy(errorMessage = e.message) }\n            reportToLogger(\"OPML_IMPORT_ERROR\") {\n                put(\"errorMessage\", e.message.orEmpty())\n                put(\"trace\", e.stackTraceToString())\n                put(\"document\", xmlDocument.take(90000))\n            }\n            emptyList<OpmlOutline>()\n        }\n    }\n}\n"
  },
  {
    "path": "feature/feeds/src/commonMain/kotlin/com/zhangke/fread/feeds/pages/manager/importing/OpenDocumentContainer.kt",
    "content": "package com.zhangke.fread.feeds.pages.manager.importing\n\nimport androidx.compose.runtime.Composable\nimport com.zhangke.framework.utils.PlatformUri\n\n@Composable\nexpect fun OpenDocumentContainer(\n    onResult: (PlatformUri) -> Unit,\n    content: @Composable OpenDocumentContainerScope.() -> Unit\n)\n\nexpect class OpenDocumentContainerScope {\n    fun launch()\n}"
  },
  {
    "path": "feature/feeds/src/commonMain/kotlin/com/zhangke/fread/feeds/pages/manager/search/SearchForAddUiState.kt",
    "content": "package com.zhangke.fread.feeds.pages.manager.search\n\nimport com.zhangke.fread.feeds.composable.StatusSourceUiState\n\ndata class SearchForAddUiState(\n    val query: String,\n    val searching: Boolean,\n    val searchedList: List<StatusSourceUiState>,\n) {\n\n    companion object {\n\n        fun default(): SearchForAddUiState {\n            return SearchForAddUiState(\n                query = \"\",\n                searching = false,\n                searchedList = emptyList(),\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "feature/feeds/src/commonMain/kotlin/com/zhangke/fread/feeds/pages/manager/search/SearchSourceForAddScreen.kt",
    "content": "package com.zhangke.fread.feeds.pages.manager.search\n\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.foundation.text.KeyboardActions\nimport androidx.compose.foundation.text.KeyboardOptions\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Search\nimport androidx.compose.material3.CircularProgressIndicator\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.OutlinedTextField\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.SnackbarHost\nimport androidx.compose.material3.SnackbarHostState\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.text.AnnotatedString\nimport androidx.compose.ui.text.buildAnnotatedString\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.input.ImeAction\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.em\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.composable.currentOrThrow\nimport com.zhangke.framework.composable.ConsumeSnackbarFlow\nimport com.zhangke.framework.composable.SimpleIconButton\nimport com.zhangke.framework.composable.Toolbar\nimport com.zhangke.framework.composable.rememberSnackbarHostState\nimport com.zhangke.framework.nav.LocalNavBackStack\nimport com.zhangke.framework.nav.ScreenEventFlow\nimport com.zhangke.framework.utils.HighlightTextBuildUtil\nimport com.zhangke.fread.feeds.composable.StatusSourceNode\nimport com.zhangke.fread.feeds.composable.StatusSourceUiState\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.source.StatusSource\nimport kotlinx.serialization.Serializable\nimport kotlinx.coroutines.launch\nimport org.jetbrains.compose.resources.stringResource\n\n@Serializable\nobject SearchSourceForAddScreenNavKey : NavKey {\n    val sourceSelectedFlow = ScreenEventFlow<StatusSource>()\n}\n\n@Composable\nfun SearchSourceForAddScreen(viewModel: SearchSourceForAddViewModel) {\n    val backStack = LocalNavBackStack.currentOrThrow\n    val coroutineScope = rememberCoroutineScope()\n    val uiState by viewModel.uiState.collectAsState()\n    val snackBarHostState = rememberSnackbarHostState()\n    SearchSourceForAdd(\n        uiState = uiState,\n        snackBarHostState = snackBarHostState,\n        onBackClick = backStack::removeLastOrNull,\n        onQueryChanged = viewModel::onQueryChanged,\n        onSearchClick = viewModel::onSearchClick,\n        onAddClick = {\n            coroutineScope.launch {\n                SearchSourceForAddScreenNavKey.sourceSelectedFlow.emit(it.source)\n                backStack.removeLastOrNull()\n            }\n        },\n    )\n    ConsumeSnackbarFlow(snackBarHostState, viewModel.snackbarMessageFlow)\n}\n\n@Composable\nprivate fun SearchSourceForAdd(\n    uiState: SearchForAddUiState,\n    snackBarHostState: SnackbarHostState,\n    onBackClick: () -> Unit,\n    onQueryChanged: (String) -> Unit,\n    onSearchClick: () -> Unit,\n    onAddClick: (StatusSourceUiState) -> Unit,\n) {\n    Scaffold(\n        modifier = Modifier.fillMaxSize(),\n        topBar = {\n            Toolbar(\n                title = stringResource(LocalizedString.searchFeedsTitle),\n                onBackClick = onBackClick,\n            )\n        },\n        snackbarHost = {\n            SnackbarHost(snackBarHostState)\n        },\n    ) { innerPadding ->\n        Column(\n            modifier = Modifier.fillMaxSize()\n                .padding(innerPadding),\n        ) {\n            OutlinedTextField(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .padding(start = 16.dp, top = 18.dp, end = 16.dp),\n                value = uiState.query,\n                onValueChange = onQueryChanged,\n                maxLines = 1,\n                keyboardOptions = KeyboardOptions.Default.copy(\n                    imeAction = ImeAction.Search\n                ),\n                placeholder = {\n                    Text(\n                        text = stringResource(LocalizedString.searchFeedsTitleHint),\n                        style = MaterialTheme.typography.labelMedium,\n                    )\n                },\n                keyboardActions = KeyboardActions(\n                    onSearch = { onSearchClick() }\n                ),\n                trailingIcon = {\n                    if (uiState.searching) {\n                        CircularProgressIndicator(\n                            modifier = Modifier.size(18.dp),\n                            strokeWidth = 2.dp,\n                            color = MaterialTheme.colorScheme.primary,\n                        )\n                    } else {\n                        SimpleIconButton(\n                            onClick = onSearchClick,\n                            imageVector = Icons.Default.Search,\n                            contentDescription = \"Search\",\n                        )\n                    }\n                },\n            )\n            Text(\n                modifier = Modifier\n                    .padding(start = 16.dp, top = 12.dp, end = 16.dp),\n                text = buildInputLabelText(),\n                lineHeight = 1.5.em,\n                style = MaterialTheme.typography.labelMedium,\n            )\n            LazyColumn(\n                modifier = Modifier\n                    .fillMaxSize()\n                    .padding(top = 16.dp),\n            ) {\n                items(uiState.searchedList) { item ->\n                    StatusSourceNode(\n                        modifier = Modifier,\n                        onClick = { onAddClick(item) },\n                        source = item,\n                    )\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun buildInputLabelText(): AnnotatedString {\n    return buildAnnotatedString {\n        append(\n            HighlightTextBuildUtil.buildHighlightText(\n                text = stringResource(LocalizedString.preAddFeedsInputLabel1),\n                fontWeight = FontWeight.Bold,\n            )\n        )\n        append(\n            HighlightTextBuildUtil.buildHighlightText(\n                text = stringResource(LocalizedString.preAddFeedsInputLabel2),\n                fontWeight = FontWeight.Bold,\n            )\n        )\n    }\n}\n"
  },
  {
    "path": "feature/feeds/src/commonMain/kotlin/com/zhangke/fread/feeds/pages/manager/search/SearchSourceForAddViewModel.kt",
    "content": "package com.zhangke.fread.feeds.pages.manager.search\n\nimport androidx.lifecycle.ViewModel\nimport com.zhangke.framework.composable.TextString\nimport com.zhangke.framework.composable.emitTextMessageFromThrowable\nimport com.zhangke.framework.ktx.launchInViewModel\nimport com.zhangke.fread.feeds.composable.StatusSourceUiState\nimport com.zhangke.fread.status.StatusProvider\nimport com.zhangke.fread.status.source.StatusSource\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.update\n\nclass SearchSourceForAddViewModel(\n    private val statusProvider: StatusProvider,\n) : ViewModel() {\n\n    private val _uiState = MutableStateFlow(SearchForAddUiState.default())\n    val uiState = _uiState.asStateFlow()\n\n    private val _snackbarMessageFlow = MutableSharedFlow<TextString>()\n    val snackbarMessageFlow = _snackbarMessageFlow\n\n    private var searchJob: Job? = null\n\n\n    fun onSearchClick() {\n        performSearch()\n    }\n\n    fun onQueryChanged(query: String) {\n        _uiState.update { it.copy(query = query) }\n        if (query.isEmpty()) {\n            _uiState.update { it.copy(searchedList = emptyList()) }\n            return\n        }\n        performSearch()\n    }\n\n    private fun performSearch() {\n        if (searchJob?.isActive == true) searchJob?.cancel()\n        searchJob = launchInViewModel {\n            _uiState.update { it.copy(searching = true) }\n            doSearch(_uiState.value.query)\n                .onSuccess { list ->\n                    _uiState.update { it.copy(searchedList = list, searching = false) }\n                }.onFailure { e ->\n                    _uiState.update { it.copy(searching = false) }\n                    _snackbarMessageFlow.emitTextMessageFromThrowable(e)\n                }\n        }\n    }\n\n    private suspend fun doSearch(query: String): Result<List<StatusSourceUiState>> {\n        return statusProvider.searchEngine.searchSourceNoToken(query)\n            .map { list -> list.map { it.toUiState() } }\n    }\n\n    private fun StatusSource.toUiState(): StatusSourceUiState {\n        return StatusSourceUiState(\n            source = this,\n            addEnabled = false,\n            removeEnabled = false,\n        )\n    }\n}\n"
  },
  {
    "path": "feature/feeds/src/iosMain/kotlin/com/zhangke/fread/feeds/pages/manager/importing/OpenDocumentContainer.ios.kt",
    "content": "package com.zhangke.fread.feeds.pages.manager.importing\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport com.zhangke.framework.utils.PlatformUri\n\n@Composable\nactual fun OpenDocumentContainer(\n    onResult: (PlatformUri) -> Unit,\n    content: @Composable OpenDocumentContainerScope.() -> Unit,\n) {\n    val scope = remember {\n        OpenDocumentContainerScope()\n    }\n    with(scope) {\n        content()\n    }\n}\n\nactual class OpenDocumentContainerScope {\n    actual fun launch() {\n        TODO(\"Not yet implemented\")\n    }\n}"
  },
  {
    "path": "feature/notifications/.gitignore",
    "content": "/build"
  },
  {
    "path": "feature/notifications/build.gradle.kts",
    "content": "plugins {\n    id(\"fread.project.feature.kmp\")\n    id(\"com.google.devtools.ksp\")\n    alias(libs.plugins.room)\n}\n\nandroid {\n    namespace = \"com.zhangke.fread.feature.notifications\"\n}\n\nkotlin {\n    sourceSets {\n        commonMain {\n            dependencies {\n                implementation(project(\":framework\"))\n                implementation(project(\":bizframework:status-provider\"))\n                implementation(project(\":commonbiz:common\"))\n                implementation(project(\":commonbiz:analytics\"))\n                implementation(project(\":commonbiz:sharedscreen\"))\n                implementation(project(\":commonbiz:status-ui\"))\n\n                implementation(compose.components.resources)\n\n                implementation(libs.jetbrains.lifecycle.viewmodel)\n\n                implementation(libs.androidx.paging.common)\n\n                implementation(libs.auto.service.annotations)\n                implementation(libs.krouter.runtime)\n                implementation(libs.androidx.room)\n\n                implementation(libs.haze)\n                implementation(libs.haze.materials)\n            }\n        }\n        commonTest {\n            dependencies {\n                implementation(kotlin(\"test\"))\n            }\n        }\n        androidMain {\n            dependencies {\n                implementation(libs.androidx.core.ktx)\n                implementation(libs.bundles.androidx.activity)\n                implementation(libs.androidx.browser)\n            }\n        }\n    }\n    configureCommonMainKsp()\n}\n\ndependencies {\n    kspAll(libs.auto.service.ksp)\n    kspAll(libs.krouter.collecting.compiler)\n    kspAll(libs.androidx.room.compiler)\n}\n\ncompose {\n    resources {\n        publicResClass = false\n        packageOfResClass = \"com.zhangke.fread.feature.notifications\"\n        generateResClass = always\n    }\n}\n\nroom {\n    schemaDirectory(\"$projectDir/schemas\")\n}\n"
  },
  {
    "path": "feature/notifications/consumer-rules.pro",
    "content": ""
  },
  {
    "path": "feature/notifications/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguardFiles setting in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n\n# Uncomment this to preserve the line number information for\n# debugging stack traces.\n#-keepattributes SourceFile,LineNumberTable\n\n# If you keep the line number information, uncomment this to\n# hide the original source file name.\n#-renamesourcefileattribute SourceFile"
  },
  {
    "path": "feature/notifications/src/androidMain/kotlin/com/zhangke/fread/feature/message/di/NotificationsAndroidModule.kt",
    "content": "package com.zhangke.fread.feature.message.di\n\nimport androidx.room.Room\nimport com.zhangke.fread.feature.message.repo.notification.NotificationsDatabase\nimport org.koin.android.ext.koin.androidContext\nimport org.koin.core.module.Module\n\nactual fun Module.createPlatformModule() {\n    single<NotificationsDatabase> {\n        Room.databaseBuilder(\n            androidContext(),\n            NotificationsDatabase::class.java,\n            NotificationsDatabase.DB_NAME,\n        ).addMigrations(NotificationsDatabase.MIGRATION_1_2).build()\n    }\n}\n"
  },
  {
    "path": "feature/notifications/src/commonMain/composeResources/drawable/ic_notification_tab.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n  <path\n      android:pathData=\"M15.771,15.665L15.535,14.999H8.465L8.229,15.665C8.078,16.091 8,16.541 8,17.001C8,19.21 9.791,21.001 12,21.001C14.209,21.001 16,19.21 16,17.001C16,16.541 15.922,16.091 15.771,15.665ZM11.851,18.995C10.816,18.919 10,18.055 10,16.999L14,17.001C14,18.105 13.105,19.001 12,19.001L11.851,18.995Z\"\n      android:fillColor=\"#828FB0\"\n      android:fillType=\"evenOdd\"/>\n  <path\n      android:pathData=\"M12.675,2.042C12.448,2.014 12.224,2 12,2C11.776,2 11.56,2.014 11.351,2.042C7.94,2.384 5.25,5.289 5.25,8.823V13.82C5.25,14.295 4.887,14.691 4.417,14.726L4.35,14.729H4.125C3.819,14.729 3.526,14.854 3.314,15.077C3.102,15.3 2.989,15.6 3.001,15.909L3.006,15.981C3.062,16.547 3.555,16.962 4.113,16.998L4.189,17H19.875C20.181,17 20.474,16.874 20.686,16.651C20.898,16.429 21.011,16.128 20.999,15.82L20.994,15.747C20.938,15.182 20.445,14.766 19.887,14.731L19.811,14.729H19.65C19.179,14.729 18.788,14.362 18.753,13.888L18.75,13.82V9.015C18.75,5.465 16.135,2.393 12.675,2.042ZM11.55,4.032C11.736,4.008 11.864,4 12,4L12.213,4.007L12.429,4.027L12.473,4.032C14.87,4.275 16.75,6.449 16.75,9.015V13.82L16.754,13.967L16.773,14.148C16.812,14.446 16.892,14.729 17.007,14.989L17.011,15H6.998L7.011,14.973C7.166,14.618 7.25,14.228 7.25,13.819V8.823C7.25,6.347 9.131,4.275 11.55,4.032Z\"\n      android:fillColor=\"#828FB0\"\n      android:fillType=\"evenOdd\"/>\n</vector>\n"
  },
  {
    "path": "feature/notifications/src/commonMain/kotlin/com/zhangke/fread/feature/message/NotificationElements.kt",
    "content": "package com.zhangke.fread.feature.message\n\nobject NotificationElements {\n\n    const val SWITCH_ACCOUNT = \"notificationSwitchAccount\"\n}\n"
  },
  {
    "path": "feature/notifications/src/commonMain/kotlin/com/zhangke/fread/feature/message/NotificationsNavEntryProvider.kt",
    "content": "package com.zhangke.fread.feature.message\n\nimport androidx.navigation3.runtime.EntryProviderScope\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.nav.NavEntryProvider\nimport kotlinx.serialization.modules.PolymorphicModuleBuilder\n\nclass NotificationsNavEntryProvider : NavEntryProvider {\n\n    override fun EntryProviderScope<NavKey>.build() {\n    }\n\n    override fun PolymorphicModuleBuilder<NavKey>.polymorph() {\n    }\n}\n"
  },
  {
    "path": "feature/notifications/src/commonMain/kotlin/com/zhangke/fread/feature/message/di/NotificationsModule.kt",
    "content": "package com.zhangke.fread.feature.message.di\n\nimport com.zhangke.framework.nav.NavEntryProvider\nimport com.zhangke.fread.feature.message.NotificationsNavEntryProvider\nimport com.zhangke.fread.feature.message.repo.notification.NotificationsRepo\nimport com.zhangke.fread.feature.message.screens.home.NotificationsHomeViewModel\nimport com.zhangke.fread.feature.message.screens.notification.NotificationContainerViewModel\nimport org.koin.core.module.Module\nimport org.koin.core.module.dsl.factoryOf\nimport org.koin.core.module.dsl.singleOf\nimport org.koin.core.module.dsl.viewModelOf\nimport org.koin.dsl.bind\nimport org.koin.dsl.module\n\nval notificationsModule = module {\n\n    createPlatformModule()\n\n    factoryOf(::NotificationsNavEntryProvider) bind NavEntryProvider::class\n\n    singleOf(::NotificationsRepo)\n\n    viewModelOf(::NotificationsHomeViewModel)\n    viewModelOf(::NotificationContainerViewModel)\n}\n\nexpect fun Module.createPlatformModule()\n"
  },
  {
    "path": "feature/notifications/src/commonMain/kotlin/com/zhangke/fread/feature/message/repo/notification/NotificationsDatabase.kt",
    "content": "package com.zhangke.fread.feature.message.repo.notification\n\nimport androidx.room.ConstructedBy\nimport androidx.room.Dao\nimport androidx.room.Database\nimport androidx.room.Entity\nimport androidx.room.Insert\nimport androidx.room.OnConflictStrategy\nimport androidx.room.PrimaryKey\nimport androidx.room.Query\nimport androidx.room.RoomDatabase\nimport androidx.room.RoomDatabaseConstructor\nimport androidx.room.TypeConverters\nimport androidx.room.migration.Migration\nimport androidx.sqlite.SQLiteConnection\nimport androidx.sqlite.execSQL\nimport com.zhangke.fread.common.db.converts.FormalUriConverter\nimport com.zhangke.fread.common.db.converts.StatusNotificationConverter\nimport com.zhangke.fread.status.notification.StatusNotification\nimport com.zhangke.fread.status.uri.FormalUri\n\nprivate const val DB_VERSION = 2\nprivate const val TABLE_NAME = \"notifications\"\n\n@Entity(tableName = TABLE_NAME)\ndata class NotificationEntity(\n    @PrimaryKey val notificationId: String,\n    val accountUri: FormalUri,\n    val notification: StatusNotification,\n)\n\n@Dao\ninterface NotificationsDao {\n\n    @Query(\"SELECT * FROM $TABLE_NAME WHERE notificationId = :id\")\n    suspend fun queryById(id: String): NotificationEntity?\n\n    @Query(\"SELECT * FROM $TABLE_NAME WHERE accountUri = :accountUri\")\n    suspend fun queryByAccountUri(accountUri: FormalUri): List<NotificationEntity>\n\n    @Insert(onConflict = OnConflictStrategy.REPLACE)\n    suspend fun insert(entity: NotificationEntity)\n\n    @Insert(onConflict = OnConflictStrategy.REPLACE)\n    suspend fun insert(list: List<NotificationEntity>)\n\n    @Query(\"DELETE FROM $TABLE_NAME WHERE accountUri = :accountUri\")\n    suspend fun delete(accountUri: FormalUri)\n}\n\n@TypeConverters(\n    FormalUriConverter::class,\n    StatusNotificationConverter::class,\n)\n@Database(entities = [NotificationEntity::class], version = DB_VERSION, exportSchema = false)\n@ConstructedBy(NotificationsDatabaseConstructor::class)\nabstract class NotificationsDatabase : RoomDatabase() {\n\n    abstract fun notificationsDao(): NotificationsDao\n\n    companion object {\n\n        internal const val DB_NAME = \"all_accounts_notifications_1.db\"\n\n        val MIGRATION_1_2 = object : Migration(1, 2) {\n\n            override fun migrate(connection: SQLiteConnection) {\n                connection.execSQL(\"DELETE FROM $TABLE_NAME\")\n            }\n        }\n    }\n}\n\n// The Room compiler generates the `actual` implementations.\n@Suppress(\"NO_ACTUAL_FOR_EXPECT\")\nexpect object NotificationsDatabaseConstructor : RoomDatabaseConstructor<NotificationsDatabase> {\n    override fun initialize(): NotificationsDatabase\n}\n"
  },
  {
    "path": "feature/notifications/src/commonMain/kotlin/com/zhangke/fread/feature/message/repo/notification/NotificationsRepo.kt",
    "content": "package com.zhangke.fread.feature.message.repo.notification\n\nimport com.zhangke.fread.status.notification.StatusNotification\nimport com.zhangke.fread.status.uri.FormalUri\n\nclass NotificationsRepo(\n    database: NotificationsDatabase,\n) {\n\n    private val dao = database.notificationsDao()\n\n    suspend fun getNotifications(\n        accountUri: FormalUri,\n    ): List<StatusNotification> {\n        return dao.queryByAccountUri(accountUri).map { it.notification }\n    }\n\n    suspend fun replaceNotifications(\n        accountUri: FormalUri,\n        notifications: List<StatusNotification>,\n    ) {\n        val entities = notifications.map { it.toEntity(accountUri) }\n        dao.delete(accountUri)\n        dao.insert(entities)\n    }\n\n    suspend fun insertNotification(\n        accountUri: FormalUri,\n        notifications: List<StatusNotification>,\n    ) {\n        val entities = notifications.map { it.toEntity(accountUri) }\n        dao.insert(entities)\n    }\n\n    suspend fun updateNotification(accountUri: FormalUri, notification: StatusNotification) {\n        dao.insert(notification.toEntity(accountUri))\n    }\n\n    private fun StatusNotification.toEntity(\n        accountUri: FormalUri,\n    ): NotificationEntity {\n        return NotificationEntity(\n            notificationId = this.id,\n            accountUri = accountUri,\n            notification = this,\n        )\n    }\n}\n"
  },
  {
    "path": "feature/notifications/src/commonMain/kotlin/com/zhangke/fread/feature/message/screens/home/NotificationScreen.kt",
    "content": "package com.zhangke.fread.feature.message.screens.home\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.pager.HorizontalPager\nimport androidx.compose.foundation.pager.rememberPagerState\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.ArrowDropDown\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.SnackbarHost\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.layout.onSizeChanged\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport com.zhangke.framework.blur.applyBlurEffect\nimport com.zhangke.framework.blur.blurEffectContainerColor\nimport com.zhangke.framework.composable.LocalContentPadding\nimport com.zhangke.framework.composable.LocalSnackbarHostState\nimport com.zhangke.framework.composable.SingleRowTopAppBar\nimport com.zhangke.framework.composable.TopAppBarColors\nimport com.zhangke.framework.composable.currentOrThrow\nimport com.zhangke.framework.composable.noRippleClick\nimport com.zhangke.framework.composable.rememberSnackbarHostState\nimport com.zhangke.framework.composable.updateTopPadding\nimport com.zhangke.framework.nav.LocalNavBackStack\nimport com.zhangke.framework.utils.pxToDp\nimport com.zhangke.fread.common.composable.EmptyContent\nimport com.zhangke.fread.common.composable.EmptyContentType\nimport com.zhangke.fread.commonbiz.shared.LocalModuleScreenVisitor\nimport com.zhangke.fread.feature.message.screens.notification.LocalNotificationTabConsumeStatusBarInsets\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.account.LoggedAccount\nimport com.zhangke.fread.status.ui.BlogAuthorAvatar\nimport com.zhangke.fread.status.ui.common.SelectAccountDialog\nimport com.zhangke.fread.status.ui.richtext.FreadRichText\nimport org.jetbrains.compose.resources.stringResource\nimport org.koin.compose.viewmodel.koinViewModel\n\n@Composable\nfun NotificationScreen() {\n    val backstack = LocalNavBackStack.currentOrThrow\n    val feedsScreenVisitor = LocalModuleScreenVisitor.current.feedsScreenVisitor\n    val viewModel: NotificationsHomeViewModel = koinViewModel()\n    val uiState by viewModel.uiState.collectAsState()\n    NotificationsHomeScreenContent(\n        uiState = uiState,\n        onAccountSelected = viewModel::onAccountSelected,\n        onAddClick = {\n            backstack.add(feedsScreenVisitor.getAddContentScreen())\n        },\n    )\n}\n\n@Composable\nprivate fun NotificationsHomeScreenContent(\n    uiState: NotificationsHomeUiState,\n    onAccountSelected: (LoggedAccount) -> Unit,\n    onAddClick: () -> Unit,\n) {\n    val density = LocalDensity.current\n    val snackbarHost = rememberSnackbarHostState()\n    val showTopBar = uiState.tabs.size > 1 && uiState.selectedAccount != null\n    Box(\n        modifier = Modifier.fillMaxSize()\n            .background(MaterialTheme.colorScheme.background),\n    ) {\n        var topBarHeight: Dp by remember { mutableStateOf(0.dp) }\n        if (uiState.tabs.isEmpty()) {\n            EmptyContent(\n                modifier = Modifier.fillMaxSize(),\n                type = EmptyContentType.Message,\n                onClick = onAddClick,\n            )\n        } else {\n            Box(modifier = Modifier.fillMaxSize()) {\n                val pagerState = rememberPagerState { uiState.tabs.size }\n                LaunchedEffect(uiState) {\n                    val index = uiState.accountList.indexOf(uiState.selectedAccount)\n                    if (index >= 0) {\n                        pagerState.scrollToPage(index)\n                    }\n                }\n                CompositionLocalProvider(\n                    LocalSnackbarHostState provides snackbarHost,\n                    LocalContentPadding provides updateTopPadding(\n                        if (showTopBar) topBarHeight else 0.dp\n                    ),\n                    LocalNotificationTabConsumeStatusBarInsets provides !showTopBar,\n                ) {\n                    HorizontalPager(\n                        modifier = Modifier.fillMaxSize(),\n                        state = pagerState,\n                        userScrollEnabled = false,\n                    ) { pageIndex ->\n                        with(uiState.tabs[pageIndex]) {\n                            Content()\n                        }\n                    }\n                }\n            }\n        }\n        if (showTopBar) {\n            NotificationTopBar(\n                account = uiState.selectedAccount,\n                accountList = uiState.accountList,\n                onAccountSelected = onAccountSelected,\n                onHeightChanged = { topBarHeight = it },\n            )\n        }\n        SnackbarHost(\n            hostState = snackbarHost,\n            modifier = Modifier.align(Alignment.BottomCenter)\n                .padding(bottom = LocalContentPadding.current.calculateBottomPadding() + 16.dp),\n        )\n    }\n}\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nprivate fun NotificationTopBar(\n    account: LoggedAccount,\n    accountList: List<LoggedAccount>,\n    onAccountSelected: (LoggedAccount) -> Unit,\n    onHeightChanged: (Dp) -> Unit,\n) {\n    val density = LocalDensity.current\n    val containerColor = MaterialTheme.colorScheme.surface\n    SingleRowTopAppBar(\n        modifier = Modifier.applyBlurEffect(containerColor = containerColor)\n            .onSizeChanged { onHeightChanged(it.height.pxToDp(density)) },\n        title = {\n            Text(\n                text = stringResource(LocalizedString.notificationTabTitle),\n            )\n        },\n        height = 48.dp,\n        colors = TopAppBarColors.default(\n            containerColor = blurEffectContainerColor(true, containerColor),\n        ),\n        actions = {\n            var showSelectAccountPopup by remember {\n                mutableStateOf(false)\n            }\n            Box(modifier = Modifier.padding(end = 8.dp)) {\n                Row(\n                    modifier = Modifier.noRippleClick {\n                        showSelectAccountPopup = !showSelectAccountPopup\n                    },\n                    verticalAlignment = Alignment.CenterVertically,\n                ) {\n                    BlogAuthorAvatar(\n                        modifier = Modifier.size(32.dp),\n                        imageUrl = account.avatar,\n                    )\n                    Spacer(modifier = Modifier.width(6.dp))\n                    Column {\n                        FreadRichText(\n                            modifier = Modifier,\n                            richText = account.humanizedName,\n                            style = MaterialTheme.typography.titleMedium,\n                            maxLines = 1,\n                            overflow = TextOverflow.Ellipsis,\n                        )\n                        Text(\n                            text = account.prettyHandle,\n                            style = MaterialTheme.typography.labelSmall,\n                            color = MaterialTheme.colorScheme.onSurfaceVariant,\n                            maxLines = 1,\n                            overflow = TextOverflow.Ellipsis,\n                        )\n                    }\n                    Icon(\n                        imageVector = Icons.Default.ArrowDropDown,\n                        contentDescription = \"Select Account\",\n                    )\n                }\n                if (showSelectAccountPopup) {\n                    SelectAccountDialog(\n                        accountList = accountList,\n                        selectedAccounts = listOf(account),\n                        onDismissRequest = { showSelectAccountPopup = false },\n                        onAccountClicked = onAccountSelected,\n                    )\n                }\n            }\n        },\n    )\n}\n"
  },
  {
    "path": "feature/notifications/src/commonMain/kotlin/com/zhangke/fread/feature/message/screens/home/NotificationsHomeUiState.kt",
    "content": "package com.zhangke.fread.feature.message.screens.home\n\nimport com.zhangke.framework.nav.Tab\nimport com.zhangke.fread.feature.message.screens.notification.NotificationTab\nimport com.zhangke.fread.status.account.LoggedAccount\n\ndata class NotificationsHomeUiState(\n    val selectedAccount: LoggedAccount? = null,\n    val accountList: List<LoggedAccount>,\n) {\n\n    val tabs: List<Tab> = accountList.map { NotificationTab(it) }\n}\n"
  },
  {
    "path": "feature/notifications/src/commonMain/kotlin/com/zhangke/fread/feature/message/screens/home/NotificationsHomeViewModel.kt",
    "content": "package com.zhangke.fread.feature.message.screens.home\n\nimport androidx.lifecycle.ViewModel\nimport com.zhangke.framework.ktx.launchInViewModel\nimport com.zhangke.fread.common.account.ActiveAccountsSynchronizer\nimport com.zhangke.fread.status.StatusProvider\nimport com.zhangke.fread.status.account.LoggedAccount\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.mapNotNull\nimport kotlinx.coroutines.flow.update\n\nclass NotificationsHomeViewModel(\n    private val statusProvider: StatusProvider,\n    private val activeAccountsSynchronizer: ActiveAccountsSynchronizer,\n) : ViewModel() {\n\n    private val _uiState = MutableStateFlow(\n        NotificationsHomeUiState(\n            selectedAccount = null,\n            accountList = emptyList(),\n        )\n    )\n    val uiState = _uiState.asStateFlow()\n\n    init {\n        launchInViewModel {\n            statusProvider.accountManager\n                .getAllAccountFlow()\n                .collect { accounts ->\n                    var selectedAccount = _uiState.value.selectedAccount\n                    if (selectedAccount == null) {\n                        val lastActiveAccountUri =\n                            activeAccountsSynchronizer.activeAccountUriFlow.value\n                        if (!lastActiveAccountUri.isNullOrEmpty()) {\n                            selectedAccount =\n                                accounts.firstOrNull { it.uri.toString() == lastActiveAccountUri }\n                        }\n                    }\n                    if (selectedAccount == null) {\n                        selectedAccount = accounts.firstOrNull()\n                    }\n                    _uiState.update {\n                        it.copy(\n                            accountList = accounts,\n                            selectedAccount = selectedAccount,\n                        )\n                    }\n                }\n        }\n        launchInViewModel {\n            activeAccountsSynchronizer.activeAccountUriFlow\n                .mapNotNull { it?.takeIf { it.isNotEmpty() } }\n                .collect { lastActiveAccountUri ->\n                    val accounts = uiState.value.accountList\n                    val selectedAccount =\n                        accounts.firstOrNull { it.uri.toString() == lastActiveAccountUri }\n                    if (selectedAccount != null && selectedAccount.uri != uiState.value.selectedAccount?.uri) {\n                        _uiState.update { it.copy(selectedAccount = selectedAccount) }\n                    }\n                }\n        }\n    }\n\n    fun onAccountSelected(account: LoggedAccount) {\n        if (account.uri == uiState.value.selectedAccount?.uri) return\n        launchInViewModel {\n            _uiState.update { it.copy(selectedAccount = account) }\n            activeAccountsSynchronizer.onAccountSelected(account.uri.toString())\n        }\n    }\n}\n"
  },
  {
    "path": "feature/notifications/src/commonMain/kotlin/com/zhangke/fread/feature/message/screens/home/NotificationsTab.kt",
    "content": "package com.zhangke.fread.feature.message.screens.home\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport com.zhangke.framework.nav.BaseTab\nimport com.zhangke.framework.nav.TabOptions\nimport com.zhangke.fread.feature.notifications.Res\nimport com.zhangke.fread.feature.notifications.ic_notification_tab\nimport org.jetbrains.compose.resources.painterResource\n\nclass NotificationsTab : BaseTab() {\n\n    override val options: TabOptions\n        @Composable get() {\n            val icon = painterResource(Res.drawable.ic_notification_tab)\n            return remember {\n                TabOptions(\n                    title = \"Notifications\",\n                    icon = icon,\n                )\n            }\n        }\n\n    @Composable\n    override fun Content() {\n        NotificationScreen()\n    }\n}\n"
  },
  {
    "path": "feature/notifications/src/commonMain/kotlin/com/zhangke/fread/feature/message/screens/notification/NotificationContainerViewModel.kt",
    "content": "package com.zhangke.fread.feature.message.screens.notification\n\nimport com.zhangke.framework.lifecycle.ContainerViewModel\nimport com.zhangke.framework.lifecycle.ContainerViewModel.SubViewModelParams\nimport com.zhangke.fread.common.adapter.StatusUiStateAdapter\nimport com.zhangke.fread.common.status.StatusUpdater\nimport com.zhangke.fread.commonbiz.shared.usecase.RefactorToNewStatusUseCase\nimport com.zhangke.fread.feature.message.repo.notification.NotificationsRepo\nimport com.zhangke.fread.status.StatusProvider\nimport com.zhangke.fread.status.account.LoggedAccount\n\nclass NotificationContainerViewModel(\n    private val statusProvider: StatusProvider,\n    private val notificationsRepo: NotificationsRepo,\n    private val statusUiStateAdapter: StatusUiStateAdapter,\n    private val refactorToNewStatus: RefactorToNewStatusUseCase,\n    private val statusUpdater: StatusUpdater,\n) : ContainerViewModel<NotificationViewModel, NotificationContainerViewModel.Params>() {\n\n    fun getSubViewModel(\n        account: LoggedAccount\n    ): NotificationViewModel {\n        return obtainSubViewModel(\n            Params(account)\n        )\n    }\n\n    override fun createSubViewModel(params: Params): NotificationViewModel {\n        return NotificationViewModel(\n            statusProvider = statusProvider,\n            account = params.account,\n            notificationsRepo = notificationsRepo,\n            statusUiStateAdapter = statusUiStateAdapter,\n            refactorToNewStatus = refactorToNewStatus,\n            statusUpdater = statusUpdater,\n        )\n    }\n\n    class Params(\n        val account: LoggedAccount,\n    ) : SubViewModelParams() {\n\n        override val key: String\n            get() = account.hashCode().toString()\n    }\n}\n"
  },
  {
    "path": "feature/notifications/src/commonMain/kotlin/com/zhangke/fread/feature/message/screens/notification/NotificationTab.kt",
    "content": "package com.zhangke.fread.feature.message.screens.notification\n\nimport androidx.compose.animation.animateColorAsState\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.statusBars\nimport androidx.compose.foundation.layout.windowInsetsPadding\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.MultiChoiceSegmentedButtonRow\nimport androidx.compose.material3.SegmentedButton\nimport androidx.compose.material3.SegmentedButtonDefaults\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.compositionLocalOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.layout.onSizeChanged\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport com.zhangke.framework.blur.applyBlurEffect\nimport com.zhangke.framework.blur.blurEffectContainerColor\nimport com.zhangke.framework.composable.ConsumeFlow\nimport com.zhangke.framework.composable.ConsumeSnackbarFlow\nimport com.zhangke.framework.composable.LocalContentPadding\nimport com.zhangke.framework.composable.LocalSnackbarHostState\nimport com.zhangke.framework.composable.currentOrThrow\nimport com.zhangke.framework.composable.plusTopPadding\nimport com.zhangke.framework.composable.textString\nimport com.zhangke.framework.loadable.lazycolumn.LoadableInlineVideoLazyColumn\nimport com.zhangke.framework.loadable.lazycolumn.rememberLoadableInlineVideoLazyColumnState\nimport com.zhangke.framework.nav.BaseTab\nimport com.zhangke.framework.nav.LocalNavBackStack\nimport com.zhangke.framework.nav.TabOptions\nimport com.zhangke.framework.utils.pxToDp\nimport com.zhangke.fread.common.composable.ErrorContent\nimport com.zhangke.fread.common.composable.ErrorType\nimport com.zhangke.fread.commonbiz.shared.notification.StatusNotificationUi\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.account.LoggedAccount\nimport com.zhangke.fread.status.author.BlogAuthor\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.ui.ComposedStatusInteraction\nimport com.zhangke.fread.status.ui.StatusListPlaceholder\nimport kotlinx.coroutines.delay\nimport org.jetbrains.compose.resources.stringResource\nimport org.koin.compose.viewmodel.koinViewModel\n\ninternal val LocalNotificationTabConsumeStatusBarInsets = compositionLocalOf { false }\n\nclass NotificationTab(\n    private val loggedAccount: LoggedAccount,\n) : BaseTab() {\n\n    override val options: TabOptions?\n        @Composable get() = null\n\n    @Composable\n    override fun Content() {\n        super.Content()\n        val backstack = LocalNavBackStack.currentOrThrow\n        val viewModel =\n            koinViewModel<NotificationContainerViewModel>().getSubViewModel(loggedAccount)\n        val uiState by viewModel.uiState.collectAsState()\n        val snackBarHostState = LocalSnackbarHostState.current\n        TabPageContent(\n            uiState = uiState,\n            onSwitchTab = viewModel::onSwitchTab,\n            onRefresh = viewModel::onRefresh,\n            onLoadMore = viewModel::onLoadMore,\n            composedStatusInteraction = viewModel.composedStatusInteraction,\n            onAcceptClick = viewModel::onAcceptClick,\n            onRejectClick = viewModel::onRejectClick,\n            onNotificationShown = viewModel::onNotificationShown,\n            onUnblockClick = viewModel::onUnblockClick,\n            onCancelFollowRequestClick = viewModel::onCancelFollowRequestClick,\n        )\n        ConsumeSnackbarFlow(snackBarHostState, viewModel.errorMessageFlow)\n        ConsumeFlow(viewModel.openScreenFlow) {\n            backstack.add(it)\n        }\n        if (uiState.dataList.isNotEmpty()) {\n            val first = uiState.dataList.first()\n            LaunchedEffect(first.id, first.fromLocal) {\n                // 停留1秒表示已读\n                delay(1000)\n                viewModel.onPageResume()\n            }\n        }\n    }\n\n    @Composable\n    private fun TabPageContent(\n        uiState: NotificationUiState,\n        composedStatusInteraction: ComposedStatusInteraction,\n        onSwitchTab: (Boolean) -> Unit,\n        onRefresh: () -> Unit,\n        onLoadMore: () -> Unit,\n        onUnblockClick: (PlatformLocator, BlogAuthor) -> Unit,\n        onRejectClick: (BlogAuthor) -> Unit,\n        onAcceptClick: (BlogAuthor) -> Unit,\n        onNotificationShown: (StatusNotificationUiState) -> Unit,\n        onCancelFollowRequestClick: (PlatformLocator, BlogAuthor) -> Unit,\n    ) {\n        var tabTitleHeight: Dp by remember { mutableStateOf(20.dp) }\n        val consumeStatusBarInsets = LocalNotificationTabConsumeStatusBarInsets.current\n        Box(\n            modifier = Modifier.fillMaxSize(),\n        ) {\n            CompositionLocalProvider(\n                LocalContentPadding provides plusTopPadding(tabTitleHeight)\n            ) {\n                if (uiState.initializing) {\n                    StatusListPlaceholder()\n                } else if (uiState.dataList.isEmpty()) {\n                    ErrorContent(\n                        modifier = Modifier.fillMaxSize(),\n                        type = ErrorType.Network,\n                        errorMessage = uiState.errorMessage?.let { textString(it) },\n                        onRetryClick = onRefresh,\n                    )\n                } else {\n                    val state = rememberLoadableInlineVideoLazyColumnState(\n                        onRefresh = onRefresh,\n                        onLoadMore = onLoadMore,\n                    )\n                    LoadableInlineVideoLazyColumn(\n                        state = state,\n                        modifier = Modifier.fillMaxSize(),\n                        refreshing = uiState.refreshing,\n                        loadState = uiState.loadMoreState,\n                    ) {\n                        itemsIndexed(\n                            items = uiState.dataList,\n                        ) { index, notification ->\n                            val backgroundColor by animateColorAsState(\n                                if (notification.unreadState) {\n                                    MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.2F)\n                                } else {\n                                    Color.Transparent\n                                }\n                            )\n                            StatusNotificationUi(\n                                modifier = Modifier.fillMaxWidth()\n                                    .background(backgroundColor),\n                                notification = notification.notification,\n                                composedStatusInteraction = composedStatusInteraction,\n                                indexInList = index,\n                                onAcceptClick = onAcceptClick,\n                                onRejectClick = onRejectClick,\n                                onUnblockClick = onUnblockClick,\n                                onCancelFollowRequestClick = onCancelFollowRequestClick,\n                            )\n                            if (notification.unreadState) {\n                                LaunchedEffect(notification) {\n                                    delay(1000)\n                                    onNotificationShown(notification)\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n            Column(modifier = Modifier.fillMaxWidth()) {\n                Spacer(\n                    modifier = Modifier.fillMaxWidth()\n                        .height(LocalContentPadding.current.calculateTopPadding())\n                )\n                NotificationTabTitle(\n                    uiState = uiState,\n                    consumeStatusBarInsets = consumeStatusBarInsets,\n                    onTabCheckedChange = onSwitchTab,\n                    onHeightChanged = { tabTitleHeight = it },\n                )\n            }\n        }\n    }\n\n    @Composable\n    private fun NotificationTabTitle(\n        uiState: NotificationUiState,\n        consumeStatusBarInsets: Boolean,\n        onTabCheckedChange: (inMentionsTab: Boolean) -> Unit,\n        onHeightChanged: (Dp) -> Unit,\n    ) {\n        val containerColor = MaterialTheme.colorScheme.surface\n        val density = LocalDensity.current\n        Box(\n            modifier = Modifier.fillMaxWidth()\n                .onSizeChanged { onHeightChanged(it.height.pxToDp(density)) }\n                .background(blurEffectContainerColor(containerColor = containerColor))\n                .applyBlurEffect(containerColor = containerColor),\n            contentAlignment = Alignment.Center,\n        ) {\n            MultiChoiceSegmentedButtonRow(\n                modifier = Modifier.fillMaxWidth(0.7F)\n                    .then(\n                        if (consumeStatusBarInsets) {\n                            Modifier.windowInsetsPadding(WindowInsets.statusBars)\n                        } else {\n                            Modifier\n                        }\n                    ),\n            ) {\n                SegmentedButton(\n                    checked = !uiState.inOnlyMentionTab,\n                    onCheckedChange = { onTabCheckedChange(false) },\n                    shape = SegmentedButtonDefaults.itemShape(index = 0, count = 2),\n                ) {\n                    Text(text = stringResource(LocalizedString.notificationsTabAll))\n                }\n                SegmentedButton(\n                    checked = uiState.inOnlyMentionTab,\n                    onCheckedChange = { onTabCheckedChange(true) },\n                    shape = SegmentedButtonDefaults.itemShape(index = 1, count = 2),\n                ) {\n                    Text(text = stringResource(LocalizedString.notificationsTabMention))\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "feature/notifications/src/commonMain/kotlin/com/zhangke/fread/feature/message/screens/notification/NotificationUiState.kt",
    "content": "package com.zhangke.fread.feature.message.screens.notification\n\nimport com.zhangke.framework.composable.TextString\nimport com.zhangke.framework.controller.LoadableUiState\nimport com.zhangke.framework.utils.LoadState\nimport com.zhangke.fread.status.account.LoggedAccount\nimport com.zhangke.fread.status.model.StatusUiState\nimport com.zhangke.fread.status.notification.StatusNotification\n\ndata class NotificationUiState(\n    val account: LoggedAccount,\n    val inOnlyMentionTab: Boolean,\n    override val initializing: Boolean,\n    override val dataList: List<StatusNotificationUiState>,\n    override val refreshing: Boolean,\n    override val loadMoreState: LoadState,\n    override val errorMessage: TextString?,\n) : LoadableUiState<StatusNotificationUiState, NotificationUiState> {\n\n    override fun copyObject(\n        dataList: List<StatusNotificationUiState>,\n        initializing: Boolean,\n        refreshing: Boolean,\n        loadMoreState: LoadState,\n        errorMessage: TextString?\n    ): NotificationUiState {\n        return copy(\n            dataList = dataList,\n            initializing = initializing,\n            refreshing = refreshing,\n            loadMoreState = loadMoreState,\n            errorMessage = errorMessage,\n        )\n    }\n\n    companion object {\n\n        fun default(\n            account: LoggedAccount,\n        ): NotificationUiState {\n            return NotificationUiState(\n                account = account,\n                inOnlyMentionTab = false,\n                initializing = false,\n                dataList = emptyList(),\n                refreshing = false,\n                loadMoreState = LoadState.Idle,\n                errorMessage = null,\n            )\n        }\n    }\n}\n\ndata class StatusNotificationUiState(\n    val notification: StatusNotification,\n    val unreadState: Boolean = notification.unread,\n    val fromLocal: Boolean,\n) {\n\n    val id: String get() = notification.id\n\n    fun updateStatus(status: StatusUiState): StatusNotificationUiState? {\n        if (notification.status?.status?.id != status.status.id) return null\n        return when (notification) {\n            is StatusNotification.Mention -> copy(notification = notification.copy(status = status))\n            is StatusNotification.Reply -> copy(notification = notification.copy(reply = status))\n            is StatusNotification.Quote -> copy(notification = notification.copy(quote = status))\n            else -> null\n        }\n    }\n}\n"
  },
  {
    "path": "feature/notifications/src/commonMain/kotlin/com/zhangke/fread/feature/message/screens/notification/NotificationViewModel.kt",
    "content": "package com.zhangke.fread.feature.message.screens.notification\n\nimport com.zhangke.framework.collections.getOrNull\nimport com.zhangke.framework.collections.removeFirstOrNull\nimport com.zhangke.framework.composable.emitTextMessageFromThrowable\nimport com.zhangke.framework.controller.LoadableController\nimport com.zhangke.framework.ktx.launchInViewModel\nimport com.zhangke.framework.lifecycle.SubViewModel\nimport com.zhangke.fread.common.adapter.StatusUiStateAdapter\nimport com.zhangke.fread.common.status.StatusUpdater\nimport com.zhangke.fread.commonbiz.shared.feeds.IInteractiveHandler\nimport com.zhangke.fread.commonbiz.shared.feeds.InteractiveHandleResult\nimport com.zhangke.fread.commonbiz.shared.feeds.InteractiveHandler\nimport com.zhangke.fread.commonbiz.shared.usecase.RefactorToNewStatusUseCase\nimport com.zhangke.fread.feature.message.repo.notification.NotificationsRepo\nimport com.zhangke.fread.status.StatusProvider\nimport com.zhangke.fread.status.account.LoggedAccount\nimport com.zhangke.fread.status.author.BlogAuthor\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.model.Relationships\nimport com.zhangke.fread.status.model.StatusUiState\nimport com.zhangke.fread.status.notification.INotificationResolver\nimport com.zhangke.fread.status.notification.StatusNotification\nimport com.zhangke.fread.status.uri.FormalUri\nimport kotlinx.coroutines.flow.update\nimport kotlinx.coroutines.launch\n\nclass NotificationViewModel(\n    private val statusProvider: StatusProvider,\n    private val account: LoggedAccount,\n    private val statusUiStateAdapter: StatusUiStateAdapter,\n    private val refactorToNewStatus: RefactorToNewStatusUseCase,\n    private val notificationsRepo: NotificationsRepo,\n    statusUpdater: StatusUpdater,\n) : SubViewModel(), IInteractiveHandler by InteractiveHandler(\n    statusProvider = statusProvider,\n    statusUpdater = statusUpdater,\n    statusUiStateAdapter = statusUiStateAdapter,\n    refactorToNewStatus = refactorToNewStatus,\n) {\n\n    private val loadableController = LoadableController(\n        coroutineScope = viewModelScope,\n        initialUiState = NotificationUiState.default(account = account),\n        onPostSnackMessage = {\n            launchInViewModel {\n                mutableErrorMessageFlow.emit(it)\n            }\n        }\n    )\n\n    private val _uiState = loadableController.mutableUiState\n\n    val uiState = loadableController.uiState\n\n    private var reportedNotificationId: String? = null\n    private var cursor: String? = null\n    private var reachEnd: Boolean = false\n\n    private val userDetailSnapshot = mutableSetOf<BlogAuthor>()\n\n    init {\n        initInteractiveHandler(\n            coroutineScope = viewModelScope,\n            onInteractiveHandleResult = { interactiveResult ->\n                when (interactiveResult) {\n                    is InteractiveHandleResult.UpdateStatus -> {\n                        updateStatus(interactiveResult.status)\n                    }\n\n                    is InteractiveHandleResult.DeleteStatus -> {\n                    }\n\n                    is InteractiveHandleResult.UpdateFollowState -> {\n                        // no-op\n                        updateUiStateRelationships(\n                            authorUri = interactiveResult.userUri,\n                            block = { relationships ->\n                                relationships?.copy(\n                                    following = interactiveResult.following,\n                                )\n                            }\n                        )\n                    }\n                }\n            }\n        )\n        loadableController.initData(\n            getDataFromServer = ::getDataFromServer,\n            getDataFromLocal = ::getDataFromLocal,\n        )\n    }\n\n    fun onSwitchTab(onlyMentions: Boolean) {\n        _uiState.update { it.copy(inOnlyMentionTab = onlyMentions) }\n        cursor = null\n        reachEnd = false\n        loadableController.initData(\n            getDataFromServer = ::getDataFromServer,\n            getDataFromLocal = ::getDataFromLocal,\n        )\n    }\n\n    fun onRefresh(hideRefreshing: Boolean = false) {\n        loadableController.onRefresh(hideRefreshing) {\n            getDataFromServer(null)\n        }\n    }\n\n    fun onLoadMore() {\n        if (reachEnd) return\n        loadableController.onLoadMore {\n            getDataFromServer(loadMore = true)\n        }\n    }\n\n    fun onNotificationShown(notification: StatusNotificationUiState) {\n        if (!notification.unreadState) return\n        _uiState.update { state ->\n            state.copy(\n                dataList = state.dataList.map {\n                    if (it.id == notification.id) {\n                        it.copy(unreadState = false)\n                    } else {\n                        it\n                    }\n                }\n            )\n        }\n    }\n\n    fun onPageResume() {\n        val firstNotificationId = uiState.value\n            .dataList\n            .firstOrNull()\n            ?.takeIf { !it.fromLocal }\n            ?.id ?: return\n        if (firstNotificationId == reportedNotificationId) return\n        reportedNotificationId = firstNotificationId\n        launchInViewModel {\n            statusProvider.notificationResolver\n                .updateUnreadNotification(\n                    account = account,\n                    notificationLastReadId = firstNotificationId\n                )\n        }\n    }\n\n    fun onRejectClick(author: BlogAuthor) {\n        launchInViewModel {\n            statusProvider.notificationResolver\n                .rejectFollowRequest(account, author)\n                .onSuccess { onRefresh(true) }\n                .onFailure { mutableErrorMessageFlow.emitTextMessageFromThrowable(it) }\n        }\n    }\n\n    fun onAcceptClick(author: BlogAuthor) {\n        launchInViewModel {\n            statusProvider.notificationResolver\n                .acceptFollowRequest(account, author)\n                .onSuccess { onRefresh(true) }\n                .onFailure { mutableErrorMessageFlow.emitTextMessageFromThrowable(it) }\n        }\n    }\n\n    fun onUnblockClick(locator: PlatformLocator, author: BlogAuthor) {\n        launchInViewModel {\n            statusProvider.accountManager.unblockAccount(\n                account = account,\n                user = author,\n            ).onSuccess {\n                updateUiStateRelationships(\n                    authorUri = author.uri,\n                    block = { relationships ->\n                        relationships?.copy(blocking = false)\n                    },\n                )\n            }.onFailure {\n                mutableErrorMessageFlow.emitTextMessageFromThrowable(it)\n            }\n        }\n    }\n\n    fun onCancelFollowRequestClick(locator: PlatformLocator, author: BlogAuthor) {\n        launchInViewModel {\n            statusProvider.accountManager.cancelFollowRequest(\n                account = account,\n                user = author,\n            ).onSuccess {\n                updateUiStateRelationships(\n                    authorUri = author.uri,\n                    block = { relationships ->\n                        relationships?.copy(requested = false)\n                    },\n                )\n            }.onFailure {\n                mutableErrorMessageFlow.emitTextMessageFromThrowable(it)\n            }\n        }\n    }\n\n    private fun updateUiStateRelationships(\n        authorUri: FormalUri,\n        block: (Relationships?) -> Relationships?,\n    ) {\n        userDetailSnapshot.removeFirstOrNull { it.uri == authorUri }?.let { user ->\n            block(user.relationships)?.let {\n                userDetailSnapshot.add(user.copy(relationships = it))\n            }\n        }\n        _uiState.update { state ->\n            state.copy(\n                dataList = updateAuthorRelationships(\n                    notifications = state.dataList,\n                    authorUri = authorUri,\n                    block = block,\n                )\n            )\n        }\n    }\n\n    private fun updateAuthorRelationships(\n        notifications: List<StatusNotificationUiState>,\n        authorUri: FormalUri,\n        block: (Relationships?) -> Relationships?,\n    ): List<StatusNotificationUiState> {\n        return notifications.map { notificationUiState ->\n            val notification = notificationUiState.notification\n            when (notification) {\n                is StatusNotification.Follow -> {\n                    if (notification.author.uri == authorUri) {\n                        notificationUiState.copy(\n                            notification = notification.copy(\n                                author = notification.author.copy(\n                                    relationships = block(notification.author.relationships)\n                                )\n                            )\n                        )\n                    } else {\n                        notificationUiState\n                    }\n                }\n\n                is StatusNotification.FollowRequest -> {\n                    if (notification.author.uri == authorUri) {\n                        notificationUiState.copy(\n                            notification = notification.copy(\n                                author = notification.author.copy(\n                                    relationships = block(notification.author.relationships)\n                                )\n                            )\n                        )\n                    } else {\n                        notificationUiState\n                    }\n                }\n\n                else -> notificationUiState\n            }\n        }\n    }\n\n    private suspend fun getDataFromServer(\n        cursor: String? = this.cursor,\n        loadMore: Boolean = false,\n    ): Result<List<StatusNotificationUiState>> {\n        return statusProvider.notificationResolver.getNotifications(\n            account = account,\n            type = if (uiState.value.inOnlyMentionTab) {\n                INotificationResolver.NotificationRequestType.MENTION\n            } else {\n                INotificationResolver.NotificationRequestType.ALL\n            },\n            cursor = cursor,\n        ).map {\n            this.cursor = it.cursor\n            this.reachEnd = it.reachEnd\n            it.notifications\n                .map { n -> StatusNotificationUiState(n, fromLocal = false) }\n                .let { list -> fillUserDetails(list, userDetailSnapshot) }\n        }.onSuccess {\n            if (loadMore || uiState.value.inOnlyMentionTab) {\n                notificationsRepo.insertNotification(\n                    account.uri,\n                    it.map { n -> n.notification })\n            } else {\n                notificationsRepo.replaceNotifications(\n                    account.uri,\n                    it.map { n -> n.notification })\n            }\n            launch { loadAdditionalData(account, it) }\n        }\n    }\n\n    private suspend fun loadAdditionalData(\n        account: LoggedAccount,\n        notifications: List<StatusNotificationUiState>\n    ) {\n        // Just Follow/FollowRequest notifications need additional data currently.\n        val users = notifications\n            .mapNotNull {\n                when (it.notification) {\n                    is StatusNotification.Follow -> it.notification.author\n                    is StatusNotification.FollowRequest -> it.notification.author\n                    else -> null\n                }\n            }\n            .distinctBy { it.uri }\n        if (users.isEmpty()) return\n        statusProvider.notificationResolver\n            .getNotificationUserDetail(\n                account = account,\n                users = users,\n            ).onSuccess { users ->\n                if (users.isNotEmpty()) {\n                    userDetailSnapshot.addAll(users)\n                    _uiState.update { state ->\n                        state.copy(\n                            dataList = fillUserDetails(state.dataList, userDetailSnapshot)\n                        )\n                    }\n                }\n            }\n    }\n\n    private fun fillUserDetails(\n        notifications: List<StatusNotificationUiState>,\n        users: Collection<BlogAuthor>,\n    ): List<StatusNotificationUiState> {\n        if (users.isEmpty() || notifications.isEmpty()) return notifications\n        return notifications.map { notificationUiState ->\n            val notification = notificationUiState.notification\n            when (notification) {\n                is StatusNotification.Follow -> {\n                    val user = users.getOrNull { it.uri == notification.author.uri }\n                    if (user != null) {\n                        notificationUiState.copy(notification = notification.copy(author = user))\n                    } else {\n                        notificationUiState\n                    }\n                }\n\n                is StatusNotification.FollowRequest -> {\n                    val user = users.getOrNull { it.uri == notification.author.uri }\n                    if (user != null) {\n                        notificationUiState.copy(notification = notification.copy(author = user))\n                    } else {\n                        notificationUiState\n                    }\n                }\n\n                else -> notificationUiState\n            }\n        }\n    }\n\n    private suspend fun getDataFromLocal(): List<StatusNotificationUiState> {\n        return notificationsRepo.getNotifications(account.uri)\n            .filter {\n                if (uiState.value.inOnlyMentionTab) {\n                    it is StatusNotification.Mention || it is StatusNotification.Quote || it is StatusNotification.Reply\n                } else {\n                    true\n                }\n            }.sortedByDescending { it.createAt.epochMillis }\n            .map { StatusNotificationUiState(it, fromLocal = true) }\n    }\n\n    private suspend fun updateStatus(newStatus: StatusUiState) {\n        var updatedNotification: StatusNotification? = null\n        _uiState.update { current ->\n            current.copy(\n                dataList = current.dataList.map { notification ->\n                    notification.updateStatus(newStatus)?.let {\n                        updatedNotification = it.notification\n                        it\n                    } ?: notification\n                }\n            )\n        }\n\n        if (updatedNotification != null) {\n            notificationsRepo.updateNotification(\n                accountUri = account.uri,\n                notification = updatedNotification,\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "feature/notifications/src/iosMain/kotlin/com/zhangke/fread/feature/message/di/NotificationsIosModule.kt",
    "content": "package com.zhangke.fread.feature.message.di\n\nimport androidx.room.Room\nimport androidx.sqlite.driver.bundled.BundledSQLiteDriver\nimport com.zhangke.fread.common.documentDirectory\nimport com.zhangke.fread.feature.message.repo.notification.NotificationsDatabase\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.IO\nimport org.koin.core.module.Module\n\nactual fun Module.createPlatformModule() {\n    factory<NotificationsDatabase> {\n        val dbFilePath = getDBFilePath(NotificationsDatabase.DB_NAME)\n        Room.databaseBuilder<NotificationsDatabase>(\n            name = dbFilePath,\n        ).setDriver(BundledSQLiteDriver())\n            .setQueryCoroutineContext(Dispatchers.IO)\n            .build()\n    }\n}\n\nprivate fun getDBFilePath(dbName: String): String {\n    return documentDirectory() + \"/$dbName\"\n}\n"
  },
  {
    "path": "feature/profile/.gitignore",
    "content": "/build"
  },
  {
    "path": "feature/profile/build.gradle.kts",
    "content": "plugins {\n    id(\"fread.project.feature.kmp\")\n    id(\"com.google.devtools.ksp\")\n}\n\nandroid {\n    namespace = \"com.zhangke.fread.profile\"\n}\n\n\nkotlin {\n    sourceSets {\n        commonMain {\n            dependencies {\n                implementation(project(\":framework\"))\n                implementation(project(\":bizframework:status-provider\"))\n                implementation(project(\":commonbiz:common\"))\n                implementation(project(\":commonbiz:analytics\"))\n                implementation(project(\":commonbiz:sharedscreen\"))\n                implementation(project(\":commonbiz:status-ui\"))\n\n                implementation(compose.components.resources)\n\n                implementation(libs.arrow.core)\n\n                implementation(libs.jetbrains.lifecycle.viewmodel)\n\n                implementation(libs.androidx.paging.common)\n\n                implementation(libs.auto.service.annotations)\n                implementation(libs.krouter.runtime)\n            }\n        }\n        commonTest {\n            dependencies {\n                implementation(kotlin(\"test\"))\n            }\n        }\n        androidMain {\n            dependencies {\n                implementation(libs.androidx.core.ktx)\n                implementation(libs.bundles.androidx.activity)\n                implementation(libs.androidx.browser)\n            }\n        }\n    }\n    configureCommonMainKsp()\n}\n\ndependencies {\n    kspAll(libs.auto.service.ksp)\n    kspAll(libs.krouter.collecting.compiler)\n}\n\ncompose {\n    resources {\n        publicResClass = false\n        packageOfResClass = \"com.zhangke.fread.feature.profile\"\n        generateResClass = always\n    }\n}\n"
  },
  {
    "path": "feature/profile/consumer-rules.pro",
    "content": ""
  },
  {
    "path": "feature/profile/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguardFiles setting in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n\n# Uncomment this to preserve the line number information for\n# debugging stack traces.\n#-keepattributes SourceFile,LineNumberTable\n\n# If you keep the line number information, uncomment this to\n# hide the original source file name.\n#-renamesourcefileattribute SourceFile"
  },
  {
    "path": "feature/profile/src/commonMain/composeResources/drawable/ic_code.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n  <path\n      android:pathData=\"M7.705,7.41L6.295,6L0.295,12L6.295,18L7.705,16.59L3.125,12L7.705,7.41Z\"\n      android:fillColor=\"#000000\"/>\n  <path\n      android:pathData=\"M17.705,6L16.295,7.41L20.875,12L16.295,16.59L17.705,18L23.705,12L17.705,6Z\"\n      android:fillColor=\"#000000\"/>\n  <path\n      android:pathData=\"M13.318,6.063l1.854,0.749l-4.495,11.126l-1.854,-0.749z\"\n      android:fillColor=\"#000000\"/>\n</vector>\n"
  },
  {
    "path": "feature/profile/src/commonMain/composeResources/drawable/ic_github_logo.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"98dp\"\n    android:height=\"96dp\"\n    android:viewportWidth=\"98\"\n    android:viewportHeight=\"96\">\n  <path\n      android:pathData=\"M48.854,0C21.839,0 0,22 0,49.217c0,21.756 13.993,40.172 33.405,46.69 2.427,0.49 3.316,-1.059 3.316,-2.362 0,-1.141 -0.08,-5.052 -0.08,-9.127 -13.59,2.934 -16.42,-5.867 -16.42,-5.867 -2.184,-5.704 -5.42,-7.17 -5.42,-7.17 -4.448,-3.015 0.324,-3.015 0.324,-3.015 4.934,0.326 7.523,5.052 7.523,5.052 4.367,7.496 11.404,5.378 14.235,4.074 0.404,-3.178 1.699,-5.378 3.074,-6.6 -10.839,-1.141 -22.243,-5.378 -22.243,-24.283 0,-5.378 1.94,-9.778 5.014,-13.2 -0.485,-1.222 -2.184,-6.275 0.486,-13.038 0,0 4.125,-1.304 13.426,5.052a46.97,46.97 0,0 1,12.214 -1.63c4.125,0 8.33,0.571 12.213,1.63 9.302,-6.356 13.427,-5.052 13.427,-5.052 2.67,6.763 0.97,11.816 0.485,13.038 3.155,3.422 5.015,7.822 5.015,13.2 0,18.905 -11.404,23.06 -22.324,24.283 1.78,1.548 3.316,4.481 3.316,9.126 0,6.6 -0.08,11.897 -0.08,13.526 0,1.304 0.89,2.853 3.316,2.364 19.412,-6.52 33.405,-24.935 33.405,-46.691C97.707,22 75.788,0 48.854,0z\"\n      android:fillColor=\"#24292f\"\n      android:fillType=\"evenOdd\"/>\n</vector>\n"
  },
  {
    "path": "feature/profile/src/commonMain/composeResources/drawable/ic_profile_tab.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n  <path\n      android:pathData=\"M16,7C16,4.791 14.209,3 12,3C9.791,3 8,4.791 8,7C8,9.209 9.791,11 12,11C14.209,11 16,9.209 16,7ZM10,7C10,5.895 10.895,5 12,5C13.105,5 14,5.895 14,7C14,8.105 13.105,9 12,9C10.895,9 10,8.105 10,7Z\"\n      android:fillColor=\"#828FB0\"\n      android:fillType=\"evenOdd\"/>\n  <path\n      android:pathData=\"M20.392,14.039C18.568,12.679 15.847,12 12.231,12C8.529,12 5.678,12.712 3.678,14.137C2.626,14.885 2.001,16.097 2,17.389V18.875C2,20.601 3.399,22 5.125,22H18.875C20.601,22 22,20.601 22,18.875V17.242C21.999,15.981 21.403,14.793 20.392,14.039ZM4.839,15.766C6.456,14.613 8.911,14 12.231,14C15.454,14 17.768,14.578 19.197,15.642C19.702,16.019 20,16.612 20,17.243V18.875C20,19.496 19.496,20 18.875,20H5.125C4.504,20 4,19.496 4,18.875V17.389C4,16.745 4.312,16.14 4.839,15.766Z\"\n      android:fillColor=\"#828FB0\"\n      android:fillType=\"evenOdd\"/>\n</vector>\n"
  },
  {
    "path": "feature/profile/src/commonMain/composeResources/drawable/ic_ratting.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n  <path\n      android:pathData=\"M14.059,9.02L14.979,9.94L5.919,19H4.999V18.08L14.059,9.02ZM17.659,3C17.409,3 17.149,3.1 16.959,3.29L15.129,5.12L18.879,8.87L20.709,7.04C21.099,6.65 21.099,6.02 20.709,5.63L18.369,3.29C18.169,3.09 17.919,3 17.659,3ZM14.059,6.19L2.999,17.25V21H6.749L17.809,9.94L14.059,6.19Z\"\n      android:fillColor=\"#000000\"/>\n  <path\n      android:pathData=\"M6,19h15v2h-15z\"\n      android:fillColor=\"#000000\"/>\n</vector>\n"
  },
  {
    "path": "feature/profile/src/commonMain/composeResources/drawable/ic_telegram.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:aapt=\"http://schemas.android.com/aapt\"\n    android:width=\"1000dp\"\n    android:height=\"1000dp\"\n    android:viewportWidth=\"1000\"\n    android:viewportHeight=\"1000\">\n  <path\n      android:pathData=\"M500,500m-500,0a500,500 0,1 1,1000 0a500,500 0,1 1,-1000 0\"\n      android:strokeWidth=\"1\"\n      android:fillType=\"evenOdd\"\n      android:strokeColor=\"#00000000\">\n    <aapt:attr name=\"android:fillColor\">\n      <gradient \n          android:startX=\"500\"\n          android:startY=\"-0\"\n          android:endX=\"500\"\n          android:endY=\"992.6\"\n          android:type=\"linear\">\n        <item android:offset=\"0\" android:color=\"#FF2AABEE\"/>\n        <item android:offset=\"1\" android:color=\"#FF229ED9\"/>\n      </gradient>\n    </aapt:attr>\n  </path>\n  <path\n      android:pathData=\"M226.3,494.7C372.1,431.2 469.3,389.4 517.9,369.1C656.8,311.4 685.6,301.3 704.4,301C708.6,300.9 717.8,302 723.8,306.8C728.9,310.9 730.3,316.5 730.9,320.4C731.6,324.2 732.4,333.1 731.8,340C724.2,419.1 691.7,611 675.1,699.5C668.1,737 654.3,749.5 640.9,750.8C611.9,753.4 589.9,731.6 561.7,713.2C517.7,684.3 492.9,666.3 450.2,638.2C400.8,605.7 432.8,587.8 460.9,558.6C468.3,550.9 596.2,434.6 598.7,424C599,422.7 599.3,417.8 596.4,415.2C593.4,412.6 589.1,413.5 586,414.2C581.6,415.2 511.3,461.6 375.1,553.6C355.2,567.3 337.1,573.9 320.9,573.6C303,573.2 268.7,563.5 243.2,555.2C211.9,545 187,539.6 189.1,522.3C190.3,513.3 202.7,504.1 226.3,494.7Z\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#FFFFFF\"\n      android:fillType=\"evenOdd\"\n      android:strokeColor=\"#00000000\"/>\n</vector>\n"
  },
  {
    "path": "feature/profile/src/commonMain/composeResources/drawable/kofi_symbol.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"241dp\"\n    android:height=\"194dp\"\n    android:viewportWidth=\"241\"\n    android:viewportHeight=\"194\">\n  <group>\n    <clip-path\n        android:pathData=\"M240.47,0.96H-0.01V193.92H240.47V0.96Z\"/>\n    <path\n        android:pathData=\"M96.13,193.91C61.13,193.91 32.66,178.26 15.97,149.83C1.2,124.91 -0.01,97.92 -0.01,67.77C-0.01,49.89 5.37,34.32 15.54,22.75C24.89,12.12 38.13,5.23 52.83,3.35C70.29,1.14 91.98,0.96 114.54,0.96C151.26,0.96 161.63,1.41 176.07,2.85C195.29,4.76 211.46,11.93 222.82,23.6C234.37,35.44 240.47,51.26 240.47,69.36V73C240.47,103.89 219.82,129.73 191.05,136.76C188.9,141.83 186.24,146.87 183.09,151.84L183.01,151.96C172.87,167.63 149.04,193.92 103.4,193.92H96.13L96.13,193.91Z\"\n        android:fillColor=\"#ffffff\"/>\n    <path\n        android:pathData=\"M174.57,17.98C160.93,16.62 151.38,16.16 114.55,16.16C90.91,16.16 70.9,16.39 54.76,18.43C33.39,21.16 15.21,37.53 15.21,67.77C15.21,98.01 16.8,121.42 29.07,142.11C42.94,165.75 66.13,178.71 96.14,178.71H103.41C140.24,178.71 160.25,159.16 170.25,143.7C174.57,136.87 177.75,130.06 179.8,123.23C205.95,120.96 225.27,99.36 225.27,72.99V69.36C225.27,40.94 206.63,21.16 174.57,17.98H174.57Z\"\n        android:fillColor=\"#ffffff\"/>\n    <path\n        android:pathData=\"M15.2,67.77C15.2,37.53 33.39,21.16 54.76,18.43C70.9,16.39 90.91,16.16 114.54,16.16C151.37,16.16 160.92,16.62 174.56,17.98C206.62,21.16 225.26,40.94 225.26,69.36V72.99C225.26,99.37 205.93,120.97 179.79,123.23C177.74,130.06 174.56,136.87 170.24,143.7C160.24,159.16 140.23,178.71 103.4,178.71H96.13C66.12,178.71 42.93,165.75 29.06,142.11C16.78,121.42 15.19,98.46 15.19,67.77\"\n        android:fillColor=\"#202020\"/>\n    <path\n        android:pathData=\"M32.25,67.99C32.25,97.32 34.07,116.18 43.61,133.69C54.52,153.92 74.3,161.65 96.81,161.65H103.86C133.41,161.65 147.74,147.33 155.69,134.83C159.56,128.46 162.97,121.42 164.78,112.55L166.15,106.86H174.33C192.52,106.86 208.21,92.09 208.21,73.22V69.81C208.21,48.67 195.02,37.52 172.06,34.8C159.1,33.66 151.37,33.21 114.54,33.21C89.76,33.21 72.03,33.44 58.62,35.48C39.75,38.21 32.24,48.9 32.24,67.99\"\n        android:fillColor=\"#ffffff\"/>\n    <path\n        android:pathData=\"M166.16,83.68C166.16,86.41 168.2,88.46 171.84,88.46C183.43,88.46 189.8,81.86 189.8,70.95C189.8,60.04 183.43,53.22 171.84,53.22C168.2,53.22 166.16,55.27 166.16,58V83.69V83.68Z\"\n        android:fillColor=\"#202020\"/>\n    <path\n        android:pathData=\"M54.53,82.32C54.53,95.73 62.03,107.33 71.58,116.42C77.95,122.56 87.95,128.93 94.77,133.02C96.81,134.16 98.86,134.84 101.14,134.84C103.87,134.84 106.13,134.16 107.96,133.02C114.78,128.93 124.78,122.56 130.92,116.42C140.69,107.33 148.2,95.74 148.2,82.32C148.2,67.77 137.29,54.81 121.6,54.81C112.28,54.81 105.91,59.59 101.14,66.18C96.81,59.58 90.23,54.81 80.9,54.81C64.99,54.81 54.53,67.77 54.53,82.32\"\n        android:fillColor=\"#FF5A16\"/>\n  </group>\n</vector>\n"
  },
  {
    "path": "feature/profile/src/commonMain/kotlin/com/zhangke/fread/profile/ProfileNavEntryProvider.kt",
    "content": "package com.zhangke.fread.profile\n\nimport androidx.navigation3.runtime.EntryProviderScope\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.nav.NavEntryProvider\nimport com.zhangke.framework.nav.dialogMetadata\nimport com.zhangke.fread.profile.screen.donate.DonateScreen\nimport com.zhangke.fread.profile.screen.donate.DonateScreenNavKey\nimport com.zhangke.fread.profile.screen.opensource.OpenSourceScreen\nimport com.zhangke.fread.profile.screen.opensource.OpenSourceScreenNavKey\nimport com.zhangke.fread.profile.screen.setting.SettingScreen\nimport com.zhangke.fread.profile.screen.setting.SettingScreenNavKey\nimport com.zhangke.fread.profile.screen.setting.about.AboutScreen\nimport com.zhangke.fread.profile.screen.setting.about.AboutScreenNavKey\nimport com.zhangke.fread.profile.screen.setting.appearance.AppearanceSettingsNavKey\nimport com.zhangke.fread.profile.screen.setting.appearance.AppearanceSettingsScreen\nimport com.zhangke.fread.profile.screen.setting.behavior.BehaviorSettingsNavKey\nimport com.zhangke.fread.profile.screen.setting.behavior.BehaviorSettingsScreen\nimport kotlinx.serialization.modules.PolymorphicModuleBuilder\nimport kotlinx.serialization.modules.subclass\nimport org.koin.compose.viewmodel.koinViewModel\n\nclass ProfileNavEntryProvider : NavEntryProvider {\n\n    override fun EntryProviderScope<NavKey>.build() {\n        entry<SettingScreenNavKey> {\n            SettingScreen(koinViewModel())\n        }\n        entry<AboutScreenNavKey> {\n            AboutScreen(koinViewModel())\n        }\n        entry<AppearanceSettingsNavKey> {\n            AppearanceSettingsScreen(koinViewModel())\n        }\n        entry<BehaviorSettingsNavKey> {\n            BehaviorSettingsScreen(koinViewModel())\n        }\n        entry<OpenSourceScreenNavKey> {\n            OpenSourceScreen()\n        }\n        entry<DonateScreenNavKey>(\n            metadata = dialogMetadata(),\n        ) {\n            DonateScreen()\n        }\n    }\n\n    override fun PolymorphicModuleBuilder<NavKey>.polymorph() {\n        subclass(SettingScreenNavKey::class)\n        subclass(AboutScreenNavKey::class)\n        subclass(AppearanceSettingsNavKey::class)\n        subclass(BehaviorSettingsNavKey::class)\n        subclass(OpenSourceScreenNavKey::class)\n        subclass(DonateScreenNavKey::class)\n    }\n}\n"
  },
  {
    "path": "feature/profile/src/commonMain/kotlin/com/zhangke/fread/profile/ProfileScreenVisitor.kt",
    "content": "package com.zhangke.fread.profile\n\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.fread.commonbiz.shared.IProfileScreenVisitor\nimport com.zhangke.fread.profile.screen.donate.DonateScreenNavKey\n\nclass ProfileScreenVisitor : IProfileScreenVisitor {\n\n    override fun getDonateScreen(): NavKey {\n        return DonateScreenNavKey\n    }\n}\n"
  },
  {
    "path": "feature/profile/src/commonMain/kotlin/com/zhangke/fread/profile/di/ProfileModule.kt",
    "content": "package com.zhangke.fread.profile.di\n\nimport com.zhangke.framework.nav.NavEntryProvider\nimport com.zhangke.fread.commonbiz.shared.IProfileScreenVisitor\nimport com.zhangke.fread.profile.ProfileNavEntryProvider\nimport com.zhangke.fread.profile.ProfileScreenVisitor\nimport com.zhangke.fread.profile.screen.home.ProfileHomeViewModel\nimport com.zhangke.fread.profile.screen.setting.SettingScreenModel\nimport com.zhangke.fread.profile.screen.setting.about.AboutViewModel\nimport com.zhangke.fread.profile.screen.setting.appearance.AppearanceSettingsViewModel\nimport com.zhangke.fread.profile.screen.setting.behavior.BehaviorSettingsViewModel\nimport org.koin.core.module.dsl.factoryOf\nimport org.koin.core.module.dsl.singleOf\nimport org.koin.core.module.dsl.viewModelOf\nimport org.koin.dsl.bind\nimport org.koin.dsl.module\n\nval profileModule = module {\n\n    factoryOf(::ProfileNavEntryProvider) bind NavEntryProvider::class\n\n    viewModelOf(::ProfileHomeViewModel)\n    viewModelOf(::SettingScreenModel)\n    viewModelOf(::AboutViewModel)\n    viewModelOf(::AppearanceSettingsViewModel)\n    viewModelOf(::BehaviorSettingsViewModel)\n\n    singleOf(::ProfileScreenVisitor) bind IProfileScreenVisitor::class\n}\n"
  },
  {
    "path": "feature/profile/src/commonMain/kotlin/com/zhangke/fread/profile/screen/donate/DonateScreen.kt",
    "content": "package com.zhangke.fread.profile.screen.donate\n\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.painter.Painter\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.composable.currentOrThrow\nimport com.zhangke.framework.composable.noRippleClick\nimport com.zhangke.framework.nav.LocalNavBackStack\nimport com.zhangke.fread.common.browser.LocalActivityBrowserLauncher\nimport com.zhangke.fread.common.browser.launchWebTabInApp\nimport com.zhangke.fread.common.config.AppCommonConfig\nimport com.zhangke.fread.feature.profile.Res\nimport com.zhangke.fread.feature.profile.af_dian\nimport com.zhangke.fread.feature.profile.kofi_symbol\nimport com.zhangke.fread.localization.LocalizedString\nimport kotlinx.serialization.Serializable\nimport org.jetbrains.compose.resources.painterResource\nimport org.jetbrains.compose.resources.stringResource\n\n@Serializable\nobject DonateScreenNavKey : NavKey\n\n@Composable\nfun DonateScreen() {\n    val browserLauncher = LocalActivityBrowserLauncher.current\n    val coroutineScope = rememberCoroutineScope()\n    val backStack = LocalNavBackStack.currentOrThrow\n    DonateContent(\n        onDonateClick = {\n            browserLauncher.launchWebTabInApp(\n                scope = coroutineScope,\n                url = it.url,\n                checkAppSupportPage = false,\n            )\n            backStack.removeLastOrNull()\n        },\n    )\n}\n\n@Composable\nprivate fun DonateContent(\n    onDonateClick: (DonateItem) -> Unit,\n) {\n    Surface(\n        modifier = Modifier.fillMaxWidth(),\n        shape = RoundedCornerShape(16.dp),\n    ) {\n        Column(\n            modifier = Modifier.fillMaxWidth()\n                .padding(horizontal = 16.dp, vertical = 16.dp),\n            verticalArrangement = Arrangement.spacedBy(16.dp),\n        ) {\n            Text(\n                text = stringResource(LocalizedString.profileDonatePageTitle),\n                fontSize = 18.sp,\n                maxLines = 1,\n                overflow = TextOverflow.Ellipsis,\n            )\n            val donateList = remember { buildDonateList() }\n            for (donateItem in donateList) {\n                Row(\n                    modifier = Modifier.fillMaxWidth()\n                        .noRippleClick { onDonateClick(donateItem) },\n                    verticalAlignment = Alignment.CenterVertically,\n                ) {\n                    Image(\n                        modifier = Modifier.size(24.dp)\n                            .clip(CircleShape),\n                        painter = donateItem.logo(),\n                        contentDescription = null,\n                    )\n                    Column(\n                        modifier = Modifier.weight(1F).padding(start = 16.dp),\n                        horizontalAlignment = Alignment.Start,\n                    ) {\n                        Text(\n                            text = donateItem.title,\n                            style = MaterialTheme.typography.bodyMedium\n                                .copy(fontWeight = FontWeight.SemiBold),\n                        )\n                        Text(\n                            modifier = Modifier,\n                            text = donateItem.url,\n                            color = MaterialTheme.colorScheme.onSurfaceVariant,\n                        )\n                    }\n                }\n            }\n        }\n    }\n}\n\nprivate fun buildDonateList(): List<DonateItem> {\n    return listOf(\n        DonateItem.kofi(),\n        DonateItem.afDian(),\n    )\n}\n\ndata class DonateItem(\n    val title: String,\n    val url: String,\n    val logo: @Composable () -> Painter,\n) {\n\n    companion object {\n\n        fun kofi(): DonateItem {\n            return DonateItem(\n                title = \"Ko-fi\",\n                url = AppCommonConfig.DONATE_KO_FI_LINK,\n                logo = {\n                    painterResource(Res.drawable.kofi_symbol)\n                },\n            )\n        }\n\n        fun afDian(): DonateItem {\n            return DonateItem(\n                title = \"AFDIAN(爱发电)\",\n                url = AppCommonConfig.DONATE_AF_DIAN_LINK,\n                logo = {\n                    painterResource(Res.drawable.af_dian)\n                },\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "feature/profile/src/commonMain/kotlin/com/zhangke/fread/profile/screen/home/ProfileHomeUiState.kt",
    "content": "package com.zhangke.fread.profile.screen.home\n\nimport com.zhangke.fread.status.model.LoggedAccountDetail\n\ndata class ProfileHomeUiState(\n    val accountDataList: List<ProfileAccountUiState>,\n)\n\ndata class ProfileAccountUiState(\n    val account: LoggedAccountDetail,\n    val authFailed: Boolean,\n    val active: Boolean,\n)\n"
  },
  {
    "path": "feature/profile/src/commonMain/kotlin/com/zhangke/fread/profile/screen/home/ProfileHomeViewModel.kt",
    "content": "package com.zhangke.fread.profile.screen.home\n\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.collections.container\nimport com.zhangke.framework.ktx.launchInViewModel\nimport com.zhangke.fread.common.account.ActiveAccountsSynchronizer\nimport com.zhangke.fread.status.StatusProvider\nimport com.zhangke.fread.status.account.AccountRefreshResult\nimport com.zhangke.fread.status.account.LoggedAccount\nimport com.zhangke.fread.status.account.isAuthenticationFailure\nimport com.zhangke.fread.status.model.PlatformLocator\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.asSharedFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.map\nimport kotlinx.coroutines.flow.mapNotNull\nimport kotlinx.coroutines.flow.update\nimport kotlinx.coroutines.launch\n\nclass ProfileHomeViewModel(\n    private val statusProvider: StatusProvider,\n    private val activeAccountsSynchronizer: ActiveAccountsSynchronizer,\n) : ViewModel() {\n\n    private val _uiState = MutableStateFlow(ProfileHomeUiState(emptyList()))\n    val uiState: StateFlow<ProfileHomeUiState> get() = _uiState.asStateFlow()\n\n    private val _openPageFlow = MutableSharedFlow<NavKey>()\n    val openPageFlow = _openPageFlow.asSharedFlow()\n\n    private var refreshAccountJob: Job? = null\n\n    init {\n        observeAccountFlow()\n        launchInViewModel {\n            activeAccountsSynchronizer.activeAccountUriFlow\n                .mapNotNull { it?.takeIf { it.isNotEmpty() } }\n                .collect { lastActiveAccountUri ->\n                    _uiState.update { state ->\n                        state.copy(\n                            accountDataList = state.accountDataList.map { account ->\n                                account.copy(\n                                    active = account.account.account.uri.toString() == lastActiveAccountUri,\n                                )\n                            }\n                        )\n                    }\n                }\n        }\n    }\n\n    private fun observeAccountFlow() {\n        viewModelScope.launch {\n            statusProvider.accountManager\n                .getAllAccountDetailFlow()\n                .map { list ->\n                    val activeAccountUri = activeAccountsSynchronizer.activeAccountUriFlow.value\n                    val dirtyAccountList = uiState.value.accountDataList\n                    list.map { account ->\n                        val authFailed = dirtyAccountList.firstOrNull {\n                            it.account.account.uri == account.account.uri\n                        }?.authFailed ?: false\n                        ProfileAccountUiState(\n                            account = account,\n                            authFailed = authFailed,\n                            active = account.account.uri.toString() == activeAccountUri,\n                        )\n                    }\n                }\n                .collect { list ->\n                    _uiState.update { it.copy(accountDataList = list) }\n                }\n        }\n    }\n\n    fun refreshAccountInfo() {\n        if (refreshAccountJob?.isActive == true) return\n        refreshAccountJob = launchInViewModel {\n            val refreshedList = statusProvider.accountManager.refreshAllAccountInfo()\n            val authFailedAccounts = refreshedList.filter {\n                it is AccountRefreshResult.Failure && it.error.isAuthenticationFailure\n            }\n            _uiState.update { state ->\n                state.copy(\n                    accountDataList = state.accountDataList\n                        .map { account ->\n                            val authFailed = authFailedAccounts.container {\n                                it.account.uri == account.account.account.uri\n                            }\n                            account.copy(authFailed = authFailed)\n                        }\n                )\n            }\n        }\n    }\n\n    fun onAccountClick(account: LoggedAccount) {\n        launchInViewModel {\n            statusProvider.screenProvider\n                .getUserDetailScreen(\n                    locator = account.platformLocator,\n                    uri = account.uri,\n                    userId = account.id,\n                )?.let { _openPageFlow.emit(it) }\n        }\n    }\n\n    fun onLoginClick(account: LoggedAccount) {\n        launchInViewModel {\n            statusProvider.accountManager.triggerAuthBySource(account.platform, account)\n        }\n    }\n\n    private val LoggedAccount.platformLocator: PlatformLocator\n        get() = PlatformLocator(\n            baseUrl = platform.baseUrl,\n            accountUri = uri,\n        )\n}\n"
  },
  {
    "path": "feature/profile/src/commonMain/kotlin/com/zhangke/fread/profile/screen/home/ProfileScreen.kt",
    "content": "package com.zhangke.fread.profile.screen.home\n\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.PersonAdd\nimport androidx.compose.material.icons.filled.Settings\nimport androidx.compose.material3.ButtonDefaults\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport com.zhangke.framework.blur.applyBlurEffect\nimport com.zhangke.framework.blur.applyBlurSource\nimport com.zhangke.framework.blur.blurEffectContainerColor\nimport com.zhangke.framework.composable.ConsumeFlow\nimport com.zhangke.framework.composable.LocalContentPadding\nimport com.zhangke.framework.composable.SimpleIconButton\nimport com.zhangke.framework.composable.SingleRowTopAppBar\nimport com.zhangke.framework.composable.TopAppBarColors\nimport com.zhangke.framework.composable.currentOrThrow\nimport com.zhangke.framework.composable.plus\nimport com.zhangke.framework.nav.LocalNavBackStack\nimport com.zhangke.fread.common.composable.EmptyContent\nimport com.zhangke.fread.common.composable.EmptyContentType\nimport com.zhangke.fread.commonbiz.shared.LocalModuleScreenVisitor\nimport com.zhangke.fread.commonbiz.shared.composable.UserInfoCard\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.profile.screen.setting.SettingScreenNavKey\nimport com.zhangke.fread.status.account.LoggedAccount\nimport org.jetbrains.compose.resources.stringResource\nimport org.koin.compose.viewmodel.koinViewModel\n\n@Composable\nfun ProfileScreen() {\n    val backStack = LocalNavBackStack.currentOrThrow\n    val viewModel = koinViewModel<ProfileHomeViewModel>()\n    val uiState by viewModel.uiState.collectAsState()\n    val moduleScreenVisitor = LocalModuleScreenVisitor.current\n    LaunchedEffect(Unit) {\n        viewModel.refreshAccountInfo()\n    }\n    ProfileHomePageContent(\n        uiState = uiState,\n        onAddAccountClick = {\n            backStack.add(moduleScreenVisitor.feedsScreenVisitor.getAddContentScreen())\n        },\n        onSettingClick = {\n            backStack.add(SettingScreenNavKey)\n        },\n        onAccountClick = {\n            viewModel.onAccountClick(it)\n        },\n        onLoginClick = viewModel::onLoginClick,\n    )\n    ConsumeFlow(viewModel.openPageFlow) {\n        backStack.add(it)\n    }\n}\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nprivate fun ProfileHomePageContent(\n    uiState: ProfileHomeUiState,\n    onAddAccountClick: () -> Unit,\n    onSettingClick: () -> Unit,\n    onAccountClick: (LoggedAccount) -> Unit,\n    onLoginClick: (LoggedAccount) -> Unit,\n) {\n    val surfaceColor = MaterialTheme.colorScheme.surface\n    Scaffold(\n        modifier = Modifier.fillMaxSize(),\n        topBar = {\n            SingleRowTopAppBar(\n                modifier = Modifier.applyBlurEffect(containerColor = surfaceColor),\n                colors = TopAppBarColors.default(\n                    containerColor = blurEffectContainerColor(containerColor = surfaceColor),\n                ),\n                height = 48.dp,\n                title = {\n                    Text(\n                        modifier = Modifier,\n                        text = stringResource(LocalizedString.profilePageTitle),\n                        color = MaterialTheme.colorScheme.onSurface,\n                    )\n                },\n                actions = {\n                    SimpleIconButton(\n                        onClick = onAddAccountClick,\n                        imageVector = Icons.Default.PersonAdd,\n                        contentDescription = \"Add Account\",\n                    )\n                    SimpleIconButton(\n                        onClick = onSettingClick,\n                        imageVector = Icons.Default.Settings,\n                        contentDescription = \"Settings\",\n                    )\n                },\n            )\n        },\n    ) { innerPadding ->\n        Box(modifier = Modifier.fillMaxSize()) {\n            if (uiState.accountDataList.isEmpty()) {\n                EmptyContent(\n                    modifier = Modifier.fillMaxSize(),\n                    type = EmptyContentType.Account,\n                    onClick = onAddAccountClick,\n                )\n            } else {\n                LazyColumn(\n                    modifier = Modifier.fillMaxWidth().applyBlurSource(),\n                    contentPadding = LocalContentPadding.current.plus(innerPadding),\n                ) {\n                    items(uiState.accountDataList) { item ->\n                        AccountDetail(\n                            modifier = Modifier.fillMaxWidth(),\n                            accountDetail = item,\n                            onAccountClick = onAccountClick,\n                            onLoginClick = onLoginClick,\n                        )\n                        Spacer(modifier = Modifier.height(16.dp))\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun AccountDetail(\n    modifier: Modifier,\n    accountDetail: ProfileAccountUiState,\n    onAccountClick: (LoggedAccount) -> Unit,\n    onLoginClick: (LoggedAccount) -> Unit,\n) {\n    UserInfoCard(\n        modifier = modifier.padding(horizontal = 16.dp),\n        user = accountDetail.account.author,\n        showActiveState = accountDetail.active,\n        actionButton = if (accountDetail.authFailed) {\n            {\n                TextButton(\n                    modifier = Modifier,\n                    onClick = { onLoginClick(accountDetail.account.account) },\n                    colors = ButtonDefaults.textButtonColors(\n                        contentColor = MaterialTheme.colorScheme.error,\n                    ),\n                ) {\n                    Text(text = stringResource(LocalizedString.profileAccountNotLogin))\n                }\n            }\n        } else {\n            null\n        },\n        onUserClick = { onAccountClick(accountDetail.account.account) },\n    )\n}\n"
  },
  {
    "path": "feature/profile/src/commonMain/kotlin/com/zhangke/fread/profile/screen/home/ProfileTab.kt",
    "content": "package com.zhangke.fread.profile.screen.home\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport com.zhangke.framework.nav.Tab\nimport com.zhangke.framework.nav.TabOptions\nimport com.zhangke.fread.feature.profile.Res\nimport com.zhangke.fread.feature.profile.ic_profile_tab\nimport org.jetbrains.compose.resources.painterResource\n\nclass ProfileTab : Tab {\n\n    override val options: TabOptions\n        @Composable get() {\n            val icon = painterResource(Res.drawable.ic_profile_tab)\n            return remember {\n                TabOptions(\n                    title = \"Profile\",\n                    icon = icon,\n                )\n            }\n        }\n\n    @Composable\n    override fun Content() {\n        ProfileScreen()\n    }\n}\n"
  },
  {
    "path": "feature/profile/src/commonMain/kotlin/com/zhangke/fread/profile/screen/opensource/OpenSourceScreen.kt",
    "content": "package com.zhangke.fread.profile.screen.opensource\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.material3.HorizontalDivider\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.composable.Toolbar\nimport com.zhangke.framework.composable.currentOrThrow\nimport com.zhangke.framework.nav.LocalNavBackStack\nimport com.zhangke.fread.common.browser.LocalActivityBrowserLauncher\nimport com.zhangke.fread.common.browser.launchWebTabInApp\nimport com.zhangke.fread.localization.LocalizedString\nimport kotlinx.serialization.Serializable\nimport org.jetbrains.compose.resources.stringResource\n\n@Serializable\nobject OpenSourceScreenNavKey : NavKey\n\n@Composable\nfun OpenSourceScreen() {\n    val backStack = LocalNavBackStack.currentOrThrow\n    val browserLauncher = LocalActivityBrowserLauncher.current\n    val coroutineScope = rememberCoroutineScope()\n    Scaffold(\n        topBar = {\n            Toolbar(\n                title = stringResource(LocalizedString.profileSettingOpenSourceTitle),\n                onBackClick = backStack::removeLastOrNull,\n            )\n        }\n    ) { innerPadding ->\n        Box(\n            modifier = Modifier\n                .fillMaxSize()\n                .padding(innerPadding),\n        ) {\n            val openSourceInfoList = remember {\n                buildOpenSourceInfoList()\n            }\n            LazyColumn(\n                modifier = Modifier.fillMaxSize(),\n            ) {\n                items(openSourceInfoList) { openSourceInfo ->\n                    OpenSourceItem(\n                        openSource = openSourceInfo,\n                        onClick = {\n                            browserLauncher.launchWebTabInApp(coroutineScope, it.url)\n                        },\n                    )\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun OpenSourceItem(\n    openSource: OpenSourceInfo,\n    onClick: (OpenSourceInfo) -> Unit,\n) {\n    Column(\n        modifier = Modifier\n            .clickable { onClick(openSource) }\n            .fillMaxWidth()\n            .padding(top = 8.dp),\n    ) {\n        Text(\n            modifier = Modifier.padding(start = 16.dp, end = 16.dp),\n            text = \"${openSource.name} - ${openSource.author}\",\n            style = MaterialTheme.typography.titleMedium,\n        )\n        Text(\n            modifier = Modifier.padding(start = 16.dp, top = 4.dp, end = 16.dp),\n            text = openSource.license,\n            style = MaterialTheme.typography.bodyMedium,\n        )\n        Text(\n            modifier = Modifier.padding(start = 16.dp, top = 4.dp, end = 16.dp),\n            text = openSource.url,\n            maxLines = 1,\n            style = MaterialTheme.typography.bodyMedium,\n        )\n        HorizontalDivider(modifier = Modifier.padding(top = 8.dp))\n    }\n}\n\nprivate fun buildOpenSourceInfoList(): List<OpenSourceInfo> {\n    return listOf(\n        OpenSourceInfo(\n            name = \"Fread\",\n            author = \"ZhangKe\",\n            license = OpenSourceInfo.LICENSE_APACHE_2,\n            url = \"https://github.com/0xZhangKe/Fread\"\n        ),\n        OpenSourceInfo(\n            name = \"KRouter\",\n            author = \"ZhangKe\",\n            license = OpenSourceInfo.LICENSE_APACHE_2,\n            url = \"https://github.com/0xZhangKe/KRouter\"\n        ),\n        OpenSourceInfo(\n            name = \"Filt\",\n            author = \"ZhangKe\",\n            license = OpenSourceInfo.LICENSE_APACHE_2,\n            url = \"https://github.com/0xZhangKe/Filt\"\n        ),\n        OpenSourceInfo(\n            name = \"ActivityPub-Kotlin\",\n            author = \"ZhangKe\",\n            license = OpenSourceInfo.LICENSE_APACHE_2,\n            url = \"https://github.com/0xZhangKe/ActivityPub-Kotlin\"\n        ),\n        OpenSourceInfo(\n            name = \"Kotlin\",\n            author = \"Jetbrains\",\n            license = OpenSourceInfo.LICENSE_APACHE_2,\n            url = \"https://kotlinlang.org/\"\n        ),\n        OpenSourceInfo(\n            name = \"Jetpack Compose\",\n            author = \"Google\",\n            license = OpenSourceInfo.LICENSE_APACHE_2,\n            url = \"https://developer.android.com/jetpack/androidx/releases/compose\"\n        ),\n        OpenSourceInfo(\n            name = \"Jetpack\",\n            author = \"Google\",\n            license = OpenSourceInfo.LICENSE_APACHE_2,\n            url = \"https://developer.android.com/jetpack\"\n        ),\n        OpenSourceInfo(\n            name = \"AndroidX\",\n            author = \"Google\",\n            license = OpenSourceInfo.LICENSE_APACHE_2,\n            url = \"https://developer.android.com/jetpack/androidx/\"\n        ),\n        OpenSourceInfo(\n            name = \"Gson\",\n            author = \"Google\",\n            license = OpenSourceInfo.LICENSE_APACHE_2,\n            url = \"https://github.com/google/gson/\"\n        ),\n        OpenSourceInfo(\n            name = \"OkHttp\",\n            author = \"Square\",\n            license = OpenSourceInfo.LICENSE_APACHE_2,\n            url = \"https://square.github.io/okhttp/\"\n        ),\n        OpenSourceInfo(\n            name = \"Retrofit\",\n            author = \"Square\",\n            license = OpenSourceInfo.LICENSE_APACHE_2,\n            url = \"https://github.com/square/retrofit\"\n        ),\n        OpenSourceInfo(\n            name = \"Accompanist\",\n            author = \"Google\",\n            license = OpenSourceInfo.LICENSE_APACHE_2,\n            url = \"https://github.com/google/accompanist\"\n        ),\n        OpenSourceInfo(\n            name = \"Material-Components\",\n            author = \"Google\",\n            license = OpenSourceInfo.LICENSE_APACHE_2,\n            url = \"https://github.com/material-components/material-components-android\"\n        ),\n        OpenSourceInfo(\n            name = \"compose-richtext\",\n            author = \"halilozercan\",\n            license = OpenSourceInfo.LICENSE_APACHE_2,\n            url = \"https://github.com/halilozercan/compose-richtext\",\n        ),\n        OpenSourceInfo(\n            name = \"ComposeReorderable\",\n            author = \"André Claßen\",\n            license = OpenSourceInfo.LICENSE_APACHE_2,\n            url = \"https://github.com/aclassen/ComposeReorderable\",\n        ),\n        OpenSourceInfo(\n            name = \"compose-wheel-picker\",\n            author = \"zj565061763\",\n            license = \"MIT license\",\n            url = \"https://github.com/zj565061763/compose-wheel-picker\",\n        ),\n    )\n}\n\ndata class OpenSourceInfo(\n    val name: String,\n    val author: String,\n    val license: String,\n    val url: String,\n) {\n\n    companion object {\n\n        const val LICENSE_APACHE_2 = \"The Apache Software License, Version 2.0\"\n    }\n}\n"
  },
  {
    "path": "feature/profile/src/commonMain/kotlin/com/zhangke/fread/profile/screen/setting/FeedbackBottomSheet.kt",
    "content": "package com.zhangke.fread.profile.screen.setting\n\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Email\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.ModalBottomSheet\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.unit.dp\nimport com.zhangke.fread.common.browser.LocalActivityBrowserLauncher\nimport com.zhangke.fread.common.browser.launchWebTabInApp\nimport com.zhangke.fread.common.config.AppCommonConfig\nimport com.zhangke.fread.common.handler.LocalTextHandler\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.feature.profile.Res\nimport com.zhangke.fread.feature.profile.ic_github_logo\nimport com.zhangke.fread.feature.profile.ic_telegram\nimport com.zhangke.framework.composable.rememberTransientModalBottomSheetState\nimport kotlinx.coroutines.launch\nimport org.jetbrains.compose.resources.stringResource\nimport org.jetbrains.compose.resources.vectorResource\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun FeedbackBottomSheet(\n    onDismissRequest: () -> Unit,\n) {\n    val textHandler = LocalTextHandler.current\n    val browserLauncher = LocalActivityBrowserLauncher.current\n    val sheetState = rememberTransientModalBottomSheetState()\n    val coroutineScope = rememberCoroutineScope()\n    ModalBottomSheet(\n        sheetState = sheetState,\n        onDismissRequest = {\n            coroutineScope.launch {\n                sheetState.hide()\n                onDismissRequest()\n            }\n        },\n    ) {\n        Column {\n            Row(\n                modifier = Modifier\n                    .clickable {\n                        onDismissRequest()\n                        browserLauncher.launchWebTabInApp(coroutineScope, AppCommonConfig.TELEGRAM_GROUP)\n                    }\n                    .fillMaxWidth()\n                    .padding(horizontal = 16.dp, vertical = 16.dp),\n                verticalAlignment = Alignment.CenterVertically,\n            ) {\n                Image(\n                    modifier = Modifier\n                        .size(24.dp)\n                        .clip(CircleShape),\n                    imageVector = vectorResource(Res.drawable.ic_telegram),\n                    contentDescription = null,\n                )\n                Spacer(modifier = Modifier.width(16.dp))\n                Text(\n                    text = stringResource(LocalizedString.profileSettingOpenSourceFeedbackTelegram),\n                    style = MaterialTheme.typography.titleMedium,\n                )\n            }\n            Row(\n                modifier = Modifier\n                    .clickable {\n                        onDismissRequest()\n                        browserLauncher.launchWebTabInApp(coroutineScope, AppCommonConfig.FEEDBACK_URL)\n                    }\n                    .fillMaxWidth()\n                    .padding(horizontal = 16.dp, vertical = 16.dp),\n                verticalAlignment = Alignment.CenterVertically,\n            ) {\n                Icon(\n                    modifier = Modifier.size(24.dp),\n                    imageVector = vectorResource(Res.drawable.ic_github_logo),\n                    contentDescription = null,\n                )\n                Spacer(modifier = Modifier.width(16.dp))\n                Text(\n                    text = stringResource(LocalizedString.profileSettingOpenSourceFeedbackGithub),\n                    style = MaterialTheme.typography.titleMedium,\n                )\n            }\n            Row(\n                modifier = Modifier\n                    .clickable {\n                        onDismissRequest()\n                        textHandler.openSendEmail()\n                    }\n                    .fillMaxWidth()\n                    .padding(horizontal = 16.dp, vertical = 16.dp),\n                verticalAlignment = Alignment.CenterVertically,\n            ) {\n                Icon(\n                    modifier = Modifier.size(24.dp),\n                    imageVector = Icons.Default.Email,\n                    contentDescription = null,\n                )\n                Spacer(modifier = Modifier.width(16.dp))\n                Text(\n                    text = stringResource(LocalizedString.profileSettingOpenSourceFeedbackEmail),\n                    style = MaterialTheme.typography.titleMedium,\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "feature/profile/src/commonMain/kotlin/com/zhangke/fread/profile/screen/setting/SettingComponents.kt",
    "content": "package com.zhangke.fread.profile.screen.setting\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.heightIn\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.material3.DropdownMenuItem\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Switch\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.unit.DpOffset\nimport androidx.compose.ui.unit.dp\nimport com.zhangke.framework.composable.PopupMenu\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.launch\n\nprivate val itemHeight = 82.dp\n\n@Composable\ninternal fun SettingItemWithSwitch(\n    icon: ImageVector,\n    title: String,\n    subtitle: String,\n    checked: Boolean,\n    onCheckedChangeRequest: (Boolean) -> Unit,\n) {\n    Row(\n        modifier = Modifier.fillMaxWidth(),\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        Box(modifier = Modifier.weight(1F)) {\n            SettingItem(\n                icon = icon,\n                title = title,\n                subtitle = subtitle,\n                onClick = {},\n            )\n        }\n        Switch(\n            modifier = Modifier,\n            checked = checked,\n            onCheckedChange = onCheckedChangeRequest,\n        )\n        Spacer(modifier = Modifier.width(16.dp))\n    }\n}\n\n@Composable\ninternal fun SettingItemWithPopup(\n    icon: ImageVector,\n    title: String,\n    subtitle: String,\n    dropDownItemCount: Int,\n    dropDownItemText: @Composable (Int) -> String,\n    onItemClick: (Int) -> Unit,\n) {\n    val coroutineScope = rememberCoroutineScope()\n    Box(modifier = Modifier.fillMaxWidth()) {\n        var showPopup by remember {\n            mutableStateOf(false)\n        }\n        SettingItem(\n            icon = icon,\n            title = title,\n            subtitle = subtitle,\n            onClick = {\n                showPopup = true\n            },\n        )\n        PopupMenu(\n            expanded = showPopup,\n            offset = DpOffset(x = 36.dp, y = 0.dp),\n            onDismissRequest = { showPopup = false },\n        ) {\n            repeat(dropDownItemCount) { index ->\n                DropdownMenuItem(\n                    text = { Text(dropDownItemText(index)) },\n                    onClick = {\n                        showPopup = false\n                        coroutineScope.launch {\n                            delay(100)\n                            onItemClick(index)\n                        }\n                    },\n                )\n            }\n        }\n    }\n}\n\n@Composable\ninternal fun SettingItem(\n    icon: ImageVector,\n    title: String,\n    subtitle: String?,\n    redDot: Boolean = false,\n    onClick: () -> Unit,\n) {\n    SettingItem(\n        icon = {\n            Icon(\n                modifier = Modifier.size(24.dp),\n                imageVector = icon,\n                contentDescription = title,\n            )\n        },\n        title = title,\n        redDot = redDot,\n        subtitle = subtitle,\n        onClick = onClick,\n    )\n}\n\n@Composable\ninternal fun SettingItem(\n    icon: @Composable () -> Unit,\n    title: String,\n    subtitle: String?,\n    redDot: Boolean = false,\n    onClick: () -> Unit,\n) {\n    Box(\n        modifier = Modifier\n            .fillMaxWidth()\n            .clickable { onClick() },\n    ) {\n        Row(\n            modifier = Modifier\n                .heightIn(min = itemHeight)\n                .padding(16.dp)\n                .fillMaxWidth(),\n            verticalAlignment = Alignment.CenterVertically,\n        ) {\n            icon()\n            Spacer(modifier = Modifier.width(16.dp))\n            Column {\n                Row(\n                    verticalAlignment = Alignment.CenterVertically,\n                ) {\n                    Text(\n                        text = title,\n                        style = MaterialTheme.typography.titleMedium,\n                        maxLines = 1,\n                    )\n                    if (redDot) {\n                        Spacer(modifier = Modifier.width(4.dp))\n                        Box(\n                            modifier = Modifier.size(4.dp)\n                                .clip(CircleShape)\n                                .background(Color.Red.copy(alpha = 0.8F)),\n                        )\n                    }\n                }\n                if (!subtitle.isNullOrBlank()) {\n                    Text(\n                        modifier = Modifier.padding(top = 4.dp),\n                        text = subtitle,\n                        style = MaterialTheme.typography.bodyMedium,\n                    )\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "feature/profile/src/commonMain/kotlin/com/zhangke/fread/profile/screen/setting/SettingItemNames.kt",
    "content": "package com.zhangke.fread.profile.screen.setting\n\nimport androidx.compose.runtime.Composable\nimport com.zhangke.fread.common.config.StatusContentSize\nimport com.zhangke.fread.common.config.TimelineDefaultPosition\nimport com.zhangke.fread.common.daynight.DayNightMode\nimport com.zhangke.fread.common.language.LanguageSettingType\nimport com.zhangke.fread.common.theme.ThemeType\nimport com.zhangke.fread.localization.LocalizedString\nimport org.jetbrains.compose.resources.stringResource\n\nval LanguageSettingType.typeName: String\n    @Composable\n    get() {\n        return when (this) {\n            LanguageSettingType.CN -> stringResource(LocalizedString.profileSettingLanguageZh)\n            LanguageSettingType.EN -> stringResource(LocalizedString.profileSettingLanguageEn)\n            LanguageSettingType.SYSTEM -> stringResource(LocalizedString.profileSettingLanguageSystem)\n        }\n    }\n\nval DayNightMode.modeName: String\n    @Composable\n    get() {\n        return when (this) {\n            DayNightMode.NIGHT -> stringResource(LocalizedString.profileSettingDarkModeDark)\n            DayNightMode.DAY -> stringResource(LocalizedString.profileSettingDarkModeLight)\n            DayNightMode.FOLLOW_SYSTEM -> stringResource(LocalizedString.profileSettingDarkModeFollowSystem)\n        }\n    }\n\nval StatusContentSize.sizeName: String\n    @Composable\n    get() = when (this) {\n        StatusContentSize.SMALL -> stringResource(LocalizedString.profileSettingFontSizeSmall)\n        StatusContentSize.MEDIUM -> stringResource(LocalizedString.profileSettingFontSizeMedium)\n        StatusContentSize.LARGE -> stringResource(LocalizedString.profileSettingFontSizeLarge)\n    }\n\nval TimelineDefaultPosition.displayName: String\n    @Composable\n    get() = when (this) {\n        TimelineDefaultPosition.NEWEST -> stringResource(LocalizedString.profileSettingTimelinePositionNewest)\n        TimelineDefaultPosition.LAST_READ -> stringResource(LocalizedString.profileSettingTimelinePositionLastRead)\n    }\n\nval ThemeType.displayName: String\n    @Composable\n    get() = when (this) {\n        ThemeType.DEFAULT -> stringResource(LocalizedString.profileSettingThemeDefault)\n        ThemeType.SYSTEM_DYNAMIC -> stringResource(LocalizedString.profileSettingThemeSystem)\n    }\n"
  },
  {
    "path": "feature/profile/src/commonMain/kotlin/com/zhangke/fread/profile/screen/setting/SettingScreen.kt",
    "content": "package com.zhangke.fread.profile.screen.setting\n\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.outlined.Chat\nimport androidx.compose.material.icons.filled.Language\nimport androidx.compose.material.icons.filled.Palette\nimport androidx.compose.material.icons.filled.ViewTimeline\nimport androidx.compose.material.icons.outlined.Coffee\nimport androidx.compose.material.icons.outlined.Info\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.composable.currentOrThrow\nimport com.zhangke.framework.composable.Toolbar\nimport com.zhangke.framework.nav.LocalNavBackStack\nimport com.zhangke.fread.common.handler.LocalTextHandler\nimport com.zhangke.fread.common.language.LanguageSettingItem\nimport com.zhangke.fread.common.language.LocalActivityLanguageHelper\nimport com.zhangke.fread.feature.profile.Res\nimport com.zhangke.fread.feature.profile.ic_code\nimport com.zhangke.fread.feature.profile.ic_ratting\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.profile.screen.donate.DonateScreenNavKey\nimport com.zhangke.fread.profile.screen.opensource.OpenSourceScreenNavKey\nimport com.zhangke.fread.profile.screen.setting.about.AboutScreenNavKey\nimport com.zhangke.fread.profile.screen.setting.appearance.AppearanceSettingsNavKey\nimport com.zhangke.fread.profile.screen.setting.behavior.BehaviorSettingsNavKey\nimport kotlinx.serialization.Serializable\nimport org.jetbrains.compose.resources.stringResource\nimport org.jetbrains.compose.resources.vectorResource\n\n@Serializable\nobject SettingScreenNavKey : NavKey\n\n@Composable\nfun SettingScreen(viewModel: SettingScreenModel) {\n    val backStack = LocalNavBackStack.currentOrThrow\n    val uiState by viewModel.uiState.collectAsState()\n\n    val activityLanguageHelper = LocalActivityLanguageHelper.current\n    val activityTextHandler = LocalTextHandler.current\n\n    SettingContent(\n        uiState = uiState,\n        onBackClick = backStack::removeLastOrNull,\n        onOpenSourceClick = {\n            backStack.add(OpenSourceScreenNavKey)\n        },\n        onLanguageClick = {\n            activityLanguageHelper.setLanguage(it)\n        },\n        onRatingClick = {\n            activityTextHandler.openAppMarket()\n        },\n        onAppearanceClick = {\n            backStack.add(AppearanceSettingsNavKey)\n        },\n        onBehaviorClick = {\n            backStack.add(BehaviorSettingsNavKey)\n        },\n        onAboutClick = {\n            backStack.add(AboutScreenNavKey)\n        },\n        onDonateClick = {\n            backStack.add(DonateScreenNavKey)\n        },\n    )\n}\n\n@Composable\nprivate fun SettingContent(\n    uiState: SettingUiState,\n    onBackClick: () -> Unit,\n    onOpenSourceClick: () -> Unit,\n    onLanguageClick: (LanguageSettingItem) -> Unit,\n    onRatingClick: () -> Unit,\n    onAboutClick: () -> Unit,\n    onDonateClick: () -> Unit,\n    onAppearanceClick: () -> Unit,\n    onBehaviorClick: () -> Unit,\n) {\n    Scaffold(\n        topBar = {\n            Toolbar(\n                title = stringResource(LocalizedString.settings),\n                onBackClick = onBackClick,\n            )\n        },\n    ) { innerPadding ->\n        Column(\n            modifier = Modifier\n                .padding(innerPadding)\n                .verticalScroll(rememberScrollState()),\n        ) {\n            SettingItem(\n                icon = Icons.Default.Palette,\n                title = stringResource(LocalizedString.setting_group_appearance),\n                subtitle = stringResource(LocalizedString.setting_group_appearance_subtitle),\n                onClick = onAppearanceClick,\n            )\n            SettingItem(\n                icon = Icons.Default.ViewTimeline,\n                title = stringResource(LocalizedString.setting_group_behavior),\n                subtitle = stringResource(LocalizedString.setting_group_behavior_subtitle),\n                onClick = onBehaviorClick,\n            )\n            LanguageItem(\n                onLanguageClick = onLanguageClick,\n            )\n            FeedbackItem()\n            SettingItem(\n                icon = vectorResource(Res.drawable.ic_code),\n                title = stringResource(LocalizedString.profileSettingOpenSourceTitle),\n                subtitle = stringResource(LocalizedString.profileSettingOpenSourceDesc),\n                onClick = onOpenSourceClick,\n            )\n            SettingItem(\n                icon = {\n                    Icon(\n                        modifier = Modifier\n                            .size(24.dp)\n                            .padding(2.dp),\n                        imageVector = vectorResource(Res.drawable.ic_ratting),\n                        contentDescription = stringResource(LocalizedString.profileSettingRatting),\n                    )\n                },\n                title = stringResource(LocalizedString.profileSettingRatting),\n                subtitle = stringResource(LocalizedString.profileSettingRattingDesc),\n                onClick = onRatingClick,\n            )\n            SettingItem(\n                icon = Icons.Outlined.Coffee,\n                title = stringResource(LocalizedString.donate),\n                subtitle = stringResource(LocalizedString.profileSettingDonateDesc),\n                onClick = onDonateClick,\n            )\n            SettingItem(\n                icon = Icons.Outlined.Info,\n                title = stringResource(LocalizedString.profileSettingAboutTitle),\n                subtitle = uiState.settingInfo,\n                redDot = uiState.haveNewAppVersion,\n                onClick = onAboutClick,\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun LanguageItem(\n    onLanguageClick: (LanguageSettingItem) -> Unit,\n) {\n    val activityLanguageHelper = LocalActivityLanguageHelper.current\n    val subtitle = activityLanguageHelper.currentLanguage.getDisplayName()\n    SettingItemWithPopup(\n        icon = Icons.Default.Language,\n        title = stringResource(LocalizedString.profileSettingLanguageTitle),\n        subtitle = subtitle,\n        dropDownItemCount = LanguageSettingItem.items.size,\n        dropDownItemText = { LanguageSettingItem.items[it].getDisplayName() },\n        onItemClick = {\n            onLanguageClick(LanguageSettingItem.items[it])\n        }\n    )\n}\n\n@Composable\nprivate fun FeedbackItem() {\n    var showFeedbackBottomSheet by remember {\n        mutableStateOf(false)\n    }\n    SettingItem(\n        icon = Icons.AutoMirrored.Outlined.Chat,\n        title = stringResource(LocalizedString.profileSettingOpenSourceFeedback),\n        subtitle = stringResource(LocalizedString.profileSettingOpenSourceFeedbackDesc),\n        onClick = {\n            showFeedbackBottomSheet = true\n        },\n    )\n    if (showFeedbackBottomSheet) {\n        FeedbackBottomSheet(\n            onDismissRequest = { showFeedbackBottomSheet = false },\n        )\n    }\n}\n"
  },
  {
    "path": "feature/profile/src/commonMain/kotlin/com/zhangke/fread/profile/screen/setting/SettingScreenModel.kt",
    "content": "package com.zhangke.fread.profile.screen.setting\n\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.zhangke.fread.common.handler.TextHandler\nimport com.zhangke.fread.common.update.AppUpdateManager\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.update\nimport kotlinx.coroutines.launch\n\nclass SettingScreenModel(\n    private val textHandler: TextHandler,\n    private val updateManager: AppUpdateManager,\n) : ViewModel() {\n\n    private val _uiState = MutableStateFlow(\n        SettingUiState(\n            settingInfo = getAppVersionInfo(),\n            haveNewAppVersion = false,\n        )\n    )\n    val uiState = _uiState.asStateFlow()\n\n    init {\n        viewModelScope.launch {\n            if (updateManager.enableAutoCheckUpdate) {\n                updateManager.checkForUpdate(false)\n                    .onSuccess { (needUpdate, _) ->\n                        _uiState.update { it.copy(haveNewAppVersion = needUpdate) }\n                    }\n            }\n        }\n    }\n\n    private fun getAppVersionInfo(): String {\n        return \"${textHandler.versionName}(${textHandler.versionCode})\"\n    }\n}\n"
  },
  {
    "path": "feature/profile/src/commonMain/kotlin/com/zhangke/fread/profile/screen/setting/SettingUiState.kt",
    "content": "package com.zhangke.fread.profile.screen.setting\n\ndata class SettingUiState(\n    val settingInfo: String,\n    val haveNewAppVersion: Boolean,\n)\n"
  },
  {
    "path": "feature/profile/src/commonMain/kotlin/com/zhangke/fread/profile/screen/setting/about/AboutScreen.kt",
    "content": "package com.zhangke.fread.profile.screen.setting.about\n\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.text.selection.SelectionContainer\nimport androidx.compose.material3.Button\nimport androidx.compose.material3.CircularProgressIndicator\nimport androidx.compose.material3.LocalContentColor\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.SnackbarHost\nimport androidx.compose.material3.SnackbarHostState\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.text.style.TextDecoration\nimport androidx.compose.ui.unit.dp\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.composable.currentOrThrow\nimport com.zhangke.framework.composable.ConsumeSnackbarFlow\nimport com.zhangke.framework.composable.Toolbar\nimport com.zhangke.framework.composable.noRippleClick\nimport com.zhangke.framework.composable.rememberSnackbarHostState\nimport com.zhangke.framework.nav.LocalNavBackStack\nimport com.zhangke.framework.toast.toast\nimport com.zhangke.fread.common.browser.LocalActivityBrowserLauncher\nimport com.zhangke.fread.common.config.AppCommonConfig\nimport com.zhangke.fread.common.handler.LocalTextHandler\nimport com.zhangke.fread.commonbiz.ic_fread_logo\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.ui.update.AppUpdateDialog\nimport kotlinx.coroutines.launch\nimport kotlinx.serialization.Serializable\nimport org.jetbrains.compose.resources.painterResource\nimport org.jetbrains.compose.resources.stringResource\n\n@Serializable\nobject AboutScreenNavKey : NavKey\n\n@Composable\nfun AboutScreen(viewModel: AboutViewModel) {\n    val backStack = LocalNavBackStack.currentOrThrow\n    val uiState by viewModel.uiState.collectAsState()\n    val snackBarState = rememberSnackbarHostState()\n    AboutScreenContent(\n        uiState = uiState,\n        snackBarState = snackBarState,\n        onBackClick = backStack::removeLastOrNull,\n        onUpdateClick = viewModel::onUpdateClick,\n        onCheckUpdateClick = viewModel::onCheckForUpdateClick,\n    )\n    ConsumeSnackbarFlow(snackBarState, viewModel.snackBarMessage)\n    var showUpdateDialog by rememberSaveable { mutableStateOf(false) }\n    LaunchedEffect(uiState) {\n        showUpdateDialog = uiState.newReleaseInfo != null\n    }\n    if (showUpdateDialog && uiState.newReleaseInfo != null) {\n        AppUpdateDialog(\n            appReleaseInfo = uiState.newReleaseInfo!!,\n            onCancel = {\n                showUpdateDialog = false\n                viewModel.onCancelClick()\n            },\n            onUpdateClick = {\n                showUpdateDialog = false\n                viewModel.onUpdateClick()\n            },\n        )\n    }\n}\n\n@Composable\nprivate fun AboutScreenContent(\n    uiState: AboutUiState,\n    snackBarState: SnackbarHostState,\n    onBackClick: () -> Unit,\n    onUpdateClick: () -> Unit,\n    onCheckUpdateClick: () -> Unit,\n) {\n    val textHandler = LocalTextHandler.current\n    val browserLauncher = LocalActivityBrowserLauncher.current\n    val coroutineScope = rememberCoroutineScope()\n    Scaffold(\n        topBar = {\n            Toolbar(\n                title = stringResource(LocalizedString.profileSettingAboutTitle),\n                onBackClick = onBackClick,\n            )\n        },\n        snackbarHost = {\n            SnackbarHost(snackBarState)\n        },\n    ) { innerPadding ->\n        SelectionContainer {\n            Column(\n                modifier = Modifier\n                    .fillMaxSize()\n                    .padding(innerPadding)\n                    .padding(horizontal = 16.dp),\n            ) {\n                Spacer(modifier = Modifier.height(32.dp))\n                Image(\n                    modifier = Modifier\n                        .align(Alignment.CenterHorizontally)\n                        .width(88.dp),\n                    painter = painterResource(com.zhangke.fread.commonbiz.Res.drawable.ic_fread_logo),\n                    contentDescription = \"Logo\",\n                    contentScale = ContentScale.Crop,\n                )\n                Text(\n                    modifier = Modifier\n                        .padding(top = 16.dp)\n                        .align(Alignment.CenterHorizontally),\n                    text = AppCommonConfig.APP_NAME,\n                    style = MaterialTheme.typography.headlineMedium,\n                )\n                Text(\n                    modifier = Modifier\n                        .padding(top = 8.dp)\n                        .align(Alignment.CenterHorizontally),\n                    text = textHandler.packageName,\n                    style = MaterialTheme.typography.bodyMedium,\n                )\n                Spacer(modifier = Modifier.height(32.dp))\n                AboutClickableItem(\n                    title = stringResource(LocalizedString.profileAboutWebsite),\n                    clickableText = AppCommonConfig.WEBSITE,\n                    showUnderline = true,\n                    onClick = {\n                        coroutineScope.launch {\n                            browserLauncher.launchFreadLandingPage()\n                        }\n                    },\n                )\n                Spacer(modifier = Modifier.height(16.dp))\n                val version = remember {\n                    val versionName = textHandler.versionName\n                    val versionCode = textHandler.versionCode\n                    \"$versionName($versionCode)\"\n                }\n                Row(\n                    modifier = Modifier.fillMaxWidth().noRippleClick { onUpdateClick() },\n                    verticalAlignment = Alignment.CenterVertically,\n                ) {\n                    AboutClickableItem(\n                        modifier = Modifier,\n                        title = stringResource(LocalizedString.profileAboutVersion),\n                        clickableText = version,\n                        showUnderline = false,\n                        onClick = {},\n                    )\n                    if (uiState.newReleaseInfo != null) {\n                        Text(\n                            modifier = Modifier.padding(start = 6.dp),\n                            text = stringResource(LocalizedString.profileSettingHaveNewVersion),\n                            style = MaterialTheme.typography.labelMedium,\n                            color = LocalContentColor.current.copy(alpha = 0.7F),\n                            maxLines = 1,\n                        )\n                        Box(\n                            modifier = Modifier.size(4.dp)\n                                .clip(CircleShape)\n                                .background(Color.Red.copy(alpha = 0.8F)),\n                        )\n                    }\n                }\n                Spacer(modifier = Modifier.height(16.dp))\n                AboutClickableItem(\n                    title = stringResource(LocalizedString.profileAboutDeveloper),\n                    clickableText = AppCommonConfig.AUTHOR,\n                    showUnderline = true,\n                    onClick = {\n                        coroutineScope.launch {\n                            browserLauncher.launchAuthorWebsite()\n                        }\n                    },\n                )\n                Spacer(modifier = Modifier.height(16.dp))\n                AboutClickableItem(\n                    title = stringResource(LocalizedString.profileAboutContractUs),\n                    clickableText = AppCommonConfig.AUTHOR_EMAIL,\n                    showUnderline = false,\n                    onClick = {\n                        textHandler.copyText(AppCommonConfig.AUTHOR_EMAIL)\n                        toast(\"Copied to clipboard\")\n                    },\n                )\n                Spacer(modifier = Modifier.height(16.dp))\n                AboutClickableItem(\n                    title = stringResource(LocalizedString.profileAboutTelegram),\n                    clickableText = AppCommonConfig.TELEGRAM_GROUP,\n                    showUnderline = false,\n                    onClick = {\n                        textHandler.copyText(AppCommonConfig.TELEGRAM_GROUP)\n                        browserLauncher.launchBySystemBrowser(AppCommonConfig.TELEGRAM_GROUP)\n                    },\n                )\n                Spacer(modifier = Modifier.height(16.dp))\n                AboutClickableItem(\n                    title = stringResource(LocalizedString.profileAboutPrivacyPolicy),\n                    clickableText = AppCommonConfig.PRIVACY_POLICY,\n                    showUnderline = false,\n                    onClick = {\n                        coroutineScope.launch {\n                            browserLauncher.launchWebTabInApp(AppCommonConfig.PRIVACY_POLICY)\n                        }\n                    },\n                )\n\n                Spacer(modifier = Modifier.height(24.dp))\n\n                Button(\n                    modifier = Modifier.align(Alignment.CenterHorizontally)\n                        .padding(horizontal = 42.dp),\n                    onClick = onCheckUpdateClick,\n                ) {\n                    if (uiState.checkingUpdate) {\n                        CircularProgressIndicator(\n                            modifier = Modifier.size(18.dp),\n                            color = Color.White,\n                        )\n                    } else {\n                        Text(\n                            text = stringResource(LocalizedString.profileSettingCheckForUpdate),\n                        )\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun AboutClickableItem(\n    modifier: Modifier = Modifier,\n    title: String,\n    clickableText: String,\n    showUnderline: Boolean,\n    onClick: () -> Unit,\n) {\n    Row(\n        modifier = modifier.padding(vertical = 4.dp),\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        Text(\n            modifier = Modifier\n                .let {\n                    if (showUnderline) {\n                        it\n                    } else {\n                        it.noRippleClick {\n                            onClick()\n                        }\n                    }\n                },\n            text = title,\n        )\n        Text(\n            modifier = Modifier.noRippleClick {\n                onClick()\n            },\n            text = clickableText,\n            color = MaterialTheme.colorScheme.primary,\n            style = MaterialTheme.typography.bodyMedium,\n            textDecoration = if (showUnderline) TextDecoration.Underline else null,\n        )\n    }\n}\n"
  },
  {
    "path": "feature/profile/src/commonMain/kotlin/com/zhangke/fread/profile/screen/setting/about/AboutUiState.kt",
    "content": "package com.zhangke.fread.profile.screen.setting.about\n\nimport com.zhangke.fread.common.update.AppReleaseInfo\n\ndata class AboutUiState(\n    val checkingUpdate: Boolean,\n    val newReleaseInfo: AppReleaseInfo?,\n) {\n\n    companion object {\n\n        fun default() = AboutUiState(\n            checkingUpdate = false,\n            newReleaseInfo = null,\n        )\n    }\n\n    sealed interface UpdatingState {\n\n        data object Idle : UpdatingState\n\n        data object Checking : UpdatingState\n\n        data class Failed(val throwable: Throwable) : UpdatingState\n\n        data object DoNotNeedUpdate : UpdatingState\n\n        data class NeedUpdate(val appReleaseInfo: AppReleaseInfo) : UpdatingState\n    }\n}\n"
  },
  {
    "path": "feature/profile/src/commonMain/kotlin/com/zhangke/fread/profile/screen/setting/about/AboutViewModel.kt",
    "content": "package com.zhangke.fread.profile.screen.setting.about\n\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.zhangke.framework.composable.TextString\nimport com.zhangke.framework.composable.emitTextMessageFromThrowable\nimport com.zhangke.framework.composable.textOf\nimport com.zhangke.fread.common.update.AppUpdateManager\nimport com.zhangke.fread.localization.LocalizedString\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asSharedFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.update\nimport kotlinx.coroutines.launch\n\nclass AboutViewModel(\n    private val updateManager: AppUpdateManager,\n) : ViewModel() {\n\n    private val _uiState = MutableStateFlow(AboutUiState.default())\n    val uiState = _uiState.asStateFlow()\n\n    private val _snackBarMessage = MutableSharedFlow<TextString>()\n    val snackBarMessage = _snackBarMessage.asSharedFlow()\n\n    fun onCheckForUpdateClick(showLoading: Boolean = true) {\n        viewModelScope.launch {\n            _uiState.update { it.copy(checkingUpdate = showLoading, newReleaseInfo = null) }\n            updateManager.checkForUpdate(false)\n                .onFailure { t ->\n                    _uiState.update { it.copy(checkingUpdate = false) }\n                    _snackBarMessage.emitTextMessageFromThrowable(t)\n                }.onSuccess { (needUpdate, releaseInfo) ->\n                    _uiState.update {\n                        it.copy(\n                            checkingUpdate = false,\n                            newReleaseInfo = if (needUpdate) releaseInfo else null,\n                        )\n                    }\n                    if (!needUpdate) {\n                        _snackBarMessage.emit(textOf(LocalizedString.profileSettingAlreadyLatestVersion))\n                    }\n                }\n        }\n    }\n\n    fun onUpdateClick() {\n        viewModelScope.launch {\n            _uiState.value.newReleaseInfo?.let {\n                updateManager.updateApp(it)\n            }\n        }\n    }\n\n    fun onCancelClick() {\n        viewModelScope.launch {\n            _uiState.value.newReleaseInfo?.let {\n                updateManager.ignoreVersion(it)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "feature/profile/src/commonMain/kotlin/com/zhangke/fread/profile/screen/setting/appearance/AppearanceSettingsScreen.kt",
    "content": "package com.zhangke.fread.profile.screen.setting.appearance\n\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.BlurOn\nimport androidx.compose.material.icons.filled.Contrast\nimport androidx.compose.material.icons.filled.DarkMode\nimport androidx.compose.material.icons.filled.Palette\nimport androidx.compose.material.icons.filled.PlayCircleOutline\nimport androidx.compose.material.icons.filled.Refresh\nimport androidx.compose.material.icons.filled.TextFields\nimport androidx.compose.material.icons.filled.ViewTimeline\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.ui.Modifier\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.composable.Toolbar\nimport com.zhangke.framework.composable.currentOrThrow\nimport com.zhangke.framework.nav.LocalNavBackStack\nimport com.zhangke.fread.common.config.StatusContentSize\nimport com.zhangke.fread.common.daynight.DayNightMode\nimport com.zhangke.fread.common.daynight.LocalActivityDayNightHelper\nimport com.zhangke.fread.common.theme.ThemeType\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.profile.screen.setting.SettingItemWithPopup\nimport com.zhangke.fread.profile.screen.setting.SettingItemWithSwitch\nimport com.zhangke.fread.profile.screen.setting.displayName\nimport com.zhangke.fread.profile.screen.setting.modeName\nimport com.zhangke.fread.profile.screen.setting.sizeName\nimport kotlinx.coroutines.launch\nimport kotlinx.serialization.Serializable\nimport org.jetbrains.compose.resources.stringResource\n\n@Serializable\nobject AppearanceSettingsNavKey : NavKey\n\n@Composable\nfun AppearanceSettingsScreen(viewModel: AppearanceSettingsViewModel) {\n    val backStack = LocalNavBackStack.currentOrThrow\n    val uiState by viewModel.uiState.collectAsState()\n\n    val activityDayNightHelper = LocalActivityDayNightHelper.current\n    val coroutineScope = rememberCoroutineScope()\n\n    AppearanceSettingsContent(\n        uiState = uiState,\n        onBackClick = backStack::removeLastOrNull,\n        onDayNightModeClick = {\n            coroutineScope.launch {\n                activityDayNightHelper.setMode(it)\n            }\n        },\n        onThemeTypeChanged = viewModel::onThemeTypeChanged,\n        onAmoledChanged = {\n            coroutineScope.launch {\n                activityDayNightHelper.setAmoledMode(it)\n            }\n        },\n        onContentSizeChanged = viewModel::onContentSizeChanged,\n        onImmersiveBarChanged = viewModel::onImmersiveBarChanged,\n        onBlurAppBarStyleChanged = viewModel::onBlurAppBarStyleChanged,\n        onHomeTabNextButtonVisibleChanged = viewModel::onHomeTabNextButtonVisibleChanged,\n        onHomeTabRefreshButtonVisibleChanged = viewModel::onHomeTabRefreshButtonVisibleChanged,\n    )\n}\n\n@Composable\nprivate fun AppearanceSettingsContent(\n    uiState: AppearanceSettingsUiState,\n    onBackClick: () -> Unit,\n    onDayNightModeClick: (DayNightMode) -> Unit,\n    onThemeTypeChanged: (ThemeType) -> Unit,\n    onAmoledChanged: (on: Boolean) -> Unit,\n    onContentSizeChanged: (StatusContentSize) -> Unit,\n    onImmersiveBarChanged: (on: Boolean) -> Unit,\n    onBlurAppBarStyleChanged: (enabled: Boolean) -> Unit,\n    onHomeTabNextButtonVisibleChanged: (on: Boolean) -> Unit,\n    onHomeTabRefreshButtonVisibleChanged: (on: Boolean) -> Unit,\n) {\n    Scaffold(\n        topBar = {\n            Toolbar(\n                title = stringResource(LocalizedString.setting_group_appearance),\n                onBackClick = onBackClick,\n            )\n        },\n    ) { innerPadding ->\n        Column(\n            modifier = Modifier\n                .padding(innerPadding)\n                .verticalScroll(rememberScrollState()),\n        ) {\n            ImmersiveNavBar(\n                immersive = uiState.immersiveNavBar,\n                onImmersiveBarChanged = onImmersiveBarChanged,\n            )\n            SolidBarBackgroundItem(\n                blurEnabled = uiState.blurAppBarStyleEnabled,\n                onBlurEnabledChanged = onBlurAppBarStyleChanged,\n            )\n            AmoledMode(\n                enabled = uiState.amoledEnabled,\n                onAmoledChanged = onAmoledChanged,\n            )\n            HomeTabNextButtonItem(\n                visible = uiState.homeTabNextButtonVisible,\n                onVisibleChanged = onHomeTabNextButtonVisibleChanged,\n            )\n            HomeTabRefreshButtonItem(\n                visible = uiState.homeTabRefreshButtonVisible,\n                onVisibleChanged = onHomeTabRefreshButtonVisibleChanged,\n            )\n            DayNightItem(\n                uiState = uiState,\n                onDayNightModeClick = onDayNightModeClick,\n            )\n            ContentSizeItem(\n                contentSize = uiState.contentSize,\n                onContentSizeChanged = onContentSizeChanged,\n            )\n            ThemeTypeItem(\n                themeType = uiState.themeType,\n                onThemeTypeChanged = onThemeTypeChanged,\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun SolidBarBackgroundItem(\n    blurEnabled: Boolean,\n    onBlurEnabledChanged: (enabled: Boolean) -> Unit,\n) {\n    val checked = !blurEnabled\n    SettingItemWithSwitch(\n        icon = Icons.Default.BlurOn,\n        title = stringResource(LocalizedString.setting_item_solid_bar_background_title),\n        subtitle = stringResource(LocalizedString.setting_item_solid_bar_background_subtitle),\n        checked = checked,\n        onCheckedChangeRequest = { onBlurEnabledChanged(!it) },\n    )\n}\n\n@Composable\nprivate fun ImmersiveNavBar(\n    immersive: Boolean,\n    onImmersiveBarChanged: (on: Boolean) -> Unit,\n) {\n    SettingItemWithSwitch(\n        icon = Icons.Default.PlayCircleOutline,\n        title = stringResource(LocalizedString.profileSettingImmersiveNavBar),\n        subtitle = stringResource(LocalizedString.profileSettingImmersiveNavBarDesc),\n        checked = immersive,\n        onCheckedChangeRequest = onImmersiveBarChanged,\n    )\n}\n\n@Composable\nprivate fun AmoledMode(\n    enabled: Boolean,\n    onAmoledChanged: (on: Boolean) -> Unit,\n) {\n    SettingItemWithSwitch(\n        icon = Icons.Default.DarkMode,\n        title = stringResource(LocalizedString.setting_item_amoled_mode),\n        subtitle = stringResource(LocalizedString.setting_item_amoled_mode_description),\n        checked = enabled,\n        onCheckedChangeRequest = onAmoledChanged,\n    )\n}\n\n@Composable\nprivate fun DayNightItem(\n    uiState: AppearanceSettingsUiState,\n    onDayNightModeClick: (DayNightMode) -> Unit,\n) {\n    SettingItemWithPopup(\n        icon = Icons.Default.Contrast,\n        title = stringResource(LocalizedString.profileSettingDarkModeTitle),\n        subtitle = uiState.dayNightMode.modeName,\n        dropDownItemCount = DayNightMode.entries.size,\n        dropDownItemText = { DayNightMode.entries[it].modeName },\n        onItemClick = { index ->\n            onDayNightModeClick(DayNightMode.entries[index])\n        },\n    )\n}\n\n@Composable\nprivate fun ThemeTypeItem(\n    themeType: ThemeType,\n    onThemeTypeChanged: (ThemeType) -> Unit,\n) {\n    SettingItemWithPopup(\n        icon = Icons.Default.Palette,\n        title = stringResource(LocalizedString.profileSettingThemeTitle),\n        subtitle = themeType.displayName,\n        dropDownItemCount = ThemeType.entries.size,\n        dropDownItemText = { ThemeType.entries[it].displayName },\n        onItemClick = {\n            onThemeTypeChanged(ThemeType.entries[it])\n        }\n    )\n}\n\n@Composable\nprivate fun ContentSizeItem(\n    contentSize: StatusContentSize,\n    onContentSizeChanged: (StatusContentSize) -> Unit,\n) {\n    SettingItemWithPopup(\n        icon = Icons.Default.TextFields,\n        title = stringResource(LocalizedString.profileSettingFontSize),\n        subtitle = contentSize.sizeName,\n        dropDownItemCount = StatusContentSize.entries.size,\n        dropDownItemText = { StatusContentSize.entries[it].sizeName },\n        onItemClick = {\n            onContentSizeChanged(StatusContentSize.entries[it])\n        }\n    )\n}\n\n@Composable\nprivate fun HomeTabNextButtonItem(\n    visible: Boolean,\n    onVisibleChanged: (on: Boolean) -> Unit,\n) {\n    SettingItemWithSwitch(\n        icon = Icons.Default.ViewTimeline,\n        title = stringResource(LocalizedString.setting_item_home_tab_next_title),\n        subtitle = stringResource(LocalizedString.setting_item_home_tab_next_subtitle),\n        checked = visible,\n        onCheckedChangeRequest = onVisibleChanged,\n    )\n}\n\n@Composable\nprivate fun HomeTabRefreshButtonItem(\n    visible: Boolean,\n    onVisibleChanged: (on: Boolean) -> Unit,\n) {\n    SettingItemWithSwitch(\n        icon = Icons.Default.Refresh,\n        title = stringResource(LocalizedString.setting_item_home_tab_refresh_title),\n        subtitle = stringResource(LocalizedString.setting_item_home_tab_refresh_subtitle),\n        checked = visible,\n        onCheckedChangeRequest = onVisibleChanged,\n    )\n}\n"
  },
  {
    "path": "feature/profile/src/commonMain/kotlin/com/zhangke/fread/profile/screen/setting/appearance/AppearanceSettingsUiState.kt",
    "content": "package com.zhangke.fread.profile.screen.setting.appearance\n\nimport com.zhangke.fread.common.config.StatusContentSize\nimport com.zhangke.fread.common.daynight.DayNightMode\nimport com.zhangke.fread.common.theme.ThemeType\n\ndata class AppearanceSettingsUiState(\n    val immersiveNavBar: Boolean,\n    val blurAppBarStyleEnabled: Boolean,\n    val amoledEnabled: Boolean,\n    val dayNightMode: DayNightMode,\n    val contentSize: StatusContentSize,\n    val themeType: ThemeType,\n    val homeTabNextButtonVisible: Boolean,\n    val homeTabRefreshButtonVisible: Boolean,\n)\n"
  },
  {
    "path": "feature/profile/src/commonMain/kotlin/com/zhangke/fread/profile/screen/setting/appearance/AppearanceSettingsViewModel.kt",
    "content": "package com.zhangke.fread.profile.screen.setting.appearance\n\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.zhangke.fread.common.config.FreadConfigManager\nimport com.zhangke.fread.common.config.StatusContentSize\nimport com.zhangke.fread.common.daynight.DayNightHelper\nimport com.zhangke.fread.common.theme.ThemeType\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.update\nimport kotlinx.coroutines.launch\n\nclass AppearanceSettingsViewModel(\n    private val freadConfigManager: FreadConfigManager,\n    private val dayNightHelper: DayNightHelper,\n) : ViewModel() {\n\n    private val _uiState = MutableStateFlow(\n        AppearanceSettingsUiState(\n            immersiveNavBar = freadConfigManager.statusConfigFlow.value.immersiveNavBar,\n            blurAppBarStyleEnabled = freadConfigManager.enableBlurAppBarStyleFlow.value,\n            amoledEnabled = dayNightHelper.amoledModeFlow.value,\n            dayNightMode = dayNightHelper.dayNightModeFlow.value,\n            contentSize = freadConfigManager.statusConfigFlow.value.contentSize,\n            themeType = ThemeType.DEFAULT,\n            homeTabNextButtonVisible = freadConfigManager.homeTabNextButtonVisibleFlow.value,\n            homeTabRefreshButtonVisible = freadConfigManager.homeTabRefreshButtonVisibleFlow.value,\n        )\n    )\n    val uiState = _uiState.asStateFlow()\n\n    init {\n        viewModelScope.launch {\n            freadConfigManager.getThemeType()\n                .let { type ->\n                    _uiState.update { it.copy(themeType = type) }\n                }\n        }\n        viewModelScope.launch {\n            dayNightHelper.dayNightModeFlow.collect { dayNightMode ->\n                _uiState.update { it.copy(dayNightMode = dayNightMode) }\n            }\n        }\n        viewModelScope.launch {\n            dayNightHelper.amoledModeFlow.collect { enabled ->\n                _uiState.update { it.copy(amoledEnabled = enabled) }\n            }\n        }\n        viewModelScope.launch {\n            freadConfigManager.statusConfigFlow.collect { config ->\n                _uiState.update {\n                    it.copy(\n                        contentSize = config.contentSize,\n                        immersiveNavBar = config.immersiveNavBar,\n                    )\n                }\n            }\n        }\n        viewModelScope.launch {\n            freadConfigManager.homeTabRefreshButtonVisibleFlow.collect { visible ->\n                _uiState.update { it.copy(homeTabRefreshButtonVisible = visible) }\n            }\n        }\n        viewModelScope.launch {\n            freadConfigManager.homeTabNextButtonVisibleFlow.collect { visible ->\n                _uiState.update { it.copy(homeTabNextButtonVisible = visible) }\n            }\n        }\n        viewModelScope.launch {\n            freadConfigManager.enableBlurAppBarStyleFlow.collect { enabled ->\n                _uiState.update { it.copy(blurAppBarStyleEnabled = enabled) }\n            }\n        }\n    }\n\n    fun onThemeTypeChanged(type: ThemeType) {\n        viewModelScope.launch {\n            freadConfigManager.updateThemeType(type)\n            _uiState.update { it.copy(themeType = type) }\n        }\n    }\n\n    fun onContentSizeChanged(contentSize: StatusContentSize) {\n        viewModelScope.launch {\n            freadConfigManager.updateStatusContentSize(contentSize)\n        }\n    }\n\n    fun onImmersiveBarChanged(on: Boolean) {\n        viewModelScope.launch {\n            freadConfigManager.updateImmersiveNavBar(on)\n        }\n    }\n\n    fun onHomeTabNextButtonVisibleChanged(visible: Boolean) {\n        viewModelScope.launch {\n            freadConfigManager.updateHomeTabNextButtonVisible(visible)\n        }\n    }\n\n    fun onHomeTabRefreshButtonVisibleChanged(visible: Boolean) {\n        viewModelScope.launch {\n            freadConfigManager.updateHomeTabRefreshButtonVisible(visible)\n        }\n    }\n\n    fun onBlurAppBarStyleChanged(enabled: Boolean) {\n        viewModelScope.launch {\n            freadConfigManager.updateEnableBlurAppBarStyle(enabled)\n        }\n    }\n}\n"
  },
  {
    "path": "feature/profile/src/commonMain/kotlin/com/zhangke/fread/profile/screen/setting/behavior/BehaviorSettingsScreen.kt",
    "content": "package com.zhangke.fread.profile.screen.setting.behavior\n\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.OpenInBrowser\nimport androidx.compose.material.icons.filled.PlayCircleOutline\nimport androidx.compose.material.icons.filled.ViewTimeline\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.ui.Modifier\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.composable.Toolbar\nimport com.zhangke.framework.composable.currentOrThrow\nimport com.zhangke.framework.nav.LocalNavBackStack\nimport com.zhangke.fread.common.config.TimelineDefaultPosition\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.profile.screen.setting.SettingItemWithPopup\nimport com.zhangke.fread.profile.screen.setting.SettingItemWithSwitch\nimport com.zhangke.fread.profile.screen.setting.displayName\nimport kotlinx.serialization.Serializable\nimport org.jetbrains.compose.resources.stringResource\n\n@Serializable\nobject BehaviorSettingsNavKey : NavKey\n\n@Composable\nfun BehaviorSettingsScreen(viewModel: BehaviorSettingsViewModel) {\n    val backStack = LocalNavBackStack.currentOrThrow\n    val uiState by viewModel.uiState.collectAsState()\n\n    BehaviorSettingsContent(\n        uiState = uiState,\n        onBackClick = backStack::removeLastOrNull,\n        onSwitchAutoPlayClick = viewModel::onChangeAutoPlayInlineVideo,\n        onAlwaysShowSensitive = viewModel::onAlwaysShowSensitiveContentChanged,\n        onTimelineDefaultPositionChanged = viewModel::onTimelineDefaultPositionChanged,\n        onOpenUrlInAppBrowserChanged = viewModel::onOpenUrlInAppBrowserChanged,\n    )\n}\n\n@Composable\nprivate fun BehaviorSettingsContent(\n    uiState: BehaviorSettingsUiState,\n    onBackClick: () -> Unit,\n    onSwitchAutoPlayClick: (on: Boolean) -> Unit,\n    onAlwaysShowSensitive: (Boolean) -> Unit,\n    onTimelineDefaultPositionChanged: (TimelineDefaultPosition) -> Unit,\n    onOpenUrlInAppBrowserChanged: (Boolean) -> Unit,\n) {\n    Scaffold(\n        topBar = {\n            Toolbar(\n                title = stringResource(LocalizedString.setting_group_behavior),\n                onBackClick = onBackClick,\n            )\n        },\n    ) { innerPadding ->\n        Column(\n            modifier = Modifier\n                .padding(innerPadding)\n                .verticalScroll(rememberScrollState()),\n        ) {\n            AutoPlayInlineVideoItem(\n                autoPlay = uiState.autoPlayInlineVideo,\n                onSwitchClick = onSwitchAutoPlayClick,\n            )\n            AlwaysShowSensitiveContentItem(\n                alwaysShowing = uiState.alwaysShowSensitiveContent,\n                onAlwaysChanged = onAlwaysShowSensitive,\n            )\n            OpenUrlBySystemBrowserItem(\n                openUrlBySystem = !uiState.openUrlInAppBrowser,\n                onOpenUrlBySystemChanged = { onOpenUrlInAppBrowserChanged(!it) },\n            )\n            TimelinePositionItem(\n                position = uiState.timelineDefaultPosition,\n                onPositionChanged = onTimelineDefaultPositionChanged,\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun AutoPlayInlineVideoItem(\n    autoPlay: Boolean,\n    onSwitchClick: (on: Boolean) -> Unit,\n) {\n    SettingItemWithSwitch(\n        icon = Icons.Default.PlayCircleOutline,\n        title = stringResource(LocalizedString.profileSettingInlineVideoAutoPlay),\n        subtitle = stringResource(LocalizedString.profileSettingInlineVideoAutoPlaySubtitle),\n        checked = autoPlay,\n        onCheckedChangeRequest = onSwitchClick,\n    )\n}\n\n@Composable\nprivate fun AlwaysShowSensitiveContentItem(\n    alwaysShowing: Boolean,\n    onAlwaysChanged: (on: Boolean) -> Unit,\n) {\n    SettingItemWithSwitch(\n        icon = Icons.Default.PlayCircleOutline,\n        title = stringResource(LocalizedString.profileSettingAlwaysShowSensitiveContent),\n        subtitle = stringResource(LocalizedString.profileSettingAlwaysShowSensitiveContentSubtitle),\n        checked = alwaysShowing,\n        onCheckedChangeRequest = onAlwaysChanged,\n    )\n}\n\n@Composable\nprivate fun TimelinePositionItem(\n    position: TimelineDefaultPosition,\n    onPositionChanged: (TimelineDefaultPosition) -> Unit,\n) {\n    SettingItemWithPopup(\n        icon = Icons.Default.ViewTimeline,\n        title = stringResource(LocalizedString.profileSettingTimelinePosition),\n        subtitle = position.displayName,\n        dropDownItemCount = TimelineDefaultPosition.entries.size,\n        dropDownItemText = {\n            TimelineDefaultPosition.entries[it].displayName\n        },\n        onItemClick = {\n            onPositionChanged(TimelineDefaultPosition.entries[it])\n        },\n    )\n}\n\n@Composable\nprivate fun OpenUrlBySystemBrowserItem(\n    openUrlBySystem: Boolean,\n    onOpenUrlBySystemChanged: (on: Boolean) -> Unit,\n) {\n    SettingItemWithSwitch(\n        icon = Icons.Default.OpenInBrowser,\n        title = stringResource(LocalizedString.setting_item_open_url_by_system_title),\n        subtitle = stringResource(LocalizedString.setting_item_open_url_by_system_subtitle),\n        checked = openUrlBySystem,\n        onCheckedChangeRequest = onOpenUrlBySystemChanged,\n    )\n}\n"
  },
  {
    "path": "feature/profile/src/commonMain/kotlin/com/zhangke/fread/profile/screen/setting/behavior/BehaviorSettingsUiState.kt",
    "content": "package com.zhangke.fread.profile.screen.setting.behavior\n\nimport com.zhangke.fread.common.config.TimelineDefaultPosition\n\ndata class BehaviorSettingsUiState(\n    val autoPlayInlineVideo: Boolean,\n    val alwaysShowSensitiveContent: Boolean,\n    val timelineDefaultPosition: TimelineDefaultPosition,\n    val openUrlInAppBrowser: Boolean,\n)\n"
  },
  {
    "path": "feature/profile/src/commonMain/kotlin/com/zhangke/fread/profile/screen/setting/behavior/BehaviorSettingsViewModel.kt",
    "content": "package com.zhangke.fread.profile.screen.setting.behavior\n\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.zhangke.fread.common.config.FreadConfigManager\nimport com.zhangke.fread.common.config.TimelineDefaultPosition\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.update\nimport kotlinx.coroutines.launch\n\nclass BehaviorSettingsViewModel(\n    private val freadConfigManager: FreadConfigManager,\n) : ViewModel() {\n\n    private val _uiState = MutableStateFlow(\n        BehaviorSettingsUiState(\n            autoPlayInlineVideo = freadConfigManager.autoPlayInlineVideo,\n            alwaysShowSensitiveContent = false,\n            timelineDefaultPosition = TimelineDefaultPosition.NEWEST,\n            openUrlInAppBrowser = freadConfigManager.openUrlInAppBrowser,\n        )\n    )\n    val uiState = _uiState.asStateFlow()\n\n    init {\n        viewModelScope.launch {\n            freadConfigManager.getTimelineDefaultPosition()\n                .let { position ->\n                    _uiState.update { it.copy(timelineDefaultPosition = position) }\n                }\n        }\n        viewModelScope.launch {\n            freadConfigManager.statusConfigFlow.collect { config ->\n                _uiState.update {\n                    it.copy(alwaysShowSensitiveContent = config.alwaysShowSensitiveContent)\n                }\n            }\n        }\n    }\n\n    fun onChangeAutoPlayInlineVideo(on: Boolean) {\n        viewModelScope.launch {\n            freadConfigManager.updateAutoPlayInlineVideo(on)\n            _uiState.update { it.copy(autoPlayInlineVideo = on) }\n        }\n    }\n\n    fun onAlwaysShowSensitiveContentChanged(always: Boolean) {\n        viewModelScope.launch {\n            freadConfigManager.updateAlwaysShowSensitiveContent(always)\n        }\n    }\n\n    fun onTimelineDefaultPositionChanged(position: TimelineDefaultPosition) {\n        viewModelScope.launch {\n            freadConfigManager.updateTimelineDefaultPosition(position)\n            _uiState.update { it.copy(timelineDefaultPosition = position) }\n        }\n    }\n\n    fun onOpenUrlInAppBrowserChanged(openInApp: Boolean) {\n        viewModelScope.launch {\n            freadConfigManager.updateOpenUrlInAppBrowser(openInApp)\n            _uiState.update { it.copy(openUrlInAppBrowser = openInApp) }\n        }\n    }\n}\n"
  },
  {
    "path": "framework/.gitignore",
    "content": "/build"
  },
  {
    "path": "framework/build.gradle.kts",
    "content": "plugins {\n    id(\"fread.project.framework.kmp\")\n    id(\"kotlin-parcelize\")\n}\n\nandroid {\n    namespace = \"com.zhangke.fread.framework\"\n    sourceSets {\n        getByName(\"main\") {\n            res.srcDirs(\"src/commonMain/res\")\n            resources.srcDirs(\"src/commonMain/resources\")\n        }\n    }\n}\n\nkotlin {\n    sourceSets {\n        commonMain {\n            dependencies {\n                api(project(path = \":localization\"))\n\n                implementation(compose.components.resources)\n\n                implementation(libs.compose.jb.backhandler)\n\n                implementation(libs.androidx.annotation)\n\n                implementation(libs.arrow.core)\n\n                implementation(libs.jetbrains.lifecycle.runtime)\n                implementation(libs.jetbrains.lifecycle.viewmodel)\n\n                implementation(libs.kotlinx.serialization.core)\n                implementation(libs.kotlinx.serialization.json)\n\n                implementation(libs.kotlinx.datetime)\n\n                implementation(libs.ktor.client.core)\n                implementation(libs.ktor.client.serialization.kotlinx.json)\n                implementation(libs.ktor.client.content.negotiation)\n\n                implementation(libs.haze)\n                implementation(libs.haze.materials)\n\n                implementation(libs.imageLoader)\n                implementation(libs.okio)\n                implementation(libs.ksoup)\n                implementation(libs.uri.kmp)\n                implementation(libs.bignum)\n                implementation(libs.kermit)\n                implementation(libs.placeholder.material3)\n                implementation(libs.compose.media.player)\n\n                implementation(libs.krouter.runtime)\n            }\n        }\n        commonTest {\n            dependencies {\n                implementation(kotlin(\"test\"))\n            }\n        }\n        androidMain {\n            dependencies {\n                implementation(compose.uiTooling)\n                implementation(compose.preview)\n\n                implementation(libs.androidx.core.ktx)\n                implementation(libs.bundles.androidx.activity)\n                implementation(libs.accompanist.permissions)\n\n                implementation(libs.okhttp3)\n                implementation(libs.okhttp3.logging)\n\n                implementation(libs.bundles.androidx.media3)\n\n                implementation(libs.ktml)\n\n                implementation(libs.ktor.client.okhttp)\n            }\n        }\n        androidUnitTest {\n            dependencies {\n            }\n        }\n        androidInstrumentedTest {\n            dependencies {\n                implementation(libs.junit)\n                implementation(libs.androidx.test.ext.junit)\n                implementation(libs.androidx.test.espresso.core)\n            }\n        }\n        iosMain {\n            dependencies {\n                implementation(libs.ktor.client.darwin)\n            }\n        }\n    }\n}\n\ncompose {\n    resources {\n        publicResClass = true\n        packageOfResClass = \"com.zhangke.fread.framework\"\n        generateResClass = always\n    }\n}\n"
  },
  {
    "path": "framework/consumer-rules.pro",
    "content": ""
  },
  {
    "path": "framework/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguardFiles setting in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n\n# Uncomment this to preserve the line number information for\n# debugging stack traces.\n#-keepattributes SourceFile,LineNumberTable\n\n# If you keep the line number information, uncomment this to\n# hide the original source file name.\n#-renamesourcefileattribute SourceFile"
  },
  {
    "path": "framework/src/androidInstrumentedTest/kotlin/com/zhangke/framework/ExampleInstrumentedTest.kt",
    "content": "package com.zhangke.framework\n\nimport androidx.test.platform.app.InstrumentationRegistry\nimport androidx.test.ext.junit.runners.AndroidJUnit4\n\nimport org.junit.Test\nimport org.junit.runner.RunWith\n\nimport org.junit.Assert.*\n\n/**\n * Instrumented test, which will execute on an Android device.\n *\n * See [testing documentation](http://d.android.com/tools/testing).\n */\n@RunWith(AndroidJUnit4::class)\nclass ExampleInstrumentedTest {\n    @Test\n    fun useAppContext() {\n        // Context of the app under test.\n        val appContext = InstrumentationRegistry.getInstrumentation().targetContext\n        assertEquals(\"com.zhangke.framework.test\", appContext.packageName)\n    }\n}"
  },
  {
    "path": "framework/src/androidInstrumentedTest/kotlin/com/zhangke/framework/utils/UtiUtilsTest.kt",
    "content": "package com.zhangke.framework.utils\n\nimport org.junit.Assert\nimport org.junit.Test\n\nclass UtiUtilsTest {\n\n    @Test\n    fun shouldRemovePathPrefixAndSuffix(){\n        val uri = uriString(\n            scheme = \"app\",\n            host = \"zhangke.com\",\n            path = \"/home/\",\n            queries = mapOf(\"name\" to \"zhangke\"),\n        )\n        Assert.assertEquals(\"app://zhangke.com/home?name=zhangke\", uri)\n    }\n\n    @Test\n    fun shouldRemovePathPrefixAndSuffixWhenEmptyHost(){\n        val uri = uriString(\n            scheme = \"app\",\n            host = \"\",\n            path = \"/home/\",\n            queries = mapOf(\"name\" to \"zhangke\"),\n        )\n        Assert.assertEquals(\"app://home?name=zhangke\", uri)\n    }\n\n    @Test\n    fun shouldRemovePathSuffixWhenEmptyScheme(){\n        val uri = uriString(\n            scheme = \"\",\n            host = \"zhangke.com\",\n            path = \"/home/\",\n            queries = mapOf(\"name\" to \"zhangke\"),\n        )\n        Assert.assertEquals(\"zhangke.com/home?name=zhangke\", uri)\n    }\n}\n"
  },
  {
    "path": "framework/src/androidMain/kotlin/com/zhangke/framework/activity/TopActivityManager.kt",
    "content": "package com.zhangke.framework.activity\n\nimport android.app.Activity\nimport android.app.Application\nimport com.zhangke.framework.utils.ActivityLifecycleCallbacksAdapter\nimport java.lang.ref.SoftReference\n\nobject TopActivityManager {\n\n    private var activeActivity: SoftReference<Activity?>? = null\n\n    val topActiveActivity: Activity? get() = activeActivity?.get()\n\n    fun init(application: Application) {\n        application.registerActivityLifecycleCallbacks(\n            object : ActivityLifecycleCallbacksAdapter() {\n                override fun onActivityResumed(activity: Activity) {\n                    super.onActivityResumed(activity)\n                    activeActivity = SoftReference(activity)\n                }\n\n                override fun onActivityPaused(activity: Activity) {\n                    super.onActivityPaused(activity)\n                    activeActivity?.clear()\n                    activeActivity = null\n                }\n            },\n        )\n    }\n\n    fun updateTopActivity(activity: Activity){\n        activeActivity = SoftReference(activity)\n    }\n}\n"
  },
  {
    "path": "framework/src/androidMain/kotlin/com/zhangke/framework/architect/http/GlobalOkHttpClient.kt",
    "content": "package com.zhangke.framework.architect.http\n\nimport android.annotation.SuppressLint\nimport android.util.Log\nimport com.zhangke.framework.utils.ifDebugging\nimport okhttp3.Interceptor\nimport okhttp3.OkHttpClient\nimport okhttp3.logging.HttpLoggingInterceptor\nimport java.security.KeyStore\nimport java.util.*\nimport java.util.concurrent.TimeUnit\nimport javax.net.ssl.*\n\nobject GlobalOkHttpClient {\n\n    private const val TIMEOUT = 15L\n\n    val client: OkHttpClient by lazy { createBuilder().build() }\n\n    private val thirdPartInterceptors = mutableListOf<Interceptor>()\n\n    fun addThirdPartInterceptor(interceptor: Interceptor) {\n        thirdPartInterceptors += interceptor\n    }\n\n    private fun createBuilder(): OkHttpClient.Builder {\n        val ssl = buildSSLFactory()\n        val builder = OkHttpClient().newBuilder()\n            .connectTimeout(TIMEOUT, TimeUnit.SECONDS)\n            .readTimeout(TIMEOUT, TimeUnit.SECONDS)\n            .writeTimeout(TIMEOUT, TimeUnit.SECONDS)\n            .sslSocketFactory(ssl.first, ssl.second)\n            .hostnameVerifier { _, _ -> true }\n        ifDebugging {\n            builder.addInterceptor(\n                HttpLoggingInterceptor(HttpLoggingInterceptor.Logger.DEFAULT)\n                    .setLevel(HttpLoggingInterceptor.Level.BODY)\n            )\n        }\n        thirdPartInterceptors.forEach {\n            Log.d(\"GlobalOkHttpClient\", \"add $it\")\n            builder.addInterceptor(it)\n        }\n        return builder\n    }\n\n    @SuppressLint(\"TrustAllX509TrustManager\", \"CustomX509TrustManager\")\n    private fun buildSSLFactory(): Pair<SSLSocketFactory, X509TrustManager> {\n        val trustManagerFactory =\n            TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())\n        trustManagerFactory.init(null as KeyStore?)\n        val trustManagers = trustManagerFactory.trustManagers\n        check(!(trustManagers.size != 1 || trustManagers[0] !is X509TrustManager)) {\n            \"Unexpected default trust managers:\" + Arrays.toString(trustManagers)\n        }\n        val trustManager = trustManagers[0] as X509TrustManager\n        val sslContext = SSLContext.getInstance(\"SSL\")\n        sslContext.init(null, arrayOf<TrustManager>(trustManager), null)\n        sslContext.defaultSSLParameters.protocols = arrayOf(\"SSLv3\")\n        val sslSocketFactory = sslContext.socketFactory\n        return Pair(sslSocketFactory, trustManager)\n    }\n}"
  },
  {
    "path": "framework/src/androidMain/kotlin/com/zhangke/framework/architect/http/HttpClientEngine.android.kt",
    "content": "package com.zhangke.framework.architect.http\n\nimport io.ktor.client.engine.HttpClientEngine\nimport io.ktor.client.engine.okhttp.OkHttp\n\nactual fun createHttpClientEngine(): HttpClientEngine {\n    return OkHttp.create {\n        preconfigured = GlobalOkHttpClient.client\n    }\n}"
  },
  {
    "path": "framework/src/androidMain/kotlin/com/zhangke/framework/architect/theme/FreadTheme.android.kt",
    "content": "package com.zhangke.framework.architect.theme\n\nimport android.app.Activity\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.SideEffect\nimport androidx.compose.ui.platform.LocalView\nimport androidx.core.view.WindowCompat\n\n@Composable\ninternal actual fun FreadPlatformTheme(darkTheme: Boolean) {\n    val view = LocalView.current\n    if (!view.isInEditMode) {\n        SideEffect {\n            val window = (view.context as Activity).window\n            WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme\n        }\n    }\n}"
  },
  {
    "path": "framework/src/androidMain/kotlin/com/zhangke/framework/blurhash/BitmapExt.android.kt",
    "content": "package com.zhangke.framework.blurhash\n\nimport android.graphics.Bitmap\nimport androidx.compose.ui.graphics.ImageBitmap\nimport androidx.compose.ui.graphics.asImageBitmap\n\nactual fun bitmapFromBuffer(buffer: IntArray, width: Int, height: Int): ImageBitmap {\n    return Bitmap.createBitmap(buffer, width, height, Bitmap.Config.ARGB_8888).asImageBitmap()\n}\n"
  },
  {
    "path": "framework/src/androidMain/kotlin/com/zhangke/framework/composable/AnimatableExt.kt",
    "content": "package com.zhangke.framework.composable\n\nimport androidx.compose.animation.core.Animatable\nimport androidx.compose.animation.core.AnimationVector1D\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.unit.Dp\nimport com.zhangke.framework.utils.pxToDp\n\nval Animatable<Float, AnimationVector1D>.dpValue: Dp\n    @Composable get() {\n        return value.pxToDp(LocalDensity.current)\n    }\n"
  },
  {
    "path": "framework/src/androidMain/kotlin/com/zhangke/framework/composable/Loading.android.kt",
    "content": "package com.zhangke.framework.composable\n\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.tooling.preview.Preview\n\n@Preview\n@Composable\nfun PreviewShortLoadErrorLineItem(){\n    LoadErrorLineItem(\n        modifier = Modifier.fillMaxWidth(),\n        errorMessage = \"123\",\n    )\n}\n\n@Preview\n@Composable\nfun PreviewLongLoadErrorLineItem(){\n    LoadErrorLineItem(\n        modifier = Modifier.fillMaxWidth(),\n        errorMessage = \"123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123\",\n    )\n}"
  },
  {
    "path": "framework/src/androidMain/kotlin/com/zhangke/framework/composable/TextString.android.kt",
    "content": "package com.zhangke.framework.composable\n\nimport androidx.compose.runtime.Composable\nimport com.zhangke.framework.utils.appContext\n\n@Composable\nactual fun stringResource(resId: Int, vararg formatArgs: Any): String {\n    return androidx.compose.ui.res.stringResource(resId, *formatArgs)\n}\n\nactual suspend fun TextString.getString(): String {\n    return when (this) {\n        is TextString.StringText -> string\n        is TextString.ResourceText -> appContext.getString(resId, *formatArgs)\n        is TextString.ComposeResourceText -> org.jetbrains.compose.resources.getString(res, *formatArgs)\n    }\n}\n"
  },
  {
    "path": "framework/src/androidMain/kotlin/com/zhangke/framework/composable/pick/PickVisualMediaLauncherContainer.android.kt",
    "content": "package com.zhangke.framework.composable.pick\n\nimport androidx.activity.compose.ManagedActivityResultLauncher\nimport androidx.activity.compose.rememberLauncherForActivityResult\nimport androidx.activity.result.PickVisualMediaRequest\nimport androidx.activity.result.contract.ActivityResultContracts\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport com.zhangke.framework.utils.PlatformUri\nimport com.zhangke.framework.utils.buildPickVisualImageRequest\nimport com.zhangke.framework.utils.buildPickVisualMediaRequest\nimport com.zhangke.framework.utils.buildPickVisualVideoRequest\nimport com.zhangke.framework.utils.toPlatformUri\n\n@Composable\nactual fun PickVisualMediaLauncherContainer(\n    onResult: (List<PlatformUri>) -> Unit,\n    maxItems: Int,\n    content: @Composable PickVisualMediaLauncherContainerScope.() -> Unit,\n) {\n    val fileLauncher = when {\n        maxItems > 1 -> {\n            rememberLauncherForActivityResult(\n                contract = ActivityResultContracts.GetMultipleContents(),\n                onResult = { uri ->\n                    onResult(uri.take(maxItems).map { it.toPlatformUri() })\n                },\n            )\n        }\n\n        else -> {\n            rememberLauncherForActivityResult(\n                contract = ActivityResultContracts.GetContent(),\n                onResult = { uri -> uri?.let { onResult(listOf(it.toPlatformUri())) } },\n            )\n        }\n    }\n\n    val launcher = when {\n        maxItems > 1 -> rememberLauncherForActivityResult(\n            contract = ActivityResultContracts.PickMultipleVisualMedia(maxItems = maxItems),\n            onResult = { uris ->\n                onResult(uris.map { it.toPlatformUri() })\n            },\n        )\n\n        else -> rememberLauncherForActivityResult(\n            contract = ActivityResultContracts.PickVisualMedia(),\n            onResult = { uri ->\n                uri?.let { onResult(listOf(it.toPlatformUri())) }\n            },\n        )\n    }\n    val scope = remember(launcher) {\n        PickVisualMediaLauncherContainerScope(launcher, fileLauncher)\n    }\n    with(scope) {\n        content()\n    }\n}\n\nactual class PickVisualMediaLauncherContainerScope(\n    private val launcher: ManagedActivityResultLauncher<PickVisualMediaRequest, *>,\n    private val fileLauncher: ManagedActivityResultLauncher<String, *>,\n) {\n\n    actual fun launchImage() {\n        launcher.launch(buildPickVisualImageRequest())\n    }\n\n    actual fun launchMedia() {\n        launcher.launch(buildPickVisualMediaRequest())\n    }\n\n    actual fun launchVideo() {\n        launcher.launch(buildPickVisualVideoRequest())\n    }\n\n    actual fun launchImageFile() {\n        fileLauncher.launch(\"image/*\")\n    }\n\n    actual fun launchVideoFile() {\n        fileLauncher.launch(\"video/*\")\n    }\n}\n"
  },
  {
    "path": "framework/src/androidMain/kotlin/com/zhangke/framework/date/InstantFormater.android.kt",
    "content": "@file:OptIn(ExperimentalTime::class)\n\npackage com.zhangke.framework.date\n\nimport kotlinx.datetime.Instant\nimport java.text.DateFormat\nimport java.util.Date\nimport java.util.Locale\nimport kotlin.time.ExperimentalTime\n\nactual class InstantFormater {\n\n    actual fun formatToMediumDate(instant: Instant): String {\n        val dateFormat = DateFormat.getDateInstance(DateFormat.MEDIUM, Locale.getDefault())\n        val timeFormat = DateFormat.getTimeInstance(DateFormat.MEDIUM, Locale.getDefault())\n        val date = Date(instant.toEpochMilliseconds())\n        return buildString {\n            append(dateFormat.format(date))\n            append(\" \")\n            append(timeFormat.format(date))\n        }\n    }\n\n    actual fun formatToMediumDateWithoutTime(instant: Instant): String {\n        val dateFormat = DateFormat.getDateInstance(DateFormat.MEDIUM, Locale.getDefault())\n        val date = Date(instant.toEpochMilliseconds())\n        return dateFormat.format(date)\n    }\n}\n"
  },
  {
    "path": "framework/src/androidMain/kotlin/com/zhangke/framework/datetime/Instant.android.kt",
    "content": "package com.zhangke.framework.datetime\n\nimport com.zhangke.framework.serialize.TimestampAsInstantSerializer\nimport com.zhangke.framework.utils.Parcelize\nimport kotlinx.serialization.Serializable\n\n//@Parcelize\n//@Serializable(with = TimestampAsInstantSerializer::class)\n//actual class Instant actual (\n//    actual val instant: kotlinx.datetime.Instant\n//) : java.io.Serializable"
  },
  {
    "path": "framework/src/androidMain/kotlin/com/zhangke/framework/ktx/SingletonDelegate.kt",
    "content": "package com.zhangke.framework.ktx\n\nimport kotlin.reflect.KProperty\n\nprivate val singletonHolder = mutableMapOf<String, Any>()\n\n@Suppress(\"FunctionName\")\ninline fun <reified T> SingletonDelegate(noinline creator: (thisRef: Any?) -> T): SingletonTypedDelegate<T> {\n    return SingletonTypedDelegate(T::class.java, creator)\n}\n\nclass SingletonTypedDelegate<T>(private val type: Class<T>, private val creator: (Any?) -> T) {\n\n    @Suppress(\"UNCHECKED_CAST\")\n    operator fun getValue(thisRef: Any?, property: KProperty<*>): T {\n        return singletonHolder.getOrPut(buildKey(thisRef)) {\n            creator(thisRef) as Any\n        } as T\n    }\n\n    private fun buildKey(thisRef: Any?): String {\n        return if (thisRef != null) {\n            \"${thisRef::class.java.canonicalName}@${thisRef.hashCode()}\" +\n                    \"_${type::class.java.canonicalName}\"\n        } else {\n            type::class.java.canonicalName!!\n        }\n    }\n}"
  },
  {
    "path": "framework/src/androidMain/kotlin/com/zhangke/framework/media/MediaFileUtil.kt",
    "content": "package com.zhangke.framework.media\n\nimport android.content.ContentValues\nimport android.content.Context\nimport android.graphics.Bitmap\nimport android.net.Uri\nimport android.os.Environment\nimport android.provider.MediaStore\nimport com.seiko.imageloader.imageLoader\nimport com.seiko.imageloader.model.ImageRequest\nimport com.zhangke.framework.architect.http.sharedHttpClient\nimport com.zhangke.framework.imageloader.executeSafety\nimport com.zhangke.framework.permission.hasWriteStoragePermission\nimport com.zhangke.framework.utils.asBitmapOrNull\nimport com.zhangke.framework.utils.ifDebugging\nimport com.zhangke.framework.utils.throwInDebug\nimport io.ktor.client.request.prepareRequest\nimport io.ktor.client.statement.bodyAsChannel\nimport io.ktor.utils.io.jvm.javaio.copyTo\nimport java.io.OutputStream\nimport java.net.URLDecoder\n\nobject MediaFileUtil {\n\n    suspend fun saveImageToGallery(context: Context, url: String): Boolean {\n        if (!context.hasWriteStoragePermission()) return false\n        val contentValues = buildContentValues(\n            fileName = \"${System.currentTimeMillis()}.jpg\",\n            mediaType = \"image/jpeg\",\n            directory = Environment.DIRECTORY_PICTURES,\n        )\n        val uri = context.contentResolver\n            .insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues) ?: return false\n        val outputStream = context.contentResolver.openOutputStream(uri)\n        if (outputStream == null) {\n            context.contentResolver.delete(uri, null, null)\n            return false\n        }\n        val bitmap = downloadImage(context, url)\n        if (bitmap == null) {\n            context.contentResolver.delete(uri, null, null)\n            return false\n        }\n        outputStream.use { out ->\n            bitmap.compress(Bitmap.CompressFormat.JPEG, 100, out)\n        }\n        return true\n    }\n\n    suspend fun saveVideoToGallery(context: Context, url: String): Boolean {\n        if (!context.hasWriteStoragePermission()) return false\n        try {\n            downloadVideo(url) { contentType ->\n                val uri = context.insertVideoMedia(contentType) ?: return@downloadVideo null\n                val outputStream =\n                    context.contentResolver.openOutputStream(uri) ?: return@downloadVideo null\n                outputStream\n            }\n        } catch (e: Throwable) {\n            throwInDebug(\"saveVideoToGallery\", e)\n            return false\n        }\n        return true\n    }\n\n    private fun Context.insertVideoMedia(mediaType: String): Uri? {\n        val fileExtension = mediaType.split(\"/\").lastOrNull() ?: \"mp4\"\n        val fileName = \"${System.currentTimeMillis()}.$fileExtension\"\n        val contentValues = buildContentValues(fileName, mediaType, Environment.DIRECTORY_MOVIES)\n        return contentResolver.insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, contentValues)\n    }\n\n    private fun buildContentValues(\n        fileName: String,\n        mediaType: String,\n        directory: String,\n    ): ContentValues {\n        return ContentValues().apply {\n            put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)\n            put(MediaStore.MediaColumns.MIME_TYPE, mediaType)\n            if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {\n                put(MediaStore.Images.Media.RELATIVE_PATH, \"$directory/Fread\")\n            }\n        }\n    }\n\n    private suspend fun downloadImage(context: Context, url: String): Bitmap? {\n        return try {\n            val request = ImageRequest(url) {\n                options {\n                    isBitmap = true\n                }\n            }\n            context.imageLoader.executeSafety(request).asBitmapOrNull()\n        } catch (e: Throwable) {\n            ifDebugging {\n                e.printStackTrace()\n            }\n            null\n        }\n    }\n\n    private suspend inline fun downloadVideo(\n        url: String,\n        crossinline block: (String) -> OutputStream?\n    ) {\n        sharedHttpClient.prepareRequest(url).execute { response ->\n            val contentType = response.headers[\"Content-Type\"] ?: \"video/mp4\"\n            val outputStream = block(contentType) ?: return@execute\n            response.bodyAsChannel().copyTo(outputStream)\n            outputStream.close()\n        }\n    }\n\n    fun queryFileName(context: Context, uri: Uri): String {\n        context.contentResolver\n            .query(uri, null, null, null, null)\n            ?.use { cursor ->\n                if (cursor.moveToFirst()) {\n                    val columnIndex = cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME)\n                    if (columnIndex >= 0) {\n                        return cursor.getString(columnIndex)\n                    }\n                }\n            }\n        return try {\n            val path = URLDecoder.decode(uri.path, \"UTF-8\")\n            path.split(\"/\").lastOrNull() ?: path\n        } catch (e: Throwable) {\n            uri.toString()\n        }\n    }\n}\n"
  },
  {
    "path": "framework/src/androidMain/kotlin/com/zhangke/framework/permission/PermissionUtils.kt",
    "content": "package com.zhangke.framework.permission\n\nimport android.Manifest\nimport android.content.Context\nimport android.content.pm.PackageManager\nimport android.os.Build\nimport androidx.core.app.ActivityCompat\n\nfun Context.hasWriteStoragePermission(): Boolean {\n    return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {\n        true\n    } else {\n        val permission = Manifest.permission.WRITE_EXTERNAL_STORAGE\n        ActivityCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED\n    }\n}\n"
  },
  {
    "path": "framework/src/androidMain/kotlin/com/zhangke/framework/permission/RequireLocalStoragePermission.android.kt",
    "content": "package com.zhangke.framework.permission\n\nimport android.Manifest\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.ui.platform.LocalContext\n\n@Composable\nactual fun RequireLocalStoragePermission(\n    onPermissionGranted: suspend () -> Unit,\n    onPermissionDenied: suspend (() -> Unit),\n) {\n    val context = LocalContext.current\n    if (context.hasWriteStoragePermission()) {\n        LaunchedEffect(Unit) {\n            onPermissionGranted()\n        }\n    } else {\n        RequirePermission(\n            permissionString = Manifest.permission.WRITE_EXTERNAL_STORAGE,\n            onPermissionGranted = onPermissionGranted,\n            onPermissionDenied = onPermissionDenied,\n        )\n    }\n}\n"
  },
  {
    "path": "framework/src/androidMain/kotlin/com/zhangke/framework/permission/RequirePermission.kt",
    "content": "package com.zhangke.framework.permission\n\nimport android.content.Context\nimport android.content.Intent\nimport android.net.Uri\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.platform.LocalContext\nimport com.google.accompanist.permissions.ExperimentalPermissionsApi\nimport com.google.accompanist.permissions.PermissionStatus\nimport com.google.accompanist.permissions.rememberPermissionState\nimport com.google.accompanist.permissions.shouldShowRationale\nimport com.zhangke.framework.composable.FreadDialog\nimport com.zhangke.framework.utils.extractActivity\nimport com.zhangke.fread.localization.LocalizedString\nimport kotlinx.coroutines.launch\nimport org.jetbrains.compose.resources.stringResource\n\n@OptIn(ExperimentalPermissionsApi::class)\n@Composable\nfun RequirePermission(\n    permissionString: String,\n    onPermissionGranted: suspend () -> Unit,\n    onPermissionDenied: suspend (() -> Unit) = {},\n) {\n    val context = LocalContext.current\n    val coroutineScope = rememberCoroutineScope()\n    val permissionState = rememberPermissionState(\n        permission = permissionString,\n        onPermissionResult = {\n            if (!it) {\n                coroutineScope.launch {\n                    onPermissionDenied()\n                }\n            }\n        }\n    )\n    val permissionStatus = permissionState.status\n    if (permissionStatus == PermissionStatus.Granted) {\n        LaunchedEffect(permissionState) {\n            onPermissionGranted()\n        }\n    } else {\n        if (permissionStatus.shouldShowRationale) {\n            LaunchedEffect(permissionState) {\n                permissionState.launchPermissionRequest()\n            }\n        } else {\n            var showDeniedDialog by remember {\n                mutableStateOf(true)\n            }\n            if (showDeniedDialog) {\n                FreadDialog(\n                    title = stringResource(LocalizedString.alert),\n                    onDismissRequest = {\n                        showDeniedDialog = false\n                    },\n                    contentText = stringResource(LocalizedString.permissionWriteExternalPermissionDenied),\n                    onNegativeClick = {\n                        showDeniedDialog = false\n                        coroutineScope.launch {\n                            onPermissionDenied()\n                        }\n                    },\n                    onPositiveClick = {\n                        coroutineScope.launch {\n                            onPermissionDenied()\n                        }\n                        openSettingActivity(context)\n                    },\n                )\n            }\n        }\n    }\n}\n\nprivate fun openSettingActivity(context: Context) {\n    val activity = context.extractActivity() ?: return\n    val localIntent = Intent()\n    localIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)\n    localIntent.action = \"android.settings.APPLICATION_DETAILS_SETTINGS\"\n    localIntent.data = Uri.fromParts(\"package\", activity.packageName, null)\n    activity.startActivity(localIntent)\n}\n"
  },
  {
    "path": "framework/src/androidMain/kotlin/com/zhangke/framework/serialize/DateSerializer.kt",
    "content": "package com.zhangke.framework.serialize\n\nimport kotlinx.serialization.KSerializer\nimport kotlinx.serialization.descriptors.PrimitiveKind\nimport kotlinx.serialization.descriptors.PrimitiveSerialDescriptor\nimport kotlinx.serialization.descriptors.SerialDescriptor\nimport kotlinx.serialization.encoding.Decoder\nimport kotlinx.serialization.encoding.Encoder\nimport java.util.Date\n\nclass DateSerializer : KSerializer<Date> {\n\n    override val descriptor: SerialDescriptor =\n        PrimitiveSerialDescriptor(\"Date\", PrimitiveKind.LONG)\n\n    override fun deserialize(decoder: Decoder): Date {\n        return Date(decoder.decodeLong())\n    }\n\n    override fun serialize(encoder: Encoder, value: Date) {\n        encoder.encodeLong(value.time)\n    }\n}\n"
  },
  {
    "path": "framework/src/androidMain/kotlin/com/zhangke/framework/toast/Toast.android.kt",
    "content": "package com.zhangke.framework.toast\n\nimport android.os.Build\nimport android.widget.Toast\nimport com.zhangke.framework.utils.appContext\nimport com.zhangke.fread.localization.LocalizedString\nimport org.jetbrains.compose.resources.getString\n\nactual fun toast(message: String?) {\n    toast(message, Toast.LENGTH_SHORT)\n}\n\nfun toast(message: String?, length: Int) {\n    if (message.isNullOrEmpty()) return\n    Toast.makeText(appContext, message, length).show()\n}\n\nprivate var savingToast: Toast? = null\n\nsuspend fun showFileSavingToast() {\n    val toast =\n        Toast.makeText(appContext, getString(LocalizedString.imageSaving), Toast.LENGTH_SHORT)\n    toast.show()\n    savingToast = toast\n    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {\n        var callback: Toast.Callback? = null\n        callback = object : Toast.Callback() {\n            override fun onToastHidden() {\n                super.onToastHidden()\n                callback?.let { toast.removeCallback(it) }\n                savingToast = null\n            }\n        }\n        toast.addCallback(callback)\n    }\n}\n\nsuspend fun showFileSaveSuccessToast() {\n    savingToast?.cancel()\n    savingToast = null\n    Toast.makeText(appContext, getString(LocalizedString.imageSaveSuccess), Toast.LENGTH_SHORT)\n        .show()\n}\n\nsuspend fun showFileSaveFailedToast() {\n    savingToast?.cancel()\n    savingToast = null\n    Toast.makeText(appContext, getString(LocalizedString.imageSaveFailed), Toast.LENGTH_SHORT)\n        .show()\n}\n"
  },
  {
    "path": "framework/src/androidMain/kotlin/com/zhangke/framework/utils/ActivityLifecycleCallbacksAdapter.kt",
    "content": "package com.zhangke.framework.utils\n\nimport android.app.Activity\nimport android.app.Application\nimport android.os.Bundle\n\nabstract class ActivityLifecycleCallbacksAdapter : Application.ActivityLifecycleCallbacks {\n\n    override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {\n    }\n\n    override fun onActivityStarted(activity: Activity) {\n    }\n\n    override fun onActivityResumed(activity: Activity) {\n    }\n\n    override fun onActivityPaused(activity: Activity) {\n    }\n\n    override fun onActivityStopped(activity: Activity) {\n    }\n\n    override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {\n    }\n\n    override fun onActivityDestroyed(activity: Activity) {\n    }\n}\n"
  },
  {
    "path": "framework/src/androidMain/kotlin/com/zhangke/framework/utils/ActivityResultContractsExt.kt",
    "content": "package com.zhangke.framework.utils\n\nimport android.net.Uri\nimport androidx.activity.compose.ManagedActivityResultLauncher\nimport androidx.activity.compose.rememberLauncherForActivityResult\nimport androidx.activity.result.PickVisualMediaRequest\nimport androidx.activity.result.contract.ActivityResultContracts\nimport androidx.compose.runtime.Composable\n\n@Composable\nfun rememberPickVisualMediaLauncher(\n    maxItems: Int,\n    onResult: (List<Uri>) -> Unit,\n): ManagedActivityResultLauncher<PickVisualMediaRequest, *>? {\n    return when {\n        maxItems < 1 -> null\n        maxItems > 1 -> rememberLauncherForActivityResult(\n            contract = ActivityResultContracts.PickMultipleVisualMedia(maxItems = maxItems),\n            onResult = onResult,\n        )\n\n        else -> rememberLauncherForActivityResult(\n            contract = ActivityResultContracts.PickVisualMedia(),\n            onResult = { uri -> uri?.let { onResult(listOf(it)) } },\n        )\n    }\n}\n\n@Composable\nfun rememberSinglePickVisualMediaLauncher(\n    onResult: (Uri) -> Unit,\n): ManagedActivityResultLauncher<PickVisualMediaRequest, *> {\n    return rememberLauncherForActivityResult(\n        contract = ActivityResultContracts.PickVisualMedia(),\n        onResult = { uri -> uri?.let { onResult(it) } },\n    )\n}\n\nfun buildPickVisualMediaRequest(): PickVisualMediaRequest {\n    return PickVisualMediaRequest.Builder()\n        .setMediaType(ActivityResultContracts.PickVisualMedia.ImageAndVideo)\n        .build()\n}\n\nfun buildPickVisualImageRequest(): PickVisualMediaRequest {\n    return PickVisualMediaRequest.Builder()\n        .setMediaType(ActivityResultContracts.PickVisualMedia.ImageOnly)\n        .build()\n}\n\nfun buildPickVisualVideoRequest(): PickVisualMediaRequest {\n    return PickVisualMediaRequest.Builder()\n        .setMediaType(ActivityResultContracts.PickVisualMedia.VideoOnly)\n        .build()\n}\n"
  },
  {
    "path": "framework/src/androidMain/kotlin/com/zhangke/framework/utils/BitmapUtils.kt",
    "content": "package com.zhangke.framework.utils\n\nimport android.graphics.Bitmap\nimport android.graphics.Canvas\nimport android.graphics.Color\nimport android.graphics.Paint\nimport android.graphics.Rect\nimport android.graphics.Typeface\nimport java.io.File\nimport java.io.FileOutputStream\n\nobject BitmapUtils {\n\n    fun buildBitmapWithText(\n        width: Int,\n        height: Int,\n        text: String,\n        backgroundColor: Int,\n    ): Bitmap {\n        val paint = Paint()\n        val textSize = if (text.length == 1) {\n            width * 0.6F\n        } else {\n            width * 0.8F / text.length\n        }\n        paint.textSize = textSize\n        paint.color = Color.WHITE\n        paint.textAlign = Paint.Align.CENTER\n        paint.isAntiAlias = true\n        paint.typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD)\n        val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)\n        val canvas = Canvas(bitmap)\n        canvas.drawColor(backgroundColor)\n        val bundle = Rect()\n        paint.getTextBounds(text, 0, text.length, bundle)\n        val y = height / 2F + bundle.height() / 2F - bundle.bottom\n        canvas.drawText(text, width / 2F, y, paint)\n        return bitmap\n    }\n\n    fun saveToFile(bitmap: Bitmap, file: File) {\n        FileOutputStream(file).use {\n            bitmap.compress(Bitmap.CompressFormat.PNG, 90, it)\n        }\n    }\n}\n"
  },
  {
    "path": "framework/src/androidMain/kotlin/com/zhangke/framework/utils/ContextUtils.kt",
    "content": "@file:JvmName(\"ContextUtils\")\npackage com.zhangke.framework.utils\n\nimport android.app.Activity\nimport android.app.Application\nimport android.content.Context\nimport android.content.ContextWrapper\nimport android.content.Intent\nimport android.content.pm.ApplicationInfo\nimport androidx.lifecycle.LifecycleOwner\n\n@Volatile\nlateinit var appContext: Context\n    private set\n\nfun initApplication(application: Application) {\n    appContext = application\n}\n\ninline fun <reified T> Context.extractTarget(): T? {\n    var context: Context? = this\n    while (context != null) {\n        if (context is T) return context\n        context = if (context is ContextWrapper) context.baseContext else null\n    }\n    return null\n}\n\nfun Context.extractActivity(): Activity? {\n    return extractTarget()\n}\n\nfun Context.extractLifecycleOwner(): LifecycleOwner? {\n    return extractTarget()\n}\n\nfun Context.isDebugMode(): Boolean {\n    return (applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE) != 0\n}\n\nfun Context.startActivityCompat(intent: Intent) {\n    val activity = extractActivity()\n    if (activity != null) {\n        activity.startActivity(intent)\n    } else {\n        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)\n        this.startActivity(intent)\n    }\n}\n"
  },
  {
    "path": "framework/src/androidMain/kotlin/com/zhangke/framework/utils/DensityUtils.android.kt",
    "content": "package com.zhangke.framework.utils\n\nimport android.content.Context\nimport android.util.TypedValue\n\ncontext(context: Context)\nfun Int.dpToPx(): Int {\n    return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, this.toFloat(), context.resources.displayMetrics)\n        .toInt()\n}\n"
  },
  {
    "path": "framework/src/androidMain/kotlin/com/zhangke/framework/utils/DrawableExt.kt",
    "content": "package com.zhangke.framework.utils\n\nimport android.graphics.drawable.Drawable\n\nfun Drawable.aspectRatio(): Float {\n    return intrinsicWidth.toFloat() / intrinsicHeight.toFloat()\n}\n"
  },
  {
    "path": "framework/src/androidMain/kotlin/com/zhangke/framework/utils/DrawableWrapper.kt",
    "content": "package com.zhangke.framework.utils\n\nimport android.content.res.ColorStateList\nimport android.graphics.Canvas\nimport android.graphics.ColorFilter\nimport android.graphics.PixelFormat\nimport android.graphics.PorterDuff\nimport android.graphics.Rect\nimport android.graphics.Region\nimport android.graphics.drawable.Drawable\nimport androidx.core.graphics.drawable.DrawableCompat\n\npublic open class DrawableWrapper(drawable: Drawable? = null) : Drawable(), Drawable.Callback {\n\n    private var wrappedDrawable: Drawable? = null\n\n    init {\n        setWrappedDrawable(drawable)\n    }\n\n    override fun draw(canvas: Canvas) {\n        wrappedDrawable?.draw(canvas)\n    }\n\n    override fun onBoundsChange(bounds: Rect) {\n        wrappedDrawable?.bounds = bounds\n    }\n\n    override fun setChangingConfigurations(configs: Int) {\n        wrappedDrawable?.changingConfigurations = configs\n    }\n\n    override fun getChangingConfigurations(): Int {\n        return wrappedDrawable?.changingConfigurations ?: super.getChangingConfigurations()\n    }\n\n    @Suppress(\"deprecation\")\n    override fun setDither(dither: Boolean) {\n        wrappedDrawable?.setDither(dither)\n    }\n\n    override fun setFilterBitmap(filter: Boolean) {\n        wrappedDrawable?.isFilterBitmap = filter\n    }\n\n    override fun setAlpha(alpha: Int) {\n        wrappedDrawable?.alpha = alpha\n    }\n\n    override fun setColorFilter(colorFilter: ColorFilter?) {\n        wrappedDrawable?.colorFilter = colorFilter\n    }\n\n    override fun isStateful(): Boolean {\n        return wrappedDrawable?.isStateful ?: false\n    }\n\n    override fun setState(stateSet: IntArray): Boolean {\n        return wrappedDrawable?.setState(stateSet) ?: false\n    }\n\n    override fun getState(): IntArray {\n        return wrappedDrawable?.state ?: super.getState()\n    }\n\n    override fun jumpToCurrentState() {\n        wrappedDrawable?.jumpToCurrentState()\n    }\n\n    override fun getCurrent(): Drawable {\n        return wrappedDrawable ?: this\n    }\n\n    override fun setVisible(visible: Boolean, restart: Boolean): Boolean {\n        var v = super.setVisible(visible, restart)\n        wrappedDrawable?.setVisible(visible, restart)?.let { v = it }\n        return v\n    }\n\n    @Suppress(\"deprecation\")\n    override fun getOpacity(): Int {\n        return wrappedDrawable?.opacity ?: PixelFormat.UNKNOWN\n    }\n\n    override fun getTransparentRegion(): Region? {\n        return wrappedDrawable?.transparentRegion\n    }\n\n    override fun getIntrinsicWidth(): Int {\n        return wrappedDrawable?.intrinsicWidth ?: -1\n    }\n\n    override fun getIntrinsicHeight(): Int {\n        return wrappedDrawable?.intrinsicHeight ?: -1\n    }\n\n    override fun getMinimumWidth(): Int {\n        return wrappedDrawable?.minimumWidth ?: super.getMinimumWidth()\n    }\n\n    override fun getMinimumHeight(): Int {\n        return wrappedDrawable?.minimumHeight ?: super.getMinimumHeight()\n    }\n\n    override fun getPadding(padding: Rect): Boolean {\n        return wrappedDrawable?.getPadding(padding) ?: super.getPadding(padding)\n    }\n\n    override fun invalidateDrawable(who: Drawable) {\n        invalidateSelf()\n    }\n\n    override fun scheduleDrawable(who: Drawable, what: Runnable, `when`: Long) {\n        scheduleSelf(what, `when`)\n    }\n\n    override fun unscheduleDrawable(who: Drawable, what: Runnable) {\n        unscheduleSelf(what)\n    }\n\n    override fun onLevelChange(level: Int): Boolean {\n        return wrappedDrawable?.setLevel(level) ?: false\n    }\n\n    override fun setAutoMirrored(mirrored: Boolean) {\n        wrappedDrawable?.let { DrawableCompat.setAutoMirrored(it, mirrored) }\n    }\n\n    override fun isAutoMirrored(): Boolean {\n        return wrappedDrawable?.let { DrawableCompat.isAutoMirrored(it) } ?: false\n    }\n\n    override fun setTint(tint: Int) {\n        wrappedDrawable?.let { DrawableCompat.setTint(it, tint) }\n    }\n\n    override fun setTintList(tint: ColorStateList?) {\n        wrappedDrawable?.let { DrawableCompat.setTintList(it, tint) }\n    }\n\n    override fun setTintMode(tintMode: PorterDuff.Mode?) {\n        tintMode ?: return\n        wrappedDrawable?.let { DrawableCompat.setTintMode(it, tintMode) }\n    }\n\n    override fun setHotspot(x: Float, y: Float) {\n        wrappedDrawable?.let { DrawableCompat.setHotspot(it, x, y) }\n    }\n\n    override fun setHotspotBounds(left: Int, top: Int, right: Int, bottom: Int) {\n        wrappedDrawable?.let { DrawableCompat.setHotspotBounds(it, left, top, right, bottom) }\n    }\n\n    public fun getWrappedDrawable(): Drawable? {\n        return wrappedDrawable\n    }\n\n    public fun setWrappedDrawable(drawable: Drawable?) {\n        wrappedDrawable?.callback = null\n        wrappedDrawable = drawable\n        drawable?.callback = this\n    }\n}\n"
  },
  {
    "path": "framework/src/androidMain/kotlin/com/zhangke/framework/utils/ExoPlayerUtils.kt",
    "content": "package com.zhangke.framework.utils\n\nimport android.net.Uri\nimport androidx.media3.common.MediaItem\nimport androidx.media3.datasource.DefaultDataSource\nimport androidx.media3.exoplayer.hls.HlsMediaSource\nimport androidx.media3.exoplayer.source.MediaSource\nimport androidx.media3.exoplayer.source.ProgressiveMediaSource\n\n@androidx.media3.common.util.UnstableApi\nfun Uri.toMediaSource(): MediaSource {\n    val defaultDataSourceFactory = DefaultDataSource.Factory(appContext)\n    val dataSourceFactory = DefaultDataSource.Factory(appContext, defaultDataSourceFactory)\n    return when {\n        this.toString().contains(\".m3u8\") -> {\n            HlsMediaSource.Factory(dataSourceFactory).createMediaSource(MediaItem.fromUri(this))\n        }\n\n        else -> ProgressiveMediaSource.Factory(dataSourceFactory)\n            .createMediaSource(MediaItem.fromUri(this))\n    }\n}\n"
  },
  {
    "path": "framework/src/androidMain/kotlin/com/zhangke/framework/utils/FileUtils.kt",
    "content": "package com.zhangke.framework.utils\n\nimport android.content.ContentResolver\nimport android.net.Uri\nimport android.provider.OpenableColumns\nimport java.io.FileNotFoundException\n\nobject FileUtils {\n\n    /**\n     * @return unit is KB\n     */\n    fun getFileSizeByUri(uri: Uri): StorageSize? {\n        val contentResolver = appContext.contentResolver\n        var bytes: Long? = null\n        if (uri.scheme.equals(ContentResolver.SCHEME_CONTENT)) {\n            contentResolver.query(uri, arrayOf(OpenableColumns.SIZE), null, null, null)\n                ?.use { cursor ->\n                    val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE)\n                    if (sizeIndex != -1) {\n                        cursor.moveToFirst()\n                        try {\n                            bytes = cursor.getLong(sizeIndex)\n                        } catch (_: Throwable) {\n                            // ignore\n                        }\n                    }\n                }\n        }\n        if (bytes == null) {\n            try {\n                contentResolver.openAssetFileDescriptor(uri, \"r\")\n                    ?.use { bytes = it.length }\n            } catch (_: FileNotFoundException) {\n                // ignore\n            }\n        }\n        return bytes?.let(::StorageSize)\n    }\n}"
  },
  {
    "path": "framework/src/androidMain/kotlin/com/zhangke/framework/utils/IDNUtils.android.kt",
    "content": "package com.zhangke.framework.utils\n\nimport java.net.IDN\n\nactual class IDNUtils {\n\n    actual fun toASCII(input: String): String {\n        return IDN.toASCII(input, IDN.ALLOW_UNASSIGNED)\n    }\n}\n"
  },
  {
    "path": "framework/src/androidMain/kotlin/com/zhangke/framework/utils/ImageCompressUtils.android.kt",
    "content": "package com.zhangke.framework.utils\n\nimport android.graphics.Bitmap\nimport android.graphics.BitmapFactory\nimport java.io.ByteArrayOutputStream\n\nactual class ImageCompressUtils {\n\n    actual fun compress(bytes: ByteArray, targetSize: StorageSize): CompressResult {\n        val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)\n            ?: return CompressResult(bytes, null)\n        if (bytes.size < targetSize.bytes) {\n            return CompressResult(bytes, bitmap.aspectRatio)\n        }\n        var quality = 90\n        val out = ByteArrayOutputStream()\n        bitmap.compress(Bitmap.CompressFormat.JPEG, quality, out)\n        val step = 3\n        while (out.size() > targetSize.bytes && quality > step) {\n            out.reset()\n            quality -= step\n            bitmap.compress(Bitmap.CompressFormat.JPEG, quality, out)\n        }\n        runCatching { bitmap.recycle() }\n        val resultBytes = out.toByteArray()\n        val tmpBitmap = BitmapFactory.decodeByteArray(resultBytes, 0, resultBytes.size)\n        val resultSize = tmpBitmap?.aspectRatio\n        runCatching { tmpBitmap?.recycle() }\n        return CompressResult(resultBytes, resultSize)\n    }\n\n    private val Bitmap.aspectRatio: AspectRatio\n        get() = AspectRatio(width = width.toLong(), height = height.toLong())\n}\n"
  },
  {
    "path": "framework/src/androidMain/kotlin/com/zhangke/framework/utils/ImageLoaderUtils.kt",
    "content": "package com.zhangke.framework.utils\n\nimport android.graphics.Bitmap\nimport android.graphics.drawable.BitmapDrawable\nimport com.seiko.imageloader.model.ImageResult\n\nfun ImageResult.asBitmapOrNull(): Bitmap? = when (this) {\n    is ImageResult.OfBitmap -> bitmap\n    is ImageResult.OfImage -> image.drawable.let { it as? BitmapDrawable }?.bitmap\n    else -> null\n}\n"
  },
  {
    "path": "framework/src/androidMain/kotlin/com/zhangke/framework/utils/LanguageUtils.android.kt",
    "content": "package com.zhangke.framework.utils\n\nactual object LanguageUtils {\n\n    actual fun getAllLanguages(): List<Locale> {\n        return Locale.getAvailableLocales()\n            .distinctBy { it.language to it.getDisplayLanguage(Locale.ENGLISH) }\n    }\n}\n\nactual typealias Locale = java.util.Locale\n\nactual val Locale.languageCode: String\n    get() = this.language\n\nactual val Locale.isO3LanguageCode: String\n    get() = this.isO3Language\n\nactual fun Locale.getDisplayName(displayLocale: Locale): String {\n    return this.getDisplayLanguage(displayLocale)\n}\n\nactual fun initLocale(language: String): Locale {\n    return java.util.Locale(language)\n}\n\nactual fun getDefaultLocale(): Locale {\n    return java.util.Locale.getDefault()\n}\n"
  },
  {
    "path": "framework/src/androidMain/kotlin/com/zhangke/framework/utils/PlatformIgnoredOnParcel.android.kt",
    "content": "package com.zhangke.framework.utils\n\nimport kotlinx.parcelize.IgnoredOnParcel\n\nactual typealias PlatformIgnoredOnParcel = IgnoredOnParcel"
  },
  {
    "path": "framework/src/androidMain/kotlin/com/zhangke/framework/utils/PlatformParcelable.android.kt",
    "content": "package com.zhangke.framework.utils\n\nactual typealias PlatformParcelable = android.os.Parcelable"
  },
  {
    "path": "framework/src/androidMain/kotlin/com/zhangke/framework/utils/PlatformTransient.android.kt",
    "content": "package com.zhangke.framework.utils\n\nactual typealias PlatformTransient = kotlin.jvm.Transient\n"
  },
  {
    "path": "framework/src/androidMain/kotlin/com/zhangke/framework/utils/PlatformUri.android.kt",
    "content": "package com.zhangke.framework.utils\n\nimport android.net.Uri\nimport androidx.core.net.toUri\nimport com.eygraber.uri.toUri\n\nfun PlatformUri.toAndroidUri(): Uri = toString().toUri()\n\nfun Uri.toPlatformUri(): PlatformUri = toUri()"
  },
  {
    "path": "framework/src/androidMain/kotlin/com/zhangke/framework/utils/Serializable.android.kt",
    "content": "package com.zhangke.framework.utils\n\nactual typealias PlatformSerializable = java.io.Serializable"
  },
  {
    "path": "framework/src/androidMain/kotlin/com/zhangke/framework/utils/SignatureUtils.kt",
    "content": "package com.zhangke.framework.utils\n\nimport android.content.Context\nimport android.content.pm.PackageManager\nimport android.content.pm.Signature\nimport android.os.Build\nimport okio.ByteString\n\n@OptIn(ExperimentalStdlibApi::class)\nfun Context.getApkSignatureSha1(): String? {\n    val signature = getApkSignatures().firstOrNull() ?: return null\n    val signatureByteString = ByteString.of(*signature.toByteArray())\n    val sha1 = signatureByteString.sha1().toByteArray().toHexString(\n        HexFormat {\n            upperCase = true\n            bytes.bytesPerGroup = 1\n            bytes.groupSeparator = \":\"\n        },\n    )\n    return sha1\n}\n\nprivate fun Context.getApkSignatures(): List<Signature> {\n    return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {\n        try {\n            packageManager.getPackageInfo(packageName, PackageManager.GET_SIGNING_CERTIFICATES)\n                ?.signingInfo?.apkContentsSigners?.mapNotNull { it } ?: emptyList()\n        } catch (_: Throwable) {\n            emptyList()\n        }\n    } else {\n        try {\n            @Suppress(\"DEPRECATION\")\n            packageManager.getPackageInfo(packageName, PackageManager.GET_SIGNATURES)\n                ?.signatures?.mapNotNull { it } ?: emptyList()\n        } catch (_: Throwable) {\n            emptyList()\n        }\n    }\n}\n"
  },
  {
    "path": "framework/src/androidMain/kotlin/com/zhangke/framework/utils/SystemPageUtils.kt",
    "content": "package com.zhangke.framework.utils\n\nimport android.content.ActivityNotFoundException\nimport android.content.Context\nimport android.content.Intent\nimport android.widget.Toast\nimport androidx.core.net.toUri\n\nobject SystemPageUtils {\n\n    fun openAppMarket(context: Context): Boolean {\n        val uri = \"market://details?id=${context.packageName}\"\n        if (!openSystemViewPage(context, uri)) {\n            Toast.makeText(context, \"Application market not found\", Toast.LENGTH_SHORT).show()\n            return false\n        }\n        return true\n    }\n\n    fun openSystemViewPage(\n        context: Context,\n        uri: String,\n    ): Boolean {\n        val intent = Intent(Intent.ACTION_VIEW, uri.toUri())\n        return try {\n            context.startActivityCompat(intent)\n            true\n        } catch (_: ActivityNotFoundException) {\n            false\n        }\n    }\n}\n"
  },
  {
    "path": "framework/src/androidMain/kotlin/com/zhangke/framework/utils/SystemUtils.kt",
    "content": "package com.zhangke.framework.utils\n\nimport android.content.ClipData\nimport android.content.ClipboardManager\nimport android.content.Context\n\nobject SystemUtils {\n\n    fun getAppVersionName(context: Context): String {\n        val pInfo = context.packageManager.getPackageInfo(context.packageName, 0)\n        return pInfo.versionName.orEmpty()\n    }\n\n    fun getAppVersionCode(context: Context): String {\n        val pInfo = context.packageManager.getPackageInfo(context.packageName, 0)\n        return pInfo.versionCode.toString()\n    }\n\n    fun copyText(context: Context, text: String){\n        val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager\n        val clip = ClipData.newPlainText(\"text\", text)\n        clipboard.setPrimaryClip(clip)\n    }\n}\n"
  },
  {
    "path": "framework/src/androidMain/kotlin/com/zhangke/framework/utils/UriUtils.android.kt",
    "content": "package com.zhangke.framework.utils\n\nimport android.content.ContentResolver\nimport android.content.Context\nimport android.database.Cursor\nimport android.graphics.Bitmap\nimport android.media.ThumbnailUtils\nimport android.net.Uri\nimport android.provider.MediaStore\nimport android.provider.OpenableColumns\nimport android.webkit.MimeTypeMap\nimport androidx.core.database.getLongOrNull\nimport androidx.core.database.getStringOrNull\nimport java.io.FileNotFoundException\n\nfun Uri.toContentProviderFile(context: Context): ContentProviderFile? {\n    val contentResolver = context.contentResolver\n    if (scheme.equals(ContentResolver.SCHEME_CONTENT)) {\n        contentResolver?.queryNameAndSize(this)\n            ?.use { cursor ->\n                val size = cursor.getSize() ?: StorageSize(0L)\n                val name = cursor.getDisplayName().orEmpty()\n                return ContentProviderFile(\n                    uri = this.toPlatformUri(),\n                    fileName = name,\n                    size = size,\n                    mimeType = contentResolver.getType(this).orEmpty(),\n                    streamProvider = {\n                        contentResolver.openInputStream(this)?.use {\n                            it.readBytes()\n                        }\n                    }\n                )\n            }\n    }\n    return try {\n        contentResolver.openAssetFileDescriptor(this, \"r\")\n            ?.use { descriptor ->\n                val size = StorageSize(descriptor.length)\n                val fileName = getAssetFileNameFromUri(this).orEmpty()\n                val extension = getExtensionFromFileName(fileName)\n                ContentProviderFile(\n                    uri = this.toPlatformUri(),\n                    fileName = fileName,\n                    size = size,\n                    mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)\n                        .orEmpty(),\n                    streamProvider = {\n                        descriptor.createInputStream()?.use {\n                            it.readBytes()\n                        }\n                    }\n                )\n            }\n    } catch (_: FileNotFoundException) {\n        null\n    }\n}\n\nprivate const val ASSET_PATH = \"android_asset\"\n\nprivate fun getAssetFileNameFromUri(uri: Uri): String? {\n    val uriPath = uri.path ?: return null\n    if (uri.scheme != \"file\" || !uriPath.contains(ASSET_PATH)) return null\n    return uriPath.substring(uriPath.indexOf(ASSET_PATH) + ASSET_PATH.length + 1)\n}\n\nprivate fun getExtensionFromFileName(fileName: String): String? {\n    val array = fileName.split(\".\")\n    if (array.size < 2) return null\n    return array.last()\n}\n\nprivate fun ContentResolver.queryNameAndSize(uri: Uri): Cursor? {\n    return query(uri, arrayOf(OpenableColumns.SIZE, OpenableColumns.DISPLAY_NAME), null, null, null)\n}\n\nprivate fun Cursor.getSize(): StorageSize? {\n    val sizeIndex = getColumnIndex(OpenableColumns.SIZE)\n    if (sizeIndex != -1) {\n        moveToFirst()\n        try {\n            return getLongOrNull(sizeIndex)?.let(::StorageSize)\n        } catch (_: Throwable) {\n            // ignore\n        }\n    }\n    return null\n}\n\nprivate fun Cursor.getDisplayName(): String? {\n    val nameIndex = getColumnIndex(OpenableColumns.DISPLAY_NAME)\n    if (nameIndex != -1) {\n        moveToFirst()\n        try {\n            return getStringOrNull(nameIndex)\n        } catch (_: Throwable) {\n            // ignore\n        }\n    }\n    return null\n}\n\nfun Uri.getThumbnail(context: Context): Bitmap? {\n    val filePathColumn = arrayOf(MediaStore.Images.Media.DATA)\n    try {\n        context.contentResolver\n            .query(this, filePathColumn, null, null, null)\n            ?.use { cursor ->\n                cursor.moveToFirst()\n                val columnIndex = cursor.getColumnIndex(filePathColumn[0])\n                val picturePath = cursor.getString(columnIndex)\n                return ThumbnailUtils.createVideoThumbnail(\n                    picturePath,\n                    MediaStore.Video.Thumbnails.MICRO_KIND\n                )\n            }\n    } catch (_: Throwable) {\n    }\n    return null\n}\n"
  },
  {
    "path": "framework/src/androidMain/kotlin/com/zhangke/framework/utils/VideoUtils.android.kt",
    "content": "package com.zhangke.framework.utils\n\nimport android.media.MediaMetadataRetriever\nimport androidx.core.net.toUri\n\nactual class VideoUtils {\n\n    actual fun getVideoAspect(uri: String): AspectRatio? {\n        val retriever = MediaMetadataRetriever()\n        retriever.setDataSource(appContext, uri.toUri())\n        val width = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)\n            ?.toIntOrNull()\n        val height = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)\n            ?.toIntOrNull()\n        retriever.release()\n        if (width == null || height == null) return null\n        return AspectRatio(\n            width.toLong(),\n            height.toLong()\n        )\n    }\n}\n"
  },
  {
    "path": "framework/src/androidUnitTest/kotlin/com/zhangke/framework/ExampleUnitTest.kt",
    "content": "package com.zhangke.framework\n\nimport org.junit.Test\n\nimport org.junit.Assert.*\n\n/**\n * Example local unit test, which will execute on the development machine (host).\n *\n * See [testing documentation](http://d.android.com/tools/testing).\n */\nclass ExampleUnitTest {\n    @Test\n    fun addition_isCorrect() {\n        assertEquals(4, 2 + 2)\n    }\n}"
  },
  {
    "path": "framework/src/androidUnitTest/kotlin/com/zhangke/framework/RegexFactoryTest.kt",
    "content": "package com.zhangke.framework\n\nimport org.junit.Test\n\nclass RegexFactoryTest {\n\n}"
  },
  {
    "path": "framework/src/androidUnitTest/kotlin/com/zhangke/framework/date/DateParserTest.kt",
    "content": "package com.zhangke.framework.date\n\nimport kotlinx.datetime.LocalDateTime\nimport kotlinx.datetime.TimeZone\nimport kotlinx.datetime.UtcOffset\nimport kotlinx.datetime.toInstant\nimport kotlinx.datetime.toJavaInstant\nimport org.junit.Assert.assertEquals\nimport org.junit.Test\n\nclass DateParserTest {\n\n    @Test\n    fun parseISODate() {\n        assertEquals(\n            LocalDateTime(2021, 9, 1, 12, 0, 0)\n                .toInstant(TimeZone.UTC)\n                .toJavaInstant(),\n            DateParser.parseISODate(\"2021-09-01T12:00:00Z\"),\n        )\n    }\n\n    @Test\n    fun parseRfc822Date() {\n        assertEquals(\n            LocalDateTime(2021, 9, 1, 12, 0, 0)\n                .toInstant(TimeZone.UTC)\n                .toJavaInstant(),\n            DateParser.parseRfc822Date(\"Wed, 01 Sep 2021 12:00:00 GMT\"),\n        )\n    }\n\n    @Test\n    fun parseRfc3339Date() {\n        assertEquals(\n            LocalDateTime(2020, 7, 31, 9, 16, 15)\n                .toInstant(UtcOffset(2))\n                .toJavaInstant(),\n            DateParser.parseRfc3339Date(\"2020-07-31T09:16:15+02:00\"),\n        )\n    }\n\n    @Test\n    fun parseISO8601() {\n        assertEquals(\n            LocalDateTime(2021, 9, 1, 12, 0, 0)\n                .toInstant(TimeZone.UTC)\n                .toJavaInstant(),\n            DateParser.parseISO8601(\"2021-09-01T12:00:00Z\"),\n        )\n    }\n}"
  },
  {
    "path": "framework/src/androidUnitTest/kotlin/com/zhangke/framework/feeds/fetcher/FeedsFetcherTest.kt",
    "content": "package com.zhangke.framework.feeds.fetcher\n\nclass FeedsFetcherTest {\n\n\n}"
  },
  {
    "path": "framework/src/androidUnitTest/kotlin/com/zhangke/framework/feeds/fetcher/FeedsGeneratorTest.kt",
    "content": "package com.zhangke.framework.feeds.fetcher\n\nimport kotlinx.coroutines.runBlocking\nimport org.junit.Assert\nimport org.junit.Test\n\nclass FeedsGeneratorTest {\n\n    @Test\n    fun normalCase() = runBlocking {\n    }\n}\n"
  },
  {
    "path": "framework/src/androidUnitTest/kotlin/com/zhangke/framework/utils/RegexFactoryTest.kt",
    "content": "package com.zhangke.framework.utils\n\nimport org.junit.Test\n\nclass RegexFactoryTest {\n\n    @Test\n    fun test(){\n        val find = RegexFactory.domainRegex.find(\"https://m3.material.io\")\n        println(find?.value)\n    }\n}"
  },
  {
    "path": "framework/src/androidUnitTest/kotlin/com/zhangke/framework/utils/StorageSizeTest.kt",
    "content": "package com.zhangke.framework.utils\n\nimport org.junit.Test\n\nclass StorageSizeTest {\n\n    @Test\n    fun test() {\n        val bytes = 1L * 1024 * 1024 * 1024\n        val storageSize = StorageSize(bytes)\n        println(storageSize.bytes)\n        println(storageSize.KB)\n        println(storageSize.MB)\n        println(storageSize.GB)\n    }\n}"
  },
  {
    "path": "framework/src/androidUnitTest/kotlin/com/zhangke/framework/utils/WebFingerTest.kt",
    "content": "package com.zhangke.framework.utils\n\nimport com.zhangke.framework.network.FormalBaseUrl\nimport org.junit.Assert\nimport org.junit.Test\n\n/**\n * Supported:\n * - jw@jakewharton.com\n * - @jw@jakewharton.com\n * - acct:@jw@jakewharton.com\n * - https://m.cmx.im/@jw@jakewharton.com\n * - https://m.cmx.im/@AtomZ\n * - https://m.cmx.im/xxx/AtomZ\n * - m.cmx.im/@jw@jakewharton.com\n * - jakewharton.com/@jw\n */\ninternal class WebFingerTest {\n\n    @Test\n    fun `should return null when content is empty`() {\n        assert(WebFinger.create(\"\") == null)\n    }\n\n    @Test\n    fun `should return null when content is not a web finger`() {\n        assert(WebFinger.create(\"Supported\") == null)\n    }\n\n    @Test\n    fun `should return WebFinger when acct and base url is present`() {\n        val baseUrl = FormalBaseUrl.parse(\"https://m.cmx.im\")!!\n        val webFinger = WebFinger.create(\"@jw@jakewharton.com\", baseUrl)\n        assert(webFinger!!.name == \"jw\")\n        assert(webFinger.host == \"jakewharton.com\")\n    }\n\n    @Test\n    fun `should return WebFinger when acct without @ and base url is present`() {\n        val baseUrl = FormalBaseUrl.parse(\"https://m.cmx.im\")!!\n        val webFinger = WebFinger.create(\"jw@jakewharton.com\", baseUrl)\n        assert(webFinger!!.name == \"jw\")\n        assert(webFinger.host == \"jakewharton.com\")\n    }\n\n    @Test\n    fun `should return WebFinger when acct just a name and base url is present`() {\n        val baseUrl = FormalBaseUrl.parse(\"https://m.cmx.im\")!!\n        val webFinger = WebFinger.create(\"@jw\", baseUrl)\n        assert(webFinger!!.name == \"jw\")\n        assert(webFinger.host == \"m.cmx.im\")\n    }\n\n    @Test\n    fun `should return WebFinger when acct just a name without @ and base url is present`() {\n        val baseUrl = FormalBaseUrl.parse(\"https://m.cmx.im\")!!\n        val webFinger = WebFinger.create(\"jw\", baseUrl)\n        assert(webFinger!!.name == \"jw\")\n        assert(webFinger.host == \"m.cmx.im\")\n    }\n\n    @Test\n    fun `should return WebFinger when content prefix not @`() {\n        val webFinger = WebFinger.create(\"jw@jakewharton.com\")\n        assert(webFinger!!.name == \"jw\")\n        assert(webFinger.host == \"jakewharton.com\")\n    }\n\n    @Test\n    fun `should return WebFinger when content is standard`() {\n        val webFinger = WebFinger.create(\"@jw@jakewharton.com\")\n        assert(webFinger!!.name == \"jw\")\n        assert(webFinger.host == \"jakewharton.com\")\n    }\n\n    @Test\n    fun `should return null when content is have two @ prefix`() {\n        assert(WebFinger.create(\"@@jw@jakewharton.com\") == null)\n    }\n\n    @Test\n    fun `should return null when content is have two @ prefix and two @@ on domain`() {\n        assert(WebFinger.create(\"@@jw@@jakewharton.com\") == null)\n    }\n\n    @Test\n    fun `should return null when content is domain`() {\n        assert(WebFinger.create(\"jakewharton.com\") == null)\n    }\n\n    @Test\n    fun `should return WebFinger when content have acct`() {\n        assert(WebFinger.create(\"acct:@jw@jakewharton.com\") != null)\n    }\n\n    @Test\n    fun `should return WebFinger when content is url`() {\n        // https://m.cmx.im/@jw@jakewharton.com\n        val webFinger = WebFinger.create(\"https://m.cmx.im/@jw@jakewharton.com\")\n        assert(webFinger != null)\n        assert(webFinger!!.name == \"jw\")\n        assert(webFinger.host == \"jakewharton.com\")\n    }\n\n    @Test\n    fun `should return WebFinger when url path no domain`() {\n        val webFinger = WebFinger.create(\"https://m.cmx.im/@AtomZ\")\n        assert(webFinger != null)\n        assert(webFinger!!.name == \"AtomZ\")\n        assert(webFinger.host == \"m.cmx.im\")\n    }\n\n    @Test\n    fun `should return WebFinger when url path no protocol`() {\n        val webFinger = WebFinger.create(\"m.cmx.im/@jw@jakewharton.com\")\n        assert(webFinger != null)\n        assert(webFinger!!.name == \"jw\")\n        assert(webFinger.host == \"jakewharton.com\")\n    }\n\n    @Test\n    fun `should return WebFinger when url path no protocol no domain`() {\n        val webFinger = WebFinger.create(\"jakewharton.com/@jw\")\n        assert(webFinger != null)\n        assert(webFinger!!.name == \"jw\")\n        assert(webFinger.host == \"jakewharton.com\")\n    }\n\n    @Test\n    fun `should return WebFinger when url container - symbol`() {\n        val webFinger = WebFinger.create(\"https://ramen-fsm.eu.org/@midX\")!!\n        Assert.assertEquals(\"@midX@ramen-fsm.eu.org\", webFinger.toString())\n    }\n}"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/sd/lib/compose/wheel_picker/WheelPicker.kt",
    "content": "package com.sd.lib.compose.wheel_picker\n\nimport androidx.compose.animation.core.DecayAnimationSpec\nimport androidx.compose.animation.core.calculateTargetValue\nimport androidx.compose.animation.core.exponentialDecay\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.heightIn\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.layout.widthIn\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.LazyListScope\nimport androidx.compose.foundation.lazy.LazyRow\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.input.nestedscroll.NestedScrollConnection\nimport androidx.compose.ui.input.nestedscroll.NestedScrollSource\nimport androidx.compose.ui.input.nestedscroll.nestedScroll\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.Velocity\nimport androidx.compose.ui.unit.dp\nimport com.zhangke.framework.utils.Log\n\ninterface FWheelPickerContentScope {\n    val state: FWheelPickerState\n}\n\ninterface FWheelPickerDisplayScope : FWheelPickerContentScope {\n    @Composable\n    fun Content(index: Int)\n}\n\n@Composable\nfun FVerticalWheelPicker(\n    modifier: Modifier = Modifier,\n    count: Int,\n    state: FWheelPickerState = rememberFWheelPickerState(),\n    key: ((index: Int) -> Any)? = null,\n    itemHeight: Dp = 35.dp,\n    unfocusedCount: Int = 1,\n    userScrollEnabled: Boolean = true,\n    reverseLayout: Boolean = false,\n    debug: Boolean = false,\n    focus: @Composable () -> Unit = { FWheelPickerFocusVertical() },\n    display: @Composable FWheelPickerDisplayScope.(index: Int) -> Unit = { DefaultWheelPickerDisplay(it) },\n    content: @Composable FWheelPickerContentScope.(index: Int) -> Unit,\n) {\n    WheelPicker(\n        modifier = modifier,\n        isVertical = true,\n        count = count,\n        state = state,\n        key = key,\n        itemSize = itemHeight,\n        unfocusedCount = unfocusedCount,\n        userScrollEnabled = userScrollEnabled,\n        reverseLayout = reverseLayout,\n        debug = debug,\n        focus = focus,\n        display = display,\n        content = content,\n    )\n}\n\n@Composable\nfun FHorizontalWheelPicker(\n    modifier: Modifier = Modifier,\n    count: Int,\n    state: FWheelPickerState = rememberFWheelPickerState(),\n    key: ((index: Int) -> Any)? = null,\n    itemWidth: Dp = 35.dp,\n    unfocusedCount: Int = 1,\n    userScrollEnabled: Boolean = true,\n    reverseLayout: Boolean = false,\n    debug: Boolean = false,\n    focus: @Composable () -> Unit = { FWheelPickerFocusHorizontal() },\n    display: @Composable FWheelPickerDisplayScope.(index: Int) -> Unit = { DefaultWheelPickerDisplay(it) },\n    content: @Composable FWheelPickerContentScope.(index: Int) -> Unit,\n) {\n    WheelPicker(\n        modifier = modifier,\n        isVertical = false,\n        count = count,\n        state = state,\n        key = key,\n        itemSize = itemWidth,\n        unfocusedCount = unfocusedCount,\n        userScrollEnabled = userScrollEnabled,\n        reverseLayout = reverseLayout,\n        debug = debug,\n        focus = focus,\n        display = display,\n        content = content,\n    )\n}\n\n@Composable\nprivate fun WheelPicker(\n    modifier: Modifier,\n    isVertical: Boolean,\n    count: Int,\n    state: FWheelPickerState,\n    key: ((index: Int) -> Any)?,\n    itemSize: Dp,\n    unfocusedCount: Int,\n    userScrollEnabled: Boolean,\n    reverseLayout: Boolean,\n    debug: Boolean,\n    focus: @Composable () -> Unit,\n    display: @Composable FWheelPickerDisplayScope.(index: Int) -> Unit,\n    content: @Composable FWheelPickerContentScope.(index: Int) -> Unit,\n) {\n    require(count >= 0) { \"require count >= 0\" }\n    require(unfocusedCount >= 0) { \"require unfocusedCount >= 0\" }\n\n    state.debug = debug\n    LaunchedEffect(state, count) {\n        state.updateCount(count)\n    }\n\n    val nestedScrollConnection = remember(state) {\n        WheelPickerNestedScrollConnection(state)\n    }.apply {\n        this.isVertical = isVertical\n        this.itemSizePx = with(LocalDensity.current) { itemSize.roundToPx() }\n        this.reverseLayout = reverseLayout\n    }\n\n    val totalSize = remember(itemSize, unfocusedCount) {\n        itemSize * (unfocusedCount * 2 + 1)\n    }\n\n    val displayScope = remember(state) {\n        FWheelPickerDisplayScopeImpl(state)\n    }.apply {\n        this.content = content\n    }\n\n    Box(\n        modifier = modifier\n            .nestedScroll(nestedScrollConnection)\n            .run {\n                if (totalSize > 0.dp) {\n                    if (isVertical) {\n                        height(totalSize).widthIn(40.dp)\n                    } else {\n                        width(totalSize).heightIn(40.dp)\n                    }\n                } else {\n                    this\n                }\n            },\n        contentAlignment = Alignment.Center,\n    ) {\n\n        val lazyListScope: LazyListScope.() -> Unit = {\n\n            repeat(unfocusedCount) {\n                item(contentType = \"placeholder\") {\n                    ItemSizeBox(\n                        isVertical = isVertical,\n                        itemSize = itemSize,\n                    )\n                }\n            }\n\n            items(\n                count = count,\n                key = key,\n            ) { index ->\n                ItemSizeBox(\n                    isVertical = isVertical,\n                    itemSize = itemSize,\n                ) {\n                    displayScope.display(index)\n                }\n            }\n\n            repeat(unfocusedCount) {\n                item(contentType = \"placeholder\") {\n                    ItemSizeBox(\n                        isVertical = isVertical,\n                        itemSize = itemSize,\n                    )\n                }\n            }\n        }\n\n        if (isVertical) {\n            LazyColumn(\n                state = state.lazyListState,\n                horizontalAlignment = Alignment.CenterHorizontally,\n                reverseLayout = reverseLayout,\n                userScrollEnabled = userScrollEnabled,\n                modifier = Modifier.matchParentSize(),\n                content = lazyListScope,\n            )\n        } else {\n            LazyRow(\n                state = state.lazyListState,\n                verticalAlignment = Alignment.CenterVertically,\n                reverseLayout = reverseLayout,\n                userScrollEnabled = userScrollEnabled,\n                modifier = Modifier.matchParentSize(),\n                content = lazyListScope,\n            )\n        }\n\n        ItemSizeBox(\n            modifier = Modifier.align(Alignment.Center),\n            isVertical = isVertical,\n            itemSize = itemSize,\n        ) {\n            focus()\n        }\n    }\n}\n\n@Composable\nprivate fun ItemSizeBox(\n    modifier: Modifier = Modifier,\n    isVertical: Boolean,\n    itemSize: Dp,\n    content: @Composable () -> Unit = { },\n) {\n    Box(\n        modifier\n            .run {\n                if (isVertical) {\n                    height(itemSize)\n                } else {\n                    width(itemSize)\n                }\n            },\n        contentAlignment = Alignment.Center,\n    ) {\n        content()\n    }\n}\n\nprivate class WheelPickerNestedScrollConnection(\n    private val state: FWheelPickerState,\n) : NestedScrollConnection {\n    var isVertical: Boolean = true\n    var itemSizePx: Int = 0\n    var reverseLayout: Boolean = false\n\n    override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset {\n        state.synchronizeCurrentIndexSnapshot()\n        return super.onPostScroll(consumed, available, source)\n    }\n\n    override suspend fun onPreFling(available: Velocity): Velocity {\n        val currentIndex = state.synchronizeCurrentIndexSnapshot()\n        return if (currentIndex >= 0) {\n            available.flingItemCount(\n                isVertical = isVertical,\n                itemSize = itemSizePx,\n                decay = exponentialDecay(2f),\n                reverseLayout = reverseLayout,\n            ).let { flingItemCount ->\n                if (flingItemCount == 0) {\n                    state.animateScrollToIndex(currentIndex)\n                } else {\n                    state.animateScrollToIndex(currentIndex - flingItemCount)\n                }\n            }\n            available\n        } else {\n            super.onPreFling(available)\n        }\n    }\n}\n\nprivate fun Velocity.flingItemCount(\n    isVertical: Boolean,\n    itemSize: Int,\n    decay: DecayAnimationSpec<Float>,\n    reverseLayout: Boolean,\n): Int {\n    if (itemSize <= 0) return 0\n    val velocity = if (isVertical) y else x\n    val targetValue = decay.calculateTargetValue(0f, velocity)\n    val flingItemCount = (targetValue / itemSize).toInt()\n    return if (reverseLayout) -flingItemCount else flingItemCount\n}\n\nprivate class FWheelPickerDisplayScopeImpl(\n    override val state: FWheelPickerState,\n) : FWheelPickerDisplayScope {\n\n    var content: @Composable FWheelPickerContentScope.(index: Int) -> Unit by mutableStateOf({})\n\n    @Composable\n    override fun Content(index: Int) {\n        content(index)\n    }\n}\n\ninternal inline fun logMsg(debug: Boolean, noinline block: () -> String) {\n    if (debug) {\n        Log.i(\"FWheelPicker\", message = block)\n    }\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/sd/lib/compose/wheel_picker/WheelPickerDefault.kt",
    "content": "package com.sd.lib.compose.wheel_picker\n\n\nimport androidx.compose.animation.core.animateFloatAsState\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.isSystemInDarkTheme\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.graphicsLayer\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\n\n/**\n * The default implementation of focus view in vertical.\n */\n@Composable\nfun FWheelPickerFocusVertical(\n    modifier: Modifier = Modifier,\n    dividerSize: Dp = 1.dp,\n    dividerColor: Color = DefaultDividerColor,\n) {\n    Box(\n        modifier = modifier.fillMaxSize()\n    ) {\n        Box(\n            modifier = Modifier\n                .background(dividerColor)\n                .height(dividerSize)\n                .fillMaxWidth()\n                .align(Alignment.TopCenter),\n        )\n        Box(\n            modifier = Modifier\n                .background(dividerColor)\n                .height(dividerSize)\n                .fillMaxWidth()\n                .align(Alignment.BottomCenter),\n        )\n    }\n}\n\n/**\n * The default implementation of focus view in horizontal.\n */\n@Composable\nfun FWheelPickerFocusHorizontal(\n    modifier: Modifier = Modifier,\n    dividerSize: Dp = 1.dp,\n    dividerColor: Color = DefaultDividerColor,\n) {\n    Box(\n        modifier = modifier.fillMaxSize()\n    ) {\n        Box(\n            modifier = Modifier\n                .background(dividerColor)\n                .width(dividerSize)\n                .fillMaxHeight()\n                .align(Alignment.CenterStart),\n        )\n        Box(\n            modifier = Modifier\n                .background(dividerColor)\n                .width(dividerSize)\n                .fillMaxHeight()\n                .align(Alignment.CenterEnd),\n        )\n    }\n}\n\n/**\n * Default divider color.\n */\nprivate val DefaultDividerColor: Color\n    @Composable\n    get() {\n        val color = if (isSystemInDarkTheme()) Color.White else Color.Black\n        return color.copy(alpha = 0.2f)\n    }\n\n/**\n * Default display.\n */\n@Composable\nfun FWheelPickerDisplayScope.DefaultWheelPickerDisplay(\n    index: Int,\n) {\n    val focused = index == state.currentIndexSnapshot\n    val targetScale = if (focused) 1.0f else 0.8f\n    val animateScale by animateFloatAsState(targetScale, label = \"\")\n    Box(\n        modifier = Modifier.graphicsLayer {\n            this.alpha = if (focused) 1.0f else 0.3f\n            this.scaleX = animateScale\n            this.scaleY = animateScale\n        }\n    ) {\n        Content(index)\n    }\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/sd/lib/compose/wheel_picker/WheelPickerState.kt",
    "content": "package com.sd.lib.compose.wheel_picker\n\n\nimport androidx.compose.foundation.interaction.InteractionSource\nimport androidx.compose.foundation.lazy.LazyListItemInfo\nimport androidx.compose.foundation.lazy.LazyListState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.saveable.Saver\nimport androidx.compose.runtime.saveable.listSaver\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport kotlinx.coroutines.suspendCancellableCoroutine\nimport kotlin.coroutines.Continuation\nimport kotlin.coroutines.resume\nimport kotlin.math.absoluteValue\n\n@Composable\nfun rememberFWheelPickerState(\n    initialIndex: Int = 0\n): FWheelPickerState {\n    return rememberSaveable(saver = FWheelPickerState.Saver) {\n        FWheelPickerState(\n            initialIndex = initialIndex,\n        )\n    }\n}\n\nclass FWheelPickerState(\n    initialIndex: Int = 0,\n) {\n    internal var debug = false\n    internal val lazyListState = LazyListState()\n\n    private var _count = 0\n    private var _currentIndex by mutableIntStateOf(-1)\n    private var _currentIndexSnapshot by mutableIntStateOf(-1)\n\n    private var _pendingIndex: Int? = initialIndex.coerceAtLeast(0)\n        set(value) {\n            logMsg(debug) { \"pendingIndex:$value\" }\n            field = value?.also {\n                check(it >= 0)\n                check(_count == 0)\n            }\n        }\n    private var _pendingIndexContinuation: Continuation<Unit>? = null\n\n    /**\n     * Index of picker when it is idle, -1 means that there is no data.\n     *\n     * Note that this property is observable and if you use it in the composable function\n     * it will be recomposed on every change.\n     */\n    val currentIndex: Int get() = _currentIndex\n\n    /**\n     * Index of picker when it is idle or drag but not fling, -1 means that there is no data.\n     *\n     * Note that this property is observable and if you use it in the composable function\n     * it will be recomposed on every change.\n     */\n    val currentIndexSnapshot: Int get() = _currentIndexSnapshot\n\n    /**\n     * [LazyListState.interactionSource]\n     */\n    val interactionSource: InteractionSource get() = lazyListState.interactionSource\n\n    /**\n     * [LazyListState.isScrollInProgress]\n     */\n    val isScrollInProgress: Boolean get() = lazyListState.isScrollInProgress\n\n    suspend fun animateScrollToIndex(index: Int) {\n        logMsg(debug) { \"animateScrollToIndex index:$index count:$_count\" }\n        @Suppress(\"NAME_SHADOWING\")\n        val index = index.coerceAtLeast(0)\n\n        lazyListState.animateScrollToItem(index)\n        synchronizeCurrentIndex()\n    }\n\n    suspend fun scrollToIndex(index: Int, pending: Boolean = true) {\n        logMsg(debug) { \"scrollToIndex index:$index pending:$pending count:$_count\" }\n        @Suppress(\"NAME_SHADOWING\")\n        val index = index.coerceAtLeast(0)\n\n        lazyListState.scrollToItem(index)\n        synchronizeCurrentIndex()\n\n        if (pending) {\n            awaitIndex(index)\n        }\n    }\n\n    private suspend fun awaitIndex(index: Int) {\n        if (index < 0) return\n        if (_count > 0) return\n        if (_currentIndex == index) return\n\n        logMsg(debug) { \"awaitIndex:$index start\" }\n\n        // Resume last continuation before suspend.\n        resumeAwaitIndex()\n\n        suspendCancellableCoroutine { cont ->\n            _pendingIndex = index\n            _pendingIndexContinuation = cont\n            cont.invokeOnCancellation {\n                logMsg(debug) { \"awaitIndex:$index canceled\" }\n                _pendingIndex = null\n                _pendingIndexContinuation = null\n            }\n        }\n\n        logMsg(debug) { \"awaitIndex:$index finish\" }\n    }\n\n    private fun resumeAwaitIndex() {\n        _pendingIndexContinuation?.let {\n            logMsg(debug) { \"resumeAwaitIndex pendingIndex:$_pendingIndex\" }\n            _pendingIndexContinuation = null\n            it.resume(Unit)\n        }\n    }\n\n    internal suspend fun updateCount(count: Int) {\n        logMsg(debug) { \"updateCount count:$count currentIndex:$_currentIndex\" }\n\n        _count = count\n\n        val maxIndex = count - 1\n        if (maxIndex < _currentIndex) {\n            scrollToIndex(maxIndex, pending = false)\n        }\n\n        if (count > 0) {\n            _pendingIndex?.let { pendingIndex ->\n                logMsg(debug) { \"found pendingIndex:$pendingIndex\" }\n                scrollToIndex(pendingIndex, pending = false)\n                _pendingIndex = null\n                resumeAwaitIndex()\n            }\n            if (_currentIndex < 0) {\n                synchronizeCurrentIndex()\n            }\n        }\n    }\n\n    private fun synchronizeCurrentIndex() {\n        val index = synchronizeCurrentIndexSnapshot().coerceAtLeast(-1)\n        if (_currentIndex != index) {\n            logMsg(debug) { \"setCurrentIndex:$index\" }\n            _currentIndex = index\n            _currentIndexSnapshot = index\n        }\n    }\n\n    internal fun synchronizeCurrentIndexSnapshot(): Int {\n        return (mostStartItemInfo()?.index ?: -1).also {\n            _currentIndexSnapshot = it\n        }\n    }\n\n    /**\n     * The item closest to the viewport start.\n     */\n    private fun mostStartItemInfo(): LazyListItemInfo? {\n        if (_count <= 0) return null\n\n        val layoutInfo = lazyListState.layoutInfo\n        val listInfo = layoutInfo.visibleItemsInfo\n\n        if (listInfo.isEmpty()) return null\n        if (listInfo.size == 1) return listInfo.first()\n\n        val firstItem = listInfo.first()\n        val firstOffsetDelta = (firstItem.offset - layoutInfo.viewportStartOffset).absoluteValue\n        return if (firstOffsetDelta < firstItem.size / 2) {\n            firstItem\n        } else {\n            listInfo[1]\n        }\n    }\n\n    companion object {\n        val Saver: Saver<FWheelPickerState, *> = listSaver(\n            save = {\n                listOf<Any>(\n                    it.currentIndex,\n                )\n            },\n            restore = {\n                FWheelPickerState(\n                    initialIndex = it[0] as Int,\n                )\n            }\n        )\n    }\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/architect/coroutines/ApplicationScope.kt",
    "content": "package com.zhangke.framework.architect.coroutines\n\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.SupervisorJob\n\nval ApplicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/architect/coroutines/Flows.kt",
    "content": "package com.zhangke.framework.architect.coroutines\n\nimport androidx.lifecycle.LifecycleOwner\nimport androidx.lifecycle.lifecycleScope\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.FlowCollector\nimport kotlinx.coroutines.launch\n\n/**\n * Created by ZhangKe on 2022/10/14.\n */\n\nfun <T> Flow<T>.collectWithLifecycle(lifecycle: LifecycleOwner, collector: FlowCollector<T>) {\n    lifecycle.lifecycleScope\n        .launch {\n            collect(collector)\n        }\n}"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/architect/http/HttpClientEngine.kt",
    "content": "package com.zhangke.framework.architect.http\n\nimport com.zhangke.framework.architect.json.globalJson\nimport io.ktor.client.HttpClient\nimport io.ktor.client.engine.HttpClientEngine\nimport io.ktor.client.plugins.DefaultRequest\nimport io.ktor.client.plugins.contentnegotiation.ContentNegotiation\nimport io.ktor.client.request.header\nimport io.ktor.http.ContentType\nimport io.ktor.http.HttpHeaders\nimport io.ktor.serialization.kotlinx.json.json\nimport kotlinx.serialization.json.Json\n\nval sharedHttpClient: HttpClient by lazy {\n    createHttpClient(globalJson, createHttpClientEngine())\n}\n\nexpect fun createHttpClientEngine(): HttpClientEngine\n\nprivate fun createHttpClient(\n    json: Json,\n    engine: HttpClientEngine,\n): HttpClient {\n    return HttpClient(engine) {\n        install(ContentNegotiation) {\n            json(json)\n        }\n        install(DefaultRequest) {\n            header(HttpHeaders.ContentType, ContentType.Application.Json)\n        }\n    }\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/architect/json/Json.kt",
    "content": "package com.zhangke.framework.architect.json\n\nimport com.zhangke.krouter.KRouter\nimport kotlinx.serialization.json.Json\nimport kotlinx.serialization.json.JsonElement\nimport kotlinx.serialization.json.JsonObject\nimport kotlinx.serialization.json.JsonPrimitive\nimport kotlinx.serialization.json.contentOrNull\nimport kotlinx.serialization.json.decodeFromJsonElement\nimport kotlinx.serialization.json.intOrNull\nimport kotlinx.serialization.json.longOrNull\nimport kotlinx.serialization.modules.SerializersModule\n\nval globalJson: Json by lazy {\n    Json {\n        ignoreUnknownKeys = true\n        // use default value if JSON value is null but the property type is non-nullable.\n        coerceInputValues = true\n\n        serializersModule = SerializersModule {\n            KRouter.getServices<JsonModuleBuilder>().forEach {\n                with(it) { buildSerializersModule() }\n            }\n        }\n    }\n}\n\ninline fun <reified T> Json.fromJson(jsonObject: JsonElement): T {\n    return decodeFromJsonElement(jsonObject)\n}\n\ninline fun <reified T> Json.fromJson(jsonString: String): T {\n    return decodeFromString(jsonString)\n}\n\ninline fun JsonObject.getStringOrNull(key: String): String? {\n    return this.getJsonPrimitiveOrNull(key)?.contentOrNull\n}\n\ninline fun JsonObject.getLongOrNull(key: String): Long? {\n    return this.getJsonPrimitiveOrNull(key)?.longOrNull\n}\n\ninline fun JsonObject.getIntOrNull(key: String): Int? {\n    return this.getJsonPrimitiveOrNull(key)?.intOrNull\n}\n\ninline fun JsonObject.getJsonPrimitiveOrNull(key: String): JsonPrimitive? {\n    return this[key]?.let { it as? JsonPrimitive }\n}\n\nval JsonObject.Companion.Empty get() = JsonObject(emptyMap())\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/architect/json/JsonModuleBuilder.kt",
    "content": "package com.zhangke.framework.architect.json\n\nimport kotlinx.serialization.modules.SerializersModuleBuilder\n\ninterface JsonModuleBuilder {\n\n    fun SerializersModuleBuilder.buildSerializersModule()\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/architect/theme/Color.kt",
    "content": "package com.zhangke.framework.architect.theme\n\nimport androidx.compose.material3.ColorScheme\nimport androidx.compose.ui.graphics.Color\n\nval primaryLight = Color(0xFF2E87FF)\nval onPrimaryLight = Color(0xFFFFFFFF)\nval primaryContainerLight = Color(0xFF689CFF)\nval onPrimaryContainerLight = Color(0xFFFFFFFF)\nval secondaryLight = Color(0xFF495E8A)\nval onSecondaryLight = Color(0xFFFFFFFF)\nval secondaryContainerLight = Color(0xFFC0D3FF)\nval onSecondaryContainerLight = Color(0xFF283E68)\nval tertiaryLight = Color(0xFFF9338E)\nval onTertiaryLight = Color(0xFFFFFFFF)\nval tertiaryContainerLight = Color(0xFFFF84AF)\nval onTertiaryContainerLight = Color(0xFFFFFFFF)\nval errorLight = Color(0xFFA0004C)\nval onErrorLight = Color(0xFFFFFFFF)\nval errorContainerLight = Color(0xFFE40070)\nval onErrorContainerLight = Color(0xFFFFFFFF)\nval backgroundLight = Color(0xFFF9F9FF)\nval onBackgroundLight = Color(0xFF191B22)\nval surfaceLight = Color(0xFFF9FBFF)\nval onSurfaceLight = Color(0xFF191B22)\nval surfaceVariantLight = Color(0xFFDEE2F2)\nval onSurfaceVariantLight = Color(0xE0424753)\nval outlineLight = Color(0xFF727785)\nval outlineVariantLight = Color(0xFFC2C6D6)\nval scrimLight = Color(0xFF000000)\nval inverseSurfaceLight = Color(0xFF2E3038)\nval inverseOnSurfaceLight = Color(0xFFEFF0FA)\nval inversePrimaryLight = Color(0xFFADC6FF)\nval surfaceDimLight = Color(0xFFD8D9E3)\nval surfaceBrightLight = Color(0xFFF9F9FF)\nval surfaceContainerLowestLight = Color(0xFFFFFFFF)\nval surfaceContainerLowLight = Color(0xFFF2F3FD)\nval surfaceContainerLight = Color(0xFFECEDF7)\nval surfaceContainerHighLight = Color(0xFFE6E8F1)\nval surfaceContainerHighestLight = Color(0xFFE1E2EC)\n\nval primaryDark = Color(0xFFADC6FF)\nval onPrimaryDark = Color(0xFF002E69)\nval primaryContainerDark = Color(0xFF2171E2)\nval onPrimaryContainerDark = Color(0xFFFFFFFF)\nval secondaryDark = Color(0xFFB1C6F8)\nval onSecondaryDark = Color(0xFF183059)\nval secondaryContainerDark = Color(0xFF2A4069)\nval onSecondaryContainerDark = Color(0xFFC4D5FF)\nval tertiaryDark = Color(0xFFFFB0C8)\nval onTertiaryDark = Color(0xFF650033)\nval tertiaryContainerDark = Color(0xFFDE167A)\nval onTertiaryContainerDark = Color(0xFFFFFFFF)\nval errorDark = Color(0xFFFFB1C4)\nval onErrorDark = Color(0xFF65002E)\nval errorContainerDark = Color(0xFFE3006F)\nval onErrorContainerDark = Color(0xFFFFFFFF)\nval backgroundDark = Color(0xFF10131A)\nval onBackgroundDark = Color(0xFFE1E2EC)\nval surfaceDark = Color(0xFF10131A)\nval onSurfaceDark = Color(0xFFE1E2EC)\nval surfaceVariantDark = Color(0xFF424753)\nval onSurfaceVariantDark = Color(0xFFC2C6D6)\nval outlineDark = Color(0xFF8C909F)\nval outlineVariantDark = Color(0xFF424753)\nval scrimDark = Color(0xFF000000)\nval inverseSurfaceDark = Color(0xFFE1E2EC)\nval inverseOnSurfaceDark = Color(0xFF2E3038)\nval inversePrimaryDark = Color(0xFF005AC1)\nval surfaceDimDark = Color(0xFF10131A)\nval surfaceBrightDark = Color(0xFF363941)\nval surfaceContainerLowestDark = Color(0xFF0B0E15)\nval surfaceContainerLowDark = Color(0xFF191B22)\nval surfaceContainerDark = Color(0xFF1D2027)\nval surfaceContainerHighDark = Color(0xFF272A31)\nval surfaceContainerHighestDark = Color(0xFF32353C)\n\nval ColorScheme.dialogScrim: Color get() = Color(0x66000000)\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/architect/theme/FreadTheme.kt",
    "content": "package com.zhangke.framework.architect.theme\n\nimport androidx.compose.foundation.isSystemInDarkTheme\nimport androidx.compose.material.Colors\nimport androidx.compose.material3.ColorScheme\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.darkColorScheme\nimport androidx.compose.material3.lightColorScheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.graphics.Color\n\nval freadLightScheme = lightColorScheme(\n    primary = primaryLight,\n    onPrimary = onPrimaryLight,\n    primaryContainer = primaryContainerLight,\n    onPrimaryContainer = onPrimaryContainerLight,\n    secondary = secondaryLight,\n    onSecondary = onSecondaryLight,\n    secondaryContainer = secondaryContainerLight,\n    onSecondaryContainer = onSecondaryContainerLight,\n    tertiary = tertiaryLight,\n    onTertiary = onTertiaryLight,\n    tertiaryContainer = tertiaryContainerLight,\n    onTertiaryContainer = onTertiaryContainerLight,\n    error = errorLight,\n    onError = onErrorLight,\n    errorContainer = errorContainerLight,\n    onErrorContainer = onErrorContainerLight,\n    background = backgroundLight,\n    onBackground = onBackgroundLight,\n    surface = surfaceLight,\n    onSurface = onSurfaceLight,\n    surfaceVariant = surfaceVariantLight,\n    onSurfaceVariant = onSurfaceVariantLight,\n    outline = outlineLight,\n    outlineVariant = outlineVariantLight,\n    scrim = scrimLight,\n    inverseSurface = inverseSurfaceLight,\n    inverseOnSurface = inverseOnSurfaceLight,\n    inversePrimary = inversePrimaryLight,\n    surfaceDim = surfaceDimLight,\n    surfaceBright = surfaceBrightLight,\n    surfaceContainerLowest = surfaceContainerLowestLight,\n    surfaceContainerLow = surfaceContainerLowLight,\n    surfaceContainer = surfaceContainerLight,\n    surfaceContainerHigh = surfaceContainerHighLight,\n    surfaceContainerHighest = surfaceContainerHighestLight,\n)\n\nval freadDarkScheme = darkColorScheme(\n    primary = primaryDark,\n    onPrimary = onPrimaryDark,\n    primaryContainer = primaryContainerDark,\n    onPrimaryContainer = onPrimaryContainerDark,\n    secondary = secondaryDark,\n    onSecondary = onSecondaryDark,\n    secondaryContainer = secondaryContainerDark,\n    onSecondaryContainer = onSecondaryContainerDark,\n    tertiary = tertiaryDark,\n    onTertiary = onTertiaryDark,\n    tertiaryContainer = tertiaryContainerDark,\n    onTertiaryContainer = onTertiaryContainerDark,\n    error = errorDark,\n    onError = onErrorDark,\n    errorContainer = errorContainerDark,\n    onErrorContainer = onErrorContainerDark,\n    background = backgroundDark,\n    onBackground = onBackgroundDark,\n    surface = surfaceDark,\n    onSurface = onSurfaceDark,\n    surfaceVariant = surfaceVariantDark,\n    onSurfaceVariant = onSurfaceVariantDark,\n    outline = outlineDark,\n    outlineVariant = outlineVariantDark,\n    scrim = scrimDark,\n    inverseSurface = inverseSurfaceDark,\n    inverseOnSurface = inverseOnSurfaceDark,\n    inversePrimary = inversePrimaryDark,\n    surfaceDim = surfaceDimDark,\n    surfaceBright = surfaceBrightDark,\n    surfaceContainerLowest = surfaceContainerLowestDark,\n    surfaceContainerLow = surfaceContainerLowDark,\n    surfaceContainer = surfaceContainerDark,\n    surfaceContainerHigh = surfaceContainerHighDark,\n    surfaceContainerHighest = surfaceContainerHighestDark,\n)\n\n@Composable\nfun FreadTheme(\n    darkTheme: Boolean = isSystemInDarkTheme(),\n    amoledMode: Boolean = false,\n    dynamicColors: ColorScheme? = null,\n    content: @Composable () -> Unit\n) {\n    val freadTheme = if (darkTheme) {\n        if (amoledMode) {\n            freadDarkScheme.copy(\n                background = Color.Black,\n                surface = Color.Black,\n                surfaceContainer = Color.Black,\n            )\n        } else {\n            freadDarkScheme\n        }\n    } else {\n        freadLightScheme\n    }\n    val colorScheme = dynamicColors?.copy(\n        tertiary = freadTheme.tertiary,\n        onTertiary = freadTheme.onTertiary,\n        tertiaryContainer = freadTheme.tertiaryContainer,\n        onTertiaryContainer = freadTheme.onTertiaryContainer,\n    ) ?: freadTheme\n\n    FreadPlatformTheme(darkTheme)\n\n    androidx.compose.material.MaterialTheme(\n        colors = colorScheme.toColors(!darkTheme),\n        content = {\n            MaterialTheme(\n                colorScheme = colorScheme,\n                content = content,\n            )\n        }\n    )\n}\n\n@Composable\ninternal expect fun FreadPlatformTheme(darkTheme: Boolean)\n\nprivate fun ColorScheme.toColors(isLight: Boolean): Colors = Colors(\n    primary = primary,\n    primaryVariant = secondary,\n    secondary = tertiary,\n    secondaryVariant = tertiary,\n    background = background,\n    surface = surface,\n    error = error,\n    onPrimary = onPrimary,\n    onSecondary = onSecondary,\n    onBackground = onBackground,\n    onSurface = onSurface,\n    onError = onError,\n    isLight = isLight,\n)\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/blur/BlurController.kt",
    "content": "package com.zhangke.framework.blur\n\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.DisposableEffect\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.Stable\nimport androidx.compose.runtime.compositionLocalOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport dev.chrisbanes.haze.HazeState\nimport dev.chrisbanes.haze.hazeEffect\nimport dev.chrisbanes.haze.hazeSource\nimport dev.chrisbanes.haze.materials.HazeMaterials\nimport dev.chrisbanes.haze.rememberHazeState\n\n@Stable\nclass BlurController private constructor(\n    initEnabled: Boolean = true,\n) {\n\n    var enabled by mutableStateOf(initEnabled)\n\n    var hazeState: HazeState? by mutableStateOf(null)\n\n    companion object {\n\n        fun create(): BlurController {\n            return BlurController(true)\n        }\n    }\n}\n\n@Composable\nfun rememberBlurController(): BlurController {\n    val controller = remember { BlurController.create() }\n    val enableBlurAppBarStyle = LocalEnableBlurAppBarStyle.current\n    LaunchedEffect(controller, enableBlurAppBarStyle) {\n        controller.enabled = enableBlurAppBarStyle\n    }\n    return controller\n}\n\nval LocalBlurController = compositionLocalOf<BlurController?> {\n    null\n}\n\n@Composable\nfun Modifier.applyBlurSource(enabled: Boolean = true): Modifier {\n    if (!enabled) return this\n    val controller = LocalBlurController.current\n    if (controller == null || !controller.enabled) return this\n    val state = rememberHazeState(blurEnabled = true)\n    DisposableEffect(enabled, controller.hazeState) {\n        controller.hazeState = state\n        onDispose {\n            if (controller.hazeState == state) {\n                controller.hazeState = null\n            }\n        }\n    }\n    return this.then(Modifier.hazeSource(state))\n}\n\n@Composable\nfun Modifier.applyBlurEffect(\n    enabled: Boolean = true,\n    containerColor: Color = MaterialTheme.colorScheme.surface,\n): Modifier {\n    if (!enabled) return this\n    val controller = LocalBlurController.current\n    if (controller == null || !controller.enabled) return this\n    val state = controller.hazeState ?: return this\n    return this.hazeEffect(\n        state = state,\n        style = HazeMaterials.ultraThick(containerColor)\n    )\n}\n\n@Composable\nfun blurEffectContainerColor(\n    enabled: Boolean = true,\n    containerColor: Color = MaterialTheme.colorScheme.surface,\n): Color {\n    val enableContainerColor = enableContainerColor(enabled)\n    return if (enableContainerColor) containerColor else Color.Transparent\n}\n\n@Composable\nprivate fun enableContainerColor(enabled: Boolean = true): Boolean {\n    if (!enabled) return true\n    val controller = LocalBlurController.current\n    if (controller == null || !controller.enabled) return true\n    controller.hazeState ?: return true\n    return false\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/blur/LocalEnableBlurAppBarStyle.kt",
    "content": "package com.zhangke.framework.blur\n\nimport androidx.compose.runtime.compositionLocalOf\n\nval LocalEnableBlurAppBarStyle = compositionLocalOf {\n    true\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/blurhash/BitmapExt.kt",
    "content": "package com.zhangke.framework.blurhash\n\nimport androidx.compose.ui.graphics.ImageBitmap\n\nexpect fun bitmapFromBuffer(buffer: IntArray, width: Int, height: Int): ImageBitmap\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/blurhash/BlurHashDecoder.kt",
    "content": "package com.zhangke.framework.blurhash\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.ImageBitmap\nimport androidx.compose.ui.graphics.toArgb\nimport kotlin.math.PI\nimport kotlin.math.cos\nimport kotlin.math.pow\nimport kotlin.math.withSign\n\n/**\n * Copy from [BlurHashDecoder](https://github.com/woltapp/blurhash/blob/master/Kotlin/lib/src/main/java/com/wolt/blurhashkt/BlurHashDecoder.kt)\n */\nobject BlurHashDecoder {\n\n    // cache Math.cos() calculations to improve performance.\n    // The number of calculations can be huge for many bitmaps: width * height * numCompX * numCompY * 2 * nBitmaps\n    // the cache is enabled by default, it is recommended to disable it only when just a few images are displayed\n    private val cacheCosinesX = mutableMapOf<Int, DoubleArray>()\n    private val cacheCosinesY = mutableMapOf<Int, DoubleArray>()\n\n    /**\n     * Clear calculations stored in memory cache.\n     * The cache is not big, but will increase when many image sizes are used,\n     * if the app needs memory it is recommended to clear it.\n     */\n    fun clearCache() {\n        cacheCosinesX.clear()\n        cacheCosinesY.clear()\n    }\n\n    /**\n     * Decode a blur hash into a new bitmap.\n     *\n     * @param useCache use in memory cache for the calculated math, reused by images with same size.\n     *                 if the cache does not exist yet it will be created and populated with new calculations.\n     *                 By default it is true.\n     */\n    fun decode(\n        blurHash: String,\n        width: Int,\n        height: Int,\n        punch: Float = 1f,\n        useCache: Boolean = true,\n    ): ImageBitmap? {\n        if (blurHash.length < 6) {\n            return null\n        }\n        if (width <= 0 || height <= 0) return null\n        val numCompEnc = decode83(blurHash, 0, 1)\n        val numCompX = (numCompEnc % 9) + 1\n        val numCompY = (numCompEnc / 9) + 1\n        if (blurHash.length != 4 + 2 * numCompX * numCompY) {\n            return null\n        }\n        val maxAcEnc = decode83(blurHash, 1, 2)\n        val maxAc = (maxAcEnc + 1) / 166f\n        val colors = Array(numCompX * numCompY) { i ->\n            if (i == 0) {\n                val colorEnc = decode83(blurHash, 2, 6)\n                decodeDc(colorEnc)\n            } else {\n                val from = 4 + i * 2\n                val colorEnc = decode83(blurHash, from, from + 2)\n                decodeAc(colorEnc, maxAc * punch)\n            }\n        }\n        return composeImageBitmap(width, height, numCompX, numCompY, colors, useCache)\n    }\n\n    private fun decode83(str: String, from: Int = 0, to: Int = str.length): Int {\n        var result = 0\n        for (i in from until to) {\n            val index = charMap[str[i]] ?: -1\n            if (index != -1) {\n                result = result * 83 + index\n            }\n        }\n        return result\n    }\n\n    private fun decodeDc(colorEnc: Int): FloatArray {\n        val r = colorEnc shr 16\n        val g = (colorEnc shr 8) and 255\n        val b = colorEnc and 255\n        return floatArrayOf(srgbToLinear(r), srgbToLinear(g), srgbToLinear(b))\n    }\n\n    private fun srgbToLinear(colorEnc: Int): Float {\n        val v = colorEnc / 255f\n        return if (v <= 0.04045f) {\n            (v / 12.92f)\n        } else {\n            ((v + 0.055f) / 1.055f).pow(2.4f)\n        }\n    }\n\n    private fun decodeAc(value: Int, maxAc: Float): FloatArray {\n        val r = value / (19 * 19)\n        val g = (value / 19) % 19\n        val b = value % 19\n        return floatArrayOf(\n            signedPow2((r - 9) / 9.0f) * maxAc,\n            signedPow2((g - 9) / 9.0f) * maxAc,\n            signedPow2((b - 9) / 9.0f) * maxAc\n        )\n    }\n\n    private fun signedPow2(value: Float) = value.pow(2f).withSign(value)\n\n    private fun composeImageBitmap(\n        width: Int, height: Int,\n        numCompX: Int, numCompY: Int,\n        colors: Array<FloatArray>,\n        useCache: Boolean,\n    ): ImageBitmap {\n        // use an array for better performance when writing pixel colors\n        val imageArray = IntArray(width * height)\n        val calculateCosX = !useCache || !cacheCosinesX.containsKey(width * numCompX)\n        val cosinesX = getArrayForCosinesX(calculateCosX, width, numCompX)\n        val calculateCosY = !useCache || !cacheCosinesY.containsKey(height * numCompY)\n        val cosinesY = getArrayForCosinesY(calculateCosY, height, numCompY)\n        for (y in 0 until height) {\n            for (x in 0 until width) {\n                var r = 0f\n                var g = 0f\n                var b = 0f\n                for (j in 0 until numCompY) {\n                    for (i in 0 until numCompX) {\n                        val cosX = cosinesX.getCos(calculateCosX, i, numCompX, x, width)\n                        val cosY = cosinesY.getCos(calculateCosY, j, numCompY, y, height)\n                        val basis = (cosX * cosY).toFloat()\n                        val color = colors[j * numCompX + i]\n                        r += color[0] * basis\n                        g += color[1] * basis\n                        b += color[2] * basis\n                    }\n                }\n                imageArray[x + width * y] = Color(\n                    red = linearToSrgb(r),\n                    green = linearToSrgb(g),\n                    blue = linearToSrgb(b)\n                ).toArgb()\n            }\n        }\n        return bitmapFromBuffer(imageArray, width, height)\n    }\n\n    private fun getArrayForCosinesY(calculate: Boolean, height: Int, numCompY: Int) = when {\n        calculate -> {\n            DoubleArray(height * numCompY).also {\n                cacheCosinesY.put(height * numCompY, it)\n            }\n        }\n\n        else -> {\n            cacheCosinesY[height * numCompY]!!\n        }\n    }\n\n    private fun getArrayForCosinesX(calculate: Boolean, width: Int, numCompX: Int) = when {\n        calculate -> {\n            DoubleArray(width * numCompX).also {\n                cacheCosinesX.put(width * numCompX, it)\n            }\n        }\n\n        else -> cacheCosinesX[width * numCompX]!!\n    }\n\n    private fun DoubleArray.getCos(\n        calculate: Boolean,\n        x: Int,\n        numComp: Int,\n        y: Int,\n        size: Int,\n    ): Double {\n        if (calculate) {\n            this[x + numComp * y] = cos(PI * y * x / size)\n        }\n        return this[x + numComp * y]\n    }\n\n    private fun linearToSrgb(value: Float): Int {\n        val v = value.coerceIn(0f, 1f)\n        return if (v <= 0.0031308f) {\n            (v * 12.92f * 255f + 0.5f).toInt()\n        } else {\n            ((1.055f * v.pow(1 / 2.4f) - 0.055f) * 255 + 0.5f).toInt()\n        }\n    }\n\n    private val charMap = listOf(\n        '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G',\n        'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',\n        'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o',\n        'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '#', '$', '%', '*', '+', ',',\n        '-', '.', ':', ';', '=', '?', '@', '[', ']', '^', '_', '{', '|', '}', '~'\n    )\n        .mapIndexed { i, c -> c to i }\n        .toMap()\n}"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/blurhash/BlurHashModifier.kt",
    "content": "package com.zhangke.framework.blurhash\n\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.runtime.DisposableEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.composed\nimport androidx.compose.ui.draw.drawBehind\nimport androidx.compose.ui.geometry.Size\nimport androidx.compose.ui.graphics.ImageBitmap\nimport androidx.compose.ui.layout.onSizeChanged\nimport androidx.compose.ui.unit.IntSize\nimport androidx.compose.ui.unit.toSize\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.IO\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport kotlin.math.roundToInt\n\nfun Modifier.blurhash(blurHash: String?): Modifier = composed {\n    if (blurHash.isNullOrEmpty()) {\n        val placeholderColor = MaterialTheme.colorScheme.surfaceDim\n        return@composed this.drawBehind {\n            drawRect(color = placeholderColor)\n        }\n    }\n    var size: Size? by remember(blurHash) {\n        mutableStateOf(null)\n    }\n    var bitmap: ImageBitmap? by remember(blurHash) {\n        mutableStateOf(null)\n    }\n    val coroutineScope = rememberCoroutineScope()\n    if (size != null) {\n        DisposableEffect(blurHash) {\n\n            if (bitmap == null && size != null) {\n                coroutineScope.launch {\n                    bitmap = withContext(Dispatchers.IO) {\n                        BlurHashDecoder.decode(\n                            blurHash = blurHash,\n                            width = (size!!.width * 0.5F).roundToInt(),\n                            height = (size!!.height * 0.5F).roundToInt(),\n                            useCache = false,\n                        )\n                    }\n                }\n            }\n\n            onDispose {\n                // TODO: check is this bitmap need to recycler and earlier recycle\n                bitmap = null\n            }\n        }\n    }\n    onSizeChanged {\n        size = it.toSize()\n    }.drawBehind {\n        if (bitmap != null && size != null) {\n            drawImage(\n                image = bitmap!!,\n                dstSize = IntSize(size!!.width.roundToInt(), size!!.height.roundToInt()),\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/collections/Collections.kt",
    "content": "package com.zhangke.framework.collections\n\ninline fun <T, R> Iterable<T>.mapFirst(transform: (T) -> R?): R {\n    return mapFirstOrNull(transform) ?: throw NoSuchElementException()\n}\n\ninline fun <T, R> Iterable<T>.mapFirstOrNull(transform: (T) -> R?): R? {\n    forEach {\n        val v = transform(it)\n        if (v != null) {\n            return v\n        }\n    }\n    return null\n}\n\ninline fun <T> Iterable<T>.container(predicate: (T) -> Boolean): Boolean {\n    return firstOrNull(predicate) != null\n}\n\ninline fun <T> Iterable<T>.remove(predicate: (T) -> Boolean): List<T> {\n    return filter { !predicate(it) }\n}\n\nfun <T> Iterable<T>.removeIndex(index: Int): List<T> {\n    return filterIndexed { i, _ -> i != index }\n}\n\nfun <T> List<T>.updateItem(item: T, predicate: (T) -> T): List<T> {\n    return map {\n        if (it == item) {\n            predicate(it)\n        } else {\n            it\n        }\n    }\n}\n\ninline fun <T> Iterable<T>.updateIndex(index: Int, predicate: (T) -> T): List<T> {\n    return mapIndexed { i, t ->\n        if (i == index) {\n            predicate(t)\n        } else {\n            t\n        }\n    }\n}\n\nfun <T> MutableIterable<T>.removeFirstOrNull(block: (T) -> Boolean): T? {\n    val iterator = iterator()\n    while (iterator.hasNext()){\n        val item = iterator.next()\n        if (block(item)) {\n            iterator.remove()\n            return item\n        }\n    }\n    return null\n}\n\nfun <T> Collection<T>.getOrNull(block: (T) -> Boolean): T? {\n    for (item in this) {\n        if (block(item)) {\n            return item\n        }\n    }\n    return null\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/AlertConfirmDialog.kt",
    "content": "package com.zhangke.framework.composable\n\nimport androidx.compose.runtime.Composable\nimport com.zhangke.fread.localization.LocalizedString\nimport org.jetbrains.compose.resources.stringResource\n\n@Composable\nfun AlertConfirmDialog(\n    content: String,\n    onConfirm: () -> Unit,\n    onDismissRequest: () -> Unit,\n) {\n    FreadDialog(\n        onDismissRequest = onDismissRequest,\n        title = stringResource(LocalizedString.alert),\n        contentText = content,\n        positiveButtonText = stringResource(LocalizedString.ok),\n        onPositiveClick = {\n            onDismissRequest()\n            onConfirm()\n        },\n        negativeButtonText = stringResource(LocalizedString.cancel),\n        onNegativeClick = onDismissRequest,\n    )\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/AvatarStatck.kt",
    "content": "package com.zhangke.framework.composable\n\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport com.seiko.imageloader.ui.AutoSizeImage\n\n@Composable\nfun AvatarHorizontalStack(\n    modifier: Modifier = Modifier,\n    avatars: List<String>,\n    avatarSize: Dp = 24.dp,\n    borderColor: Color = Color.White,\n) {\n    Box(\n        modifier = modifier,\n    ) {\n        avatars.forEachIndexed { index, imageUrl ->\n            val startPadding = (avatarSize * index - avatarSize * 0.2F).coerceAtLeast(0.dp)\n            AutoSizeImage(\n                imageUrl,\n                modifier = Modifier\n                    .padding(start = startPadding)\n                    .size(avatarSize)\n                    .border(color = borderColor, width = 1.dp, shape = CircleShape)\n                    .clip(CircleShape),\n                contentScale = ContentScale.Crop,\n                contentDescription = \"avatar\",\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/BackHandler.kt",
    "content": "package com.zhangke.framework.composable\n\nimport androidx.compose.runtime.Composable\nimport androidx.navigationevent.NavigationEventInfo\nimport androidx.navigationevent.compose.NavigationBackHandler\nimport androidx.navigationevent.compose.rememberNavigationEventState\n\n@Composable\nfun BackHandler(enabled: Boolean, block: () -> Unit) {\n    val navState = rememberNavigationEventState(NavigationEventInfo.None)\n    NavigationBackHandler(\n        state = navState,\n        isBackEnabled = enabled,\n        onBackCancelled = {},\n        onBackCompleted = block,\n    )\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/BezierCurve.kt",
    "content": "package com.zhangke.framework.composable\n\nimport androidx.compose.foundation.Canvas\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.graphics.Path\nimport androidx.compose.ui.graphics.drawscope.DrawScope\nimport androidx.compose.ui.graphics.drawscope.Fill\nimport androidx.compose.ui.graphics.drawscope.Stroke\nimport androidx.compose.ui.layout.onSizeChanged\nimport androidx.compose.ui.unit.IntSize\n\n@Composable\nfun BezierCurve(\n    modifier: Modifier,\n    points: List<Float>,\n    minPoint: Float? = null,\n    maxPoint: Float? = null,\n    style: BezierCurveStyle,\n) {\n    var size by remember {\n        mutableStateOf(IntSize.Zero)\n    }\n\n    Canvas(\n        modifier = modifier.onSizeChanged {\n            size = it\n        },\n        onDraw = {\n            if (size != IntSize.Zero && points.size > 1) {\n                drawBezierCurve(\n                    size = size,\n                    points = points,\n                    fixedMinPoint = minPoint,\n                    fixedMaxPoint = maxPoint,\n                    style = style,\n                )\n            }\n        },\n    )\n}\n\nprivate fun DrawScope.drawBezierCurve(\n    size: IntSize,\n    points: List<Float>,\n    fixedMinPoint: Float? = null,\n    fixedMaxPoint: Float? = null,\n    style: BezierCurveStyle,\n) {\n    val maxPoint = fixedMaxPoint ?: points.max()\n    val minPoint = fixedMinPoint ?: points.min()\n    val total = maxPoint - minPoint\n    val height = size.height\n    val width = size.width\n    val xSpacing = width / (points.size - 1F)\n    var lastPoint: Offset? = null\n    val path = Path()\n    var firstPoint = Offset(0F, 0F)\n    for (index in points.indices) {\n        val x = index * xSpacing\n        val y = height - height * ((points[index] - minPoint) / total)\n        if (lastPoint != null) {\n            buildCurveLine(path, lastPoint, Offset(x, y))\n        }\n        lastPoint = Offset(x, y)\n        if (index == 0) {\n            path.moveTo(x, y)\n            firstPoint = Offset(x, y)\n        }\n    }\n    fun closeWithBottomLine() {\n        path.lineTo(width.toFloat(), height.toFloat())\n        path.lineTo(0F, height.toFloat())\n        path.lineTo(firstPoint.x, firstPoint.y)\n    }\n\n    when (style) {\n        is BezierCurveStyle.Fill -> {\n            closeWithBottomLine()\n            drawPath(\n                path = path,\n                style = Fill,\n                brush = style.brush,\n            )\n        }\n\n        is BezierCurveStyle.CurveStroke -> {\n            drawPath(\n                path = path,\n                brush = style.brush,\n                style = style.stroke,\n            )\n        }\n\n        is BezierCurveStyle.StrokeAndFill -> {\n            drawPath(\n                path = path,\n                brush = style.strokeBrush,\n                style = style.stroke,\n            )\n            closeWithBottomLine()\n            drawPath(\n                path = path,\n                brush = style.fillBrush,\n                style = Fill,\n            )\n        }\n    }\n}\n\nprivate fun buildCurveLine(path: Path, startPoint: Offset, endPoint: Offset) {\n    val firstControlPoint = Offset(\n        x = startPoint.x + (endPoint.x - startPoint.x) / 2F,\n        y = startPoint.y,\n    )\n    val secondControlPoint = Offset(\n        x = startPoint.x + (endPoint.x - startPoint.x) / 2F,\n        y = endPoint.y,\n    )\n    path.cubicTo(\n        x1 = firstControlPoint.x,\n        y1 = firstControlPoint.y,\n        x2 = secondControlPoint.x,\n        y2 = secondControlPoint.y,\n        x3 = endPoint.x,\n        y3 = endPoint.y,\n    )\n}\n\nsealed class BezierCurveStyle {\n\n    class Fill(val brush: Brush) : BezierCurveStyle()\n\n    class CurveStroke(\n        val brush: Brush,\n        val stroke: Stroke,\n    ) : BezierCurveStyle()\n\n    class StrokeAndFill(\n        val fillBrush: Brush,\n        val strokeBrush: Brush,\n        val stroke: Stroke,\n    ) : BezierCurveStyle()\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/BottomSheetState.kt",
    "content": "package com.zhangke.framework.composable\n\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.SheetState\nimport androidx.compose.material3.SheetValue\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.unit.dp\n\nprivate val ModalBottomSheetPositionalThreshold = 56.dp\nprivate val ModalBottomSheetVelocityThreshold = 125.dp\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun rememberTransientModalBottomSheetState(\n    skipPartiallyExpanded: Boolean = false,\n    confirmValueChange: (SheetValue) -> Boolean = { true },\n    initialValue: SheetValue = SheetValue.Hidden,\n    skipHiddenState: Boolean = false,\n): SheetState {\n    val density = LocalDensity.current\n    return remember(\n        skipPartiallyExpanded,\n        confirmValueChange,\n        initialValue,\n        skipHiddenState,\n        density,\n    ) {\n        // Avoid rememberSaveable here so SheetValue is never serialized into saved state.\n        SheetState(\n            skipPartiallyExpanded = skipPartiallyExpanded,\n            positionalThreshold = { with(density) { ModalBottomSheetPositionalThreshold.toPx() } },\n            velocityThreshold = { with(density) { ModalBottomSheetVelocityThreshold.toPx() } },\n            initialValue = initialValue,\n            confirmValueChange = confirmValueChange,\n            skipHiddenState = skipHiddenState,\n        )\n    }\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/Bounds.kt",
    "content": "package com.zhangke.framework.composable\n\nimport androidx.compose.ui.geometry.Offset\n\ndata class Bounds(\n    val left: Float,\n    val top: Float,\n    val right: Float,\n    val bottom: Float,\n) {\n\n    val isEmpty = (right - left) * (bottom - top) == 0F\n\n    fun inside(x: Float, y: Float): Boolean {\n        return xInside(x) && yInside(y)\n    }\n\n    fun outside(x: Float, y: Float) = !inside(x, y)\n\n    fun inside(offset: Offset) = inside(x = offset.x, y = offset.y)\n\n    fun outside(offset: Offset) = outside(x = offset.x, y = offset.y)\n\n    fun xInside(x: Float) = x in left..right\n\n    fun yInside(y: Float) = y in top..bottom\n\n    fun xOutside(x: Float) = !xInside(x)\n\n    fun yOutside(y: Float) = !yInside(y)\n\n    fun outsideAbsolute(offset: Offset) = xOutside(offset.x) && yOutside(offset.y)\n\n    fun coerceInY(y: Float): Float {\n        return y.coerceAtLeast(top).coerceAtMost(bottom)\n    }\n\n    fun coerceInX(x: Float): Float{\n        return x.coerceAtLeast(left).coerceAtMost(right)\n    }\n\n    fun coerceIn(offset: Offset): Offset {\n        return Offset(\n            x = offset.x.coerceIn(left..right),\n            y = offset.y.coerceIn(top..bottom),\n        )\n    }\n\n    companion object {\n\n        val EMPTY = Bounds(0F, 0F, 0F, 0F)\n\n    }\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/CollapsableTopBarScrollConnection.kt",
    "content": "package com.zhangke.framework.composable\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableFloatStateOf\nimport androidx.compose.runtime.saveable.Saver\nimport androidx.compose.runtime.saveable.listSaver\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.input.nestedscroll.NestedScrollConnection\nimport androidx.compose.ui.input.nestedscroll.NestedScrollSource\n\n@Composable\nfun rememberCollapsableTopBarScrollConnection(\n    maxPx: Float,\n    minPx: Float,\n): CollapsableTopBarScrollConnection {\n    return rememberSaveable(minPx, maxPx, saver = CollapsableTopBarScrollConnection.Saver) {\n        CollapsableTopBarScrollConnection(maxPx, minPx)\n    }\n}\n\nclass CollapsableTopBarScrollConnection(\n    private val maxPx: Float,\n    private val minPx: Float,\n) : NestedScrollConnection {\n\n    private var topBarHeight: Float = maxPx\n        set(value) {\n            field = value\n            progress = 1 - (topBarHeight - minPx) / (maxPx - minPx)\n        }\n\n    var progress: Float by mutableFloatStateOf(0F)\n        private set\n\n    override fun onPostScroll(\n        consumed: Offset,\n        available: Offset,\n        source: NestedScrollSource\n    ): Offset {\n        return handleScroll(available)\n    }\n\n    override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {\n        return handleScroll(available)\n    }\n\n    private fun handleScroll(available: Offset): Offset {\n        if (available.y <= 0 && topBarHeight <= minPx) return Offset.Zero\n        if (available.y >= 0 && topBarHeight >= maxPx) return Offset.Zero\n        val height = topBarHeight\n\n        if (height == minPx) {\n            if (available.y > 0F) {\n                topBarHeight += available.y\n                return Offset(0F, available.y)\n            }\n        }\n\n        if (height + available.y > maxPx) {\n            topBarHeight = maxPx\n            return Offset(0f, maxPx - height)\n        }\n\n        if (height + available.y < minPx) {\n            topBarHeight = minPx\n            return Offset(0f, minPx - height)\n        }\n\n        topBarHeight += available.y\n\n        return Offset.Zero\n    }\n\n    companion object {\n\n        val Saver: Saver<CollapsableTopBarScrollConnection, *> = listSaver(\n            save = {\n                listOf(it.maxPx, it.minPx, it.topBarHeight)\n            },\n            restore = {\n                CollapsableTopBarScrollConnection(\n                    maxPx = it[0],\n                    minPx = it[1],\n                ).apply {\n                    topBarHeight = it[2]\n                }\n            },\n        )\n    }\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/CompositionLocal.kt",
    "content": "package com.zhangke.framework.composable\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.ProvidableCompositionLocal\n\nval <T> ProvidableCompositionLocal<T?>.currentOrThrow: T\n    @Composable\n    get() = current ?: error(\"CompositionLocal is null\")\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/DatePickerDialog.kt",
    "content": "package com.zhangke.framework.composable\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material3.DatePicker\nimport androidx.compose.material3.DatePickerDefaults\nimport androidx.compose.material3.DatePickerState\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.ModalBottomSheet\nimport androidx.compose.material3.SelectableDates\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.material3.rememberDatePickerState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport com.zhangke.fread.localization.LocalizedString\nimport io.ktor.util.date.getTimeMillis\nimport kotlinx.coroutines.launch\nimport kotlinx.datetime.TimeZone\nimport kotlinx.datetime.toLocalDateTime\nimport org.jetbrains.compose.resources.stringResource\nimport kotlin.time.Clock\nimport kotlin.time.ExperimentalTime\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun DatePickerDialog(\n    datePickerState: DatePickerState,\n    visible: Boolean,\n    onDismissRequest: () -> Unit,\n    onConfirmClick: () -> Unit,\n) {\n    if (!visible) return\n    val bottomSheetState = rememberTransientModalBottomSheetState(skipPartiallyExpanded = true)\n    val coroutineScope = rememberCoroutineScope()\n    ModalBottomSheet(\n        sheetState = bottomSheetState,\n        onDismissRequest = {\n            coroutineScope.launch {\n                bottomSheetState.hide()\n                onDismissRequest()\n            }\n        },\n        dragHandle = null,\n    ) {\n        Column(\n            modifier = Modifier\n        ) {\n            Row(\n                modifier = Modifier\n                    .padding(top = 16.dp, end = 16.dp)\n                    .fillMaxWidth(),\n                horizontalArrangement = Arrangement.End,\n                verticalAlignment = Alignment.CenterVertically,\n            ) {\n                TextButton(onClick = {\n                    onDismissRequest()\n                    onConfirmClick()\n                }) {\n                    Text(text = stringResource(LocalizedString.ok))\n                }\n            }\n            DatePicker(\n                state = datePickerState,\n                showModeToggle = false,\n                colors = DatePickerDefaults.colors(\n                    containerColor = MaterialTheme.colorScheme.surfaceContainerLow,\n                )\n            )\n        }\n    }\n}\n\n@OptIn(ExperimentalMaterial3Api::class, ExperimentalTime::class)\n@Composable\nfun rememberFutureDatePickerState(\n    initialSelectedDateMillis: Long? = null,\n): DatePickerState {\n    val currentYear: Int = remember {\n        Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).year\n    }\n    return rememberDatePickerState(\n        initialSelectedDateMillis = initialSelectedDateMillis,\n        yearRange = currentYear..2100,\n        selectableDates = remember {\n            FutureDates()\n        },\n    )\n}\n\n@OptIn(ExperimentalMaterial3Api::class)\nclass FutureDates : SelectableDates {\n\n    override fun isSelectableDate(utcTimeMillis: Long): Boolean {\n        return getTimeMillis() < utcTimeMillis\n    }\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/DirectionalLazyListState.kt",
    "content": "package com.zhangke.framework.composable\n\nimport androidx.compose.foundation.lazy.LazyListState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.remember\n\n@Composable\nfun rememberDirectionalLazyListState(\n    lazyListState: LazyListState,\n): DirectionalLazyListState {\n    return remember(lazyListState) {\n        DirectionalLazyListState(lazyListState)\n    }\n}\n\nenum class ScrollDirection {\n    Up, Down, None\n}\n\nclass DirectionalLazyListState(\n    private val lazyListState: LazyListState\n) {\n    private var positionY = lazyListState.firstVisibleItemScrollOffset\n    private var visibleItem = lazyListState.firstVisibleItemIndex\n\n\n    val scrollDirection by derivedStateOf {\n        if (lazyListState.isScrollInProgress.not()) {\n            ScrollDirection.None\n        } else {\n            val firstVisibleItemIndex = lazyListState.firstVisibleItemIndex\n            val firstVisibleItemScrollOffset =\n                lazyListState.firstVisibleItemScrollOffset\n\n            // We are scrolling while first visible item hasn't changed yet\n            if (firstVisibleItemIndex == visibleItem) {\n                val direction = if (firstVisibleItemScrollOffset > positionY) {\n                    ScrollDirection.Down\n                } else {\n                    ScrollDirection.Up\n                }\n                positionY = firstVisibleItemScrollOffset\n\n                direction\n            } else {\n\n                val direction = if (firstVisibleItemIndex > visibleItem) {\n                    ScrollDirection.Down\n                } else {\n                    ScrollDirection.Up\n                }\n                positionY = firstVisibleItemScrollOffset\n                visibleItem = firstVisibleItemIndex\n                direction\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/DpSaver.kt",
    "content": "package com.zhangke.framework.composable\n\nimport androidx.compose.runtime.saveable.Saver\nimport androidx.compose.ui.unit.Dp\n\nobject DpSaver {\n\n    val Saver: Saver<Dp, *> = Saver(\n        save = {\n            it.value\n        },\n        restore = {\n            Dp(it)\n        }\n    )\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/DurationSelector.kt",
    "content": "package com.zhangke.framework.composable\n\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport com.sd.lib.compose.wheel_picker.FVerticalWheelPicker\nimport com.sd.lib.compose.wheel_picker.FWheelPickerState\nimport com.sd.lib.compose.wheel_picker.rememberFWheelPickerState\nimport com.zhangke.framework.utils.format\nimport com.zhangke.fread.localization.LocalizedString\nimport kotlinx.coroutines.launch\nimport org.jetbrains.compose.resources.stringResource\nimport kotlin.time.Duration\nimport kotlin.time.Duration.Companion.days\nimport kotlin.time.Duration.Companion.hours\nimport kotlin.time.Duration.Companion.minutes\n\n@Composable\nfun DurationSelector(\n    defaultDuration: Duration,\n    onDismissRequest: () -> Unit,\n    onDurationSelect: (Duration) -> Unit,\n) {\n    var currentDuration by remember {\n        mutableStateOf(defaultDuration)\n    }\n    FreadDialog(\n        onDismissRequest = onDismissRequest,\n        title = stringResource(LocalizedString.durationSelectorTitle),\n        onNegativeClick = {\n            onDismissRequest()\n        },\n        onPositiveClick = {\n            onDismissRequest()\n            onDurationSelect(currentDuration)\n        },\n        content = {\n            DurationSelectorContent(\n                defaultDuration = defaultDuration,\n                onDurationChanged = {\n                    currentDuration = it\n                }\n            )\n        },\n    )\n}\n\n@Composable\nprivate fun DurationSelectorContent(\n    defaultDuration: Duration,\n    onDurationChanged: (Duration) -> Unit,\n) {\n    val dayList = remember {\n        mutableListOf(0, 1, 2, 3, 4, 5, 6, 7)\n    }\n    val dayState = rememberFWheelPickerState(0)\n    val hourList = remember {\n        mutableListOf<Int>().apply {\n            repeat(24) {\n                add(it)\n            }\n        }\n    }\n    val hourState = rememberFWheelPickerState(0)\n    val minutesList = remember {\n        mutableListOf<Int>().apply {\n            repeat(60) {\n                add(it)\n            }\n        }\n    }\n    val minutesState = rememberFWheelPickerState(0)\n    val currentDay = dayState.currentIndex\n    val currentHour = hourState.currentIndex\n    val currentMinutes = minutesState.currentIndex\n    LaunchedEffect(currentDay, currentHour, currentMinutes) {\n        if (currentDay < 0 || currentHour < 0 || currentMinutes < 0) return@LaunchedEffect\n        val currentDuration = dayList[currentDay].days +\n                hourList[currentHour].hours +\n                minutesList[currentMinutes].minutes\n        onDurationChanged(currentDuration)\n    }\n    LaunchedEffect(defaultDuration) {\n        launch {\n            val formatted = defaultDuration.format()\n            dayList.indexOf(formatted.days).takeIf { it >= 0 }?.let {\n                dayState.animateScrollToIndex(it)\n            }\n            hourList.indexOf(formatted.hours).takeIf { it >= 0 }?.let {\n                hourState.animateScrollToIndex(it)\n            }\n            minutesList.indexOf(formatted.minutes).takeIf { it >= 0 }?.let {\n                minutesState.animateScrollToIndex(it)\n            }\n        }\n    }\n    Row(\n        modifier = Modifier\n            .padding(16.dp)\n            .fillMaxWidth()\n    ) {\n        Column(modifier = Modifier.weight(1F)) {\n            Text(\n                modifier = Modifier.align(Alignment.CenterHorizontally),\n                text = stringResource(LocalizedString.durationDay),\n            )\n            DurationSelectorItem(\n                state = dayState,\n                modifier = Modifier.fillMaxWidth(),\n                list = dayList,\n            )\n        }\n        Box(modifier = Modifier.width(8.dp))\n        Column(modifier = Modifier.weight(1F)) {\n            Text(\n                modifier = Modifier.align(Alignment.CenterHorizontally),\n                text = stringResource(LocalizedString.durationHour),\n            )\n            DurationSelectorItem(\n                state = hourState,\n                modifier = Modifier.fillMaxWidth(),\n                list = hourList,\n            )\n        }\n        Box(modifier = Modifier.width(8.dp))\n        Column(modifier = Modifier.weight(1F)) {\n            Text(\n                modifier = Modifier.align(Alignment.CenterHorizontally),\n                text = stringResource(LocalizedString.durationMinute),\n            )\n            DurationSelectorItem(\n                state = minutesState,\n                modifier = Modifier.fillMaxWidth(),\n                list = minutesList,\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun DurationSelectorItem(\n    state: FWheelPickerState,\n    modifier: Modifier = Modifier,\n    list: List<Int>,\n) {\n    FVerticalWheelPicker(\n        modifier = modifier,\n        count = list.size,\n        state = state,\n    ) { index ->\n        Text(text = list[index].toString())\n    }\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/FlowUtils.android.kt",
    "content": "package com.zhangke.framework.composable\n\nimport kotlinx.coroutines.flow.MutableSharedFlow\n\nsuspend fun MutableSharedFlow<TextString>.tryEmitException(exception: Throwable) {\n    exception.message\n        ?.takeIf { it.isNotEmpty() }\n        ?.let { textOf(it) }\n        ?.let {\n            this.emit(it)\n        }\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/FlowUtils.kt",
    "content": "package com.zhangke.framework.composable\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.rememberUpdatedState\nimport androidx.lifecycle.ViewModel\nimport com.zhangke.framework.ktx.launchInViewModel\nimport com.zhangke.framework.lifecycle.SubViewModel\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.MutableSharedFlow\n\n@Composable\nfun <T> ConsumeFlow(\n    flow: Flow<T>,\n    block: suspend (T) -> Unit\n) {\n    val updatedBlock by rememberUpdatedState(block)\n    LaunchedEffect(flow) {\n        flow.collect {\n            updatedBlock(it)\n        }\n    }\n}\n\ncontext(viewModel: ViewModel)\nfun <T> MutableSharedFlow<T>.emitInViewModel(element: T) {\n    viewModel.launchInViewModel {\n        emit(element)\n    }\n}\n\ncontext(viewModel: SubViewModel)\nfun <T> MutableSharedFlow<T>.emitInViewModel(element: T) {\n    viewModel.launchInViewModel {\n        emit(element)\n    }\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/FreadDialog.kt",
    "content": "package com.zhangke.framework.composable\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.wrapContentHeight\nimport androidx.compose.material3.Card\nimport androidx.compose.material3.LocalContentColor\nimport androidx.compose.material3.LocalTextStyle\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.text.TextStyle\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport androidx.compose.ui.window.Dialog\nimport androidx.compose.ui.window.DialogProperties\nimport com.zhangke.framework.ktx.ifNullOrEmpty\nimport com.zhangke.fread.localization.LocalizedString\nimport org.jetbrains.compose.resources.stringResource\n\n@Composable\nfun FreadDialog(\n    onDismissRequest: () -> Unit,\n    properties: DialogProperties = DialogProperties(),\n    title: String? = null,\n    contentText: String,\n    negativeButtonText: String? = null,\n    positiveButtonText: String? = null,\n    onNegativeClick: (() -> Unit)? = null,\n    onPositiveClick: (() -> Unit)? = null,\n) {\n    FreadDialog(\n        onDismissRequest = onDismissRequest,\n        properties = properties,\n        title = title,\n        content = {\n            Text(text = contentText)\n        },\n        negativeButtonText = negativeButtonText,\n        positiveButtonText = positiveButtonText,\n        onNegativeClick = onNegativeClick,\n        onPositiveClick = onPositiveClick,\n    )\n}\n\n@Composable\nfun FreadDialog(\n    onDismissRequest: () -> Unit,\n    properties: DialogProperties = DialogProperties(),\n    title: String? = null,\n    content: (@Composable () -> Unit)? = null,\n    negativeButtonText: String? = null,\n    positiveButtonText: String? = null,\n    onNegativeClick: (() -> Unit)? = null,\n    onPositiveClick: (() -> Unit)? = null,\n) {\n    FreadDialog(\n        onDismissRequest = onDismissRequest,\n        properties = properties,\n        header = {\n            Text(\n                text = title.ifNullOrEmpty { stringResource(LocalizedString.alert) },\n            )\n        },\n        content = content,\n        negativeButton = if (negativeButtonText.isNullOrEmpty() && onNegativeClick == null) {\n            null\n        } else {\n            {\n                TextButton(onClick = { onNegativeClick?.invoke() }) {\n                    Text(text = negativeButtonText.ifNullOrEmpty { stringResource(LocalizedString.cancel) })\n                }\n            }\n        },\n        positiveButton = if (positiveButtonText.isNullOrEmpty() && onPositiveClick == null) {\n            null\n        } else {\n            {\n                TextButton(onClick = { onPositiveClick?.invoke() }) {\n                    Text(text = positiveButtonText.ifNullOrEmpty { stringResource(LocalizedString.ok) })\n                }\n            }\n        }\n    )\n}\n\n@Composable\nfun FreadDialog(\n    onDismissRequest: () -> Unit,\n    properties: DialogProperties = DialogProperties(),\n    header: (@Composable () -> Unit)? = null,\n    content: (@Composable () -> Unit)? = null,\n    titleContentColor: Color = MaterialTheme.colorScheme.onSurface,\n    textContentColor: Color = MaterialTheme.colorScheme.onSurface,\n    negativeButton: (@Composable () -> Unit)? = null,\n    positiveButton: (@Composable () -> Unit)? = null,\n) {\n    Dialog(\n        onDismissRequest = onDismissRequest,\n        properties = properties,\n    ) {\n        Card(\n            modifier = Modifier\n                .fillMaxWidth()\n                .wrapContentHeight(),\n            shape = MaterialTheme.shapes.extraLarge,\n        ) {\n            Column(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .wrapContentHeight()\n                    .padding(start = 24.dp, top = 16.dp, end = 20.dp, bottom = 8.dp)\n            ) {\n                if (header != null) {\n                    ProvideContentColorTextStyle(\n                        contentColor = titleContentColor,\n                        textStyle = MaterialTheme.typography.titleMedium\n                            .copy(\n                                fontSize = 18.sp,\n                                fontWeight = FontWeight.SemiBold,\n                            ),\n                    ) {\n                        Box(\n                            Modifier\n                                .align(Alignment.CenterHorizontally)\n                                .fillMaxWidth(),\n                            contentAlignment = Alignment.CenterStart,\n                        ) {\n                            header()\n                        }\n                    }\n                }\n                if (content != null) {\n                    val textStyle = MaterialTheme.typography.bodyMedium\n                    ProvideContentColorTextStyle(\n                        contentColor = textContentColor,\n                        textStyle = textStyle\n                    ) {\n                        Box(\n                            Modifier\n                                .fillMaxWidth()\n                                .padding(top = 12.dp)\n                                .align(Alignment.Start),\n                            contentAlignment = Alignment.CenterStart,\n                        ) {\n                            content()\n                        }\n                    }\n                }\n                Row(\n                    modifier = Modifier\n                        .fillMaxWidth(),\n                    horizontalArrangement = Arrangement.End,\n                    verticalAlignment = Alignment.CenterVertically,\n                ) {\n                    negativeButton?.invoke()\n                    if (positiveButton != null) {\n                        Spacer(modifier = Modifier.size(width = 8.dp, height = 1.dp))\n                        positiveButton()\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun ProvideContentColorTextStyle(\n    contentColor: Color,\n    textStyle: TextStyle,\n    content: @Composable () -> Unit\n) {\n    val mergedStyle = LocalTextStyle.current.merge(textStyle)\n    CompositionLocalProvider(\n        LocalContentColor provides contentColor,\n        LocalTextStyle provides mergedStyle,\n        content = content\n    )\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/FreadTabRow.kt",
    "content": "package com.zhangke.framework.composable\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.HorizontalDivider\nimport androidx.compose.material3.LocalContentColor\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.ScrollableTabRow\nimport androidx.compose.material3.Tab\nimport androidx.compose.material3.TabPosition\nimport androidx.compose.material3.TabRowDefaults\nimport androidx.compose.material3.TabRowDefaults.primaryContainerColor\nimport androidx.compose.material3.TabRowDefaults.tabIndicatorOffset\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateMapOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.Shape\nimport androidx.compose.ui.layout.onSizeChanged\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport com.zhangke.framework.blur.applyBlurEffect\nimport com.zhangke.framework.blur.blurEffectContainerColor\nimport com.zhangke.framework.utils.pxToDp\nimport dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi\n\n@OptIn(ExperimentalHazeMaterialsApi::class)\n@Composable\nfun FreadTabRow(\n    selectedTabIndex: Int,\n    modifier: Modifier = Modifier,\n    containerColor: Color = primaryContainerColor,\n    blurEffectEnabled: Boolean = true,\n    indicatorContent: @Composable (tabPosition: TabPosition) -> Unit =\n        @Composable {\n            FreadTabRowDefault.Indicator()\n        },\n    divider: @Composable () -> Unit = @Composable {\n        HorizontalDivider(thickness = 0.5.dp)\n    },\n    tabCount: Int,\n    tabContent: @Composable (index: Int) -> Unit,\n    onTabClick: (index: Int) -> Unit,\n) {\n    val density = LocalDensity.current\n    val tabContentWidth = remember {\n        mutableStateMapOf<Int, Dp>()\n    }\n    FreadTabRow(\n        modifier = modifier.applyBlurEffect(\n            enabled = blurEffectEnabled,\n            containerColor = containerColor\n        ),\n        selectedTabIndex = selectedTabIndex,\n        containerColor = blurEffectContainerColor(\n            enabled = blurEffectEnabled,\n            containerColor = containerColor\n        ),\n        indicator = { tabPositions ->\n            Box(\n                modifier = Modifier.fillMaxSize().padding(bottom = 1.5.dp),\n                contentAlignment = Alignment.BottomCenter,\n            ) {\n                divider()\n            }\n            val position = tabPositions.getOrNull(selectedTabIndex)\n            if (position != null) {\n                Column(\n                    modifier = Modifier\n                        .ownTabIndicatorOffset(\n                            currentTabPosition = position,\n                            currentTabWidth = tabContentWidth[selectedTabIndex] ?: 0.dp,\n                        ),\n                    horizontalAlignment = Alignment.CenterHorizontally,\n                ) {\n                    indicatorContent(position)\n                    Box(modifier = Modifier.height(1.5.dp))\n                }\n            }\n        },\n        divider = {},\n        tabs = {\n            repeat(tabCount) { index ->\n                Tab(\n                    selected = index == selectedTabIndex,\n                    onClick = { onTabClick(index) },\n                ) {\n                    Box(\n                        modifier = Modifier\n                            .onSizeChanged {\n                                tabContentWidth[index] = it.width.pxToDp(density)\n                            }\n                            .padding(8.dp)\n                    ) {\n                        val contentColor = if (index == selectedTabIndex) {\n                            MaterialTheme.colorScheme.primary\n                        } else {\n                            MaterialTheme.colorScheme.onSurface\n                        }\n                        CompositionLocalProvider(\n                            LocalContentColor provides contentColor\n                        ) {\n                            tabContent(index)\n                        }\n                    }\n                }\n            }\n        },\n    )\n}\n\n@Composable\nprivate fun FreadTabRow(\n    selectedTabIndex: Int,\n    modifier: Modifier = Modifier,\n    containerColor: Color = primaryContainerColor,\n    indicator: @Composable (tabPositions: List<TabPosition>) -> Unit =\n        @Composable { tabPositions ->\n            TabRowDefaults.Indicator(\n                Modifier.tabIndicatorOffset(tabPositions[selectedTabIndex])\n            )\n        },\n    divider: @Composable () -> Unit = @Composable {\n        HorizontalDivider()\n    },\n    tabs: @Composable () -> Unit\n) {\n    val density = LocalDensity.current\n    var tabContainerWidth: Dp? by rememberSaveable(saver = StateSaver.MutableNullableDpSaver) {\n        mutableStateOf(null)\n    }\n    var allTabSumWidth: Dp? by rememberSaveable(saver = StateSaver.MutableNullableDpSaver) {\n        mutableStateOf(null)\n    }\n    var tabEdgePadding by rememberSaveable(saver = StateSaver.MutableDpSaver) {\n        mutableStateOf(0.dp)\n    }\n    if (tabContainerWidth != null && allTabSumWidth != null) {\n        LaunchedEffect(tabContainerWidth, allTabSumWidth) {\n            tabEdgePadding = if (tabContainerWidth!! <= allTabSumWidth!!) {\n                0.dp\n            } else {\n                (tabContainerWidth!! - allTabSumWidth!!) / 2\n            }\n        }\n    }\n    ScrollableTabRow(\n        selectedTabIndex = selectedTabIndex,\n        modifier = modifier.onSizeChanged {\n            tabContainerWidth = it.width.pxToDp(density)\n        },\n        containerColor = containerColor,\n        edgePadding = tabEdgePadding,\n        indicator = { tabPositions ->\n            var allWidth = 0.dp\n            for (position in tabPositions) {\n                allWidth += position.width\n            }\n            allTabSumWidth = allWidth\n            indicator(tabPositions)\n        },\n        divider = divider,\n        tabs = tabs,\n    )\n}\n\nobject FreadTabRowDefault {\n\n    private val IndicatorHeight = 3.dp\n\n    @Composable\n    fun Indicator(\n        modifier: Modifier = Modifier,\n        height: Dp = IndicatorHeight,\n        color: Color = MaterialTheme.colorScheme.primary,\n        shape: Shape = RoundedCornerShape(\n            topStart = 3.dp, topEnd = 3.dp,\n        ),\n    ) {\n        Box(\n            modifier\n                .fillMaxWidth()\n                .height(height)\n                .background(color = color, shape = shape)\n        )\n    }\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/Grid.kt",
    "content": "package com.zhangke.framework.composable\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.layout.Layout\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport kotlin.math.max\n\n@Composable\nfun Grid(\n    modifier: Modifier,\n    horizontalSpacing: Dp = 0.dp,\n    verticalSpacing: Dp = 0.dp,\n    columnCount: Int,\n    content: @Composable () -> Unit,\n) {\n    Layout(\n        modifier = modifier,\n        content = content,\n    ) { measurables, constraints ->\n        val itemWidth =\n            (constraints.maxWidth - horizontalSpacing.roundToPx() * (columnCount - 1)) / columnCount\n        val rowCount = (measurables.size - 1) / columnCount + 1\n        val itemConstraints = constraints.copy(minWidth = itemWidth, maxWidth = itemWidth)\n        val placeables = measurables.map { it.measure(itemConstraints) }\n        val itemTotalHeight = placeables.chunked(columnCount).sumOf { list ->\n            list.maxBy { it.height }.height\n        }\n        val totalHeight = (rowCount - 1) * verticalSpacing.roundToPx() + itemTotalHeight\n        layout(constraints.maxWidth, totalHeight) {\n            var yOffset = 0\n            var itemHeight = -1\n            placeables.forEachIndexed { index, placeable ->\n                val xSpacing = (index % columnCount) * horizontalSpacing.roundToPx()\n                placeable.placeRelative(\n                    x = itemWidth * (index % columnCount) + xSpacing,\n                    y = yOffset,\n                )\n                itemHeight = max(itemHeight, placeable.height)\n                if (index % columnCount == columnCount - 1) {\n                    // next round is new line\n                    yOffset += itemHeight\n                    yOffset += verticalSpacing.roundToPx()\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/HorizontalPageIndicator.kt",
    "content": "package com.zhangke.framework.composable\n\nimport androidx.compose.foundation.Canvas\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.geometry.CornerRadius\nimport androidx.compose.ui.geometry.Size\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\n\n@Composable\nfun HorizontalPageIndicator(\n    currentIndex: Int,\n    pageCount: Int,\n    modifier: Modifier,\n    style: IndicatorStyle = IndicatorStyle.default(),\n) {\n    Canvas(modifier = modifier) {\n        val inactiveDiameter = style.inactiveDiameter.toPx()\n        val inactiveRadius = inactiveDiameter / 2\n        val activeLineWidth = style.activeLineWidth.toPx()\n        val activeColor = style.activeColor\n        val inactiveColor = style.inactiveColor\n        val indicatorSpacing = inactiveDiameter\n        var totalWidth = 0f\n        for (i in 0 until pageCount) {\n            totalWidth += if (i == currentIndex) activeLineWidth else inactiveDiameter\n        }\n        totalWidth += (pageCount - 1) * indicatorSpacing\n        val startX = (size.width - totalWidth) / 2\n        val startY = (size.height - inactiveDiameter) / 2\n        var currentX = startX\n        for (i in 0 until pageCount) {\n            if (i == currentIndex) {\n                drawRoundRect(\n                    color = activeColor,\n                    topLeft = Offset(currentX, startY),\n                    size = Size(activeLineWidth, inactiveDiameter),\n                    cornerRadius = CornerRadius(inactiveRadius, inactiveRadius),\n                )\n                currentX += activeLineWidth + indicatorSpacing\n            } else {\n                drawCircle(\n                    color = inactiveColor,\n                    radius = inactiveRadius,\n                    center = Offset(\n                        x = currentX + inactiveRadius,\n                        y = startY + inactiveRadius\n                    )\n                )\n                currentX += inactiveDiameter + indicatorSpacing\n            }\n        }\n    }\n}\n\ndata class IndicatorStyle(\n    val activeLineWidth: Dp,\n    val inactiveDiameter: Dp,\n    val activeColor: Color,\n    val inactiveColor: Color,\n) {\n\n    companion object {\n\n        @Composable\n        fun default(\n            activeLineWidth: Dp = 12.dp,\n            inactiveDiameter: Dp = 4.dp,\n            activeColor: Color = MaterialTheme.colorScheme.inverseOnSurface,\n            inactiveColor: Color = MaterialTheme.colorScheme.inverseOnSurface,\n        ): IndicatorStyle = IndicatorStyle(\n            activeLineWidth = activeLineWidth,\n            inactiveDiameter = inactiveDiameter,\n            activeColor = activeColor,\n            inactiveColor = inactiveColor,\n        )\n    }\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/IconButton.kt",
    "content": "package com.zhangke.framework.composable\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.combinedClickable\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.IconButtonColors\nimport androidx.compose.material3.IconButtonDefaults\nimport androidx.compose.material3.LocalContentColor\nimport androidx.compose.material3.minimumInteractiveComponentSize\nimport androidx.compose.material3.ripple\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.Stable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.painter.Painter\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.semantics.Role\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\n\n@Composable\nfun SimpleIconButton(\n    onClick: () -> Unit,\n    modifier: Modifier = Modifier,\n    iconModifier: Modifier = Modifier,\n    iconSize: Dp,\n    enabled: Boolean = true,\n    imageVector: ImageVector,\n    contentDescription: String?,\n    colors: IconButtonColors = IconButtonDefaults.iconButtonColors(),\n    tint: Color = LocalContentColor.current,\n    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },\n) {\n    Box(\n        modifier = modifier\n            .size(iconSize)\n            .clip(CircleShape)\n            .background(color = if (enabled) colors.containerColor else colors.disabledContainerColor)\n            .clickable(\n                enabled = enabled,\n                role = Role.Button,\n                interactionSource = interactionSource,\n                indication = ripple(),\n                onClick = onClick,\n            ),\n        contentAlignment = Alignment.Center\n    ) {\n        val contentColor = if (enabled) colors.contentColor else colors.disabledContentColor\n        CompositionLocalProvider(LocalContentColor provides contentColor) {\n            Icon(\n                modifier = iconModifier,\n                imageVector = imageVector,\n                contentDescription = contentDescription,\n                tint = tint,\n            )\n        }\n    }\n}\n\n@Composable\nfun SimpleIconButton(\n    onClick: () -> Unit,\n    modifier: Modifier = Modifier,\n    iconModifier: Modifier = Modifier,\n    enabled: Boolean = true,\n    imageVector: ImageVector,\n    contentDescription: String?,\n    colors: IconButtonColors = IconButtonDefaults.iconButtonColors(),\n    tint: Color? = null,\n    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },\n) {\n    IconButton(\n        onClick = onClick,\n        modifier = modifier,\n        enabled = enabled,\n        colors = colors,\n        interactionSource = interactionSource,\n    ) {\n        val icTint = tint ?: LocalContentColor.current\n        Icon(\n            modifier = iconModifier,\n            imageVector = imageVector,\n            contentDescription = contentDescription,\n            tint = icTint,\n        )\n    }\n}\n\n@Composable\nfun SimpleIconButton(\n    onClick: () -> Unit,\n    modifier: Modifier = Modifier,\n    iconModifier: Modifier = Modifier,\n    enabled: Boolean = true,\n    painter: Painter,\n    contentDescription: String?,\n    colors: IconButtonColors = IconButtonDefaults.iconButtonColors(),\n    tint: Color? = null,\n    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },\n) {\n    IconButton(\n        onClick = onClick,\n        modifier = modifier,\n        enabled = enabled,\n        colors = colors,\n        interactionSource = interactionSource,\n    ) {\n        val icTint = tint ?: LocalContentColor.current\n        Icon(\n            modifier = iconModifier,\n            painter = painter,\n            contentDescription = contentDescription,\n            tint = icTint,\n        )\n    }\n}\n\n@Composable\nfun SimpleIconButton(\n    onClick: () -> Unit,\n    onLongClick: () -> Unit,\n    imageVector: ImageVector,\n    modifier: Modifier = Modifier,\n    iconModifier: Modifier = Modifier,\n    enabled: Boolean = true,\n    contentDescription: String?,\n    tint: Color? = null,\n    colors: IconButtonColors = IconButtonDefaults.iconButtonColors(),\n    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },\n) {\n    Box(\n        modifier = modifier\n            .minimumInteractiveComponentSize()\n            .size(40.dp)\n            .clip(RoundedCornerShape(50))\n            .background(color = colors.containerColor(enabled), shape = RoundedCornerShape(50))\n            .combinedClickable(\n                enabled = enabled,\n                onClick = onClick,\n                onLongClick = onLongClick,\n                interactionSource = interactionSource,\n            ),\n    ) {\n        val icTint = tint ?: LocalContentColor.current\n        Icon(\n            modifier = iconModifier.align(Alignment.Center),\n            imageVector = imageVector,\n            contentDescription = contentDescription,\n            tint = icTint,\n        )\n    }\n}\n\n@Stable\nprivate fun IconButtonColors.containerColor(enabled: Boolean): Color =\n    if (enabled) containerColor else disabledContainerColor\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/InsetAwareSearchBar.kt",
    "content": "package com.zhangke.framework.composable\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.ColumnScope\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.statusBars\nimport androidx.compose.foundation.layout.windowInsetsPadding\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.SearchBar\nimport androidx.compose.material3.SearchBarColors\nimport androidx.compose.material3.SearchBarDefaults\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.Shape\nimport androidx.compose.ui.unit.Dp\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun InsetAwareSearchBar(\n    expanded: Boolean,\n    onExpandedChange: (Boolean) -> Unit,\n    inputField: @Composable () -> Unit,\n    modifier: Modifier = Modifier,\n    windowInsets: WindowInsets = WindowInsets.statusBars,\n    insetContainerColor: Color = MaterialTheme.colorScheme.surface,\n    colors: SearchBarColors = SearchBarDefaults.colors(),\n    tonalElevation: Dp = SearchBarDefaults.TonalElevation,\n    shadowElevation: Dp = SearchBarDefaults.ShadowElevation,\n    shape: Shape = SearchBarDefaults.inputFieldShape,\n    content: @Composable ColumnScope.() -> Unit = {},\n) {\n    Box(\n        modifier = modifier\n            .windowInsetsPadding(windowInsets)\n            .background(insetContainerColor),\n    ) {\n        SearchBar(\n            modifier = Modifier.fillMaxWidth(),\n            windowInsets = WindowInsets(0, 0, 0, 0),\n            expanded = expanded,\n            onExpandedChange = onExpandedChange,\n            inputField = inputField,\n            colors = colors,\n            tonalElevation = tonalElevation,\n            shadowElevation = shadowElevation,\n            shape = shape,\n            content = content,\n        )\n    }\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/Keyboard.kt",
    "content": "package com.zhangke.framework.composable\n\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.ime\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.State\nimport androidx.compose.runtime.rememberUpdatedState\nimport androidx.compose.ui.platform.LocalDensity\n\n@Composable\nfun keyboardAsState(): State<Boolean> {\n    val isImeVisible = WindowInsets.ime.getBottom(LocalDensity.current) > 0\n    return rememberUpdatedState(isImeVisible)\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/LazyListStateUtils.kt",
    "content": "package com.zhangke.framework.composable\n\nimport androidx.compose.foundation.lazy.LazyListState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.remember\n\n@Composable\nfun canScrollBackward(state: LazyListState): Boolean {\n    val canScrollBackward by remember {\n        derivedStateOf {\n            state.firstVisibleItemIndex != 0 || state.firstVisibleItemScrollOffset != 0\n        }\n    }\n    return canScrollBackward\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/LoadableLayout.kt",
    "content": "package com.zhangke.framework.composable\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.BoxScope\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.material3.Button\nimport androidx.compose.material3.CircularProgressIndicator\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.sp\nimport com.zhangke.fread.localization.LocalizedString\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.update\n\nsealed class LoadableState<T> {\n\n    val isSuccess: Boolean get() = this is Success\n\n    val isFailed: Boolean get() = this is Failed\n\n    val isIdle: Boolean get() = this is Idle\n\n    val isLoading: Boolean get() = this is Loading\n\n    class Idle<T> : LoadableState<T>()\n\n    class Failed<T>(val exception: Throwable) : LoadableState<T>()\n\n    class Loading<T> : LoadableState<T>()\n\n    class Success<T>(val data: T) : LoadableState<T>()\n\n    companion object {\n\n        fun <T> idle(): LoadableState<T> {\n            return Idle()\n        }\n\n        fun <T> success(data: T): LoadableState<T> {\n            return Success(data)\n        }\n\n        fun <T> loading(): LoadableState<T> {\n            return Loading()\n        }\n\n        fun <T> failed(exception: Throwable): LoadableState<T> {\n            return Failed(exception)\n        }\n    }\n}\n\n@Composable\nfun <T> LoadableLayout(\n    modifier: Modifier = Modifier,\n    state: LoadableState<T>,\n    failed: (@Composable BoxScope.(Throwable) -> Unit)? = null,\n    loading: (@Composable BoxScope.() -> Unit)? = null,\n    idle: (@Composable BoxScope.() -> Unit)? = null,\n    content: @Composable BoxScope.(T) -> Unit,\n) {\n    Box(modifier = modifier) {\n        when (state) {\n            is LoadableState.Loading -> {\n                loading?.invoke(this) ?: DefaultLoading()\n            }\n\n            is LoadableState.Failed -> {\n                failed?.invoke(this, state.exception) ?: DefaultFailed(exception = state.exception)\n            }\n\n            is LoadableState.Success -> {\n                content(state.data)\n            }\n\n            is LoadableState.Idle -> {\n                idle?.invoke(this) ?: DefaultIdle()\n            }\n        }\n    }\n}\n\n@Composable\nfun BoxScope.DefaultLoading(modifier: Modifier = Modifier) {\n    Box(modifier = modifier.fillMaxWidth(0.3F).align(Alignment.Center)) {\n        CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))\n    }\n}\n\n@Composable\nfun BoxScope.DefaultFailed(\n    modifier: Modifier = Modifier,\n    errorMessage: String,\n    onRetryClick: (() -> Unit)? = null,\n) {\n    Column(\n        modifier = modifier.align(Alignment.Center),\n        horizontalAlignment = Alignment.CenterHorizontally,\n        verticalArrangement = Arrangement.Center,\n    ) {\n        Text(\n            fontSize = 18.sp,\n            text = errorMessage,\n        )\n        if (onRetryClick != null) {\n            Button(\n                onClick = onRetryClick,\n            ) {\n                Text(org.jetbrains.compose.resources.stringResource(LocalizedString.retry))\n            }\n        }\n    }\n}\n\n@Composable\nfun BoxScope.DefaultFailed(\n    modifier: Modifier = Modifier,\n    exception: Throwable,\n    onRetryClick: (() -> Unit)? = null,\n) {\n    DefaultFailed(\n        modifier = modifier,\n        errorMessage = exception.message.orEmpty(),\n        onRetryClick = onRetryClick,\n    )\n}\n\n@Composable\nfun BoxScope.DefaultEmpty(\n    modifier: Modifier = Modifier,\n    message: String = org.jetbrains.compose.resources.stringResource(LocalizedString.empty),\n) {\n    Box(\n        modifier = modifier.fillMaxSize(),\n        contentAlignment = Alignment.Center\n    ) {\n        Text(text = message)\n    }\n}\n\n@Composable\nfun BoxScope.DefaultIdle(\n    modifier: Modifier = Modifier,\n) {\n    Box(modifier = modifier)\n}\n\nfun <T> MutableStateFlow<LoadableState<T>>.updateToSuccess(\n    data: T\n) {\n    update {\n        LoadableState.success(data)\n    }\n}\n\nfun <T> MutableStateFlow<LoadableState<T>>.updateToLoading() {\n    update {\n        LoadableState.loading()\n    }\n}\n\nfun <T> MutableStateFlow<LoadableState<T>>.updateToFailed(e: Throwable) {\n    update {\n        LoadableState.failed(e)\n    }\n}\n\nfun <T> MutableStateFlow<LoadableState<T>>.updateOnSuccess(\n    updater: (T) -> T,\n) {\n    update {\n        if (it.isSuccess) {\n            val data = it.requireSuccessData()\n            LoadableState.success(updater(data))\n        } else {\n            it\n        }\n    }\n}\n\nfun <T> LoadableState<T>.requireSuccessData(): T {\n    return (this as LoadableState.Success).data\n}\n\nfun <T> LoadableState<T>.successDataOrNull(): T? {\n    return (this as? LoadableState.Success)?.data\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/Loading.kt",
    "content": "package com.zhangke.framework.composable\n\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.heightIn\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.material3.CircularProgressIndicator\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\n\nprivate val lineHeight = 68.dp\n\n@Composable\nfun LoadingLineItem(\n    modifier: Modifier = Modifier,\n) {\n    Box(\n        modifier = modifier.height(lineHeight),\n        contentAlignment = Alignment.Center,\n    ) {\n        CircularProgressIndicator(\n            modifier = Modifier.size(32.dp)\n        )\n    }\n}\n\n@Composable\nfun LoadErrorLineItem(\n    modifier: Modifier,\n    errorMessage: String,\n) {\n    Box(\n        modifier = modifier.heightIn(min = lineHeight),\n        contentAlignment = Alignment.Center,\n    ) {\n        Text(\n            modifier = Modifier\n                .fillMaxWidth()\n                .padding(horizontal = 16.dp, vertical = 16.dp),\n            textAlign = TextAlign.Center,\n            text = errorMessage,\n            maxLines = 3,\n            overflow = TextOverflow.Ellipsis,\n        )\n    }\n}\n\n@Composable\nfun LoadErrorLineItem(\n    modifier: Modifier,\n    errorMessage: TextString,\n) {\n    LoadErrorLineItem(\n        modifier = modifier,\n        errorMessage = textString(text = errorMessage),\n    )\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/LoadingDialog.kt",
    "content": "package com.zhangke.framework.composable\n\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.CircularProgressIndicator\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.window.Dialog\nimport androidx.compose.ui.window.DialogProperties\nimport com.zhangke.fread.localization.LocalizedString\nimport org.jetbrains.compose.resources.stringResource\n\n@Composable\nfun CancelableLoadingDialog(\n    loading: Boolean,\n    onDismissRequest: () -> Unit,\n    onCancelClick: () -> Unit,\n    properties: DialogProperties = DialogProperties(),\n) {\n    if (loading) {\n        Dialog(\n            onDismissRequest = onDismissRequest,\n            properties = properties,\n        ) {\n            Surface(\n                modifier = Modifier.fillMaxWidth(),\n                shape = RoundedCornerShape(16.dp),\n            ) {\n                Column(\n                    modifier = Modifier.fillMaxWidth()\n                        .padding(16.dp),\n                    horizontalAlignment = Alignment.CenterHorizontally,\n                ) {\n                    Spacer(modifier = Modifier.height(16.dp))\n                    CircularProgressIndicator(\n                        modifier = Modifier\n                            .padding(vertical = 24.dp, horizontal = 64.dp)\n                            .size(80.dp)\n                    )\n                    Box(modifier = Modifier.fillMaxWidth()) {\n                        TextButton(\n                            modifier = Modifier.align(Alignment.CenterEnd),\n                            onClick = onCancelClick,\n                        ) {\n                            Text(\n                                text = stringResource(LocalizedString.cancel)\n                            )\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nfun LoadingDialog(\n    loading: Boolean,\n    properties: DialogProperties = DialogProperties(),\n    onDismissRequest: () -> Unit = {},\n) {\n    if (loading) {\n        Dialog(\n            onDismissRequest = onDismissRequest,\n            properties = properties,\n        ) {\n            Surface(\n                modifier = Modifier,\n                shape = RoundedCornerShape(16.dp),\n            ) {\n                CircularProgressIndicator(\n                    modifier = Modifier\n                        .padding(vertical = 24.dp, horizontal = 64.dp)\n                        .size(80.dp)\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/LocalContentPadding.kt",
    "content": "package com.zhangke.framework.composable\n\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.calculateEndPadding\nimport androidx.compose.foundation.layout.calculateStartPadding\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.compositionLocalOf\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.composed\nimport androidx.compose.ui.platform.LocalLayoutDirection\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\n\nval LocalContentPadding = compositionLocalOf {\n    PaddingValues(0.dp)\n}\n\n@Composable\nfun updateTopPadding(topPadding: Dp): PaddingValues {\n    val paddings = LocalContentPadding.current\n    val layoutDirection = LocalLayoutDirection.current\n    return PaddingValues(\n        start = paddings.calculateStartPadding(layoutDirection),\n        top = topPadding,\n        end = paddings.calculateEndPadding(layoutDirection),\n        bottom = paddings.calculateBottomPadding(),\n    )\n}\n\n@Composable\nfun plusTopPadding(topPadding: Dp): PaddingValues {\n    val paddings = LocalContentPadding.current\n    val layoutDirection = LocalLayoutDirection.current\n    return PaddingValues(\n        start = paddings.calculateStartPadding(layoutDirection),\n        top = topPadding + paddings.calculateTopPadding(),\n        end = paddings.calculateEndPadding(layoutDirection),\n        bottom = paddings.calculateBottomPadding(),\n    )\n}\n\n@Composable\nfun plusContentPadding(paddingValues: PaddingValues): PaddingValues {\n    val localPaddingValues = LocalContentPadding.current\n    val layoutDirection = LocalLayoutDirection.current\n    return PaddingValues(\n        start = localPaddingValues.calculateStartPadding(layoutDirection) +\n                paddingValues.calculateStartPadding(layoutDirection),\n        top = localPaddingValues.calculateTopPadding() + paddingValues.calculateTopPadding(),\n        end = localPaddingValues.calculateEndPadding(layoutDirection) +\n                paddingValues.calculateEndPadding(layoutDirection),\n        bottom = localPaddingValues.calculateBottomPadding() + paddingValues.calculateBottomPadding(),\n    )\n}\n\n@Composable\nfun PaddingValues.plus(paddingValues: PaddingValues): PaddingValues {\n    val layoutDirection = LocalLayoutDirection.current\n    return PaddingValues(\n        start = this.calculateStartPadding(layoutDirection) +\n                paddingValues.calculateStartPadding(layoutDirection),\n        top = this.calculateTopPadding() + paddingValues.calculateTopPadding(),\n        end = this.calculateEndPadding(layoutDirection) +\n                paddingValues.calculateEndPadding(layoutDirection),\n        bottom = this.calculateBottomPadding() + paddingValues.calculateBottomPadding(),\n    )\n}\n\nfun Modifier.contentBottomPadding(): Modifier = composed {\n    val paddings = LocalContentPadding.current\n    this.padding(bottom = paddings.calculateBottomPadding())\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/LocalSnackMessage.kt",
    "content": "package com.zhangke.framework.composable\n\nimport androidx.compose.material3.SnackbarHostState\nimport androidx.compose.runtime.ProvidableCompositionLocal\nimport androidx.compose.runtime.staticCompositionLocalOf\n\nval LocalSnackbarHostState: ProvidableCompositionLocal<SnackbarHostState?> =\n    staticCompositionLocalOf { null }\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/NavigationBar.kt",
    "content": "package com.zhangke.framework.composable\n\nimport androidx.compose.animation.animateColorAsState\nimport androidx.compose.animation.core.animateFloatAsState\nimport androidx.compose.animation.core.tween\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.indication\nimport androidx.compose.foundation.interaction.InteractionSource\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.interaction.PressInteraction\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.RowScope\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.windowInsetsPadding\nimport androidx.compose.foundation.selection.selectable\nimport androidx.compose.foundation.selection.selectableGroup\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.material.ripple\nimport androidx.compose.material3.ColorScheme\nimport androidx.compose.material3.LocalContentColor\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.NavigationBarDefaults\nimport androidx.compose.material3.NavigationBarItemColors\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.contentColorFor\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.State\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.graphicsLayer\nimport androidx.compose.ui.layout.Layout\nimport androidx.compose.ui.layout.MeasureResult\nimport androidx.compose.ui.layout.MeasureScope\nimport androidx.compose.ui.layout.Placeable\nimport androidx.compose.ui.layout.layoutId\nimport androidx.compose.ui.layout.onSizeChanged\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.semantics.Role\nimport androidx.compose.ui.semantics.clearAndSetSemantics\nimport androidx.compose.ui.unit.Constraints\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.constrainHeight\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.util.fastFirst\nimport androidx.compose.ui.util.fastFirstOrNull\nimport com.zhangke.framework.blur.applyBlurEffect\nimport com.zhangke.framework.blur.blurEffectContainerColor\nimport kotlinx.coroutines.flow.map\nimport kotlin.math.roundToInt\n\n@Composable\nfun NavigationBar(\n    modifier: Modifier = Modifier,\n    containerColor: Color = NavigationBarDefaults.containerColor,\n    contentColor: Color = MaterialTheme.colorScheme.contentColorFor(containerColor),\n    tonalElevation: Dp = NavigationBarDefaults.Elevation,\n    windowInsets: WindowInsets = NavigationBarDefaults.windowInsets,\n    content: @Composable RowScope.() -> Unit\n) {\n    Surface(\n        color = blurEffectContainerColor(containerColor = containerColor),\n        contentColor = contentColor,\n        tonalElevation = tonalElevation,\n        modifier = modifier.applyBlurEffect(\n            containerColor = containerColor,\n        ),\n    ) {\n        Row(\n            modifier = Modifier\n                .fillMaxWidth()\n                .windowInsetsPadding(windowInsets)\n                .padding(top = 16.dp, bottom = 16.dp)\n                .selectableGroup(),\n            horizontalArrangement = Arrangement.spacedBy(8.dp),\n            verticalAlignment = Alignment.CenterVertically,\n            content = content,\n        )\n    }\n}\n\n@Composable\nfun RowScope.NavigationBarItem(\n    selected: Boolean,\n    onClick: () -> Unit,\n    icon: @Composable () -> Unit,\n    modifier: Modifier = Modifier,\n    enabled: Boolean = true,\n    label: @Composable (() -> Unit)? = null,\n    alwaysShowLabel: Boolean = true,\n    colors: NavigationBarItemColors = NavigationBarItemDefaults.colors(),\n    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }\n) {\n    val styledIcon = @Composable {\n        val iconColor by colors.iconColor(selected = selected, enabled = enabled)\n        // If there's a label, don't have a11y services repeat the icon description.\n        val clearSemantics = label != null && (alwaysShowLabel || selected)\n        Box(modifier = if (clearSemantics) Modifier.clearAndSetSemantics {} else Modifier) {\n            CompositionLocalProvider(LocalContentColor provides iconColor, content = icon)\n        }\n    }\n\n    var itemWidth by remember { mutableIntStateOf(0) }\n\n    Box(\n        modifier\n            .selectable(\n                selected = selected,\n                onClick = onClick,\n                enabled = enabled,\n                role = Role.Tab,\n                interactionSource = interactionSource,\n                indication = null,\n            )\n            .weight(1f)\n            .onSizeChanged {\n                itemWidth = it.width\n            },\n        contentAlignment = Alignment.Center,\n        propagateMinConstraints = true,\n    ) {\n        val animationProgress: State<Float> = animateFloatAsState(\n            targetValue = if (selected) 1f else 0f,\n            animationSpec = tween(ItemAnimationDurationMillis),\n            label = \"navigation-bar\",\n        )\n\n        val deltaOffset: Offset\n        with(LocalDensity.current) {\n            val indicatorWidth = 64.dp.roundToPx()\n            deltaOffset = Offset(\n                (itemWidth - indicatorWidth).toFloat() / 2,\n                IndicatorVerticalOffset.toPx()\n            )\n        }\n        val offsetInteractionSource = remember(interactionSource, deltaOffset) {\n            MappedInteractionSource(interactionSource, deltaOffset)\n        }\n\n        val indicatorRipple = @Composable {\n            @Suppress(\"DEPRECATION_ERROR\")\n            (Box(\n                Modifier\n                    .layoutId(IndicatorRippleLayoutIdTag)\n                    .clip(CircleShape)\n                    .indication(\n                        offsetInteractionSource,\n                        ripple()\n                    )\n            ))\n        }\n        val indicator = @Composable {\n            Box(\n                Modifier\n                    .layoutId(IndicatorLayoutIdTag)\n                    .graphicsLayer { alpha = animationProgress.value }\n                    .background(\n                        color = colors.selectedIndicatorColor,\n                        shape = CircleShape,\n                    )\n            )\n        }\n\n        NavigationBarItemLayout(\n            indicatorRipple = indicatorRipple,\n            indicator = indicator,\n            icon = styledIcon,\n            animationProgress = { animationProgress.value },\n        )\n    }\n}\n\n@Composable\nprivate fun NavigationBarItemColors.iconColor(selected: Boolean, enabled: Boolean): State<Color> {\n    val targetValue = when {\n        !enabled -> disabledIconColor\n        selected -> selectedIconColor\n        else -> unselectedIconColor\n    }\n    return animateColorAsState(\n        targetValue = targetValue,\n        animationSpec = tween(ItemAnimationDurationMillis),\n        label = \"BottomNavIconColorAnimation\",\n    )\n}\n\nobject NavigationBarItemDefaults {\n\n    @Composable\n    fun colors() = MaterialTheme.colorScheme.defaultNavigationBarItemColors\n\n    @Composable\n    fun colors(\n        selectedIconColor: Color = Color.Unspecified,\n        selectedTextColor: Color = Color.Unspecified,\n        indicatorColor: Color = Color.Unspecified,\n        unselectedIconColor: Color = Color.Unspecified,\n        unselectedTextColor: Color = Color.Unspecified,\n        disabledIconColor: Color = Color.Unspecified,\n        disabledTextColor: Color = Color.Unspecified,\n    ): NavigationBarItemColors = MaterialTheme.colorScheme.defaultNavigationBarItemColors.copy(\n        selectedIconColor = selectedIconColor,\n        selectedTextColor = selectedTextColor,\n        selectedIndicatorColor = indicatorColor,\n        unselectedIconColor = unselectedIconColor,\n        unselectedTextColor = unselectedTextColor,\n        disabledIconColor = disabledIconColor,\n        disabledTextColor = disabledTextColor,\n    )\n\n    private const val DisabledAlpha = 0.38f\n\n    private val ColorScheme.defaultNavigationBarItemColors: NavigationBarItemColors\n        @Composable\n        get() {\n            return NavigationBarItemColors(\n                selectedIconColor = onSecondaryContainer,\n                selectedTextColor = onSurface,\n                selectedIndicatorColor = secondaryContainer,\n                unselectedIconColor = onSurfaceVariant,\n                unselectedTextColor = onSurfaceVariant,\n                disabledIconColor = onSurfaceVariant.copy(alpha = DisabledAlpha),\n                disabledTextColor = onSurfaceVariant.copy(alpha = DisabledAlpha),\n            )\n        }\n}\n\n@Composable\nprivate fun NavigationBarItemLayout(\n    indicatorRipple: @Composable () -> Unit,\n    indicator: @Composable () -> Unit,\n    icon: @Composable () -> Unit,\n    animationProgress: () -> Float,\n) {\n    Layout(\n        content = {\n            indicatorRipple()\n            indicator()\n            Box(Modifier.layoutId(IconLayoutIdTag)) { icon() }\n        },\n        measurePolicy = { measurables, constraints ->\n            @Suppress(\"NAME_SHADOWING\")\n            val animationProgress = animationProgress()\n            val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)\n            val iconPlaceable =\n                measurables.fastFirst { it.layoutId == IconLayoutIdTag }.measure(looseConstraints)\n\n            val totalIndicatorWidth =\n                iconPlaceable.width + (IndicatorHorizontalPadding * 2).roundToPx()\n            val animatedIndicatorWidth = (totalIndicatorWidth * animationProgress).roundToInt()\n            val indicatorHeight = iconPlaceable.height + (IndicatorVerticalPadding * 2).roundToPx()\n            val indicatorRipplePlaceable =\n                measurables\n                    .fastFirst { it.layoutId == IndicatorRippleLayoutIdTag }\n                    .measure(\n                        Constraints.fixed(\n                            width = totalIndicatorWidth,\n                            height = indicatorHeight\n                        )\n                    )\n            val indicatorPlaceable =\n                measurables\n                    .fastFirstOrNull { it.layoutId == IndicatorLayoutIdTag }\n                    ?.measure(\n                        Constraints.fixed(\n                            width = animatedIndicatorWidth,\n                            height = indicatorHeight\n                        )\n                    )\n            placeIcon(iconPlaceable, indicatorRipplePlaceable, indicatorPlaceable, constraints)\n        }\n    )\n}\n\n/**\n * Places the provided [Placeable]s in the center of the provided [constraints].\n */\nprivate fun MeasureScope.placeIcon(\n    iconPlaceable: Placeable,\n    indicatorRipplePlaceable: Placeable,\n    indicatorPlaceable: Placeable?,\n    constraints: Constraints\n): MeasureResult {\n    val width = constraints.maxWidth\n    val height = constraints.constrainHeight(32.dp.roundToPx())\n\n    val iconX = (width - iconPlaceable.width) / 2\n    val iconY = (height - iconPlaceable.height) / 2\n\n    val rippleX = (width - indicatorRipplePlaceable.width) / 2\n    val rippleY = (height - indicatorRipplePlaceable.height) / 2\n\n    return layout(width, height) {\n        indicatorPlaceable?.let {\n            val indicatorX = (width - it.width) / 2\n            val indicatorY = (height - it.height) / 2\n            it.placeRelative(indicatorX, indicatorY)\n        }\n        iconPlaceable.placeRelative(iconX, iconY)\n        indicatorRipplePlaceable.placeRelative(rippleX, rippleY)\n    }\n}\n\nprivate class MappedInteractionSource(\n    underlyingInteractionSource: InteractionSource,\n    private val delta: Offset\n) : InteractionSource {\n\n    private val mappedPresses = mutableMapOf<PressInteraction.Press, PressInteraction.Press>()\n\n    override val interactions = underlyingInteractionSource.interactions.map { interaction ->\n        when (interaction) {\n            is PressInteraction.Press -> {\n                val mappedPress = mapPress(interaction)\n                mappedPresses[interaction] = mappedPress\n                mappedPress\n            }\n\n            is PressInteraction.Cancel -> {\n                val mappedPress = mappedPresses.remove(interaction.press)\n                if (mappedPress == null) {\n                    interaction\n                } else {\n                    PressInteraction.Cancel(mappedPress)\n                }\n            }\n\n            is PressInteraction.Release -> {\n                val mappedPress = mappedPresses.remove(interaction.press)\n                if (mappedPress == null) {\n                    interaction\n                } else {\n                    PressInteraction.Release(mappedPress)\n                }\n            }\n\n            else -> interaction\n        }\n    }\n\n    private fun mapPress(press: PressInteraction.Press): PressInteraction.Press =\n        PressInteraction.Press(press.pressPosition - delta)\n}\n\nprivate const val IndicatorRippleLayoutIdTag: String = \"indicatorRipple\"\n\nprivate const val IndicatorLayoutIdTag: String = \"indicator\"\n\nprivate const val IconLayoutIdTag: String = \"icon\"\n\nprivate const val ItemAnimationDurationMillis: Int = 100\n\nprivate val IndicatorHorizontalPadding: Dp =\n    (64.dp - 24.dp) / 2\n\nprivate val IndicatorVerticalPadding: Dp =\n    (32.dp - 24.dp) / 2\n\nprivate val IndicatorVerticalOffset: Dp = 12.dp\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/NestedScrollConnection.kt",
    "content": "package com.zhangke.framework.composable\n\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.input.nestedscroll.NestedScrollConnection\nimport androidx.compose.ui.input.nestedscroll.nestedScroll\n\nfun Modifier.applyNestedScrollConnection(\n    nestedScrollConnection: NestedScrollConnection?,\n): Modifier {\n    if (nestedScrollConnection == null) return this\n    return Modifier.nestedScroll(nestedScrollConnection) then this\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/NoDoubleClick.kt",
    "content": "package com.zhangke.framework.composable\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.semantics.Role\nimport kotlin.time.Clock\nimport kotlin.time.ExperimentalTime\n\nprivate var latestClickTime = 0L\nprivate var intervalThreshold = 700L\n\n@OptIn(ExperimentalTime::class)\nfun Modifier.noDoubleClick(\n    enabled: Boolean = true,\n    onClickLabel: String? = null,\n    role: Role? = null,\n    onClick: () -> Unit\n): Modifier {\n    return clickable(\n        enabled = enabled,\n        onClickLabel = onClickLabel,\n        role = role,\n        onClick = {\n            val currentTime = Clock.System.now().toEpochMilliseconds()\n            if (currentTime - latestClickTime > intervalThreshold) {\n                onClick()\n            }\n            latestClickTime = currentTime\n        },\n    )\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/NoRippleClick.kt",
    "content": "package com.zhangke.framework.composable\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\n\n@Composable\nfun Modifier.noRippleClick(enabled: Boolean = true, onClick: () -> Unit): Modifier {\n    return Modifier.clickable(\n        enabled = enabled,\n        interactionSource = remember { MutableInteractionSource() },\n        onClick = onClick,\n        indication = null,\n    ) then this\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/Offset.kt",
    "content": "package com.zhangke.framework.composable\n\nimport androidx.compose.foundation.layout.offset\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.composed\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.platform.LocalDensity\nimport com.zhangke.framework.utils.pxToDp\n\nfun Modifier.offset(offset: Offset): Modifier = composed {\n    val density = LocalDensity.current\n    Modifier.offset(\n        x = offset.x.pxToDp(density),\n        y = offset.y.pxToDp(density),\n    ) then this\n}\n\nval Offset.isZero: Boolean get() = x == 0F && y == 0F\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/PaddingValuesUtils.kt",
    "content": "package com.zhangke.framework.composable\n\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.calculateEndPadding\nimport androidx.compose.foundation.layout.calculateStartPadding\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.composed\nimport androidx.compose.ui.platform.LocalLayoutDirection\n\nfun Modifier.startPadding(paddings: PaddingValues): Modifier = composed {\n    val dir = LocalLayoutDirection.current\n    this.padding(start = paddings.calculateStartPadding(dir))\n}\n\nfun Modifier.endPadding(paddings: PaddingValues): Modifier = composed {\n    val dir = LocalLayoutDirection.current\n    this.padding(end = paddings.calculateEndPadding(dir))\n}\n\nfun Modifier.horizontalPadding(paddings: PaddingValues): Modifier = composed {\n    val dir = LocalLayoutDirection.current\n    this.padding(\n        start = paddings.calculateStartPadding(dir),\n        end = paddings.calculateEndPadding(dir),\n    )\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/Placeholder.kt",
    "content": "package com.zhangke.framework.composable\n\nimport androidx.compose.animation.core.AnimationSpec\nimport androidx.compose.animation.core.spring\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.RectangleShape\nimport androidx.compose.ui.graphics.Shape\nimport com.eygraber.compose.placeholder.PlaceholderHighlight\nimport com.eygraber.compose.placeholder.material3.fade\nimport com.eygraber.compose.placeholder.material3.placeholder\n\n@Composable\nfun Modifier.freadPlaceholder(\n    visible: Boolean,\n    color: Color = Color.Unspecified,\n    shape: Shape = RectangleShape,\n    // highlight: PlaceholderHighlight? = null,\n    placeholderFadeAnimationSpec: AnimationSpec<Float> = spring(),\n    contentFadeAnimationSpec: AnimationSpec<Float> = spring(),\n): Modifier = then(\n    Modifier.placeholder(\n        visible = visible,\n        color = color,\n        shape = shape,\n        highlight = PlaceholderHighlight.fade(),\n        placeholderFadeAnimationSpec = placeholderFadeAnimationSpec,\n        contentFadeAnimationSpec = contentFadeAnimationSpec,\n    )\n)\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/PopupFloatingActionButton.kt",
    "content": "package com.zhangke.framework.composable\n\nimport androidx.compose.animation.core.animateFloatAsState\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.ColumnScope\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Add\nimport androidx.compose.material3.DropdownMenu\nimport androidx.compose.material3.FloatingActionButton\nimport androidx.compose.material3.Icon\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.rotate\nimport androidx.compose.ui.unit.dp\n\n@Composable\nfun PopupFloatingActionButton(\n    modifier: Modifier = Modifier,\n    popupContent: @Composable ColumnScope.() -> Unit,\n) {\n    var expanded by remember { mutableStateOf(false) }\n    FloatingActionButton(\n        modifier = modifier,\n        onClick = { if (!expanded) expanded = true },\n    ) {\n        val rotate by animateFloatAsState(if (expanded) 45f else 0f, label = \"PopupFloatingActionButton\")\n        Icon(\n            modifier = Modifier.rotate(rotate),\n            imageVector = Icons.Default.Add,\n            contentDescription = \"Add\",\n        )\n    }\n    DropdownMenu(\n        modifier = Modifier.size(200.dp),\n        expanded = expanded, onDismissRequest = { expanded = false }) {\n        Box(modifier = Modifier.size(200.dp))\n    }\n//    if (expanded) {\n//        Popup(\n//            onDismissRequest = {\n//                expanded = false\n//            },\n//        ) {\n//            Column(modifier = Modifier.size(200.dp)) {\n//                popupContent()\n//            }\n//        }\n//    }\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/PopupMenu.kt",
    "content": "package com.zhangke.framework.composable\n\nimport androidx.compose.foundation.layout.ColumnScope\nimport androidx.compose.material3.DropdownMenu\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.DpOffset\nimport androidx.compose.ui.unit.dp\n\n@Composable\nfun PopupMenu(\n    expanded: Boolean,\n    onDismissRequest: () -> Unit,\n    modifier: Modifier = Modifier,\n    offset: DpOffset = DpOffset(0.dp, 0.dp),\n    content: @Composable ColumnScope.() -> Unit,\n) {\n    DropdownMenu(\n        expanded = expanded,\n        onDismissRequest = onDismissRequest,\n        modifier = modifier,\n        offset = offset,\n        containerColor = MaterialTheme.colorScheme.surface.copy(alpha = 0.98F),\n        content = content,\n    )\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/PredictiveBackProgressState.kt",
    "content": "package com.zhangke.framework.composable\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.Stable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableFloatStateOf\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.navigationevent.NavigationEventInfo\nimport androidx.navigationevent.compose.NavigationEventHandler\nimport androidx.navigationevent.compose.rememberNavigationEventState\n\n@Stable\nclass PredictiveBackProgressState internal constructor(initial: Float) {\n    var progress by mutableFloatStateOf(initial) // 0f..1f\n}\n\n@Composable\nfun rememberPredictiveBackProgressState(): PredictiveBackProgressState {\n    val state = rememberSaveable { PredictiveBackProgressState(0f) }\n    val navEventState = rememberNavigationEventState(NavigationEventInfo.None)\n    NavigationEventHandler(\n        state = navEventState,\n        isBackEnabled = true,\n        onBackCancelled = { state.progress = 0f },\n        onBackCompleted = { state.progress = 0f },\n    )\n    return state\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/ScreenUtils.kt",
    "content": "package com.zhangke.framework.composable\n\nimport androidx.compose.runtime.Composable\nimport androidx.navigation3.runtime.NavBackStack\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.nav.LocalNavBackStack\nimport kotlinx.coroutines.flow.Flow\n\n@Composable\nfun ConsumeOpenScreenFlow(\n    openScreenFlow: Flow<NavKey>,\n    backStack: NavBackStack<NavKey> = LocalNavBackStack.currentOrThrow,\n) {\n    ConsumeFlow(openScreenFlow) {\n        val lastItem = backStack.lastOrNull()\n        if (lastItem != it) {\n            backStack.add(it)\n        }\n    }\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/ScrollTopAppBar.kt",
    "content": "package com.zhangke.framework.composable\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.RowScope\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.TopAppBarScrollBehavior\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.lerp\nimport androidx.compose.ui.layout.SubcomposeLayout\nimport com.zhangke.framework.blur.applyBlurEffect\nimport com.zhangke.framework.blur.blurEffectContainerColor\nimport kotlin.math.roundToInt\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun ScrollTopAppBar(\n    modifier: Modifier,\n    title: @Composable () -> Unit,\n    actions: @Composable RowScope.() -> Unit,\n    scrollBehavior: TopAppBarScrollBehavior,\n    colors: ScrollTopAppBarColors = ScrollTopAppBarColors.default(),\n    navigationIcon: @Composable () -> Unit = {},\n) {\n    val appBarContainerColor by remember(scrollBehavior, colors) {\n        derivedStateOf {\n            val fraction = scrollBehavior.state.overlappedFraction.coerceIn(0F, 1F)\n            lerp(colors.containerColor, colors.scrolledContainerColor, fraction)\n        }\n    }\n    SubcomposeLayout(\n        modifier = modifier.background(appBarContainerColor),\n    ) { constraints ->\n        val topAppBarPlaceable = subcompose(\"topAppBar\") {\n            SingleRowTopAppBar(\n                modifier = modifier.applyBlurEffect(containerColor = appBarContainerColor),\n                navigationIcon = navigationIcon,\n                title = title,\n                actions = actions,\n                colors = TopAppBarColors.default(\n                    containerColor = blurEffectContainerColor(containerColor = appBarContainerColor),\n                    navigationIconContentColor = colors.contentColor,\n                    titleContentColor = colors.contentColor,\n                    actionIconContentColor = colors.contentColor,\n                ),\n            )\n        }.first().measure(constraints)\n        val totalHeight = topAppBarPlaceable.height\n        if (scrollBehavior.state.heightOffsetLimit != -totalHeight.toFloat()) {\n            scrollBehavior.state.heightOffsetLimit = -totalHeight.toFloat()\n        }\n        val heightOffset = scrollBehavior.state.heightOffset\n        val layoutHeight = (totalHeight + heightOffset).roundToInt().coerceAtLeast(0)\n        layout(constraints.maxWidth, layoutHeight) {\n            val topAppBarY = heightOffset.roundToInt()\n            topAppBarPlaceable.placeRelative(0, topAppBarY)\n        }\n    }\n}\n\ndata class ScrollTopAppBarColors(\n    val containerColor: Color,\n    val scrolledContainerColor: Color,\n    val contentColor: Color,\n) {\n\n    companion object {\n\n        @Composable\n        fun default(\n            containerColor: Color = MaterialTheme.colorScheme.surface,\n            scrolledContainerColor: Color = MaterialTheme.colorScheme.surfaceContainer,\n            contentColor: Color = MaterialTheme.colorScheme.onSurface,\n        ): ScrollTopAppBarColors {\n            return ScrollTopAppBarColors(\n                containerColor = containerColor,\n                scrolledContainerColor = scrolledContainerColor,\n                contentColor = contentColor,\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/SearchToolbar.kt",
    "content": "package com.zhangke.framework.composable\n\nimport androidx.compose.foundation.layout.ColumnScope\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Clear\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.SearchBar\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.graphics.vector.rememberVectorPainter\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun SearchToolbar(\n    onBackClick: () -> Unit,\n    placeholderText: String,\n    onQueryChange: (query: String) -> Unit,\n    onSearch: (query: String) -> Unit,\n    content: @Composable ColumnScope.() -> Unit,\n) {\n    var query by remember {\n        mutableStateOf(\"\")\n    }\n    SearchBar(\n        query = query,\n        onQueryChange = {\n            query = it\n            onQueryChange(it)\n        },\n        onSearch = onSearch,\n        leadingIcon = {\n            Toolbar.BackButton(onBackClick = onBackClick)\n        },\n        trailingIcon = {\n            IconButton(onClick = { query = \"\" }) {\n                Icon(\n                    painter = rememberVectorPainter(image = Icons.Filled.Clear),\n                    contentDescription = \"clear\"\n                )\n            }\n        },\n        active = true,\n        placeholder = {\n            Text(text = placeholderText)\n        },\n        onActiveChange = {\n            if (!it) onBackClick()\n        },\n        content = content,\n    )\n}"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/Size.kt",
    "content": "package com.zhangke.framework.composable\n\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.composed\nimport androidx.compose.ui.geometry.Size\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.unit.IntSize\nimport com.zhangke.framework.utils.pxToDp\n\nfun Modifier.size(size: IntSize): Modifier = composed {\n    val density = LocalDensity.current\n    size(width = size.width.pxToDp(density), height = size.height.pxToDp(density))\n}\n\nfun Size.aspectRatio(): Float {\n    return width / height\n}\n\nfun IntSize.aspectRatio(): Float {\n    return width.toFloat() / height.toFloat()\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/SlickRoundCornerShape.kt",
    "content": "package com.zhangke.framework.composable\n\nimport androidx.compose.foundation.shape.CornerSize\nimport androidx.compose.ui.geometry.CornerRadius\nimport androidx.compose.ui.geometry.Rect\nimport androidx.compose.ui.geometry.RoundRect\nimport androidx.compose.ui.geometry.Size\nimport androidx.compose.ui.geometry.toRect\nimport androidx.compose.ui.graphics.Outline\nimport androidx.compose.ui.graphics.Path\nimport androidx.compose.ui.graphics.Shape\nimport androidx.compose.ui.unit.Density\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.LayoutDirection\nimport androidx.compose.ui.unit.dp\nimport kotlin.math.sqrt\n\nclass SlickRoundCornerShape(\n    private val topStart: CornerSize,\n    private val topEnd: CornerSize,\n    private val bottomEnd: CornerSize,\n    private val bottomStart: CornerSize,\n) : Shape {\n\n    override fun createOutline(\n        size: Size,\n        layoutDirection: LayoutDirection,\n        density: Density,\n    ): Outline {\n        return createOutline(\n            size = size,\n            topStart = topStart.toPx(size, density),\n            topEnd = topEnd.toPx(size, density),\n            bottomEnd = bottomEnd.toPx(size, density),\n            bottomStart = bottomStart.toPx(size, density),\n            layoutDirection = layoutDirection\n        )\n    }\n\n    private fun createOutline(\n        size: Size,\n        topStart: Float,\n        topEnd: Float,\n        bottomEnd: Float,\n        bottomStart: Float,\n        layoutDirection: LayoutDirection,\n    ): Outline {\n        val height = size.height\n        val width = size.width\n        return if (topStart + topEnd + bottomEnd + bottomStart == 0.0f) {\n            Outline.Rectangle(size.toRect())\n        } else if (topStart == bottomStart && width < (topStart * 2F)) {\n            val radius = topStart\n            val path = Path()\n            if (height > radius * 2) {\n                buildSlickRoundCornerPath(path, size, radius)\n            } else {\n                buildSingleArcPath(path, size, radius)\n            }\n            Outline.Generic(path)\n        } else {\n            Outline.Rounded(\n                RoundRect(\n                    rect = size.toRect(),\n                    topLeft = CornerRadius(if (layoutDirection == LayoutDirection.Ltr) topStart else topEnd),\n                    topRight = CornerRadius(if (layoutDirection == LayoutDirection.Ltr) topEnd else topStart),\n                    bottomRight = CornerRadius(if (layoutDirection == LayoutDirection.Ltr) bottomEnd else bottomStart),\n                    bottomLeft = CornerRadius(if (layoutDirection == LayoutDirection.Ltr) bottomStart else bottomEnd)\n                )\n            )\n        }\n    }\n\n    private fun buildSlickRoundCornerPath(path: Path, size: Size, radius: Float) {\n        val width = size.width\n        val height = size.height\n        if (width > radius) {\n            path.addRoundRect(\n                RoundRect(\n                    rect = size.toRect(),\n                    topLeft = CornerRadius(radius),\n                    topRight = CornerRadius(0F),\n                    bottomRight = CornerRadius(0F),\n                    bottomLeft = CornerRadius(radius),\n                )\n            )\n            return\n        }\n        val arcHeight = sqrt(radius * radius - (radius - width) * (radius - width))\n        val yOffset = radius - arcHeight\n        path.arcTo(\n            rect = Rect(\n                left = 0F,\n                top = yOffset,\n                right = width * 2F,\n                bottom = yOffset + arcHeight * 2F,\n            ),\n            startAngleDegrees = 180F,\n            sweepAngleDegrees = 90F,\n            forceMoveTo = true,\n        )\n        val bottomArcBottom = height - yOffset\n        path.lineTo(x = width, y = bottomArcBottom)\n        path.arcTo(\n            rect = Rect(\n                left = 0F,\n                top = bottomArcBottom - arcHeight * 2,\n                right = width * 2F,\n                bottom = bottomArcBottom,\n            ),\n            startAngleDegrees = 90F,\n            sweepAngleDegrees = 90F,\n            forceMoveTo = true,\n        )\n        path.lineTo(0F, yOffset + arcHeight)\n        path.close()\n    }\n\n    private fun buildSingleArcPath(path: Path, size: Size, radius: Float) {\n        path.moveTo(size.width, 0F)\n        path.arcTo(\n            rect = Rect(0F, 0F, radius * 2F, radius * 2F),\n            startAngleDegrees = 90F,\n            sweepAngleDegrees = 180F,\n            forceMoveTo = true,\n        )\n        path.close()\n    }\n}\n\nfun SlickRoundCornerShape(corner: CornerSize) =\n    SlickRoundCornerShape(corner, corner, corner, corner)\n\nfun SlickRoundCornerShape(size: Dp) =\n    SlickRoundCornerShape(CornerSize(size))\n\nfun SlickRoundCornerShape(size: Float) =\n    SlickRoundCornerShape(CornerSize(size))\n\nfun SlickRoundCornerShape(percent: Int) =\n    SlickRoundCornerShape(CornerSize(percent))\n\nfun SlickRoundCornerShape(\n    topStart: Dp = 0.dp,\n    topEnd: Dp = 0.dp,\n    bottomEnd: Dp = 0.dp,\n    bottomStart: Dp = 0.dp\n) = SlickRoundCornerShape(\n    topStart = CornerSize(topStart),\n    topEnd = CornerSize(topEnd),\n    bottomEnd = CornerSize(bottomEnd),\n    bottomStart = CornerSize(bottomStart)\n)\n\nfun SlickRoundCornerShape(\n    topStart: Float = 0.0f,\n    topEnd: Float = 0.0f,\n    bottomEnd: Float = 0.0f,\n    bottomStart: Float = 0.0f\n) = SlickRoundCornerShape(\n    topStart = CornerSize(topStart),\n    topEnd = CornerSize(topEnd),\n    bottomEnd = CornerSize(bottomEnd),\n    bottomStart = CornerSize(bottomStart)\n)\n\nfun SlickRoundCornerShape(\n    /*@IntRange(from = 0, to = 100)*/\n    topStartPercent: Int = 0,\n    /*@IntRange(from = 0, to = 100)*/\n    topEndPercent: Int = 0,\n    /*@IntRange(from = 0, to = 100)*/\n    bottomEndPercent: Int = 0,\n    /*@IntRange(from = 0, to = 100)*/\n    bottomStartPercent: Int = 0\n) = SlickRoundCornerShape(\n    topStart = CornerSize(topStartPercent),\n    topEnd = CornerSize(topEndPercent),\n    bottomEnd = CornerSize(bottomEndPercent),\n    bottomStart = CornerSize(bottomStartPercent)\n)\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/Snackbar.kt",
    "content": "package com.zhangke.framework.composable\n\nimport androidx.compose.material3.SnackbarDuration\nimport androidx.compose.material3.SnackbarHost\nimport androidx.compose.material3.SnackbarHostState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.remember\nimport kotlinx.coroutines.flow.Flow\n\n@Composable\nfun rememberSnackbarHostState(): SnackbarHostState {\n    return remember { SnackbarHostState() }\n}\n\n@Composable\nfun snackbarHost(hostState: SnackbarHostState): @Composable (() -> Unit) {\n    return {\n        SnackbarHost(hostState = hostState)\n    }\n}\n\n@Composable\nfun ObserveSnackbar(\n    hostState: SnackbarHostState,\n    messageText: TextString?,\n    actionLabel: String? = null,\n    duration: SnackbarDuration = SnackbarDuration.Short\n) {\n    if (!messageText.isNullOrEmpty()) {\n        val message = textString(text = messageText!!)\n        LaunchedEffect(message) {\n            hostState.showSnackbar(message, actionLabel, duration = duration)\n        }\n    }\n}\n\n@Composable\nfun ConsumeSnackbarFlow(\n    hostState: SnackbarHostState?,\n    messageTextFlow: Flow<TextString>,\n    actionLabel: String? = null,\n    duration: SnackbarDuration = SnackbarDuration.Short\n) {\n    hostState ?: return\n    ConsumeFlow(messageTextFlow) {\n        val message = it.getString().take(180)\n        if (message.isNotEmpty()) {\n            hostState.showSnackbar(message, actionLabel, duration = duration)\n        }\n    }\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/StateSaver.kt",
    "content": "package com.zhangke.framework.composable\n\nimport androidx.compose.runtime.MutableState\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.saveable.Saver\nimport androidx.compose.ui.unit.Dp\n\nobject StateSaver {\n\n    val MutableNullableDpSaver: Saver<MutableState<Dp?>, *> = Saver(\n        save = {\n            it.value?.value\n        },\n        restore = {\n            mutableStateOf(Dp(it))\n        }\n    )\n\n    val MutableDpSaver: Saver<MutableState<Dp>, *> = Saver(\n        save = {\n            it.value.value\n        },\n        restore = {\n            mutableStateOf(Dp(it))\n        }\n    )\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/StyledIconButton.kt",
    "content": "package com.zhangke.framework.composable\n\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.IconButtonDefaults\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.vector.ImageVector\n\n@Composable\nfun StyledIconButton(\n    modifier: Modifier = Modifier,\n    imageVector: ImageVector,\n    style: IconButtonStyle,\n    onClick: () -> Unit,\n    contentDescription: String? = null,\n) {\n    val containerColor: Color\n    val contentColor: Color\n    when (style) {\n        IconButtonStyle.DISABLE -> {\n            // in disable style, btn will use disable-container-color, it's transparent.\n            containerColor = MaterialTheme.colorScheme.primaryContainer\n            contentColor = MaterialTheme.colorScheme.onSurface\n        }\n\n        IconButtonStyle.ALERT -> {\n            containerColor = MaterialTheme.colorScheme.errorContainer\n            contentColor = MaterialTheme.colorScheme.onErrorContainer\n        }\n\n        IconButtonStyle.STANDARD -> {\n            containerColor = MaterialTheme.colorScheme.primaryContainer\n            contentColor = MaterialTheme.colorScheme.onPrimaryContainer\n        }\n\n        IconButtonStyle.ACTIVE -> {\n            containerColor = MaterialTheme.colorScheme.primary\n            contentColor = MaterialTheme.colorScheme.onPrimary\n        }\n    }\n    IconButton(\n        modifier = modifier,\n        onClick = onClick,\n        colors = IconButtonDefaults.iconButtonColors(\n            containerColor = containerColor,\n            contentColor = contentColor,\n        ),\n    ) {\n        Icon(\n            imageVector = imageVector,\n            contentDescription = contentDescription,\n        )\n    }\n}\n\nenum class IconButtonStyle {\n\n    /**\n     * 禁用状态\n     */\n    DISABLE,\n\n    /**\n     * 警告状态，略微负面。\n     */\n    ALERT,\n\n    /**\n     * 普通状态，中性。\n     */\n    STANDARD,\n\n    /**\n     * 活跃状态，正面，希望被点击。\n     */\n    ACTIVE,\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/StyledTextButton.kt",
    "content": "package com.zhangke.framework.composable\n\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.material3.ButtonDefaults\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\n\n@Composable\nfun StyledTextButton(\n    modifier: Modifier,\n    text: String,\n    style: TextButtonStyle,\n    onClick: () -> Unit,\n    contentPadding: PaddingValues = ButtonDefaults.TextButtonContentPadding,\n) {\n    val containerColor: Color\n    val textColor: Color\n    when (style) {\n        TextButtonStyle.DISABLE -> {\n            // in disable style, btn will use disable-container-color, it's transparent.\n            containerColor = MaterialTheme.colorScheme.primaryContainer\n            textColor = MaterialTheme.colorScheme.onSurface\n        }\n\n        TextButtonStyle.ALERT -> {\n            containerColor = MaterialTheme.colorScheme.errorContainer\n            textColor = MaterialTheme.colorScheme.onErrorContainer\n        }\n\n        TextButtonStyle.STANDARD -> {\n            containerColor = MaterialTheme.colorScheme.primaryContainer\n            textColor = MaterialTheme.colorScheme.onPrimaryContainer\n        }\n\n        TextButtonStyle.ACTIVE -> {\n            containerColor = MaterialTheme.colorScheme.primary\n            textColor = MaterialTheme.colorScheme.onPrimary\n        }\n    }\n    TextButton(\n        modifier = modifier,\n        colors = ButtonDefaults.textButtonColors(\n            containerColor = containerColor,\n        ),\n        enabled = style != TextButtonStyle.DISABLE,\n        onClick = onClick,\n        contentPadding = contentPadding,\n    ) {\n        Text(\n            text = text,\n            color = textColor,\n        )\n    }\n}\n\nenum class TextButtonStyle {\n\n    /**\n     * 禁用状态\n     */\n    DISABLE,\n\n    /**\n     * 警告状态，略微负面。\n     */\n    ALERT,\n\n    /**\n     * 普通状态，中性。\n     */\n    STANDARD,\n\n    /**\n     * 活跃状态，正面，希望被点击。\n     */\n    ACTIVE,\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/SystemUi.kt",
    "content": "package com.zhangke.framework.composable\n\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.navigationBars\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.unit.Dp\nimport com.zhangke.framework.utils.pxToDp\n\n@Composable\nfun getNavigationBarHeight(insets: WindowInsets = WindowInsets.navigationBars): Dp {\n    val density = LocalDensity.current\n    return insets.getTop(density).pxToDp(density)\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/TabIndicator.kt",
    "content": "package com.zhangke.framework.composable\n\nimport androidx.compose.animation.core.FastOutSlowInEasing\nimport androidx.compose.animation.core.animateDpAsState\nimport androidx.compose.animation.core.tween\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.offset\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.layout.wrapContentSize\nimport androidx.compose.material3.TabPosition\nimport androidx.compose.runtime.getValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.composed\nimport androidx.compose.ui.platform.debugInspectorInfo\nimport androidx.compose.ui.unit.Dp\n\nfun Modifier.ownTabIndicatorOffset(\n    currentTabPosition: TabPosition,\n    currentTabWidth: Dp = currentTabPosition.width\n): Modifier = composed(\n    inspectorInfo = debugInspectorInfo {\n        name = \"tabIndicatorOffset\"\n        value = currentTabPosition\n    }\n) {\n    val indicatorOffset by animateDpAsState(\n        targetValue = currentTabPosition.left,\n        animationSpec = tween(durationMillis = 250, easing = FastOutSlowInEasing),\n        label = \"\"\n    )\n    fillMaxWidth()\n        .wrapContentSize(Alignment.BottomStart)\n        .offset(x = indicatorOffset + ((currentTabPosition.width - currentTabWidth) / 2))\n        .width(currentTabWidth)\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/TabsTopAppBar.kt",
    "content": "package com.zhangke.framework.composable\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.RowScope\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.TopAppBarScrollBehavior\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.lerp\nimport androidx.compose.ui.layout.SubcomposeLayout\nimport androidx.compose.ui.unit.dp\nimport com.zhangke.framework.blur.applyBlurEffect\nimport com.zhangke.framework.blur.blurEffectContainerColor\nimport kotlin.math.roundToInt\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun TabsTopAppBar(\n    modifier: Modifier,\n    title: @Composable () -> Unit,\n    actions: @Composable RowScope.() -> Unit,\n    selectedTabIndex: Int,\n    scrollBehavior: TopAppBarScrollBehavior,\n    tabCount: Int,\n    tabContent: @Composable (index: Int) -> Unit,\n    onTabClick: (index: Int) -> Unit,\n    colors: TabsTopAppBarColors = TabsTopAppBarColors.default(),\n    navigationIcon: @Composable () -> Unit = {},\n) {\n    val appBarContainerColor by remember(scrollBehavior, colors) {\n        derivedStateOf {\n            val fraction = scrollBehavior.state.overlappedFraction.coerceIn(0F, 1F)\n            lerp(colors.containerColor, colors.scrolledContainerColor, fraction)\n        }\n    }\n    SubcomposeLayout(\n        modifier = modifier.background(appBarContainerColor),\n    ) { constraints ->\n        val tabRowPlaceable = subcompose(\"tabRow\") {\n            FreadTabRow(\n                selectedTabIndex = selectedTabIndex,\n                containerColor = appBarContainerColor,\n                tabCount = tabCount,\n                tabContent = tabContent,\n                onTabClick = onTabClick,\n            )\n        }.first().measure(constraints)\n        val topAppBarPlaceable = subcompose(\"topAppBar\") {\n            SingleRowTopAppBar(\n                modifier = modifier.applyBlurEffect(containerColor = appBarContainerColor),\n                navigationIcon = navigationIcon,\n                title = title,\n                actions = actions,\n                height = 48.dp,\n                colors = TopAppBarColors.default(\n                    containerColor = blurEffectContainerColor(containerColor = appBarContainerColor),\n                    navigationIconContentColor = colors.contentColor,\n                    titleContentColor = colors.contentColor,\n                    actionIconContentColor = colors.contentColor,\n                ),\n            )\n        }.first().measure(constraints)\n        val totalHeight = topAppBarPlaceable.height + tabRowPlaceable.height\n        if (scrollBehavior.state.heightOffsetLimit != -totalHeight.toFloat()) {\n            scrollBehavior.state.heightOffsetLimit = -totalHeight.toFloat()\n        }\n        val heightOffset = scrollBehavior.state.heightOffset\n        val tabOffset = heightOffset.coerceIn(-tabRowPlaceable.height.toFloat(), 0f)\n        val topOffset = heightOffset - tabOffset\n        val layoutHeight = (totalHeight + heightOffset).roundToInt().coerceAtLeast(0)\n        layout(constraints.maxWidth, layoutHeight) {\n            val topAppBarY = topOffset.roundToInt()\n            val tabRowY = (topAppBarPlaceable.height + tabOffset + topOffset).roundToInt()\n            tabRowPlaceable.placeRelative(0, tabRowY)\n            topAppBarPlaceable.placeRelative(0, topAppBarY)\n        }\n    }\n}\n\ndata class TabsTopAppBarColors(\n    val containerColor: Color,\n    val scrolledContainerColor: Color,\n    val contentColor: Color,\n) {\n\n    companion object {\n\n        @Composable\n        fun default(\n            containerColor: Color = MaterialTheme.colorScheme.surface,\n            scrolledContainerColor: Color = MaterialTheme.colorScheme.surfaceContainer,\n            contentColor: Color = MaterialTheme.colorScheme.onSurface,\n        ): TabsTopAppBarColors {\n            return TabsTopAppBarColors(\n                containerColor = containerColor,\n                scrolledContainerColor = scrolledContainerColor,\n                contentColor = contentColor,\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/TextString.kt",
    "content": "package com.zhangke.framework.composable\n\nimport androidx.compose.runtime.Composable\nimport com.zhangke.fread.localization.LocalizedString\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport org.jetbrains.compose.resources.StringResource\nimport org.jetbrains.compose.resources.getString\nimport org.jetbrains.compose.resources.stringResource\n\nsealed class TextString {\n    class ResourceText(val resId: Int, val formatArgs: Array<Any>) : TextString()\n    class ComposeResourceText(val res: StringResource, val formatArgs: Array<Any>) : TextString()\n    class StringText(val string: String) : TextString() {\n        override fun toString(): String {\n            return string\n        }\n    }\n}\n\nfun textOf(stringResId: Int, vararg formatArgs: Any): TextString {\n    return TextString.ResourceText(stringResId, arrayOf(*formatArgs))\n}\n\nfun textOf(string: String): TextString {\n    return TextString.StringText(string)\n}\n\nfun textOf(stringResource: StringResource, vararg formatArgs: Any): TextString {\n    return TextString.ComposeResourceText(stringResource, arrayOf(*formatArgs))\n}\n\n@Composable\nfun textString(text: TextString): String {\n    return when (text) {\n        is TextString.StringText -> text.string\n        is TextString.ComposeResourceText -> stringResource(text.res, *text.formatArgs)\n        is TextString.ResourceText -> stringResource(text.resId, *text.formatArgs)\n    }\n}\n\n@Composable\nexpect fun stringResource(resId: Int, vararg formatArgs: Any): String\n\n@Composable\nfun TextString?.isNullOrEmpty(): Boolean {\n    return this == null || textString(text = this).isEmpty()\n}\n\nfun Throwable.toTextStringOrNull(): TextString? {\n    val errorMessage = this.message\n    if (errorMessage.isNullOrEmpty()) return null\n    return textOf(errorMessage)\n}\n\nsuspend fun MutableSharedFlow<TextString>.emitTextMessageFromThrowable(t: Throwable) {\n    val message = t.toTextStringOrNull()\n        ?: textOf(getString(LocalizedString.unknownError))\n    this.emit(message)\n}\n\nexpect suspend fun TextString.getString(): String\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/TextWithIcon.kt",
    "content": "package com.zhangke.framework.composable\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.material3.LocalTextStyle\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.text.TextLayoutResult\nimport androidx.compose.ui.text.TextStyle\nimport androidx.compose.ui.text.font.FontFamily\nimport androidx.compose.ui.text.font.FontStyle\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.text.style.TextDecoration\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.TextUnit\n\n@Composable\nfun TextWithIcon(\n    modifier: Modifier = Modifier,\n    text: String,\n    color: Color = Color.Unspecified,\n    fontSize: TextUnit = TextUnit.Unspecified,\n    fontStyle: FontStyle? = null,\n    fontWeight: FontWeight? = null,\n    fontFamily: FontFamily? = null,\n    letterSpacing: TextUnit = TextUnit.Unspecified,\n    textDecoration: TextDecoration? = null,\n    textAlign: TextAlign? = null,\n    lineHeight: TextUnit = TextUnit.Unspecified,\n    overflow: TextOverflow = TextOverflow.Clip,\n    softWrap: Boolean = true,\n    maxLines: Int = Int.MAX_VALUE,\n    startIcon: (@Composable () -> Unit)? = null,\n    endIcon: (@Composable () -> Unit)? = null,\n    onTextLayout: (TextLayoutResult) -> Unit = {},\n    style: TextStyle = LocalTextStyle.current\n) {\n    Row(\n        modifier = modifier,\n        horizontalArrangement = Arrangement.Center,\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        startIcon?.invoke()\n        Text(\n            text = text,\n            color = color,\n            fontSize = fontSize,\n            fontWeight = fontWeight,\n            fontFamily = fontFamily,\n            fontStyle = fontStyle,\n            letterSpacing = letterSpacing,\n            textDecoration = textDecoration,\n            textAlign = textAlign,\n            lineHeight = lineHeight,\n            overflow = overflow,\n            softWrap = softWrap,\n            maxLines = maxLines,\n            onTextLayout = onTextLayout,\n            style = style,\n        )\n        endIcon?.invoke()\n    }\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/Toolbar.kt",
    "content": "package com.zhangke.framework.composable\n\nimport androidx.compose.foundation.layout.RowScope\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.filled.ArrowBack\nimport androidx.compose.material.icons.filled.Delete\nimport androidx.compose.material.icons.filled.Download\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.LocalContentColor\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.text.TextStyle\nimport androidx.compose.ui.text.font.FontFamily\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.LineHeightStyle\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\n\nobject ToolbarTokens {\n\n    val ContainerHeight = 64.0.dp\n\n    val LeadingIconSize = 24.0.dp\n\n    val TrailingIconSize = 24.0.dp\n\n    val TitleLargeSize = 22.sp\n\n    val TopAppBarHorizontalPadding = 4.dp\n    val TitleLargeLineHeight = 28.0.sp\n    val TitleLargeTracking = 0.0.sp\n\n    val DefaultLineHeightStyle = LineHeightStyle(\n        alignment = LineHeightStyle.Alignment.Center,\n        trim = LineHeightStyle.Trim.None,\n    )\n\n    val DefaultTextStyle = TextStyle.Default.copy(\n        lineHeightStyle = DefaultLineHeightStyle,\n    )\n\n    val titleTextStyle = DefaultTextStyle.copy(\n        fontFamily = FontFamily.SansSerif,\n        fontWeight = FontWeight.Normal,\n        fontSize = TitleLargeSize,\n        lineHeight = TitleLargeLineHeight,\n        letterSpacing = TitleLargeTracking,\n    )\n}\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun Toolbar(\n    title: String,\n    onBackClick: (() -> Unit)? = null,\n    actions: @Composable RowScope.() -> Unit = {}\n) {\n    val navigationIcon: (@Composable (() -> Unit)) = if (onBackClick != null) {\n        @Composable\n        {\n            Toolbar.BackButton(onBackClick = onBackClick)\n        }\n    } else {\n        {}\n    }\n    TopAppBar(\n        navigationIcon = navigationIcon,\n        actions = actions,\n        title = {\n            Text(text = title)\n        },\n    )\n}\n\nobject Toolbar {\n\n    @Composable\n    fun BackButton(\n        onBackClick: () -> Unit,\n        modifier: Modifier = Modifier,\n        tint: Color = LocalContentColor.current,\n    ) {\n        IconButton(\n            modifier = modifier,\n            onClick = onBackClick\n        ) {\n            Icon(\n                imageVector = Icons.AutoMirrored.Filled.ArrowBack,\n                \"back\",\n                tint = tint,\n            )\n        }\n    }\n\n    @Composable\n    fun DownloadButton(\n        modifier: Modifier = Modifier,\n        tint: Color = LocalContentColor.current,\n        onClick: () -> Unit,\n    ) {\n        SimpleIconButton(\n            modifier = modifier,\n            onClick = onClick,\n            tint = tint,\n            imageVector = Icons.Default.Download,\n            contentDescription = \"Download\",\n        )\n    }\n\n    @Composable\n    fun DeleteButton(\n        onDeleteClick: () -> Unit,\n        modifier: Modifier = Modifier,\n        tint: Color = LocalContentColor.current,\n    ) {\n        IconButton(\n            modifier = modifier,\n            onClick = onDeleteClick\n        ) {\n            Icon(\n                imageVector = Icons.Default.Delete,\n                \"delete\",\n                tint = tint,\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/TopAppBar.kt",
    "content": "package com.zhangke.framework.composable\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.RowScope\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.windowInsetsPadding\nimport androidx.compose.material3.LocalContentColor\nimport androidx.compose.material3.ProvideTextStyle\nimport androidx.compose.material3.TopAppBarDefaults\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.Stable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clipToBounds\nimport androidx.compose.ui.draw.drawBehind\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.layout.Layout\nimport androidx.compose.ui.layout.layoutId\nimport androidx.compose.ui.text.TextStyle\nimport androidx.compose.ui.unit.Constraints\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.LayoutDirection\nimport androidx.compose.ui.unit.dp\nimport kotlin.math.max\n\n@Composable\nfun SingleRowTopAppBar(\n    title: @Composable () -> Unit,\n    modifier: Modifier = Modifier,\n    navigationIcon: @Composable () -> Unit = {},\n    actions: @Composable RowScope.() -> Unit = {},\n    windowInsets: WindowInsets = TopAppBarDefaults.windowInsets,\n    colors: TopAppBarColors = TopAppBarColors.default(),\n    height: Dp = TopAppBarDefaults.TopAppBarExpandedHeight,\n) {\n    val actionsRow: @Composable () -> Unit = {\n        Row(\n            horizontalArrangement = Arrangement.End,\n            verticalAlignment = Alignment.CenterVertically,\n            content = actions,\n        )\n    }\n    Box(\n        modifier = modifier.drawBehind {\n            val color = colors.containerColor\n            if (color != Color.Unspecified) {\n                drawRect(color = color)\n            }\n        },\n    ) {\n        SingleRowTopAppBarLayout(\n            modifier = Modifier.windowInsetsPadding(windowInsets).clipToBounds(),\n            navigationIconContentColor = colors.navigationIconContentColor,\n            titleContentColor = colors.titleContentColor,\n            actionIconContentColor = colors.actionIconContentColor,\n            title = title,\n            titleTextStyle = ToolbarTokens.titleTextStyle,\n            navigationIcon = navigationIcon,\n            actions = actionsRow,\n            height = height,\n        )\n    }\n}\n\n@Stable\nclass TopAppBarColors(\n    val containerColor: Color,\n    val navigationIconContentColor: Color,\n    val titleContentColor: Color,\n    val actionIconContentColor: Color,\n) {\n\n    companion object {\n\n        @Composable\n        fun default(\n            containerColor: Color = TopAppBarDefaults.topAppBarColors().containerColor,\n            navigationIconContentColor: Color = TopAppBarDefaults.topAppBarColors().navigationIconContentColor,\n            titleContentColor: Color = TopAppBarDefaults.topAppBarColors().titleContentColor,\n            actionIconContentColor: Color = TopAppBarDefaults.topAppBarColors().actionIconContentColor,\n        ): TopAppBarColors {\n            return TopAppBarColors(\n                containerColor = containerColor,\n                navigationIconContentColor = navigationIconContentColor,\n                titleContentColor = titleContentColor,\n                actionIconContentColor = actionIconContentColor,\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun SingleRowTopAppBarLayout(\n    modifier: Modifier,\n    navigationIconContentColor: Color,\n    titleContentColor: Color,\n    actionIconContentColor: Color,\n    title: @Composable () -> Unit,\n    titleTextStyle: TextStyle,\n    navigationIcon: @Composable () -> Unit,\n    actions: @Composable () -> Unit,\n    height: Dp,\n) {\n    Layout(\n        content = {\n            Box(Modifier.layoutId(\"navigationIcon\").padding(start = TopAppBarHorizontalPadding)) {\n                CompositionLocalProvider(\n                    LocalContentColor provides navigationIconContentColor,\n                    content = navigationIcon,\n                )\n            }\n            Box(\n                modifier =\n                    Modifier.layoutId(\"title\").padding(horizontal = TopAppBarHorizontalPadding)\n            ) {\n                CompositionLocalProvider(LocalContentColor provides titleContentColor) {\n                    ProvideTextStyle(titleTextStyle, content = title)\n                }\n            }\n            Box(Modifier.layoutId(\"actionIcons\").padding(end = TopAppBarHorizontalPadding)) {\n                CompositionLocalProvider(\n                    LocalContentColor provides actionIconContentColor,\n                    content = actions,\n                )\n            }\n        },\n        modifier = modifier,\n        measurePolicy = { measurables, constraints ->\n            val navigationIconPlaceable = measurables.first { it.layoutId == \"navigationIcon\" }\n                .measure(constraints.copy(minWidth = 0))\n            val actionIconsPlaceable = measurables.first { it.layoutId == \"actionIcons\" }\n                .measure(constraints.copy(minWidth = 0))\n\n            val maxTitleWidth = if (constraints.maxWidth == Constraints.Infinity) {\n                constraints.maxWidth\n            } else {\n                (constraints.maxWidth - navigationIconPlaceable.width - actionIconsPlaceable.width)\n                    .coerceAtLeast(0)\n            }\n            val titlePlaceable = measurables.first { it.layoutId == \"title\" }\n                .measure(constraints.copy(minWidth = 0, maxWidth = maxTitleWidth))\n\n            val maxLayoutHeight =\n                max(\n                    height.roundToPx(),\n                    max(\n                        titlePlaceable.height,\n                        max(navigationIconPlaceable.height, actionIconsPlaceable.height),\n                    ),\n                )\n\n            layout(constraints.maxWidth, maxLayoutHeight) {\n                navigationIconPlaceable.placeRelative(\n                    x = 0,\n                    y = (maxLayoutHeight - navigationIconPlaceable.height) / 2,\n                )\n\n                val start = max(TopAppBarTitleInset.roundToPx(), navigationIconPlaceable.width)\n                val end = actionIconsPlaceable.width\n                var titleX = Alignment.Start.align(\n                    size = titlePlaceable.width,\n                    space = constraints.maxWidth,\n                    layoutDirection = LayoutDirection.Ltr,\n                )\n                if (titleX < start) {\n                    titleX += (start - titleX)\n                } else if (titleX + titlePlaceable.width > constraints.maxWidth - end) {\n                    titleX += ((constraints.maxWidth - end) - (titleX + titlePlaceable.width))\n                }\n\n                val titleY = (maxLayoutHeight - titlePlaceable.height) / 2\n                titlePlaceable.placeRelative(titleX, titleY)\n\n                actionIconsPlaceable.placeRelative(\n                    x = constraints.maxWidth - actionIconsPlaceable.width,\n                    y = (maxLayoutHeight - actionIconsPlaceable.height) / 2,\n                )\n            }\n        },\n    )\n}\n\nprivate val TopAppBarHorizontalPadding = 4.dp\nprivate val TopAppBarTitleInset = 16.dp - TopAppBarHorizontalPadding\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/TopBarWithTabLayout.kt",
    "content": "package com.zhangke.framework.composable\n\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.BoxScope\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.TopAppBarColors\nimport androidx.compose.material3.TopAppBarDefaults\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.rememberUpdatedState\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.input.nestedscroll.nestedScroll\nimport androidx.compose.ui.layout.Layout\nimport androidx.compose.ui.layout.layoutId\nimport androidx.compose.ui.layout.onSizeChanged\nimport androidx.compose.ui.platform.LocalDensity\nimport com.zhangke.framework.ktx.second\nimport com.zhangke.framework.utils.pxToDp\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun TopBarWithTabLayout(\n    topBarContent: @Composable BoxScope.() -> Unit,\n    tabContent: @Composable BoxScope.() -> Unit,\n    modifier: Modifier = Modifier,\n    scrollableContent: @Composable () -> Unit,\n) {\n    val density = LocalDensity.current\n    var topBarHeightInPx: Int by rememberSaveable { mutableIntStateOf(0) }\n    var tabHeightInPx: Int by rememberSaveable { mutableIntStateOf(0) }\n    val nestedScrollConnection = rememberCollapsableTopBarScrollConnection(\n        minPx = 0F,\n        maxPx = (topBarHeightInPx + tabHeightInPx).toFloat(),\n    )\n    val process by rememberUpdatedState(newValue = nestedScrollConnection.progress)\n    Box(\n        modifier = modifier\n            .fillMaxSize()\n            .nestedScroll(nestedScrollConnection)\n    ) {\n        Layout(\n            modifier = Modifier,\n            content = {\n                Box(\n                    modifier = Modifier.fillMaxSize().layoutId(\"scrollable\"),\n                ) {\n                    val topPaddingInPx = topBarHeightInPx + tabHeightInPx\n                    val newPaddings = updateTopPadding((topPaddingInPx.pxToDp(density)))\n                    CompositionLocalProvider(\n                        LocalContentPadding provides newPaddings\n                    ) {\n                        scrollableContent()\n                    }\n                }\n                Box(\n                    modifier = Modifier\n                        .layoutId(\"topBar\")\n                        .fillMaxWidth()\n                        .onSizeChanged { topBarHeightInPx = it.height },\n                ) {\n                    topBarContent()\n                }\n                Box(\n                    modifier = Modifier.layoutId(\"tab\").fillMaxWidth()\n                        .onSizeChanged { tabHeightInPx = it.height },\n                ) {\n                    tabContent()\n                }\n            },\n            measurePolicy = { measurables, constraints ->\n                val scrollableContentPlaceable =\n                    measurables.first { it.layoutId == \"scrollable\" }.measure(constraints)\n                val topBarPlaceable =\n                    measurables.first { it.layoutId == \"topBar\" }.measure(constraints)\n                val tabBarPlaceable =\n                    measurables.first { it.layoutId == \"tab\" }.measure(constraints)\n                layout(constraints.maxWidth, constraints.maxHeight) {\n                    scrollableContentPlaceable.placeRelative(0, 0)\n                    val totalHeaderHeight = topBarHeightInPx + tabHeightInPx\n                    val processedOffset = totalHeaderHeight * process\n                    val tabBarYOffset = topBarHeightInPx - processedOffset.coerceAtLeast(0F)\n                    tabBarPlaceable.placeRelative(0, tabBarYOffset.toInt())\n                    val topBarYOffset = -((processedOffset - tabHeightInPx).coerceAtLeast(0F))\n                    topBarPlaceable.placeRelative(0, topBarYOffset.toInt())\n                }\n            },\n        )\n    }\n}\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun TopBarWithTabLayoutClassical(\n    topBarContent: @Composable BoxScope.() -> Unit,\n    tabContent: @Composable BoxScope.() -> Unit,\n    modifier: Modifier = Modifier,\n    windowInsets: WindowInsets = TopAppBarDefaults.windowInsets,\n    colors: TopAppBarColors = TopAppBarDefaults.topAppBarColors(),\n    scrollableContent: @Composable BoxScope.() -> Unit,\n) {\n    val density = LocalDensity.current\n    var topBarHeightInPx: Int by rememberSaveable { mutableIntStateOf(0) }\n    var tabHeightInPx: Int by rememberSaveable { mutableIntStateOf(0) }\n    val nestedScrollConnection = rememberCollapsableTopBarScrollConnection(\n        minPx = 0F,\n        maxPx = (topBarHeightInPx + tabHeightInPx).toFloat(),\n    )\n    val process by rememberUpdatedState(newValue = nestedScrollConnection.progress)\n    Box(\n        modifier = modifier\n            .fillMaxSize()\n            .nestedScroll(nestedScrollConnection)\n    ) {\n        Layout(\n            modifier = Modifier,\n            content = {\n                Box(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .onSizeChanged { topBarHeightInPx = it.height },\n                ) {\n                    topBarContent()\n                }\n                Box(\n                    modifier = Modifier.fillMaxWidth()\n                        .onSizeChanged { tabHeightInPx = it.height },\n                ) {\n                    tabContent()\n                }\n                Box(\n                    modifier = Modifier.fillMaxSize(),\n                ) {\n                    scrollableContent()\n                }\n            },\n            measurePolicy = { measurables, constraints ->\n                val topBarPlaceable = measurables.first().measure(constraints)\n                val tabBarPlaceable = measurables.second().measure(constraints)\n                val scrollableContentPlaceable = measurables[2].measure(constraints)\n                layout(constraints.maxWidth, constraints.maxHeight) {\n                    val totalHeaderHeight = topBarHeightInPx + tabHeightInPx\n                    val processedOffset = totalHeaderHeight * process\n                    val tabBarYOffset = topBarHeightInPx - processedOffset.coerceAtLeast(0F)\n                    tabBarPlaceable.placeRelative(0, tabBarYOffset.toInt())\n                    val topBarYOffset = -((processedOffset - tabHeightInPx).coerceAtLeast(0F))\n                    topBarPlaceable.placeRelative(0, topBarYOffset.toInt())\n                    scrollableContentPlaceable.placeRelative(\n                        0,\n                        totalHeaderHeight - processedOffset.toInt()\n                    )\n                }\n            },\n        )\n//        val statusBarHeight = windowInsets.getTop(density).pxToDp(density)\n//        if (statusBarHeight > 0.dp) {\n//            Box(\n//                modifier = Modifier\n//                    .fillMaxWidth()\n//                    .height(statusBarHeight)\n//                    .background(colors.containerColor)\n//            )\n//        }\n    }\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/TwoTextsInRow.kt",
    "content": "package com.zhangke.framework.composable\n\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.layout.FirstBaseline\nimport androidx.compose.ui.layout.Layout\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.unit.Dp\n\n@Composable\nfun TwoTextsInRow(\n    firstText: @Composable () -> Unit,\n    secondText: @Composable () -> Unit,\n    spacing: Dp,\n    modifier: Modifier = Modifier\n) {\n    val density = LocalDensity.current\n    val spacingPx = with(density) { spacing.roundToPx().toFloat() }\n\n    Box(modifier = modifier) {\n        Layout(\n            content = {\n                firstText()\n                secondText()\n            },\n            measurePolicy = { measurables, constraints ->\n                val firstTextMeasurable = measurables[0]\n                val firstTextPlaceable = firstTextMeasurable.measure(\n                    constraints.copy(maxWidth = constraints.maxWidth - spacingPx.toInt())\n                )\n                val secondTextMeasurable = measurables[1]\n                val secondTextPlaceable = secondTextMeasurable.measure(\n                    constraints.copy(maxWidth = constraints.maxWidth - spacingPx.toInt() - firstTextPlaceable.width)\n                )\n                val firstBaseLine = firstTextPlaceable[FirstBaseline]\n                val secondBaseLine = secondTextPlaceable[FirstBaseline]\n\n                val secondTextHeight = secondTextPlaceable.height\n                layout(\n                    width = constraints.maxWidth,\n                    height = maxOf(firstTextPlaceable.height, secondTextHeight)\n                ) {\n                    firstTextPlaceable.place(\n                        x = 0,\n                        y = 0,\n                    )\n                    secondTextPlaceable.place(\n                        x = firstTextPlaceable.width + spacingPx.toInt(),\n                        y = firstBaseLine - secondBaseLine,\n                    )\n                }\n            }\n        )\n    }\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/VelocityExt.kt",
    "content": "package com.zhangke.framework.composable\n\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.unit.Velocity\n\nfun Velocity.toOffset() = Offset(x = x, y = y)"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/VerticalIndentLayout.kt",
    "content": "package com.zhangke.framework.composable\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.layout.Layout\nimport androidx.compose.ui.unit.Dp\n\n@Composable\nfun VerticalIndentLayout(\n    modifier: Modifier,\n    indentHeight: Dp,\n    headerContent: @Composable () -> Unit,\n    indentContent: @Composable () -> Unit,\n) {\n    Layout(\n        modifier = modifier,\n        measurePolicy = { measurables, constraints ->\n            val headerPlaceable = measurables.first().measure(constraints)\n            val indentPlaceable = measurables.last().measure(constraints)\n            val containerHeight =\n                headerPlaceable.height + indentPlaceable.height - indentHeight.roundToPx()\n            layout(constraints.maxWidth, containerHeight) {\n                headerPlaceable.placeRelative(0, 0)\n                indentPlaceable.placeRelative(\n                    x = 0,\n                    y = headerPlaceable.height - indentHeight.roundToPx()\n                )\n            }\n        },\n        content = {\n            headerContent()\n            indentContent()\n        }\n    )\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/collapsable/CollapsableTopBarLayout.kt",
    "content": "package com.zhangke.framework.composable.collapsable\n\nimport androidx.compose.foundation.gestures.Orientation\nimport androidx.compose.foundation.gestures.scrollable\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.State\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableFloatStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.input.nestedscroll.nestedScroll\nimport androidx.compose.ui.layout.onGloballyPositioned\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.unit.Dp\n\n@Composable\nfun CollapsableTopBarLayout(\n    modifier: Modifier = Modifier,\n    minTopBarHeight: Dp,\n    contentCanScrollBackward: State<Boolean>,\n    topBar: @Composable (collapsableProgress: Float) -> Unit,\n    scrollableContent: @Composable () -> Unit,\n) {\n    val density = LocalDensity.current\n    val minTopBarHeightPx = with(density) { minTopBarHeight.toPx() }\n    var maxTopBarHeightPx: Float? by remember {\n        mutableStateOf(null)\n    }\n    var progress: Float by remember {\n        mutableFloatStateOf(0F)\n    }\n    val connection = rememberCollapsableTopBarLayoutConnection(\n        contentCanScrollBackward = contentCanScrollBackward,\n        maxPx = maxTopBarHeightPx ?: 0F,\n        minPx = minTopBarHeightPx,\n    )\n    progress = connection.progress\n\n    Column(modifier = modifier.nestedScroll(connection)) {\n        Box(\n            modifier = Modifier\n                .scrollable(rememberScrollState(), Orientation.Vertical)\n                .onGloballyPositioned {\n                    if (maxTopBarHeightPx == null || maxTopBarHeightPx == 0F) {\n                        maxTopBarHeightPx = it.size.height.toFloat()\n                    }\n                }\n        ) {\n            topBar(progress)\n        }\n        Box(\n            modifier = Modifier.scrollable(rememberScrollState(), Orientation.Vertical)\n        ) {\n            scrollableContent()\n        }\n    }\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/collapsable/CollapsableTopBarLayoutConnection.kt",
    "content": "package com.zhangke.framework.composable.collapsable\n\nimport androidx.compose.animation.core.animate\nimport androidx.compose.animation.core.calculateTargetValue\nimport androidx.compose.animation.core.exponentialDecay\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.State\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableFloatStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.saveable.Saver\nimport androidx.compose.runtime.saveable.listSaver\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.input.nestedscroll.NestedScrollConnection\nimport androidx.compose.ui.input.nestedscroll.NestedScrollSource\nimport androidx.compose.ui.unit.Velocity\n\n@Composable\nfun rememberCollapsableTopBarLayoutConnection(\n    contentCanScrollBackward: State<Boolean>,\n    maxPx: Float,\n    minPx: Float,\n): ICollapsableTopBarLayoutConnection {\n    return if (maxPx <= 0F) {\n        remember {\n            StaticTopBarLayoutConnection()\n        }\n    } else {\n        rememberSaveable(maxPx, minPx, saver = CollapsableTopBarLayoutConnection.Saver) {\n            CollapsableTopBarLayoutConnection(maxPx, minPx)\n        }.also {\n            it.contentCanScrollBackward = contentCanScrollBackward\n        }\n    }\n}\n\ninterface ICollapsableTopBarLayoutConnection : NestedScrollConnection {\n\n    val progress: Float\n}\n\nclass StaticTopBarLayoutConnection : NestedScrollConnection, ICollapsableTopBarLayoutConnection {\n\n    override val progress: Float = 0F\n}\n\nclass CollapsableTopBarLayoutConnection(\n    private val maxPx: Float,\n    private val minPx: Float,\n) : NestedScrollConnection, ICollapsableTopBarLayoutConnection {\n\n    var contentCanScrollBackward: State<Boolean>? = null\n\n    private var topBarHeight: Float = maxPx\n        set(value) {\n            field = value\n            progress = 1 - (topBarHeight - minPx) / (maxPx - minPx)\n        }\n\n    override var progress: Float by mutableFloatStateOf(0F)\n        private set\n\n    override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {\n        if (available.y == 0f) return Velocity.Zero\n        val startHeight = topBarHeight\n        val targetHeight = exponentialDecay<Float>().calculateTargetValue(\n            initialValue = startHeight,\n            initialVelocity = available.y,\n        ).coerceAtLeast(minPx).coerceAtMost(maxPx)\n        if (topBarHeight == targetHeight) return available\n        animate(\n            initialValue = startHeight,\n            targetValue = targetHeight\n        ) { value, _ ->\n            topBarHeight = value\n        }\n        val consumedY = targetHeight - startHeight\n        return if (available.y > 0 && consumedY > 0 || available.y < 0 && consumedY < 0) {\n            available\n        } else {\n            Velocity.Zero\n        }\n    }\n\n    override fun onPostScroll(\n        consumed: Offset,\n        available: Offset,\n        source: NestedScrollSource\n    ): Offset {\n        return handleScroll(available)\n    }\n\n    override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {\n        return handleScroll(available)\n    }\n\n    private fun handleScroll(available: Offset): Offset {\n        val height = topBarHeight\n        if (height == minPx) {\n            if (available.y > 0F) {\n                return if (contentCanScrollBackward?.value == true) {\n                    Offset.Zero\n                } else {\n                    topBarHeight += available.y\n                    Offset(0F, available.y)\n                }\n            }\n        }\n        if (height + available.y > maxPx) {\n            topBarHeight = maxPx\n            return Offset(0f, maxPx - height)\n        }\n        if (height + available.y < minPx) {\n            topBarHeight = minPx\n            return Offset(0f, minPx - height)\n        }\n        topBarHeight += available.y\n        return Offset(0f, available.y)\n    }\n\n    companion object {\n\n        val Saver: Saver<CollapsableTopBarLayoutConnection, *> = listSaver(\n            save = {\n                listOf(it.minPx, it.maxPx, it.topBarHeight)\n            },\n            restore = {\n                CollapsableTopBarLayoutConnection(\n                    minPx = it[0],\n                    maxPx = it[1],\n                ).apply {\n                    topBarHeight = it[2]\n                }\n            }\n        )\n    }\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/collapsable/ScrollUpTopBarLayout.kt",
    "content": "package com.zhangke.framework.composable.collapsable\n\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.BoxScope\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.State\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.rememberUpdatedState\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.input.nestedscroll.nestedScroll\nimport androidx.compose.ui.layout.Layout\nimport androidx.compose.ui.platform.LocalDensity\n\n@Composable\nfun ScrollUpTopBarLayout(\n    modifier: Modifier = Modifier,\n    topBarContent: @Composable BoxScope.(progress: Float) -> Unit,\n    headerContent: @Composable BoxScope.(progress: Float) -> Unit,\n    contentCanScrollBackward: State<Boolean>,\n    /**\n     * true: headerContent will immersive behind the top bar.\n     * false: headerContent will below the top bar.\n     */\n    immersiveToTopBar: Boolean = true,\n    /**\n     * PaddingValues.top is the current visible height occupied by top bar + header area.\n     */\n    scrollableContent: @Composable BoxScope.(PaddingValues, Float) -> Unit,\n) {\n    var topBarHeightPx: Int by rememberSaveable { mutableIntStateOf(0) }\n    var headerContentHeightPx: Int by rememberSaveable { mutableIntStateOf(0) }\n    val density = LocalDensity.current\n    val nestedScrollConnection = if (immersiveToTopBar) {\n        rememberCollapsableTopBarLayoutConnection(\n            contentCanScrollBackward = contentCanScrollBackward,\n            maxPx = headerContentHeightPx.toFloat(),\n            minPx = topBarHeightPx.toFloat(),\n        )\n    } else {\n        rememberCollapsableTopBarLayoutConnection(\n            contentCanScrollBackward = contentCanScrollBackward,\n            maxPx = headerContentHeightPx.toFloat(),\n            minPx = 0F,\n        )\n    }\n    val progress by rememberUpdatedState(newValue = nestedScrollConnection.progress)\n    val topPaddingPx = calculateScrollableTopPadding(\n        topBarHeightPx = topBarHeightPx,\n        headerContentHeightPx = headerContentHeightPx,\n        progress = progress,\n        immersiveToTopBar = immersiveToTopBar,\n    )\n    val scrollableContentPadding = with(density) {\n        PaddingValues(top = topPaddingPx.toDp())\n    }\n    Box(\n        modifier = modifier\n            .fillMaxSize()\n            .nestedScroll(nestedScrollConnection),\n    ) {\n        Layout(\n            modifier = Modifier,\n            content = {\n                Box(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .verticalScroll(rememberScrollState()),\n                ) {\n                    topBarContent(progress)\n                }\n                Box(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .verticalScroll(rememberScrollState()),\n                ) {\n                    headerContent(progress)\n                }\n                Box(\n                    modifier = Modifier\n                        .fillMaxSize(),\n                ) {\n                    scrollableContent(scrollableContentPadding, progress)\n                }\n            },\n            measurePolicy = { measurables, constraints ->\n                val topBarPlaceable = measurables.first().measure(constraints)\n                val headerContentPlaceable = measurables[1].measure(\n                    constraints.copy(maxHeight = constraints.maxHeight * 6)\n                )\n                val scrollableContentPlaceable = measurables[2].measure(constraints)\n                if (topBarHeightPx != topBarPlaceable.measuredHeight) {\n                    topBarHeightPx = topBarPlaceable.measuredHeight\n                }\n                if (headerContentHeightPx != headerContentPlaceable.measuredHeight) {\n                    headerContentHeightPx = headerContentPlaceable.measuredHeight\n                }\n                layout(constraints.maxWidth, constraints.maxHeight) {\n                    val totalScrollOffset = if (immersiveToTopBar) {\n                        headerContentHeightPx - topBarHeightPx\n                    } else {\n                        headerContentHeightPx\n                    }\n                    val progressOffset = totalScrollOffset * progress\n                    val headerYOffset = if (immersiveToTopBar) {\n                        -((progressOffset).coerceAtLeast(0F))\n                    } else {\n                        topBarHeightPx - ((progressOffset).coerceAtLeast(0F))\n                    }\n                    scrollableContentPlaceable.placeRelative(0, 0)\n                    headerContentPlaceable.placeRelative(0, headerYOffset.toInt())\n                    topBarPlaceable.placeRelative(0, 0)\n                }\n            },\n        )\n    }\n}\n\nprivate fun calculateScrollableTopPadding(\n    topBarHeightPx: Int,\n    headerContentHeightPx: Int,\n    progress: Float,\n    immersiveToTopBar: Boolean,\n): Float {\n    val headerYOffset = if (immersiveToTopBar) {\n        val totalScrollOffset = (headerContentHeightPx - topBarHeightPx).coerceAtLeast(0)\n        -(totalScrollOffset * progress)\n    } else {\n        topBarHeightPx - (headerContentHeightPx * progress)\n    }\n    return (headerContentHeightPx + headerYOffset).coerceAtLeast(0F)\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/icons/Toufu.kt",
    "content": "package com.zhangke.framework.composable.icons\n\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.materialIcon\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.graphics.vector.PathNode\nimport androidx.compose.ui.graphics.vector.addPathNodes\n\nprivate var _tofu: ImageVector? = null\n\nval Icons.Filled.Tofu: ImageVector\n    get() {\n        if (_tofu != null) {\n            return _tofu!!\n        }\n        _tofu = materialIcon(name = \"Filled.Tofu\") {\n            addPath(\n                buildPathData(\n                    width = 24.0f,\n                    height = 24.0f,\n                    radius = 4.0f,\n                ),\n                fill = SolidColor(Color.Gray.copy(alpha = 0.6F))\n            )\n        }\n        return _tofu!!\n    }\n\nprivate fun buildPathData(width: Float, height: Float, radius: Float): List<PathNode> {\n    return addPathNodes(\n        \"M${radius},0 \" +\n                \"H${width - radius} \" +\n                \"A${radius},${radius} 0 0,1 $width,$radius \" +\n                \"V${height - radius} \" +\n                \"A${radius},${radius} 0 0,1 ${width - radius},$height \" +\n                \"H$radius \" +\n                \"A${radius},${radius} 0 0,1 0,${height - radius} \" +\n                \"V$radius \" +\n                \"A${radius},${radius} 0 0,1 $radius,0 \" +\n                \"Z\"\n    )\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/image/viewer/ImageViewer.kt",
    "content": "package com.zhangke.framework.composable.image.viewer\n\nimport androidx.compose.foundation.gestures.detectDragGestures\nimport androidx.compose.foundation.gestures.detectTapGestures\nimport androidx.compose.foundation.gestures.detectVerticalDragGestures\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.BoxWithConstraints\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.offset\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.ExperimentalComposeUiApi\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.geometry.Size\nimport androidx.compose.ui.input.pointer.pointerInput\nimport androidx.compose.ui.input.pointer.util.VelocityTracker\nimport androidx.compose.ui.input.pointer.util.addPointerInputChange\nimport androidx.compose.ui.layout.Layout\nimport androidx.compose.ui.layout.onGloballyPositioned\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.unit.Constraints\nimport androidx.compose.ui.unit.Velocity\nimport androidx.compose.ui.unit.toSize\nimport com.zhangke.framework.composable.BackHandler\nimport com.zhangke.framework.utils.asSize\nimport com.zhangke.framework.utils.pxToDp\nimport kotlinx.coroutines.launch\n\nprivate val infinityConstraints = Constraints()\n\n@OptIn(ExperimentalComposeUiApi::class)\n@Composable\nfun ImageViewer(\n    state: ImageViewerState,\n    modifier: Modifier = Modifier,\n    content: @Composable () -> Unit,\n) {\n    val density = LocalDensity.current\n    val coroutineScope = rememberCoroutineScope()\n    var latestSize: Size? by remember {\n        mutableStateOf(null)\n    }\n    BackHandler(true) {\n        coroutineScope.launch { state.startDismiss() }\n    }\n    BoxWithConstraints(modifier = modifier) {\n        if (constraints.hasBoundedWidth && constraints.hasBoundedHeight) {\n            state.updateLayoutSize(constraints.asSize())\n        }\n        Layout(\n            modifier = Modifier\n                .onGloballyPositioned { position ->\n                    val currentSize = position.size.toSize()\n                    if (currentSize != latestSize) {\n                        state.updateLayoutSize(currentSize)\n                        latestSize = currentSize\n                    }\n                }\n                .pointerInput(state) {\n                    detectTapGestures(\n                        onDoubleTap = {\n                            if (state.exceed) {\n                                coroutineScope.launch {\n                                    state.animateToStandard()\n                                }\n                            } else {\n                                coroutineScope.launch {\n                                    state.animateToBig(it)\n                                }\n                            }\n                        },\n                        onTap = {\n                            coroutineScope.launch {\n                                state.startDismiss()\n                            }\n                        },\n                    )\n                }\n                .draggableInfinity(\n                    exceed = state.exceed,\n                    isBigVerticalImage = state.isBigVerticalImage,\n                    onDrag = { offset ->\n                        state.drag(offset)\n                    },\n                    onDragStopped = { velocity ->\n                        coroutineScope.launch {\n                            state.dragStop(velocity)\n                        }\n                    },\n                )\n                .pointerInput(state) {\n                    detectZoom { centroid, zoom ->\n                        state.zoom(centroid, zoom)\n                    }\n                },\n            content = {\n                Box(\n                    modifier = Modifier\n                        .offset(\n                            x = state.currentOffsetXPixel.pxToDp(density),\n                            y = state.currentOffsetYPixel.pxToDp(density)\n                        )\n                        .width(state.currentWidthPixel.pxToDp(density))\n                        .height(state.currentHeightPixel.pxToDp(density))\n                ) {\n                    content()\n                }\n            }\n        ) { measurables, constraints ->\n            if (measurables.size != 1) {\n                throw IllegalStateException(\"InfiniteBox is only allowed to have one children!\")\n            }\n            val placeable = measurables.first().measure(infinityConstraints)\n\n            layout(constraints.maxWidth, constraints.maxHeight) {\n                placeable.placeRelative(0, 0)\n            }\n        }\n    }\n}\n\nprivate fun Modifier.draggableInfinity(\n    exceed: Boolean,\n    isBigVerticalImage: Boolean,\n    onDrag: (dragAmount: Offset) -> Unit,\n    onDragStopped: (velocity: Velocity) -> Unit,\n): Modifier {\n    val velocityTracker = VelocityTracker()\n    return Modifier.pointerInput(exceed || isBigVerticalImage) {\n        if (exceed) {\n            detectDragGestures(\n                onDrag = { change, dragAmount ->\n                    velocityTracker.addPointerInputChange(change)\n                    onDrag(dragAmount)\n                },\n                onDragEnd = {\n                    val velocity = velocityTracker.calculateVelocity()\n                    onDragStopped(velocity)\n                },\n                onDragCancel = {\n                    val velocity = velocityTracker.calculateVelocity()\n                    onDragStopped(velocity)\n                },\n            )\n        } else {\n            detectVerticalDragGestures(\n                onVerticalDrag = { change, dragAmount ->\n                    velocityTracker.addPointerInputChange(change)\n                    onDrag(Offset(x = 0F, y = dragAmount))\n                },\n                onDragEnd = {\n                    val velocity = velocityTracker.calculateVelocity()\n                    onDragStopped(velocity)\n                },\n                onDragCancel = {\n                    val velocity = velocityTracker.calculateVelocity()\n                    onDragStopped(velocity)\n                },\n            )\n        }\n    } then this\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/image/viewer/ImageViewerState.kt",
    "content": "package com.zhangke.framework.composable.image.viewer\n\nimport androidx.compose.animation.core.AnimationScope\nimport androidx.compose.animation.core.AnimationState\nimport androidx.compose.animation.core.AnimationVector1D\nimport androidx.compose.animation.core.AnimationVector2D\nimport androidx.compose.animation.core.VectorConverter\nimport androidx.compose.animation.core.animateDecay\nimport androidx.compose.animation.core.animateTo\nimport androidx.compose.animation.core.exponentialDecay\nimport androidx.compose.animation.core.tween\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.Stable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableFloatStateOf\nimport androidx.compose.runtime.rememberUpdatedState\nimport androidx.compose.runtime.saveable.Saver\nimport androidx.compose.runtime.saveable.listSaver\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.geometry.Size\nimport androidx.compose.ui.geometry.isSpecified\nimport androidx.compose.ui.geometry.isUnspecified\nimport androidx.compose.ui.unit.Velocity\nimport com.zhangke.framework.composable.Bounds\nimport com.zhangke.framework.composable.aspectRatio\nimport com.zhangke.framework.composable.toOffset\nimport com.zhangke.framework.utils.equalsExactly\n\n@Composable\nfun rememberImageViewerState(\n    aspectRatio: Float,\n    initialSize: Size = Size.Unspecified,\n    minimumScale: Float = 1f,\n    maximumScale: Float = 3f,\n    onDismissRequest: () -> Unit,\n): ImageViewerState {\n    val dismissRequest by rememberUpdatedState(newValue = onDismissRequest)\n    return rememberSaveable(\n        saver = ImageViewerState.Saver,\n    ) {\n        ImageViewerState(\n            aspectRatio = aspectRatio,\n            initialSize = initialSize,\n            minimumScale = minimumScale,\n            maximumScale = maximumScale,\n        )\n    }.apply {\n        this.onDismissRequest = dismissRequest\n    }\n}\n\n@Stable\nclass ImageViewerState(\n    private val aspectRatio: Float,\n    private val initialSize: Size,\n    private val minimumScale: Float = 1f,\n    private val maximumScale: Float = 3f,\n) {\n\n    var onDismissRequest: (() -> Unit)? = null\n\n    private var _currentWidthPixel = mutableFloatStateOf(0F)\n    private var _currentHeightPixel = mutableFloatStateOf(0F)\n    private var _currentOffsetXPixel = mutableFloatStateOf(0F)\n    private var _currentOffsetYPixel = mutableFloatStateOf(0F)\n\n    val currentWidthPixel: Float by _currentWidthPixel\n    val currentHeightPixel: Float by _currentHeightPixel\n    val currentOffsetXPixel: Float by _currentOffsetXPixel\n    val currentOffsetYPixel: Float by _currentOffsetYPixel\n\n    private var layoutSize: Size = Size.Zero\n    private val standardWidth: Float get() = layoutSize.width\n    private val standardHeight: Float get() = standardWidth / aspectRatio\n\n    val exceed: Boolean get() = !_currentWidthPixel.floatValue.equalsExactly(layoutSize.width)\n\n    internal val isBigVerticalImage: Boolean\n        get() {\n            if (layoutSize == Size.Zero) return false\n            return aspectRatio <= layoutSize.aspectRatio()\n        }\n\n    private var flingAnimation: AnimationScope<Offset, AnimationVector2D>? = null\n    private var scaleAnimation: AnimationScope<Float, AnimationVector1D>? = null\n    private var resumeOffsetYAnimation: AnimationScope<Float, AnimationVector1D>? = null\n\n    private val draggableBounds: Bounds\n        get() {\n            return calculateDragBounds(\n                imageWidth = _currentWidthPixel.floatValue,\n                imageHeight = _currentHeightPixel.floatValue,\n            )\n        }\n\n    init {\n        if (initialSize != Size.Unspecified) {\n            _currentWidthPixel.floatValue = initialSize.width\n            _currentHeightPixel.floatValue = initialSize.height\n        }\n    }\n\n    fun updateLayoutSize(size: Size) {\n        if (size == layoutSize) return\n        layoutSize = size\n        onLayoutSizeChanged()\n    }\n\n    private fun onLayoutSizeChanged() {\n        val offsetY = if (isBigVerticalImage) {\n            0F\n        } else {\n            layoutSize.height / 2F - standardHeight / 2F\n        }\n        if (initialSize.isUnspecified) {\n            _currentWidthPixel.floatValue = standardWidth\n            _currentHeightPixel.floatValue = standardHeight\n            _currentOffsetXPixel.floatValue = 0F\n            _currentOffsetYPixel.floatValue = offsetY\n        } else {\n            _currentWidthPixel.floatValue = standardWidth\n            _currentHeightPixel.floatValue = standardHeight\n            _currentOffsetXPixel.floatValue = 0F\n            _currentOffsetYPixel.floatValue = offsetY\n        }\n    }\n\n    suspend fun animateToStandard() {\n        val layoutSize = layoutSize\n        if (layoutSize == Size.Zero) return\n        val targetWidth = standardWidth\n        val targetHeight = standardHeight\n        animateToTarget(\n            targetWidth = targetWidth,\n            targetHeight = targetHeight,\n            targetOffsetX = 0F,\n            targetOffsetY = layoutSize.height / 2F - targetHeight / 2F,\n        )\n    }\n\n    suspend fun animateToBig(point: Offset) {\n        val layoutSize = layoutSize\n        if (layoutSize == Size.Zero) return\n\n        val targetWidth = standardWidth * maximumScale\n        val targetHeight = targetWidth / aspectRatio\n        var targetOffsetX = currentOffsetXPixel * maximumScale\n        var targetOffsetY = layoutSize.height / 2F - targetHeight / 2F\n\n        if (point.isSpecified && point.isValid()) {\n            // tap point must be in the image bounds\n            if (point.y < currentOffsetYPixel || point.y > (currentOffsetYPixel + currentHeightPixel)) return\n            val xRatio = point.x / currentWidthPixel\n            val yRatio = (point.y - currentOffsetYPixel) / currentHeightPixel\n            targetOffsetX = -(targetWidth * xRatio - point.x)\n            targetOffsetY = point.y - targetHeight * yRatio\n        }\n        val dragBounds = calculateDragBounds(\n            imageWidth = targetWidth,\n            imageHeight = targetHeight,\n        )\n        animateToTarget(\n            targetWidth = targetWidth,\n            targetHeight = targetHeight,\n            targetOffsetX = dragBounds.coerceInX(targetOffsetX),\n            targetOffsetY = dragBounds.coerceInY(targetOffsetY),\n        )\n    }\n\n    fun drag(dragAmount: Offset) {\n        cancelAnimation()\n        if (exceed || isBigVerticalImage) {\n            dragForVisit(dragAmount)\n        } else {\n            dragForExit(dragAmount)\n        }\n    }\n\n    private fun dragForVisit(dragAmount: Offset) {\n        val currentOffset = Offset(_currentOffsetXPixel.floatValue, _currentOffsetYPixel.floatValue)\n        val newOffset = currentOffset + dragAmount\n        val fixedOffset = draggableBounds.coerceIn(newOffset)\n        _currentOffsetXPixel.floatValue = fixedOffset.x\n        _currentOffsetYPixel.floatValue = fixedOffset.y\n    }\n\n    private fun dragForExit(dragAmount: Offset) {\n        val dragAmountY = dragAmount.y\n        if (dragAmountY <= 0F) {\n            if (_currentOffsetYPixel.floatValue > 0F) {\n                _currentOffsetYPixel.floatValue += dragAmountY\n            }\n        } else {\n            _currentOffsetYPixel.floatValue += dragAmountY\n        }\n    }\n\n    suspend fun dragStop(initialVelocity: Velocity) {\n        cancelAnimation()\n        if (!exceed && !isBigVerticalImage) {\n            dragStopForExit()\n            return\n        }\n        val initialValue = Offset(_currentOffsetXPixel.floatValue, _currentOffsetYPixel.floatValue)\n        AnimationState(\n            typeConverter = Offset.VectorConverter,\n            initialValue = initialValue,\n            initialVelocity = initialVelocity.toOffset(),\n        ).animateDecay(exponentialDecay()) {\n            flingAnimation = this\n            if (draggableBounds.outsideAbsolute(value) ||\n                velocity.getDistance() <= 300\n            ) {\n                flingAnimation = null\n                cancelAnimation()\n                return@animateDecay\n            }\n            val progressOffset = draggableBounds.coerceIn(value)\n            _currentOffsetXPixel.floatValue = progressOffset.x\n            _currentOffsetYPixel.floatValue = progressOffset.y\n        }\n    }\n\n    private suspend fun dragStopForExit() {\n        cancelAnimation()\n        val standardOffsetY = layoutSize.height / 2F - _currentHeightPixel.floatValue / 2F\n        val totalAmount = _currentOffsetYPixel.floatValue - standardOffsetY\n        val exitOffsetYThresholds = standardHeight * 0.3F\n        if (totalAmount > exitOffsetYThresholds) {\n            startDismiss()\n        } else {\n            val anim = AnimationState(initialValue = _currentOffsetYPixel.floatValue)\n            anim.animateTo(\n                targetValue = standardOffsetY,\n                animationSpec = tween(durationMillis = ImageViewerDefault.ANIMATION_DURATION),\n            ) {\n                resumeOffsetYAnimation = this\n                _currentOffsetYPixel.floatValue = value\n            }\n        }\n    }\n\n    internal suspend fun startDismiss() {\n        if ((initialSize.isUnspecified || initialSize.isEmpty())) {\n            onDismissRequest?.invoke()\n            return\n        }\n        val targetWidth =\n            if (initialSize.isEmpty()) _currentWidthPixel.floatValue else initialSize.width\n        val targetHeight =\n            if (initialSize.isEmpty()) _currentHeightPixel.floatValue else initialSize.height\n        val targetOffsetX = _currentOffsetXPixel.floatValue\n        val targetOffsetY = _currentOffsetYPixel.floatValue\n        animateToTarget(\n            targetWidth = targetWidth,\n            targetHeight = targetHeight,\n            targetOffsetX = targetOffsetX,\n            targetOffsetY = targetOffsetY,\n        )\n        onDismissRequest?.invoke()\n    }\n\n    private suspend fun animateToTarget(\n        targetWidth: Float,\n        targetHeight: Float,\n        targetOffsetX: Float,\n        targetOffsetY: Float,\n    ) {\n        cancelAnimation()\n        val startWidth = currentWidthPixel\n        val startHeight = currentHeightPixel\n        val startOffsetX = currentOffsetXPixel\n        val startOffsetY = currentOffsetYPixel\n        if (startWidth != targetWidth || startHeight != targetHeight\n            || startOffsetX != targetOffsetX || startOffsetY != targetOffsetY\n        ) {\n            val widthDiff = targetWidth - startWidth\n            val heightDiff = targetHeight - startHeight\n            val offsetXDiff = targetOffsetX - startOffsetX\n            val offsetYDiff = targetOffsetY - startOffsetY\n            val anim = AnimationState(initialValue = 0f)\n            anim.animateTo(\n                targetValue = 1f,\n                animationSpec = tween(durationMillis = ImageViewerDefault.ANIMATION_DURATION),\n            ) {\n                scaleAnimation = this\n                val progress = value\n                if (widthDiff != 0F) {\n                    _currentWidthPixel.floatValue = startWidth + widthDiff * progress\n                }\n                if (heightDiff != 0F) {\n                    _currentHeightPixel.floatValue = startHeight + heightDiff * progress\n                }\n                if (offsetXDiff != 0F) {\n                    _currentOffsetXPixel.floatValue = startOffsetX + offsetXDiff * progress\n                }\n                if (offsetYDiff != 0F) {\n                    _currentOffsetYPixel.floatValue = startOffsetY + offsetYDiff * progress\n                }\n            }\n        }\n    }\n\n    private fun cancelAnimation() {\n        scaleAnimation?.takeIf { it.isRunning }?.cancelAnimation()\n        scaleAnimation = null\n        flingAnimation?.takeIf { it.isRunning }?.cancelAnimation()\n        flingAnimation = null\n        resumeOffsetYAnimation?.takeIf { it.isRunning }?.cancelAnimation()\n        resumeOffsetYAnimation = null\n    }\n\n    internal fun zoom(centroid: Offset, zoom: Float) {\n        val newWidth = (currentWidthPixel * zoom).coerceInWidth()\n        val newHeight = (currentHeightPixel * zoom).coerceInHeight()\n        val xRatio = (centroid.x - currentOffsetXPixel) / currentWidthPixel\n        val yRatio = (centroid.y - currentOffsetYPixel) / currentHeightPixel\n        _currentWidthPixel.floatValue = newWidth\n        _currentHeightPixel.floatValue = newHeight\n        val xOffset = -(newWidth * xRatio - centroid.x)\n        val yOffset = -(newHeight * yRatio - centroid.y)\n        val bounds = calculateDragBounds(newWidth, newHeight)\n        _currentOffsetYPixel.floatValue = bounds.coerceInY(yOffset)\n        if (newWidth == standardWidth) {\n            _currentOffsetXPixel.floatValue = 0F\n        } else {\n            _currentOffsetXPixel.floatValue = bounds.coerceInX(xOffset)\n        }\n    }\n\n    private fun calculateDragBounds(imageWidth: Float, imageHeight: Float): Bounds {\n        val left: Float\n        val right: Float\n        if (imageWidth > layoutSize.width) {\n            left = -(imageWidth - layoutSize.width)\n            right = 0F\n        } else {\n            left = (layoutSize.width - imageWidth) / 2F\n            right = left\n        }\n        val top: Float\n        val bottom: Float\n        if (imageHeight > layoutSize.height) {\n            top = -(imageHeight - layoutSize.height)\n            bottom = 0F\n        } else {\n            top = (layoutSize.height - imageHeight) / 2F\n            bottom = top\n        }\n        return Bounds(\n            left = left,\n            top = top,\n            right = right,\n            bottom = bottom,\n        )\n    }\n\n    private fun Float.coerceInWidth(): Float {\n        val maxWidth = standardWidth * maximumScale\n        return coerceAtLeast(standardWidth).coerceAtMost(maxWidth)\n    }\n\n    private fun Float.coerceInHeight(): Float {\n        val maxHeight = standardHeight * maximumScale\n        return coerceAtLeast(standardHeight).coerceAtMost(maxHeight)\n    }\n\n    internal companion object {\n\n        private const val MAGIC_NUMBER = -21039142F\n\n        private val Size.magicWidth: Float\n            get() = if (isUnspecified) MAGIC_NUMBER else width\n\n        private val Size.magicHeight: Float\n            get() = if (isUnspecified) MAGIC_NUMBER else height\n\n        private fun magicSize(width: Float, height: Float): Size {\n            if (width == MAGIC_NUMBER || height == MAGIC_NUMBER) return Size.Unspecified\n            return Size(width = width, height = height)\n        }\n\n        val Saver: Saver<ImageViewerState, *> = listSaver(\n            save = {\n                listOf<Any>(\n                    it.aspectRatio,\n                    it.initialSize.magicWidth,\n                    it.initialSize.magicHeight,\n                    it.minimumScale,\n                    it.maximumScale,\n                )\n            },\n            restore = {\n                ImageViewerState(\n                    aspectRatio = it[0] as Float,\n                    initialSize = magicSize(width = it[1] as Float, height = it[2] as Float),\n                    minimumScale = it[3] as Float,\n                    maximumScale = it[4] as Float,\n                )\n            }\n        )\n    }\n}\n\nobject ImageViewerDefault {\n\n    const val ANIMATION_DURATION = 200\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/image/viewer/TransformGestureDetector.kt",
    "content": "package com.zhangke.framework.composable.image.viewer\n\nimport androidx.compose.foundation.gestures.awaitEachGesture\nimport androidx.compose.foundation.gestures.awaitFirstDown\nimport androidx.compose.foundation.gestures.calculateCentroid\nimport androidx.compose.foundation.gestures.calculateCentroidSize\nimport androidx.compose.foundation.gestures.calculateZoom\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.input.pointer.PointerInputScope\nimport androidx.compose.ui.input.pointer.positionChanged\nimport androidx.compose.ui.util.fastAny\nimport androidx.compose.ui.util.fastForEach\nimport kotlin.math.abs\n\nsuspend fun PointerInputScope.detectZoom(\n    onGesture: (centroid: Offset, zoom: Float) -> Unit\n) {\n    awaitEachGesture {\n        var zoom = 1f\n        var pastTouchSlop = false\n        val touchSlop = viewConfiguration.touchSlop\n\n        awaitFirstDown(requireUnconsumed = false)\n        do {\n            val event = awaitPointerEvent()\n            val canceled = event.changes.fastAny { it.isConsumed }\n            if (!canceled) {\n                val zoomChange = event.calculateZoom()\n\n                if (!pastTouchSlop) {\n                    zoom *= zoomChange\n\n                    val centroidSize = event.calculateCentroidSize(useCurrent = false)\n                    val zoomMotion = abs(1 - zoom) * centroidSize\n\n                    if (zoomMotion > touchSlop) {\n                        pastTouchSlop = true\n                    }\n                }\n\n                if (pastTouchSlop) {\n                    val centroid = event.calculateCentroid(useCurrent = false)\n                    if (zoomChange != 1f) {\n                        onGesture(centroid, zoomChange)\n                    }\n                    event.changes.fastForEach {\n                        if (it.positionChanged()) {\n                            it.consume()\n                        }\n                    }\n                }\n            }\n        } while (!canceled && event.changes.fastAny { it.pressed })\n    }\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/infinite/InfiniteBox.kt",
    "content": "package com.zhangke.framework.composable.infinite\n\nimport androidx.compose.foundation.gestures.detectDragGestures\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.geometry.Size\nimport androidx.compose.ui.input.pointer.pointerInput\nimport androidx.compose.ui.input.pointer.util.VelocityTracker\nimport androidx.compose.ui.input.pointer.util.addPointerInputChange\nimport androidx.compose.ui.layout.Layout\nimport androidx.compose.ui.layout.onSizeChanged\nimport androidx.compose.ui.unit.Constraints\nimport androidx.compose.ui.unit.Velocity\nimport androidx.compose.ui.unit.toSize\nimport com.zhangke.framework.composable.Bounds\nimport com.zhangke.framework.ktx.isSingle\nimport kotlinx.coroutines.launch\nimport kotlin.math.roundToInt\n\nprivate val infinityConstraints = Constraints()\n\n@Composable\nfun InfiniteBox(\n    modifier: Modifier = Modifier,\n    state: InfinityBoxState = rememberInfinityBoxState(),\n    content: @Composable () -> Unit,\n) {\n    val coroutineScope = rememberCoroutineScope()\n    var layoutSize by remember {\n        mutableStateOf(Size.Zero)\n    }\n    Layout(\n        modifier = modifier\n            .onSizeChanged {\n                layoutSize = it.toSize()\n                state.layoutSize = it.toSize()\n            }\n            .draggableInfinity(\n                enabled = state.exceed,\n                onDrag = { dragAmount ->\n                    state.drag(dragAmount)\n                },\n                onDragStopped = { initialVelocity ->\n                    coroutineScope.launch {\n                        state.fling(initialVelocity)\n                    }\n                }\n            ),\n        content = content,\n    ) { measurables, constraints ->\n        if (measurables.isSingle().not()) {\n            throw IllegalStateException(\"InfiniteBox is only allowed to have one children!\")\n        }\n        val placeable = measurables.first().measure(infinityConstraints)\n        state.exceed =\n            placeable.width > constraints.maxWidth || placeable.height > constraints.maxHeight\n        state.draggableBounds = Bounds(\n            left = (-(placeable.width - layoutSize.width)).coerceAtMost(0F),\n            top = (-(placeable.height - layoutSize.height)).coerceAtMost(0F),\n            right = 0F,\n            bottom = 0F,\n        )\n        layout(constraints.maxWidth, constraints.maxHeight) {\n            placeable.placeRelative(\n                x = state.currentOffset.x.roundToInt(),\n                y = state.currentOffset.y.roundToInt(),\n            )\n        }\n    }\n}\n\nprivate fun Modifier.draggableInfinity(\n    enabled: Boolean,\n    onDrag: (dragAmount: Offset) -> Unit,\n    onDragStopped: (velocity: Velocity) -> Unit,\n): Modifier {\n    val velocityTracker = VelocityTracker()\n    return pointerInput(enabled) {\n        if (enabled) {\n            detectDragGestures(\n                onDrag = { change, dragAmount ->\n                    velocityTracker.addPointerInputChange(change)\n                    onDrag(dragAmount)\n                },\n                onDragEnd = {\n                    val velocity = velocityTracker.calculateVelocity()\n                    onDragStopped(velocity)\n                },\n                onDragCancel = {\n                    val velocity = velocityTracker.calculateVelocity()\n                    onDragStopped(velocity)\n                },\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/infinite/InfinityBoxState.kt",
    "content": "package com.zhangke.framework.composable.infinite\n\nimport androidx.compose.animation.core.AnimationScope\nimport androidx.compose.animation.core.AnimationState\nimport androidx.compose.animation.core.AnimationVector2D\nimport androidx.compose.animation.core.VectorConverter\nimport androidx.compose.animation.core.animateDecay\nimport androidx.compose.animation.core.exponentialDecay\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.Stable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.saveable.Saver\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.geometry.Size\nimport androidx.compose.ui.unit.Velocity\nimport com.zhangke.framework.composable.Bounds\nimport com.zhangke.framework.composable.toOffset\n\n@Composable\nfun rememberInfinityBoxState(): InfinityBoxState {\n    return rememberSaveable(saver = InfinityBoxState.Saver) {\n        InfinityBoxState()\n    }\n}\n\n@Stable\nclass InfinityBoxState {\n\n    var exceed: Boolean by mutableStateOf(false)\n\n    private val _currentOffset = mutableStateOf(Offset.Zero)\n    val currentOffset by _currentOffset\n\n    internal var layoutSize = Size.Zero\n\n    internal var draggableBounds = Bounds.EMPTY\n        set(value) {\n            cancelAnimation()\n            field = value\n            _currentOffset.value = Offset.Zero\n        }\n\n    private var flingAnimation: AnimationScope<Offset, AnimationVector2D>? = null\n\n    fun moveToCenter() {\n        cancelAnimation()\n        if (!exceed) return\n        _currentOffset.value = Offset(\n            x = draggableBounds.left / 2F,\n            y = draggableBounds.top / 2F,\n        )\n    }\n\n    fun drag(dragAmount: Offset) {\n        cancelAnimation()\n        if (!exceed) return\n        val newOffset = currentOffset + dragAmount\n        _currentOffset.value = draggableBounds.coerceIn(newOffset)\n    }\n\n    suspend fun fling(initialVelocity: Velocity) {\n        if (!exceed) return\n        val initialValue = currentOffset\n        AnimationState(\n            typeConverter = Offset.VectorConverter,\n            initialValue = initialValue,\n            initialVelocity = initialVelocity.toOffset(),\n        ).animateDecay(exponentialDecay()) {\n            flingAnimation = this\n            if (draggableBounds.outsideAbsolute(value) ||\n                velocity.getDistance() <= 300\n            ) {\n                flingAnimation = null\n                cancelAnimation()\n                return@animateDecay\n            }\n            _currentOffset.value = draggableBounds.coerceIn(value)\n        }\n    }\n\n    private fun cancelAnimation() {\n        val animation = flingAnimation ?: return\n        flingAnimation = null\n        if (!animation.isRunning) return\n        animation.cancelAnimation()\n    }\n\n    companion object {\n\n        val Saver: Saver<InfinityBoxState, *> = Saver(\n            save = {\n                emptyArray<Unit>()\n            },\n            restore = {\n                InfinityBoxState()\n            },\n        )\n    }\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/inline/InlineVideoLazyColumn.kt",
    "content": "package com.zhangke.framework.composable.inline\n\nimport androidx.compose.foundation.gestures.FlingBehavior\nimport androidx.compose.foundation.gestures.ScrollableDefaults\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.lazy.LazyListScope\nimport androidx.compose.foundation.lazy.LazyListState\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport com.zhangke.framework.composable.sensitive.SensitiveLazyColumn\nimport com.zhangke.framework.composable.sensitive.SensitiveLazyColumnState\nimport com.zhangke.framework.composable.sensitive.transform\n\n@Composable\nfun InlineVideoLazyColumn(\n    modifier: Modifier = Modifier,\n    state: LazyListState = rememberLazyListState(),\n    contentPadding: PaddingValues = PaddingValues(0.dp),\n    reverseLayout: Boolean = false,\n    verticalArrangement: Arrangement.Vertical =\n        if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,\n    horizontalAlignment: Alignment.Horizontal = Alignment.Start,\n    flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),\n    userScrollEnabled: Boolean = true,\n    indexMapping: ((Int) -> Int) = { it },\n    content: LazyListScope.() -> Unit,\n) {\n    val sensitiveState = remember(state) {\n        mutableStateOf(\n            SensitiveLazyColumnState(\n                firstVisibleIndex = 0,\n                firstVisiblePercent = 0F,\n                lastVisibleIndex = 0,\n                lastVisiblePercent = 0F,\n                isScrollInProgress = false,\n            )\n        )\n    }\n    val localSensitiveState by sensitiveState\n    val playableIndexRecorder = remember(state) {\n        PlayableIndexRecorder()\n    }\n    LaunchedEffect(localSensitiveState) {\n        playableIndexRecorder.updateLayoutState(localSensitiveState)\n    }\n    CompositionLocalProvider(\n        LocalPlayableIndexRecorder provides playableIndexRecorder\n    ) {\n        SensitiveLazyColumn(\n            modifier = modifier,\n            state = state,\n            onSensitiveLayoutChanged = {\n                sensitiveState.value = it.transform(indexMapping)\n            },\n            contentPadding = contentPadding,\n            reverseLayout = reverseLayout,\n            verticalArrangement = verticalArrangement,\n            horizontalAlignment = horizontalAlignment,\n            flingBehavior = flingBehavior,\n            userScrollEnabled = userScrollEnabled,\n            content = content,\n        )\n    }\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/inline/PlayableIndexRecorderLocal.kt",
    "content": "package com.zhangke.framework.composable.inline\n\nimport androidx.compose.runtime.ProvidableCompositionLocal\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.staticCompositionLocalOf\nimport com.zhangke.framework.composable.sensitive.SensitiveLazyColumnState\nimport com.zhangke.framework.ktx.isSingle\nimport com.zhangke.framework.ktx.second\nimport kotlin.math.max\n\nval LocalPlayableIndexRecorder: ProvidableCompositionLocal<PlayableIndexRecorder?> =\n    staticCompositionLocalOf { null }\n\nclass PlayableIndexRecorder {\n\n    companion object {\n\n        private const val PLAYABLE_PERCENT_THRESHOLD = 0.3F\n        private const val UNSPECIFIED_INDEX = -1\n    }\n\n    private val _recorder = mutableSetOf<Int>()\n\n    private val _currentActiveIndex = mutableIntStateOf(-1)\n    val currentActiveIndex: Int by _currentActiveIndex\n\n    fun updateLayoutState(state: SensitiveLazyColumnState) {\n        val threshold = PLAYABLE_PERCENT_THRESHOLD\n        val currentActiveInlineIndex = _currentActiveIndex.intValue\n        if (currentActiveInlineIndex >= 0) {\n            val currentActivePlayablePercent =\n                state.getVisiblePercentOfIndex(currentActiveInlineIndex)\n            if (currentActivePlayablePercent >= threshold) return\n        }\n        _currentActiveIndex.intValue = getCenterIndex(state) ?: UNSPECIFIED_INDEX\n    }\n\n    fun changeActiveIndex(index: Int) {\n        _currentActiveIndex.intValue = index\n    }\n\n    private fun getCenterIndex(state: SensitiveLazyColumnState): Int? {\n        val indexList = getIntervalIndexList(state.firstVisibleIndex, state.lastVisibleIndex)\n        if (indexList.isEmpty()) return null\n        if (indexList.isSingle()) {\n            return state.getValidateIndexOrNull(indexList.first(), PLAYABLE_PERCENT_THRESHOLD)\n        }\n        if (indexList.size == 2) {\n            val maxIndex = max(indexList.first(), indexList.second())\n            return state.getValidateIndexOrNull(maxIndex, PLAYABLE_PERCENT_THRESHOLD)\n        }\n        val centerIndex = indexList[indexList.size / 2]\n        return state.getValidateIndexOrNull(centerIndex, PLAYABLE_PERCENT_THRESHOLD)\n    }\n\n    private fun SensitiveLazyColumnState.getValidateIndexOrNull(\n        index: Int,\n        playablePercentThreshold: Float,\n    ): Int? {\n        val percent = getVisiblePercentOfIndex(index)\n        if (percent >= playablePercentThreshold) return index\n        return null\n    }\n\n    private fun getIntervalIndexList(startIndex: Int, endIndex: Int): List<Int> {\n        val intervalList = mutableListOf<Int>()\n        _recorder.sorted().forEach { index ->\n            if (index > endIndex) return@forEach\n            if (index in startIndex..endIndex) {\n                intervalList += index\n            }\n        }\n        return intervalList\n    }\n\n    fun recordePlayableIndex(index: Int) {\n        _recorder += index\n    }\n\n    fun removePlayableIndex(index: Int) {\n        _recorder -= index\n    }\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/pick/PickVisualMediaLauncherContainer.kt",
    "content": "package com.zhangke.framework.composable.pick\n\nimport androidx.compose.runtime.Composable\nimport com.zhangke.framework.utils.PlatformUri\n\n@Composable\nexpect fun PickVisualMediaLauncherContainer(\n    onResult: (List<PlatformUri>) -> Unit,\n    maxItems: Int = 1,\n    content: @Composable PickVisualMediaLauncherContainerScope.() -> Unit,\n)\n\nexpect class PickVisualMediaLauncherContainerScope {\n\n    fun launchImage()\n\n    fun launchMedia()\n\n    fun launchVideo()\n\n    fun launchImageFile()\n\n    fun launchVideoFile()\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/sensitive/SensitiveLazyColumn.kt",
    "content": "package com.zhangke.framework.composable.sensitive\n\nimport androidx.compose.foundation.gestures.FlingBehavior\nimport androidx.compose.foundation.gestures.ScrollableDefaults\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.LazyListItemInfo\nimport androidx.compose.foundation.lazy.LazyListScope\nimport androidx.compose.foundation.lazy.LazyListState\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.MutableState\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport kotlin.math.max\n\n@Composable\nfun SensitiveLazyColumn(\n    modifier: Modifier = Modifier,\n    state: LazyListState = rememberLazyListState(),\n    onSensitiveLayoutChanged: (SensitiveLazyColumnState) -> Unit,\n    contentPadding: PaddingValues = PaddingValues(0.dp),\n    reverseLayout: Boolean = false,\n    verticalArrangement: Arrangement.Vertical =\n        if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,\n    horizontalAlignment: Alignment.Horizontal = Alignment.Start,\n    flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),\n    userScrollEnabled: Boolean = true,\n    content: LazyListScope.() -> Unit,\n) {\n    val layoutInfo by remember { derivedStateOf { state.layoutInfo } }\n    val visibleItemsInfo = layoutInfo.visibleItemsInfo\n    if (visibleItemsInfo.isNotEmpty()) {\n        val firstItemLayoutInfo = visibleItemsInfo.first()\n        val lastItemLayoutInfo = visibleItemsInfo.last()\n        val firstVisiblePercent = state.visibilityPercent(firstItemLayoutInfo)\n        val lastVisiblePercent = state.visibilityPercent(lastItemLayoutInfo)\n        val isScrollInProgress = state.isScrollInProgress\n        LaunchedEffect(\n            visibleItemsInfo,\n            isScrollInProgress,\n            firstVisiblePercent,\n            lastVisiblePercent,\n            isScrollInProgress,\n        ) {\n            onSensitiveLayoutChanged(\n                SensitiveLazyColumnState(\n                    firstVisibleIndex = firstItemLayoutInfo.index,\n                    firstVisiblePercent = firstVisiblePercent,\n                    lastVisibleIndex = lastItemLayoutInfo.index,\n                    lastVisiblePercent = lastVisiblePercent,\n                    isScrollInProgress = isScrollInProgress,\n                )\n            )\n        }\n    }\n    LazyColumn(\n        modifier = modifier,\n        state = state,\n        contentPadding = contentPadding,\n        reverseLayout = reverseLayout,\n        verticalArrangement = verticalArrangement,\n        horizontalAlignment = horizontalAlignment,\n        flingBehavior = flingBehavior,\n        userScrollEnabled = userScrollEnabled,\n        content = content,\n    )\n}\n\nfun LazyListState.visibilityPercent(info: LazyListItemInfo): Float {\n    val cutTop = max(0, layoutInfo.viewportStartOffset - info.offset)\n    val cutBottom = max(0, info.offset + info.size - layoutInfo.viewportEndOffset)\n    return max(0f, 100f - (cutTop + cutBottom) * 100f / info.size)\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/sensitive/SensitiveLazyColumnState.kt",
    "content": "package com.zhangke.framework.composable.sensitive\n\ndata class SensitiveLazyColumnState(\n    val firstVisibleIndex: Int,\n    val firstVisiblePercent: Float,\n    val lastVisibleIndex: Int,\n    val lastVisiblePercent: Float,\n    val isScrollInProgress: Boolean,\n) {\n\n    fun getVisiblePercentOfIndex(index: Int): Float {\n        if (index == firstVisibleIndex) return firstVisiblePercent\n        if (index == lastVisibleIndex) return lastVisiblePercent\n        if (index in firstVisibleIndex..lastVisibleIndex) return 1F\n        return 0F\n    }\n}\n\nfun SensitiveLazyColumnState.transform(indexMapping: (Int) -> Int): SensitiveLazyColumnState{\n    return this.copy(\n        firstVisibleIndex = indexMapping(firstVisibleIndex),\n        lastVisibleIndex = indexMapping(lastVisibleIndex),\n    )\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/topout/TopOutTopBarLayout.kt",
    "content": "package com.zhangke.framework.composable.topout\n\nimport androidx.compose.foundation.gestures.Orientation\nimport androidx.compose.foundation.gestures.scrollable\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.graphicsLayer\nimport androidx.compose.ui.input.nestedscroll.nestedScroll\nimport androidx.compose.ui.layout.onGloballyPositioned\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.unit.dp\nimport com.zhangke.framework.composable.StateSaver\nimport com.zhangke.framework.utils.dpToPx\nimport com.zhangke.framework.utils.pxToDp\n\n@Composable\nfun TopOutTopBarLayout(\n    modifier: Modifier = Modifier,\n    topBar: @Composable () -> Unit,\n    content: @Composable () -> Unit,\n) {\n    val density = LocalDensity.current\n    var topBarHeightState by rememberSaveable(saver = StateSaver.MutableDpSaver) {\n        mutableStateOf(0.dp)\n    }\n    var topMarginState by rememberSaveable(saver = StateSaver.MutableDpSaver) {\n        mutableStateOf(0.dp)\n    }\n\n    val finalModifier = if (topBarHeightState == 0.dp) {\n        Modifier.then(modifier)\n    } else {\n        val connection = rememberTopOutTopBarLayoutConnection(\n            topBarHeight = topBarHeightState.dpToPx(density),\n        )\n        topMarginState = connection.topMargin.pxToDp(density)\n        Modifier\n            .then(modifier)\n            .nestedScroll(connection)\n    }\n    Box(modifier = finalModifier) {\n        Box(\n            modifier = Modifier\n                .scrollable(rememberScrollState(), Orientation.Vertical)\n                .padding(top = topMarginState),\n        ) {\n            content()\n        }\n        Box(\n            modifier = Modifier\n                .onGloballyPositioned {\n                    val heightDp = it.size.height.pxToDp(density)\n                    if (heightDp > 0.dp && heightDp != topBarHeightState) {\n                        topBarHeightState = heightDp\n                    }\n                }\n                .graphicsLayer(translationY = (-(topBarHeightState - topMarginState)).dpToPx(density)),\n        ) {\n            topBar()\n        }\n    }\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/topout/TopOutTopBarLayoutConnection.kt",
    "content": "package com.zhangke.framework.composable.topout\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.saveable.Saver\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.input.nestedscroll.NestedScrollConnection\nimport androidx.compose.ui.input.nestedscroll.NestedScrollSource\n\n@Composable\nfun rememberTopOutTopBarLayoutConnection(\n    topBarHeight: Float,\n): TopOutTopBarLayoutConnection {\n    return rememberSaveable(topBarHeight, saver = TopOutTopBarLayoutConnection.Saver) {\n        TopOutTopBarLayoutConnection(topBarHeight)\n    }\n}\n\nclass TopOutTopBarLayoutConnection(\n    private val topBarHeight: Float,\n    initialTopMargin: Float = topBarHeight,\n) : NestedScrollConnection {\n\n    companion object {\n\n        val Saver: Saver<TopOutTopBarLayoutConnection, *> = Saver(\n            save = {\n                arrayOf(it.topBarHeight, it.topMargin)\n            },\n            restore = {\n                TopOutTopBarLayoutConnection(it.first(), it[1])\n            },\n        )\n    }\n\n    var topMargin by mutableStateOf(initialTopMargin)\n\n    // content to screen margin\n    private var _topMargin: Float = initialTopMargin\n        private set(value) {\n            field = value\n            topMargin = value\n        }\n\n    override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {\n        val margin = _topMargin\n\n        if (margin + available.y > topBarHeight) {\n            _topMargin = topBarHeight\n            return Offset(0f, topBarHeight - margin)\n        }\n\n        if (margin + available.y < 0F) {\n            _topMargin = 0F\n            return Offset(0f, -margin)\n        }\n\n        _topMargin += available.y\n\n        return Offset(0f, available.y)\n    }\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/composable/video/VideoPlayer.kt",
    "content": "package com.zhangke.framework.composable.video\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.Stable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableFloatStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.layout.ContentScale\nimport io.github.kdroidfilter.composemediaplayer.InitialPlayerState\nimport io.github.kdroidfilter.composemediaplayer.VideoPlayerSurface\nimport io.github.kdroidfilter.composemediaplayer.rememberVideoPlayerState\nimport io.github.kdroidfilter.composemediaplayer.VideoPlayerError as KdVideoPlayerError\nimport io.github.kdroidfilter.composemediaplayer.VideoPlayerState as KdVideoPlayerState\n\n@Stable\nclass VideoPlayerController internal constructor(\n    val state: KdVideoPlayerState,\n    initialContentScale: ContentScale,\n) {\n\n    private var rememberedUnmuteVolume by mutableFloatStateOf(1f)\n\n    var contentScale by mutableStateOf(initialContentScale)\n        private set\n\n    val isPlaying: Boolean\n        get() = state.isPlaying\n\n    val isBuffering: Boolean\n        get() = state.isLoading\n\n    val isMuted: Boolean\n        get() = state.volume <= 0f\n\n    val currentTimeInSeconds: Float\n        get() = state.currentTime.toFloat()\n\n    val totalDurationInSeconds: Float\n        get() = resolveDurationSeconds()\n\n    val hasPlaybackEnded: Boolean\n        get() {\n            val duration = totalDurationInSeconds\n            if (duration <= 0f) return false\n            if (state.loop || state.isPlaying || state.isLoading) return false\n            return currentTimeInSeconds >= (duration - 0.2f)\n        }\n\n    val lastError: KdVideoPlayerError?\n        get() = state.error\n\n    val positionText: String\n        get() = state.positionText\n\n    val durationText: String\n        get() = state.durationText\n\n    val progress: Float\n        get() = state.sliderPos\n\n    fun load(\n        mediaUrl: String,\n        autoPlay: Boolean = true,\n    ) {\n        val initialState = if (autoPlay) {\n            InitialPlayerState.PLAY\n        } else {\n            InitialPlayerState.PAUSE\n        }\n        state.openUri(mediaUrl, initialState)\n    }\n\n    fun play() {\n        if (hasPlaybackEnded) {\n            state.seekTo(0F)\n        }\n        state.play()\n    }\n\n    fun pause() {\n        state.pause()\n    }\n\n    fun stop() {\n        state.stop()\n    }\n\n    fun togglePlayPause() {\n        if (state.isPlaying) {\n            state.pause()\n        } else {\n            state.play()\n        }\n    }\n\n    fun mute() {\n        val currentVolume = state.volume\n        if (currentVolume > 0f) {\n            rememberedUnmuteVolume = currentVolume\n        }\n        state.volume = 0f\n    }\n\n    fun unmute() {\n        state.volume = rememberedUnmuteVolume.coerceIn(0.01f, 1f)\n    }\n\n    fun toggleMute() {\n        if (isMuted) {\n            unmute()\n        } else {\n            mute()\n        }\n    }\n\n    fun setMuted(muted: Boolean) {\n        if (muted) {\n            mute()\n        } else {\n            unmute()\n        }\n    }\n\n    fun setLooping(looping: Boolean) {\n        state.loop = looping\n    }\n\n    fun setPlaybackSpeed(speed: Float) {\n        state.playbackSpeed = speed.coerceIn(0.5f, 2f)\n    }\n\n    fun setVolume(level: Float) {\n        val safeLevel = level.coerceIn(0f, 1f)\n        if (safeLevel > 0f) {\n            rememberedUnmuteVolume = safeLevel\n        }\n        state.volume = safeLevel\n    }\n\n    fun seekToProgress(progress: Float) {\n        val target = progress.coerceIn(0f, 1000f)\n        state.sliderPos = target\n        state.userDragging = false\n        state.seekTo(target)\n    }\n\n    fun seekTo(seconds: Float?) {\n        if (seconds == null) return\n        seekToSeconds(seconds)\n    }\n\n    fun seekToSeconds(seconds: Float) {\n        val duration = totalDurationInSeconds\n        if (duration <= 0f) return\n        seekToProgress(seconds / duration * 1000f)\n    }\n\n    fun updateContentScale(contentScale: ContentScale) {\n        this.contentScale = contentScale\n    }\n\n    fun toggleFullScreen() {\n        state.toggleFullscreen()\n    }\n\n    fun clearError() {\n        state.clearError()\n    }\n\n    private fun resolveDurationSeconds(): Float {\n        val metadataDurationSeconds = state.metadata.duration\n            ?.takeIf { it > 0L }\n            ?.toFloat()\n            ?.div(1000f)\n        if (metadataDurationSeconds != null) return metadataDurationSeconds\n        return parseDurationText(state.durationText)\n    }\n}\n\n@Composable\nfun rememberVideoPlayerController(\n    mediaUrl: String,\n    autoPlay: Boolean = true,\n    initialMuted: Boolean = false,\n    initialPlaybackSpeed: Float = 1f,\n    initialContentScale: ContentScale = ContentScale.Crop,\n    isLooping: Boolean = true,\n    startTimeInSeconds: Float? = null,\n): VideoPlayerController {\n    val playerState = rememberVideoPlayerState()\n    val controller = remember(playerState) {\n        VideoPlayerController(\n            state = playerState,\n            initialContentScale = initialContentScale,\n        )\n    }\n\n    LaunchedEffect(mediaUrl, autoPlay) {\n        controller.load(\n            mediaUrl = mediaUrl,\n            autoPlay = autoPlay,\n        )\n    }\n\n    LaunchedEffect(isLooping) {\n        controller.setLooping(isLooping)\n    }\n\n    LaunchedEffect(initialPlaybackSpeed) {\n        controller.setPlaybackSpeed(initialPlaybackSpeed)\n    }\n\n    LaunchedEffect(initialContentScale) {\n        controller.updateContentScale(initialContentScale)\n    }\n\n    LaunchedEffect(initialMuted) {\n        controller.setMuted(initialMuted)\n    }\n\n    LaunchedEffect(startTimeInSeconds) {\n        startTimeInSeconds?.let(controller::seekToSeconds)\n    }\n\n    return controller\n}\n\n@Composable\nfun VideoPlayer(\n    mediaUrl: String,\n    modifier: Modifier = Modifier,\n    autoPlay: Boolean = true,\n    initialMuted: Boolean = false,\n    initialPlaybackSpeed: Float = 1f,\n    initialContentScale: ContentScale = ContentScale.Crop,\n    isLooping: Boolean = true,\n    startTimeInSeconds: Float? = null,\n    overlay: @Composable () -> Unit = {},\n) {\n    val controller = rememberVideoPlayerController(\n        mediaUrl = mediaUrl,\n        autoPlay = autoPlay,\n        initialMuted = initialMuted,\n        initialPlaybackSpeed = initialPlaybackSpeed,\n        initialContentScale = initialContentScale,\n        isLooping = isLooping,\n        startTimeInSeconds = startTimeInSeconds,\n    )\n    VideoPlayer(\n        controller = controller,\n        modifier = modifier,\n        overlay = overlay,\n    )\n}\n\n@Composable\nfun VideoPlayer(\n    controller: VideoPlayerController,\n    modifier: Modifier = Modifier,\n    overlay: @Composable () -> Unit = {},\n) {\n    VideoPlayerSurface(\n        playerState = controller.state,\n        modifier = modifier,\n        contentScale = controller.contentScale,\n        overlay = overlay,\n    )\n}\n\nprivate fun parseDurationText(durationText: String): Float {\n    if (durationText.isBlank()) return 0f\n    val parts = durationText.split(\":\")\n    return when (parts.size) {\n        2 -> {\n            val minutes = parts[0].toFloatOrNull() ?: return 0f\n            val seconds = parts[1].toFloatOrNull() ?: return 0f\n            minutes * 60f + seconds\n        }\n\n        3 -> {\n            val hours = parts[0].toFloatOrNull() ?: return 0f\n            val minutes = parts[1].toFloatOrNull() ?: return 0f\n            val seconds = parts[2].toFloatOrNull() ?: return 0f\n            hours * 3600f + minutes * 60f + seconds\n        }\n\n        else -> 0f\n    }\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/controller/CommonLoadableController.kt",
    "content": "package com.zhangke.framework.controller\n\nimport com.zhangke.framework.composable.TextString\nimport com.zhangke.framework.utils.LoadState\nimport kotlinx.coroutines.CoroutineScope\n\ndata class CommonLoadableUiState<T>(\n    override val dataList: List<T>,\n    override val initializing: Boolean,\n    override val refreshing: Boolean,\n    override val loadMoreState: LoadState,\n    override val errorMessage: TextString?,\n) : LoadableUiState<T, CommonLoadableUiState<T>> {\n\n    override fun copyObject(\n        dataList: List<T>,\n        initializing: Boolean,\n        refreshing: Boolean,\n        loadMoreState: LoadState,\n        errorMessage: TextString?\n    ): CommonLoadableUiState<T> {\n        return copy(\n            dataList = dataList,\n            initializing = initializing,\n            refreshing = refreshing,\n            loadMoreState = loadMoreState,\n            errorMessage = errorMessage,\n        )\n    }\n}\n\nclass CommonLoadableController<T>(\n    coroutineScope: CoroutineScope,\n    onPostSnackMessage: (TextString) -> Unit,\n) : LoadableController<T, CommonLoadableUiState<T>>(\n    coroutineScope = coroutineScope,\n    initialUiState = CommonLoadableUiState(\n        dataList = emptyList(),\n        initializing = false,\n        refreshing = false,\n        loadMoreState = LoadState.Idle,\n        errorMessage = null,\n    ),\n    onPostSnackMessage = onPostSnackMessage,\n)\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/controller/LoadableController.kt",
    "content": "package com.zhangke.framework.controller\n\nimport com.zhangke.framework.composable.TextString\nimport com.zhangke.framework.composable.textOf\nimport com.zhangke.framework.composable.toTextStringOrNull\nimport com.zhangke.framework.utils.LoadState\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.update\nimport kotlinx.coroutines.launch\n\n/**\n * type DATA is the type of the data to be loaded,\n * type IMPL is the type of the implementation of the LoadableUiState.\n */\ninterface LoadableUiState<DATA, IMPL : LoadableUiState<DATA, IMPL>> {\n\n    val dataList: List<DATA>\n\n    val initializing: Boolean\n\n    val refreshing: Boolean\n\n    val loadMoreState: LoadState\n\n    val errorMessage: TextString?\n\n    fun copyObject(\n        dataList: List<DATA> = this.dataList,\n        initializing: Boolean = this.initializing,\n        refreshing: Boolean = this.refreshing,\n        loadMoreState: LoadState = this.loadMoreState,\n        errorMessage: TextString? = this.errorMessage,\n    ): IMPL\n}\n\n/**\n * type DATA is the type of the data to be loaded,\n * type IMPL is the type of the implementation of the LoadableUiState.\n */\nopen class LoadableController<DATA, IMPL : LoadableUiState<DATA, IMPL>>(\n    private val coroutineScope: CoroutineScope,\n    initialUiState: IMPL,\n    private val onPostSnackMessage: (TextString) -> Unit,\n) {\n\n    val mutableUiState: MutableStateFlow<IMPL> = MutableStateFlow(initialUiState)\n    val uiState = mutableUiState.asStateFlow()\n\n    private var initJob: Job? = null\n    private var refreshJob: Job? = null\n    private var loadMoreJob: Job? = null\n\n    /**\n     * 一般来说初始化的时候调用一次，之后只需要调用 onRefresh 和 onLoadMore 即可。\n     * 如果提供了 getDataFromLocal 参数，那么会先从本地获取数据，然后再调用 getDataFromServer。\n     */\n    fun initData(\n        getDataFromServer: suspend () -> Result<List<DATA>>,\n        getDataFromLocal: (suspend () -> List<DATA>)? = null,\n    ) {\n        mutableUiState.update {\n            it.copyObject(dataList = emptyList())\n        }\n        initJob?.cancel()\n        initJob = coroutineScope.launch {\n            mutableUiState.update {\n                it.copyObject(initializing = true)\n            }\n            if (getDataFromLocal != null) {\n                val localData = getDataFromLocal()\n                if (localData.isNotEmpty()) {\n                    mutableUiState.update {\n                        it.copyObject(\n                            dataList = localData,\n                            initializing = false,\n                        )\n                    }\n                }\n            }\n            getDataFromServer().handleAsRefresh()\n        }\n    }\n\n    fun onRefresh(\n        hideRefreshing: Boolean = false,\n        getDataFromServer: suspend () -> Result<List<DATA>>,\n    ) {\n        if (mutableUiState.value.refreshing) return\n        mutableUiState.update {\n            it.copyObject(refreshing = !hideRefreshing, errorMessage = null)\n        }\n        loadMoreJob?.cancel()\n        refreshJob?.cancel()\n        refreshJob = coroutineScope.launch {\n            getDataFromServer().handleAsRefresh()\n        }\n    }\n\n    private fun Result<List<DATA>>.handleAsRefresh() {\n        this.onSuccess { list ->\n            mutableUiState.update {\n                it.copyObject(\n                    dataList = list,\n                    refreshing = false,\n                    initializing = false,\n                )\n            }\n        }.onFailure { e ->\n            val errorMessage = e.message?.let { textOf(it) }\n            if (uiState.value.dataList.isEmpty()) {\n                mutableUiState.update {\n                    it.copyObject(\n                        errorMessage = errorMessage,\n                        refreshing = false,\n                        initializing = false,\n                    )\n                }\n            } else {\n                errorMessage?.let(onPostSnackMessage)\n                mutableUiState.update {\n                    it.copyObject(\n                        refreshing = false,\n                        initializing = false,\n                    )\n                }\n            }\n        }\n    }\n\n    fun onLoadMore(\n        loadMoreFromServer: suspend () -> Result<List<DATA>>,\n    ) {\n        if (mutableUiState.value.refreshing) return\n        if (mutableUiState.value.loadMoreState == LoadState.Loading) return\n        mutableUiState.update { it.copyObject(loadMoreState = LoadState.Loading) }\n        loadMoreJob?.cancel()\n        loadMoreJob = coroutineScope.launch {\n            loadMoreFromServer()\n                .onSuccess { list ->\n                    mutableUiState.update {\n                        it.copyObject(\n                            dataList = it.dataList + list,\n                            loadMoreState = LoadState.Idle,\n                        )\n                    }\n                }.onFailure { e ->\n                    mutableUiState.update {\n                        it.copyObject(loadMoreState = LoadState.Failed(e.toTextStringOrNull()))\n                    }\n                }\n        }\n    }\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/coroutines/JobExt.kt",
    "content": "package com.zhangke.framework.coroutines\n\nimport kotlinx.coroutines.CancellationException\nimport kotlinx.coroutines.Job\n\nfun Job.invokeOnCancel(block: (CancellationException) -> Unit) {\n    invokeOnCompletion {\n        if (it is CancellationException) {\n            block(it)\n        }\n    }\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/date/DateParser.kt",
    "content": "@file:OptIn(ExperimentalTime::class)\n\npackage com.zhangke.framework.date\n\nimport com.zhangke.framework.utils.Rfc822InstantParser\nimport kotlinx.datetime.Clock\nimport kotlinx.datetime.Instant\nimport kotlin.time.ExperimentalTime\n\nobject DateParser {\n\n    @OptIn(ExperimentalTime::class)\n    fun parseOrCurrent(datetime: String): com.zhangke.framework.datetime.Instant {\n        val instant = parseAll(datetime) ?: Clock.System.now()\n        return com.zhangke.framework.datetime.Instant(instant)\n    }\n\n    fun parseAll(datetime: String): Instant? {\n        return parseISODate(datetime) ?: parseRfc822Date(datetime) ?: parseRfc3339Date(datetime)\n        ?: parseISO8601(datetime)\n    }\n\n    fun parseISODate(datetime: String): Instant? {\n        return try {\n            Instant.parse(datetime)\n        } catch (e: Throwable) {\n            null\n        }\n    }\n\n    fun parseRfc822Date(datetime: String): Instant? {\n        return try {\n            Rfc822InstantParser.parse(datetime)\n        } catch (e: Throwable) {\n            null\n        }\n    }\n\n    fun parseRfc3339Date(datetime: String): Instant? {\n        return try {\n            Instant.parse(datetime)\n        } catch (e: Throwable) {\n            null\n        }\n    }\n\n    fun parseISO8601(datetime: String): Instant? {\n        return try {\n            Instant.parse(datetime)\n        } catch (e: Throwable) {\n            e.printStackTrace()\n            null\n        }\n    }\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/date/InstantFormater.kt",
    "content": "@file:OptIn(ExperimentalTime::class)\n\npackage com.zhangke.framework.date\n\nimport kotlinx.datetime.Instant\nimport kotlin.time.ExperimentalTime\n\nexpect class InstantFormater() {\n\n    fun formatToMediumDate(instant: Instant): String\n\n    fun formatToMediumDateWithoutTime(instant: Instant): String\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/datetime/Instant.kt",
    "content": "package com.zhangke.framework.datetime\n\nimport com.zhangke.framework.serialize.TimestampAsInstantSerializer\nimport kotlinx.serialization.Serializable\nimport kotlin.time.ExperimentalTime\n\n@Serializable(with = TimestampAsInstantSerializer::class)\ndata class Instant(val epochMillis: Long) {\n\n    @OptIn(ExperimentalTime::class)\n    val instant: kotlinx.datetime.Instant\n        get() = kotlinx.datetime.Instant.fromEpochMilliseconds(epochMillis)\n}\n\n@OptIn(ExperimentalTime::class)\nfun Instant(instant: kotlinx.datetime.Instant): Instant {\n    return Instant(instant.toEpochMilliseconds())\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/imageloader/ImageLoaderUtils.kt",
    "content": "package com.zhangke.framework.imageloader\n\nimport com.seiko.imageloader.ImageLoader\nimport com.seiko.imageloader.model.ImageRequest\nimport com.seiko.imageloader.model.ImageResult\n\nsuspend fun ImageLoader.executeSafety(request: ImageRequest): ImageResult {\n    return try {\n        this.execute(request)\n    } catch (e: Throwable) {\n        ImageResult.OfError(e)\n    }\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/ktx/CollectionsExt.kt",
    "content": "package com.zhangke.framework.ktx\n\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.util.fastForEachIndexed\n\nfun <T> Collection<T>.isSingle(): Boolean = size == 1\n\nfun <T> List<T>.second(): T {\n    return this[1]\n}\n\nfun <T> List<T>.third(): T {\n    return this[2]\n}\n\nfun <T> List<T>.fourth(): T {\n    return this[3]\n}\n\nfun List<Float>.averageDropFirst(count: Int): Double {\n    var sum = 0.0\n    for (index in count .. lastIndex) {\n        sum += get(index)\n    }\n    return sum / count\n}\n\nfun Iterable<Dp>.sum(): Dp {\n    var sum: Dp = 0.dp\n    for (element in this) {\n        sum += element\n    }\n    return sum\n}\n\nfun <T> List<T>.distinctByKey(getKey: (index: Int, item: T) -> String): List<T> {\n    if (this.size < 2) return this\n    val keySet = mutableSetOf<String>()\n    val newList = mutableListOf<T>()\n    this.fastForEachIndexed { index, item ->\n        val key = getKey(index, item)\n        if (!keySet.contains(key)) {\n            keySet += key\n            newList += item\n        }\n    }\n    return newList\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/ktx/FlowExt.kt",
    "content": "package com.zhangke.framework.ktx\n\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.flow.SharingStarted\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.map\nimport kotlinx.coroutines.flow.stateIn\n\nfun <T, M> StateFlow<T>.map(\n    coroutineScope: CoroutineScope,\n    mapper: (value: T) -> M\n): StateFlow<M> = map { mapper(it) }.stateIn(\n    coroutineScope,\n    SharingStarted.Eagerly,\n    mapper(value)\n)\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/ktx/LazyBackingFieldDelegate.kt",
    "content": "package com.zhangke.framework.ktx\n\nimport kotlin.reflect.KProperty\n\nclass LazyBackingFieldDelegate<T>(private val provider: () -> T) {\n\n    private var backingField: T? = null\n\n    operator fun getValue(thisRef: Any?, property: KProperty<*>): T {\n        if (backingField == null) {\n            backingField = provider()\n        }\n        return backingField!!\n    }\n\n    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {\n        backingField = value\n    }\n}"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/ktx/StringExt.kt",
    "content": "package com.zhangke.framework.ktx\n\ninline fun String?.ifNullOrEmpty(block: () -> String): String {\n    if (this == null) return block()\n    return ifEmpty(block)\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/ktx/ViewModels.kt",
    "content": "package com.zhangke.framework.ktx\n\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.zhangke.framework.lifecycle.SubViewModel\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.CoroutineStart\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.launch\nimport kotlin.coroutines.CoroutineContext\nimport kotlin.coroutines.EmptyCoroutineContext\n\nfun ViewModel.launchInViewModel(\n    context: CoroutineContext = EmptyCoroutineContext,\n    start: CoroutineStart = CoroutineStart.DEFAULT,\n    block: suspend CoroutineScope.() -> Unit\n): Job {\n    return viewModelScope.launch(context, start, block)\n}\n\nfun SubViewModel.launchInViewModel(\n    context: CoroutineContext = EmptyCoroutineContext,\n    start: CoroutineStart = CoroutineStart.DEFAULT,\n    block: suspend CoroutineScope.() -> Unit\n): Job {\n    return viewModelScope.launch(context, start, block)\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/lifecycle/ContainerViewModel.kt",
    "content": "package com.zhangke.framework.lifecycle\n\nimport androidx.lifecycle.ViewModel\n\nabstract class ContainerViewModel<T : SubViewModel, P : ContainerViewModel.SubViewModelParams> :\n    ViewModel() {\n\n    abstract fun createSubViewModel(params: P): T\n\n    private val subViewModelStore = mutableMapOf<String, T>()\n\n    protected fun obtainSubViewModel(params: P): T {\n        return subViewModelStore.getOrPut(params.key) {\n            createSubViewModel(params)\n        }.also { addCloseable(it) }\n    }\n\n    abstract class SubViewModelParams {\n\n        abstract val key: String\n    }\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/lifecycle/SubViewModel.kt",
    "content": "package com.zhangke.framework.lifecycle\n\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.SupervisorJob\nimport kotlinx.coroutines.cancel\n\nimport kotlin.coroutines.CoroutineContext\n\nabstract class SubViewModel : AutoCloseable, CoroutineScope {\n\n    final override val coroutineContext: CoroutineContext = SupervisorJob() + Dispatchers.Main.immediate\n\n    val viewModelScope = CoroutineScope(coroutineContext)\n\n    override fun close() {\n        coroutineContext.cancel()\n    }\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/loadable/lazycolumn/LoadMoreUi.kt",
    "content": "package com.zhangke.framework.loadable.lazycolumn\n\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.lazy.LazyListState\nimport androidx.compose.material3.CircularProgressIndicator\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.unit.dp\nimport com.zhangke.framework.composable.ScrollDirection\nimport com.zhangke.framework.composable.rememberDirectionalLazyListState\nimport com.zhangke.framework.composable.textString\nimport com.zhangke.framework.utils.LoadState\nimport com.zhangke.fread.localization.LocalizedString\nimport org.jetbrains.compose.resources.stringResource\n\n@Composable\nfun LoadMoreUi(\n    loadState: LoadState,\n    onLoadMore: () -> Unit,\n) {\n    when (loadState) {\n        is LoadState.Loading -> {\n            Box(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .padding(vertical = 24.dp)\n            ) {\n                CircularProgressIndicator(\n                    modifier = Modifier\n                        .size(18.dp)\n                        .align(Alignment.Center)\n                )\n            }\n        }\n\n        is LoadState.Failed -> {\n            Column(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .padding(16.dp),\n                horizontalAlignment = Alignment.CenterHorizontally,\n            ) {\n                var errorMessage = loadState.message?.let { textString(it) }\n                if (errorMessage.isNullOrEmpty()) {\n                    errorMessage = stringResource(LocalizedString.loadMoreError)\n                }\n                Text(\n                    modifier = Modifier.fillMaxWidth(),\n                    text = errorMessage,\n                    textAlign = TextAlign.Center,\n                )\n                TextButton(\n                    modifier = Modifier.padding(top = 6.dp),\n                    onClick = onLoadMore,\n                ) {\n                    Text(text = stringResource(LocalizedString.retry))\n                }\n            }\n        }\n\n        else -> {}\n    }\n}\n\n@Composable\nfun ObserveLoadMore(\n    lazyListState: LazyListState,\n    onLoadMore: () -> Unit,\n    loadMoreRemainCountThreshold: Int = 3,\n) {\n    val listLayoutInfo by remember { derivedStateOf { lazyListState.layoutInfo } }\n    val directional = rememberDirectionalLazyListState(lazyListState).scrollDirection\n    val totalItemsCount = listLayoutInfo.totalItemsCount\n    var inLoadingMoreZone by remember { mutableStateOf(false) }\n    val currentLastVisibleIndex = listLayoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0\n    val remainToBottomCount = totalItemsCount - currentLastVisibleIndex - 1\n    inLoadingMoreZone = totalItemsCount > 0 &&\n            remainToBottomCount <= loadMoreRemainCountThreshold &&\n            totalItemsCount > loadMoreRemainCountThreshold\n    LaunchedEffect(inLoadingMoreZone, directional) {\n        if (inLoadingMoreZone) {\n            onLoadMore()\n        }\n    }\n}\n\n@Composable\nfun ObserveLazyListLoadEvent(\n    lazyListState: LazyListState,\n    loadPreviousPageRemainCountThreshold: Int,\n    loadMoreRemainCountThreshold: Int,\n    onLoadPrevious: () -> Unit,\n    onLoadMore: () -> Unit,\n) {\n    val listLayoutInfo by remember { derivedStateOf { lazyListState.layoutInfo } }\n    val directional = rememberDirectionalLazyListState(lazyListState).scrollDirection\n    val totalItemsCount = listLayoutInfo.totalItemsCount\n    var inLoadPreviousZone by remember {\n        mutableStateOf(false)\n    }\n    val currentFirstVisibleIndex = listLayoutInfo.visibleItemsInfo.firstOrNull()?.index ?: 0\n    inLoadPreviousZone = totalItemsCount > 0 &&\n            currentFirstVisibleIndex <= loadPreviousPageRemainCountThreshold &&\n            totalItemsCount > loadPreviousPageRemainCountThreshold\n    LaunchedEffect(inLoadPreviousZone, directional) {\n        if (inLoadPreviousZone && directional == ScrollDirection.Up) {\n            onLoadPrevious()\n        }\n    }\n    var inLoadingMoreZone by remember {\n        mutableStateOf(false)\n    }\n    val currentLastVisibleIndex = listLayoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0\n    val remainToBottomCount = totalItemsCount - currentLastVisibleIndex - 1\n    inLoadingMoreZone = totalItemsCount > 0 &&\n            remainToBottomCount <= loadMoreRemainCountThreshold &&\n            totalItemsCount > loadMoreRemainCountThreshold\n    LaunchedEffect(inLoadingMoreZone, directional) {\n        if (inLoadingMoreZone) {\n            onLoadMore()\n        }\n    }\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/loadable/lazycolumn/LoadableInlineVideoLazyColumn.kt",
    "content": "package com.zhangke.framework.loadable.lazycolumn\n\nimport androidx.compose.foundation.gestures.FlingBehavior\nimport androidx.compose.foundation.gestures.ScrollableDefaults\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.LazyListScope\nimport androidx.compose.foundation.lazy.LazyListState\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.material.ExperimentalMaterialApi\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.pulltorefresh.PullToRefreshBox\nimport androidx.compose.material3.pulltorefresh.PullToRefreshDefaults\nimport androidx.compose.material3.pulltorefresh.PullToRefreshState\nimport androidx.compose.material3.pulltorefresh.rememberPullToRefreshState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberUpdatedState\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport com.zhangke.framework.blur.applyBlurSource\nimport com.zhangke.framework.composable.LocalContentPadding\nimport com.zhangke.framework.composable.inline.InlineVideoLazyColumn\nimport com.zhangke.framework.utils.LoadState\n\n@Composable\nfun LoadableInlineVideoLazyColumn(\n    modifier: Modifier = Modifier,\n    state: LoadableLazyInlineVideoColumnState,\n    refreshing: Boolean,\n    loadState: LoadState,\n    reverseLayout: Boolean = false,\n    verticalArrangement: Arrangement.Vertical =\n        if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,\n    horizontalAlignment: Alignment.Horizontal = Alignment.Start,\n    flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),\n    userScrollEnabled: Boolean = true,\n    onLoadPrevious: (() -> Unit)? = null,\n    loadingContent: (@Composable () -> Unit)? = null,\n    content: LazyListScope.() -> Unit,\n) {\n    val lazyListState = state.lazyListState\n    val loadMoreStateInternal by remember(loadState) {\n        mutableStateOf(loadState)\n    }\n    val loadMoreFunction by rememberUpdatedState(newValue = state.loadMoreState.onLoadMore)\n    PullToRefreshBox(\n        modifier = modifier,\n        state = state.pullRefreshState.pullRefreshState,\n        isRefreshing = refreshing,\n        onRefresh = { state.pullRefreshState.onRefresh() },\n        indicator = {\n            PullToRefreshIndicator(\n                state = state.pullRefreshState.pullRefreshState,\n                refreshing = refreshing,\n            )\n        },\n    ) {\n        InlineVideoLazyColumn(\n            contentPadding = LocalContentPadding.current,\n            state = state.lazyListState,\n            modifier = Modifier.applyBlurSource(),\n            reverseLayout = reverseLayout,\n            verticalArrangement = verticalArrangement,\n            horizontalAlignment = horizontalAlignment,\n            flingBehavior = flingBehavior,\n            userScrollEnabled = userScrollEnabled,\n            content = {\n                content()\n                item {\n                    if (loadingContent != null) {\n                        loadingContent()\n                    } else {\n                        LoadMoreUi(\n                            loadState = loadMoreStateInternal,\n                            onLoadMore = loadMoreFunction,\n                        )\n                    }\n                }\n            },\n        )\n    }\n    ObserveLazyListLoadEvent(\n        lazyListState = lazyListState,\n        loadMoreRemainCountThreshold = state.loadMoreState.loadMoreRemainCountThreshold,\n        loadPreviousPageRemainCountThreshold = 3,\n        onLoadPrevious = {\n            onLoadPrevious?.invoke()\n        },\n        onLoadMore = state.loadMoreState.onLoadMore,\n    )\n}\n\n@OptIn(ExperimentalMaterialApi::class)\n@Composable\nfun rememberLoadableInlineVideoLazyColumnState(\n    onRefresh: () -> Unit,\n    onLoadMore: () -> Unit,\n    loadMoreRemainCountThreshold: Int = 3,\n    initialFirstVisibleItemIndex: Int = 0,\n    initialFirstVisibleItemScrollOffset: Int = 0\n): LoadableLazyInlineVideoColumnState {\n    val pullRefreshState = rememberPullToRefreshState()\n    val pullToRefreshingState = remember(pullRefreshState, onRefresh) {\n        PullToRefreshingState(\n            pullRefreshState = pullRefreshState,\n            onRefresh = onRefresh,\n        )\n    }\n\n    val lazyListState = rememberLazyListState(\n        initialFirstVisibleItemScrollOffset = initialFirstVisibleItemScrollOffset,\n        initialFirstVisibleItemIndex = initialFirstVisibleItemIndex,\n    )\n\n    val loadMoreState = rememberLoadMoreState(loadMoreRemainCountThreshold, onLoadMore)\n\n    return remember(pullRefreshState, lazyListState, loadMoreState) {\n        LoadableLazyInlineVideoColumnState(\n            lazyListState = lazyListState,\n            pullRefreshState = pullToRefreshingState,\n            loadMoreState = loadMoreState,\n        )\n    }\n}\n\n@Composable\nfun rememberLoadMoreState(\n    loadMoreRemainCountThreshold: Int,\n    onLoadMore: () -> Unit,\n): LoadMoreState {\n    return remember {\n        LoadMoreState(loadMoreRemainCountThreshold, onLoadMore)\n    }\n}\n\ndata class LoadMoreState(\n    val loadMoreRemainCountThreshold: Int,\n    val onLoadMore: () -> Unit,\n)\n\ndata class LoadableLazyInlineVideoColumnState(\n    val lazyListState: LazyListState,\n    val pullRefreshState: PullToRefreshingState,\n    val loadMoreState: LoadMoreState,\n)\n\ndata class PullToRefreshingState(\n    val pullRefreshState: PullToRefreshState,\n    val onRefresh: () -> Unit,\n)\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/loadable/lazycolumn/LoadableLazyColumn.kt",
    "content": "package com.zhangke.framework.loadable.lazycolumn\n\nimport androidx.compose.foundation.gestures.FlingBehavior\nimport androidx.compose.foundation.gestures.ScrollableDefaults\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.LazyListScope\nimport androidx.compose.foundation.lazy.LazyListState\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.material.ExperimentalMaterialApi\nimport androidx.compose.material.pullrefresh.PullRefreshIndicator\nimport androidx.compose.material.pullrefresh.pullRefresh\nimport androidx.compose.material3.pulltorefresh.PullToRefreshBox\nimport androidx.compose.material3.pulltorefresh.rememberPullToRefreshState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport com.zhangke.framework.utils.LoadState\n\n@OptIn(ExperimentalMaterialApi::class)\n@Composable\nfun LoadableLazyColumn(\n    modifier: Modifier,\n    state: LoadableLazyColumnState,\n    refreshing: Boolean,\n    loadState: LoadState,\n    lazyColumnModifier: Modifier = Modifier,\n    contentPadding: PaddingValues = PaddingValues(0.dp),\n    reverseLayout: Boolean = false,\n    verticalArrangement: Arrangement.Vertical =\n        if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,\n    horizontalAlignment: Alignment.Horizontal = Alignment.Start,\n    flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),\n    userScrollEnabled: Boolean = true,\n    loadingContent: (@Composable () -> Unit)? = null,\n    content: LazyListScope.() -> Unit,\n) {\n    val lazyListState = state.lazyListState\n    val listLayoutInfo by remember { derivedStateOf { lazyListState.layoutInfo } }\n    PullToRefreshBox(\n        modifier = modifier,\n        state = state.pullRefreshState.pullRefreshState,\n        isRefreshing = refreshing,\n        onRefresh = { state.pullRefreshState.onRefresh() },\n        indicator = {\n            PullToRefreshIndicator(\n                state = state.pullRefreshState.pullRefreshState,\n                refreshing = refreshing,\n            )\n        },\n    ) {\n        LazyColumn(\n            modifier = lazyColumnModifier,\n            state = state.lazyListState,\n            contentPadding = contentPadding,\n            reverseLayout = reverseLayout,\n            verticalArrangement = verticalArrangement,\n            horizontalAlignment = horizontalAlignment,\n            flingBehavior = flingBehavior,\n            userScrollEnabled = userScrollEnabled,\n            content = {\n                content()\n                item {\n                    if (loadingContent != null) {\n                        loadingContent()\n                    } else {\n                        LoadMoreUi(\n                            loadState = loadState,\n                            onLoadMore = state.loadMoreState.onLoadMore,\n                        )\n                    }\n                }\n            },\n        )\n    }\n\n    val currentLastVisibleIndex = listLayoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0\n    var inLoadingMoreZone by remember {\n        mutableStateOf(false)\n    }\n    val remainCount = listLayoutInfo.totalItemsCount - currentLastVisibleIndex - 1\n    inLoadingMoreZone = listLayoutInfo.totalItemsCount > 0 &&\n            remainCount <= state.loadMoreState.loadMoreRemainCountThreshold &&\n            listLayoutInfo.totalItemsCount > state.loadMoreState.loadMoreRemainCountThreshold\n    if (inLoadingMoreZone) {\n        LaunchedEffect(Unit) {\n            state.loadMoreState.onLoadMore()\n        }\n    }\n}\n\n@OptIn(ExperimentalMaterialApi::class)\n@Composable\nfun rememberLoadableLazyColumnState(\n    onRefresh: () -> Unit,\n    onLoadMore: () -> Unit,\n    loadMoreRemainCountThreshold: Int = 5,\n    lazyListState: LazyListState = rememberLazyListState(),\n): LoadableLazyColumnState {\n    val pullRefreshState = rememberPullToRefreshState()\n    val pullToRefreshingState = remember(pullRefreshState, onRefresh) {\n        PullToRefreshingState(\n            pullRefreshState = pullRefreshState,\n            onRefresh = onRefresh,\n        )\n    }\n    val loadMoreState = rememberLoadMoreState(\n        loadMoreRemainCountThreshold = loadMoreRemainCountThreshold,\n        onLoadMore = onLoadMore,\n    )\n    return remember(pullRefreshState, lazyListState, loadMoreState) {\n        LoadableLazyColumnState(\n            lazyListState = lazyListState,\n            pullRefreshState = pullToRefreshingState,\n            loadMoreState = loadMoreState,\n        )\n    }\n}\n\n@OptIn(ExperimentalMaterialApi::class)\ndata class LoadableLazyColumnState(\n    val lazyListState: LazyListState,\n    val pullRefreshState: PullToRefreshingState,\n    val loadMoreState: LoadMoreState,\n)\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/loadable/lazycolumn/PullToRefreshIndicator.kt",
    "content": "package com.zhangke.framework.loadable.lazycolumn\n\nimport androidx.compose.foundation.layout.BoxScope\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.pulltorefresh.PullToRefreshDefaults\nimport androidx.compose.material3.pulltorefresh.PullToRefreshState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport com.zhangke.framework.composable.LocalContentPadding\n\n@Composable\nfun BoxScope.PullToRefreshIndicator(\n    state: PullToRefreshState,\n    refreshing: Boolean,\n) {\n    PullToRefreshDefaults.Indicator(\n        state = state,\n        isRefreshing = refreshing,\n        modifier = Modifier.align(Alignment.TopCenter)\n            .padding(LocalContentPadding.current.calculateTopPadding()),\n        containerColor = MaterialTheme.colorScheme.primaryContainer,\n        color = MaterialTheme.colorScheme.onPrimaryContainer,\n    )\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/loadable/previous/PreviousPageLoadingState.kt",
    "content": "package com.zhangke.framework.loadable.previous\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.material3.Card\nimport androidx.compose.material3.CircularProgressIndicator\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport com.zhangke.framework.composable.TextString\nimport com.zhangke.fread.localization.LocalizedString\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport org.jetbrains.compose.resources.stringResource\n\nsealed interface PreviousPageLoadingState {\n\n    data object Idle : PreviousPageLoadingState\n\n    data object Loading : PreviousPageLoadingState\n\n    data class Failed(val errorMessage: TextString?) : PreviousPageLoadingState\n}\n\ndata class LoadPreviousPageUiState(\n    val onLoadPreviousPage: () -> Unit,\n    val initialState: PreviousPageLoadingState,\n    val loadPreviousPageThreshold: Int,\n) {\n\n    internal val loadingState = MutableStateFlow(initialState)\n\n    fun update(state: PreviousPageLoadingState) {\n        loadingState.value = state\n    }\n}\n\n@Composable\nfun rememberLoadPreviousPageUiState(\n    onLoadPreviousPage: () -> Unit,\n    initialState: PreviousPageLoadingState = PreviousPageLoadingState.Idle,\n    loadPreviousPageThreshold: Int = 3,\n): LoadPreviousPageUiState {\n    return remember(onLoadPreviousPage, loadPreviousPageThreshold) {\n        LoadPreviousPageUiState(\n            onLoadPreviousPage = onLoadPreviousPage,\n            initialState = initialState,\n            loadPreviousPageThreshold = loadPreviousPageThreshold,\n        )\n    }\n}\n\n@Composable\nfun LoadPreviousPageItem(\n    modifier: Modifier,\n    state: PreviousPageLoadingState,\n    onLoadPreviousPage: () -> Unit,\n) {\n    when (state) {\n        is PreviousPageLoadingState.Loading -> {\n            LoadingPreviousUi(modifier)\n        }\n\n        is PreviousPageLoadingState.Failed -> {\n            LoadPreviousFailedUi(modifier, onLoadPreviousPage)\n        }\n\n        else -> {}\n    }\n}\n\n@Composable\nprivate fun LoadingPreviousUi(\n    modifier: Modifier = Modifier,\n) {\n    Row(\n        modifier = modifier.padding(vertical = 16.dp),\n        verticalAlignment = Alignment.CenterVertically,\n        horizontalArrangement = Arrangement.Center,\n    ) {\n        Text(\n            text = stringResource(LocalizedString.feedsLoadPreviousPageLabel),\n            style = MaterialTheme.typography.labelMedium,\n        )\n        Spacer(modifier = Modifier.width(6.dp))\n        CircularProgressIndicator(\n            modifier = Modifier.size(24.dp)\n        )\n    }\n}\n\n@Composable\nprivate fun LoadPreviousFailedUi(\n    modifier: Modifier,\n    onClick: () -> Unit,\n) {\n    Box(\n        modifier = modifier\n            .fillMaxWidth()\n            .padding(horizontal = 16.dp, vertical = 6.dp),\n    ) {\n        Card(\n            modifier = Modifier\n                .fillMaxWidth()\n                .clickable { onClick() },\n        ) {\n            Text(\n                modifier = Modifier\n                    .padding(vertical = 6.dp)\n                    .align(Alignment.CenterHorizontally),\n                text = stringResource(LocalizedString.feedsLoadPreviousPageFailedLabel),\n                style = MaterialTheme.typography.labelMedium,\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/module/ModuleStartup.kt",
    "content": "package com.zhangke.framework.module\n\ninterface ModuleStartup {\n\n    fun onAppCreate()\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/nav/DialogNavMetadata.kt",
    "content": "package com.zhangke.framework.nav\n\nimport androidx.compose.ui.window.DialogProperties\nimport androidx.navigation3.scene.DialogSceneStrategy\n\nconst val FREAD_DIALOG_METADATA_KEY = \"fread.dialog\"\n\nfun dialogMetadata(\n    properties: DialogProperties = DialogProperties(),\n): Map<String, Any> {\n    return DialogSceneStrategy.dialog(properties) + mapOf(FREAD_DIALOG_METADATA_KEY to true)\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/nav/LocalNavBackStack.kt",
    "content": "package com.zhangke.framework.nav\n\nimport androidx.compose.runtime.ProvidableCompositionLocal\nimport androidx.compose.runtime.staticCompositionLocalOf\nimport androidx.navigation3.runtime.NavBackStack\nimport androidx.navigation3.runtime.NavKey\n\nval LocalNavBackStack: ProvidableCompositionLocal<NavBackStack<NavKey>?> =\n    staticCompositionLocalOf { null }\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/nav/NavBackStackExt.kt",
    "content": "package com.zhangke.framework.nav\n\nimport androidx.navigation3.runtime.NavBackStack\nimport androidx.navigation3.runtime.NavKey\n\nfun <T : NavKey> NavBackStack<T>.popIfNotRoot(): Boolean {\n    if (size <= 1) return false\n    removeAt(lastIndex)\n    return true\n}\n\nfun <T : NavKey> NavBackStack<T>.replaceTopOrAdd(key: T) {\n    if (isEmpty()) {\n        add(key)\n        return\n    }\n    this[lastIndex] = key\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/nav/NavEntryProvider.kt",
    "content": "package com.zhangke.framework.nav\n\nimport androidx.navigation3.runtime.EntryProviderScope\nimport androidx.navigation3.runtime.NavKey\nimport kotlinx.serialization.modules.PolymorphicModuleBuilder\n\ninterface NavEntryProvider {\n\n    fun EntryProviderScope<NavKey>.build()\n\n    fun PolymorphicModuleBuilder<NavKey>.polymorph()\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/nav/ScreenEventFlow.kt",
    "content": "package com.zhangke.framework.nav\n\nimport kotlinx.coroutines.channels.Channel\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.receiveAsFlow\n\nclass ScreenEventFlow<T> {\n\n    private val channel = Channel<T>(capacity = Channel.BUFFERED)\n\n    val flow: Flow<T> = channel.receiveAsFlow()\n\n    suspend fun emit(value: T) {\n        channel.send(value)\n    }\n\n    fun tryEmit(value: T): Boolean {\n        return channel.trySend(value).isSuccess\n    }\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/nav/SharedElementScope.kt",
    "content": "package com.zhangke.framework.nav\n\nimport androidx.compose.animation.ExperimentalSharedTransitionApi\nimport androidx.compose.animation.SharedTransitionScope\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.compositionLocalOf\nimport androidx.compose.ui.Modifier\nimport androidx.navigation3.ui.LocalNavAnimatedContentScope\n\n@OptIn(ExperimentalSharedTransitionApi::class)\nval LocalSharedTransitionScope = compositionLocalOf<SharedTransitionScope> {\n    error(\"No SharedElementScope provided\")\n}\n\n@Composable\nfun Modifier.sharedElement(key: String): Modifier {\n    val sharedTransitionScope = LocalSharedTransitionScope.current\n    val animatedContentScope = LocalNavAnimatedContentScope.current\n    return with(sharedTransitionScope) {\n        sharedElement(\n            sharedContentState = rememberSharedContentState(key = key),\n            animatedVisibilityScope = animatedContentScope,\n        )\n    }\n}\n\n@Composable\nfun Modifier.sharedBounds(key: String): Modifier {\n    val sharedTransitionScope = LocalSharedTransitionScope.current\n    val animatedContentScope = LocalNavAnimatedContentScope.current\n    return with(sharedTransitionScope) {\n        sharedBounds(\n            sharedContentState = rememberSharedContentState(key = key),\n            animatedVisibilityScope = animatedContentScope,\n        )\n    }\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/nav/Tab.kt",
    "content": "package com.zhangke.framework.nav\n\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.pager.HorizontalPager\nimport androidx.compose.foundation.pager.rememberPagerState\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.TabRowDefaults.primaryContainerColor\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.painter.Painter\nimport androidx.compose.ui.layout.SubcomposeLayout\nimport androidx.compose.ui.platform.LocalDensity\nimport com.zhangke.framework.composable.FreadTabRow\nimport com.zhangke.framework.composable.LocalContentPadding\nimport com.zhangke.framework.composable.plusTopPadding\nimport com.zhangke.framework.utils.pxToDp\nimport com.zhangke.framework.utils.roundToPx\nimport com.zhangke.framework.utils.toPx\nimport kotlinx.coroutines.launch\nimport kotlin.math.roundToInt\n\ninterface Tab {\n\n    val options: TabOptions?\n        @Composable get\n\n    @Composable\n    fun Content()\n}\n\nabstract class BaseTab : Tab {\n\n    @Composable\n    override fun Content() {\n\n    }\n}\n\ndata class TabOptions(\n    val title: String,\n    val icon: Painter? = null\n)\n\n@Composable\nfun HorizontalPagerWithTab(\n    tabList: List<Tab>,\n    initialPage: Int = 0,\n    pagerUserScrollEnabled: Boolean = true,\n    onPageChanged: ((Int) -> Unit)? = null,\n) {\n    val coroutineScope = rememberCoroutineScope()\n    Column(modifier = Modifier.fillMaxSize()) {\n        val pagerState = rememberPagerState(initialPage = initialPage) {\n            tabList.size\n        }\n        if (onPageChanged != null) {\n            LaunchedEffect(pagerState.currentPage) {\n                onPageChanged(pagerState.currentPage)\n            }\n        }\n        FreadTabRow(\n            modifier = Modifier.fillMaxWidth(),\n            selectedTabIndex = pagerState.currentPage,\n            tabCount = tabList.size,\n            tabContent = {\n                Text(\n                    text = tabList[it].options?.title.orEmpty(),\n                    maxLines = 1,\n                )\n            },\n            onTabClick = {\n                coroutineScope.launch {\n                    pagerState.scrollToPage(it)\n                }\n            }\n        )\n        HorizontalPager(\n            modifier = Modifier.fillMaxSize(),\n            state = pagerState,\n            userScrollEnabled = pagerUserScrollEnabled,\n        ) { pageIndex ->\n            with(tabList[pageIndex]) {\n                Content()\n            }\n        }\n    }\n}\n\n@Composable\nfun ContentPaddingsHorizontalPagerWithTab(\n    tabList: List<Tab>,\n    modifier: Modifier = Modifier,\n    initialPage: Int = 0,\n    blurEnabled: Boolean = true,\n    pagerUserScrollEnabled: Boolean = true,\n    containerColor: Color = MaterialTheme.colorScheme.surface,\n    onPageChanged: ((Int) -> Unit)? = null,\n) {\n    val coroutineScope = rememberCoroutineScope()\n    val density = LocalDensity.current\n    val pagerState = rememberPagerState(initialPage = initialPage) {\n        tabList.size\n    }\n    if (onPageChanged != null) {\n        LaunchedEffect(pagerState.currentPage) {\n            onPageChanged(pagerState.currentPage)\n        }\n    }\n    val topOffset = LocalContentPadding.current.calculateTopPadding().roundToPx()\n    SubcomposeLayout(modifier = modifier) { constraints ->\n        val tabRowPlaceable = subcompose(\"tabRow\") {\n            FreadTabRow(\n                modifier = Modifier.fillMaxWidth(),\n                selectedTabIndex = pagerState.currentPage,\n                tabCount = tabList.size,\n                containerColor = containerColor,\n                blurEffectEnabled = blurEnabled,\n                tabContent = {\n                    Text(\n                        text = tabList[it].options?.title.orEmpty(),\n                        maxLines = 1,\n                    )\n                },\n                onTabClick = {\n                    coroutineScope.launch {\n                        pagerState.scrollToPage(it)\n                    }\n                },\n            )\n        }.first().measure(constraints)\n        val tabRowHeightDp = tabRowPlaceable.height.pxToDp(density)\n        val pagerPlaceable = subcompose(\"pager\") {\n            HorizontalPager(\n                modifier = Modifier.fillMaxSize(),\n                state = pagerState,\n                userScrollEnabled = pagerUserScrollEnabled,\n            ) { pageIndex ->\n                CompositionLocalProvider(\n                    LocalContentPadding provides plusTopPadding(tabRowHeightDp)\n                ) {\n                    with(tabList[pageIndex]) { Content() }\n                }\n            }\n        }.first().measure(constraints)\n        layout(constraints.maxWidth, constraints.maxHeight) {\n            pagerPlaceable.placeRelative(0, 0)\n            tabRowPlaceable.placeRelative(0, topOffset)\n        }\n    }\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/network/FormalBaseUrl.kt",
    "content": "package com.zhangke.framework.network\n\nimport com.zhangke.framework.utils.Parcelize\nimport com.zhangke.framework.utils.PlatformParcelable\nimport com.zhangke.framework.utils.PlatformSerializable\nimport com.zhangke.framework.utils.UrlEncoder\nimport com.zhangke.framework.utils.uriString\nimport kotlinx.serialization.Serializable\n\n@Parcelize\n@Serializable\nclass FormalBaseUrl private constructor(\n    val scheme: String,\n    val host: String,\n) : PlatformParcelable, PlatformSerializable {\n\n    override fun toString(): String {\n        return \"$scheme$SCHEME_SEPARATOR$host\"\n    }\n\n    override fun hashCode(): Int {\n        var result = scheme.hashCode()\n        result = 31 * result + host.hashCode()\n        return result\n    }\n\n    override fun equals(other: Any?): Boolean {\n        if (other === this) return true\n        if (other == null) return false\n        if (other !is FormalBaseUrl) return false\n        return (other.scheme == scheme) && (other.host == host)\n    }\n\n    fun equalsDomain(other: FormalBaseUrl): Boolean {\n        if (this == other) return true\n        if (this.host.endsWith(other.host)) return true\n        if (other.host.endsWith(this.host)) return true\n        return false\n    }\n\n    /**\n     * Not encode string\n     */\n    fun toRawString(): String {\n        return uriString(\n            scheme = scheme,\n            host = host,\n            path = \"\",\n            queries = emptyMap(),\n            encode = false,\n        )\n    }\n\n    companion object {\n\n        private const val SCHEME_SEPARATOR = \"://\"\n\n        fun build(scheme: String, host: String): FormalBaseUrl {\n            return FormalBaseUrl(scheme, host)\n        }\n\n        fun parse(string: String): FormalBaseUrl? {\n            val url = SimpleUri.parse(string.addProtocolIfNecessary()) ?: return null\n            val scheme = url.scheme?.lowercase() ?: return null\n            if (scheme !in arrayOf(\"http\", \"https\")) return null\n            val host = url.host ?: return null\n            if (host.isEmpty()) return null\n            return FormalBaseUrl(scheme, host.removeHostSuffix())\n        }\n\n        private fun String.removeHostSuffix(): String {\n            return this.removeSuffix(\"/\")\n        }\n    }\n}\n\nfun FormalBaseUrl.encode(): String {\n    return UrlEncoder.encode(toRawString())\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/network/GlobalRoutes.kt",
    "content": "package com.zhangke.framework.network\n\nobject GlobalRoutes {\n    private const val SCHEME = \"fread\"\n    const val ROOT_PREFIX = \"$SCHEME://\"\n}"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/network/HttpScheme.kt",
    "content": "package com.zhangke.framework.network\n\nobject HttpScheme {\n\n    const val HTTP = \"http://\"\n    const val HTTPS = \"https://\"\n\n    fun validate(scheme: String): Boolean {\n        val fixedScheme = scheme.lowercase()\n        return fixedScheme == HTTP || fixedScheme == HTTPS\n    }\n}\n\nfun String.addProtocolIfNecessary(): String {\n    if (this.contains(\"://\")) return this\n    return \"${HttpScheme.HTTPS}$this\"\n}\n\nfun String.addProtocolSuffixIfNecessary(): String {\n    if (this.endsWith(\"://\")) return this\n    return \"${this}://\"\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/network/SimpleUri.kt",
    "content": "package com.zhangke.framework.network\n\nimport com.zhangke.framework.utils.toPlatformUri\nimport com.zhangke.framework.utils.uriString\n\ndata class SimpleUri(\n    val scheme: String?,\n    val host: String?,\n    val path: String?,\n    val queries: Map<String, String>,\n) {\n\n    override fun toString(): String {\n        return uriString(\n            scheme = scheme.orEmpty(),\n            host = host.orEmpty(),\n            path = path.orEmpty(),\n            queries = queries,\n        )\n    }\n\n    companion object {\n\n        fun parse(uri: String): SimpleUri? {\n            if (uri.isEmpty()) return null\n            val formalUri = try {\n                uri.toPlatformUri()\n            } catch (_: Throwable) {\n                null\n            } ?: return null\n            val scheme = formalUri.scheme\n            val host = formalUri.host\n            val path = formalUri.path\n            val queries = runCatching {\n                formalUri.getQueryParameterNames().associateWith {\n                    formalUri.getQueryParameter(it).orEmpty()\n                }\n            }.getOrNull() ?: return null\n            return SimpleUri(scheme, host, path, queries)\n        }\n    }\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/opml/OpmlOutline.kt",
    "content": "package com.zhangke.framework.opml\n\ndata class OpmlOutline(\n    val title: String,\n    val xmlUrl: String,\n    val children: List<OpmlOutline>,\n)\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/opml/OpmlParser.kt",
    "content": "package com.zhangke.framework.opml\n\nimport com.fleeksoft.ksoup.nodes.Node\nimport com.fleeksoft.ksoup.parser.XmlTreeBuilder\nimport com.zhangke.framework.ktx.ifNullOrEmpty\n\nobject OpmlParser {\n\n    fun parse(xml: String): List<OpmlOutline> {\n        val outlineList = mutableListOf<Outline>()\n        val outlineStack = mutableListOf<Outline>()\n\n        fun parserNode(node: Node) {\n            if (node.nodeName() == \"outline\") {\n                val text = node.attr(\"text\")\n                val title = node.attr(\"title\")\n                val xmlUrl = node.attr(\"xmlUrl\")\n                val htmlUrl = node.attr(\"htmlUrl\")\n                val outline = Outline(\n                    title = title,\n                    text = text,\n                    xmlUrl = xmlUrl,\n                    htmlUrl = htmlUrl,\n                    children = mutableListOf(),\n                )\n                if (outlineStack.isEmpty()) {\n                    outlineList.add(outline)\n                } else {\n                    outlineStack.last().children.add(outline)\n                }\n                if (node.childNodes().isNotEmpty()) {\n                    outlineStack.add(outline)\n                    node.childNodes().forEach {\n                        parserNode(it)\n                    }\n                    outlineStack.removeAt(outlineStack.size - 1)\n                }\n            } else if (node.childNodes().isNotEmpty()) {\n                node.childNodes().forEach {\n                    parserNode(it)\n                }\n            }\n        }\n\n        val doc = XmlTreeBuilder().parse(xml)\n        parserNode(doc)\n\n\n        return outlineList.map {\n            it.toOpmlOutline()\n        }\n    }\n\n    private fun Outline.toOpmlOutline(): OpmlOutline {\n        return OpmlOutline(\n            title = text.ifNullOrEmpty { title },\n            xmlUrl = xmlUrl,\n            children = children.map { it.toOpmlOutline() },\n        )\n    }\n\n    private class Outline(\n        val title: String,\n        val text: String,\n        val xmlUrl: String,\n        val htmlUrl: String?,\n        val children: MutableList<Outline>,\n    ) {\n        override fun toString(): String {\n            return \"Outline(title='$title', text='$text', xmlUrl='$xmlUrl', htmlUrl=$htmlUrl, children=$children)\"\n        }\n    }\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/permission/RequireLocalStoragePermission.kt",
    "content": "package com.zhangke.framework.permission\n\nimport androidx.compose.runtime.Composable\n\n@Composable\nexpect fun RequireLocalStoragePermission(\n    onPermissionGranted: suspend () -> Unit,\n    onPermissionDenied: suspend (() -> Unit) = {},\n)"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/security/Md5.kt",
    "content": "package com.zhangke.framework.security\n\nimport io.ktor.utils.io.core.toByteArray\nimport okio.ByteString\n\nobject Md5 {\n\n    fun md5(input: String): String {\n        return ByteString.of(*input.toByteArray()).md5().hex()\n    }\n}"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/serialize/TimestampAsInstantSerializer.kt",
    "content": "@file:OptIn(ExperimentalTime::class)\n\npackage com.zhangke.framework.serialize\n\nimport com.zhangke.framework.datetime.Instant\nimport kotlinx.serialization.KSerializer\nimport kotlinx.serialization.descriptors.PrimitiveKind\nimport kotlinx.serialization.descriptors.PrimitiveSerialDescriptor\nimport kotlinx.serialization.descriptors.SerialDescriptor\nimport kotlinx.serialization.encoding.Decoder\nimport kotlinx.serialization.encoding.Encoder\nimport kotlin.time.ExperimentalTime\n\nobject TimestampAsInstantSerializer : KSerializer<Instant> {\n    override val descriptor: SerialDescriptor =\n        PrimitiveSerialDescriptor(\"kotlinx.datetime.Instant\", PrimitiveKind.LONG)\n\n    override fun deserialize(decoder: Decoder): Instant {\n        val timestamp = decoder.decodeLong()\n        return Instant(kotlinx.datetime.Instant.fromEpochMilliseconds(timestamp))\n    }\n\n    override fun serialize(encoder: Encoder, value: Instant) {\n        encoder.encodeLong(value.instant.toEpochMilliseconds())\n    }\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/toast/Toast.kt",
    "content": "package com.zhangke.framework.toast\n\nexpect fun toast(message: String?)"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/utils/AspectRatio.kt",
    "content": "package com.zhangke.framework.utils\n\nimport kotlinx.serialization.Serializable\n\n@Parcelize\n@Serializable\ndata class AspectRatio(\n    val width: Long,\n    val height: Long,\n) : PlatformSerializable, PlatformParcelable {\n\n    val ratio: Float get() = width.toFloat() / height\n\n    init {\n        require(width >= 1) {\n            \"width must be >= 1, but was $width\"\n        }\n        require(height >= 1) {\n            \"height must be >= 1, but was $height\"\n        }\n    }\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/utils/BlendColorUtils.kt",
    "content": "package com.zhangke.framework.utils\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.toArgb\nimport kotlin.math.pow\nimport kotlin.math.round\n\nobject BlendColorUtils {\n\n    fun blend(\n        fraction: Float,\n        startColor: Color,\n        endColor: Color,\n    ): Color {\n        val startInt: Int = startColor.toArgb()\n        val startA = ((startInt shr 24) and 0xff) / 255.0f\n        var startR = ((startInt shr 16) and 0xff) / 255.0f\n        var startG = ((startInt shr 8) and 0xff) / 255.0f\n        var startB = (startInt and 0xff) / 255.0f\n\n        val endInt: Int = endColor.toArgb()\n        val endA = ((endInt shr 24) and 0xff) / 255.0f\n        var endR = ((endInt shr 16) and 0xff) / 255.0f\n        var endG = ((endInt shr 8) and 0xff) / 255.0f\n        var endB = (endInt and 0xff) / 255.0f\n\n\n        // convert from sRGB to linear\n        startR = startR.pow(2.2f)\n        startG = startG.pow(2.2f)\n        startB = startB.pow(2.2f)\n\n        endR = endR.pow(2.2f)\n        endG = endG.pow(2.2f)\n        endB = endB.pow(2.2f)\n\n\n        // compute the interpolated color in linear space\n        var a = startA + fraction * (endA - startA)\n        var r = startR + fraction * (endR - startR)\n        var g = startG + fraction * (endG - startG)\n        var b = startB + fraction * (endB - startB)\n\n        // convert back to sRGB in the [0..255] range\n        a *= 255.0f\n        r = r.pow(1.0f / 2.2f) * 255.0f\n        g = g.pow(1.0f / 2.2f) * 255.0f\n        b = b.pow(1.0f / 2.2f) * 255.0f\n\n        return Color(\n            round(r).toInt(),\n            round(g).toInt(),\n            round(b).toInt(),\n            round(a).toInt(),\n        )\n    }\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/utils/ContentProviderFile.kt",
    "content": "package com.zhangke.framework.utils\n\ndata class ContentProviderFile(\n    val uri: PlatformUri,\n    val fileName: String,\n    val size: StorageSize,\n    val mimeType: String,\n    private val streamProvider: () -> ByteArray?,\n) {\n    fun readBytes(): ByteArray? {\n        return streamProvider()\n    }\n\n    val isVideo: Boolean get() = mimeType.contains(\"video\")\n}\n\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/utils/DebugUtils.kt",
    "content": "package com.zhangke.framework.utils\n\nimport com.zhangke.framework.toast.toast\n\nvar appDebuggable = false\n    private set\n\nfun initDebuggable(debug: Boolean) {\n    appDebuggable = debug\n}\n\ninline fun ifDebugging(block: () -> Unit) {\n    if (appDebuggable) {\n        block()\n    }\n}\n\nfun throwInDebug(message: String?, throwable: Throwable? = null) {\n    if (appDebuggable) {\n        toast(\"Non-fatal error! ${message ?: throwable?.message}\")\n        if (throwable != null) throw throwable\n        throw ThrowInDebugException(message)\n    } else {\n        // TODO report to server\n    }\n}\n\nclass ThrowInDebugException(message: String?) : RuntimeException(message)"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/utils/DensityUtils.kt",
    "content": "package com.zhangke.framework.utils\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.unit.Density\nimport androidx.compose.ui.unit.Dp\nimport kotlin.math.roundToInt\n\n@Composable\nfun Dp.toPx(): Float {\n    return with(LocalDensity.current) {\n        toPx()\n    }\n}\n\n@Composable\nfun Dp.roundToPx(): Int {\n    return with(LocalDensity.current) {\n        toPx().roundToInt()\n    }\n}\n\nfun Dp.dpToPx(density: Density): Float {\n    return with(density) {\n        toPx()\n    }\n}\n\nfun Int.dpToPx(density: Density): Float {\n    return Dp(this.toFloat()).dpToPx(density)\n}\n\nfun Int.pxToDp(density: Density): Dp {\n    val pxValue = this\n    return with(density) { pxValue.toDp() }\n}\n\nfun Float.pxToDp(density: Density): Dp {\n    val pxValue = this\n    return with(density) { pxValue.toDp() }\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/utils/DomainValidator.kt",
    "content": "package com.zhangke.framework.utils\n\nobject DomainValidator {\n\n    fun validate(domain: String): Boolean {\n        if (domain.isEmpty()) return false\n        try {\n            if (RegexFactory.domainRegex.matches(domain)) return true\n            val idnDomain = IDNUtils().toASCII(domain)\n            return RegexFactory.domainRegex.matches(idnDomain)\n        } catch (_: Throwable) {\n            return false\n        }\n    }\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/utils/DurationFormatUtils.kt",
    "content": "package com.zhangke.framework.utils\n\nimport com.zhangke.fread.localization.LocalizedString\nimport org.jetbrains.compose.resources.getString\nimport kotlin.time.Duration\nimport kotlin.time.Duration.Companion.days\nimport kotlin.time.Duration.Companion.hours\nimport kotlin.time.Duration.Companion.minutes\n\nsuspend fun Duration.formattedString(): String {\n    val builder = StringBuilder()\n    val formattedDuration = format()\n    val weeks = dayToWeek(formattedDuration.days)\n    if (weeks > 0) {\n        builder.append(\"$weeks${getString(LocalizedString.durationWeek)}\")\n    }\n    val days = (formattedDuration.days - weeks * 7).coerceAtLeast(0)\n    if (days > 0) {\n        if (builder.isNotEmpty()) {\n            builder.append(\" \")\n        }\n        builder.append(\"$days${getString(LocalizedString.durationDay)}\")\n    }\n    if (formattedDuration.hours > 0) {\n        if (builder.isNotEmpty()) {\n            builder.append(\" \")\n        }\n        builder.append(\"${formattedDuration.hours}${getString(LocalizedString.durationHour)}\")\n    }\n    if (formattedDuration.minutes > 0) {\n        if (builder.isNotEmpty()) {\n            builder.append(\" \")\n        }\n        builder.append(\"${formattedDuration.minutes}${getString(LocalizedString.durationMinute)}\")\n    }\n    return builder.toString()\n}\n\nfun dayToWeek(day: Int): Int {\n    return day / 7\n}\n\nfun Duration.format(): FormattedDuration {\n    var leftDuration = this\n    val days = leftDuration.inWholeDays.toInt()\n    leftDuration -= days.days\n    val hours = leftDuration.inWholeHours.toInt()\n    leftDuration -= hours.hours\n    val minutes = leftDuration.inWholeMinutes.toInt()\n    leftDuration -= minutes.minutes\n    return FormattedDuration(days = days, hours = hours, minutes = minutes)\n}\n\ndata class FormattedDuration(\n    val days: Int,\n    val hours: Int,\n    val minutes: Int,\n)\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/utils/ExtractUrlFromTextUtils.kt",
    "content": "package com.zhangke.framework.utils\n\nobject ExtractUrlFromTextUtils {\n\n    private val urlRegex = \"\"\"\n        (?:https?://)?(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\\.)+[A-Za-z]{2,63}(?!\\.[A-Za-z0-9-])(?=$|[:/?#]|[^\\w-])(?::\\d{2,5})?(?:[/?#][^\\s'\"]*)?\n    \"\"\".trimIndent().toRegex()\n\n    private val trailingPunctuation = setOf('.', ',', '!', '?', ':', ';', '\"', '\\'', '”', '’')\n    private val closingToOpeningBracket = mapOf(')' to '(', ']' to '[', '}' to '{')\n\n    fun extract(text: String): List<String> {\n        if (text.isEmpty()) return emptyList()\n        return urlRegex.findAll(text)\n            .mapNotNull { sanitize(it.value) }\n            .toList()\n    }\n\n    private fun sanitize(candidate: String): String? {\n        var url = candidate\n        while (url.isNotEmpty() && shouldTrimLastChar(url)) {\n            url = url.dropLast(1)\n        }\n        return url.takeIf { it.isNotEmpty() && urlRegex.matchEntire(it) != null }\n    }\n\n    private fun shouldTrimLastChar(url: String): Boolean {\n        val lastChar = url.last()\n        if (lastChar in trailingPunctuation) return true\n        val openingBracket = closingToOpeningBracket[lastChar] ?: return false\n        return url.count { it == lastChar } > url.count { it == openingBracket }\n    }\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/utils/FloatExt.kt",
    "content": "package com.zhangke.framework.utils\n\nimport kotlin.math.abs\n\nfun Float.equalsExactly(target: Float): Boolean {\n    return abs(target - this) <= 0.000001F\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/utils/Handle.kt",
    "content": "package com.zhangke.framework.utils\n\nfun String.prettyHandle(): String = if (this.startsWith('@')) this else \"@$this\"\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/utils/HighlightTextBuildUtil.kt",
    "content": "package com.zhangke.framework.utils\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.text.AnnotatedString\nimport androidx.compose.ui.text.SpanStyle\nimport androidx.compose.ui.text.buildAnnotatedString\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.withStyle\nimport androidx.compose.ui.unit.TextUnit\n\nobject HighlightTextBuildUtil {\n\n    const val HIGHLIGHT_START_SYMBOL = \"[[\"\n    const val HIGHLIGHT_END_SYMBOL = \"]]\"\n\n    /**\n     * some text[[high light text]]end text\n     */\n    fun buildHighlightText(\n        text: String,\n        fontWeight: FontWeight? = null,\n        highLightColor: Color? = null,\n        highLightSize: TextUnit? = null,\n    ): AnnotatedString {\n        return buildAnnotatedString {\n            var leftText = text\n            while (leftText.isNotEmpty()) {\n                val (prefix, highlight, suffix) = findFirstHighlightRange(leftText)\n                if (prefix.isNotEmpty()) {\n                    append(prefix)\n                }\n                if (highlight.isNotEmpty()) {\n                    withStyle(\n                        style = SpanStyle(\n                            color = highLightColor ?: Color.Unspecified,\n                            fontSize = highLightSize ?: TextUnit.Unspecified,\n                            fontWeight = fontWeight,\n                        ),\n                    ) {\n                        append(highlight)\n                    }\n                }\n                leftText = suffix\n            }\n        }\n    }\n\n    private fun findFirstHighlightRange(text: String): Triple<String, String, String> {\n        val start = text.indexOf(HIGHLIGHT_START_SYMBOL)\n        if (start == -1) return Triple(text, \"\", \"\")\n        val end = text.indexOf(HIGHLIGHT_END_SYMBOL, start)\n        if (end == -1) {\n            return Triple(text, \"\", \"\")\n        }\n        val prefix = text.substring(0, start)\n        val highlight = text.substring(start + HIGHLIGHT_START_SYMBOL.length, end)\n        val suffix = text.substring(end + HIGHLIGHT_END_SYMBOL.length)\n        return Triple(prefix, highlight, suffix)\n    }\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/utils/IDNUtils.kt",
    "content": "package com.zhangke.framework.utils\n\nexpect class IDNUtils() {\n\n    fun toASCII(input: String): String\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/utils/ImageCompressUtils.kt",
    "content": "package com.zhangke.framework.utils\n\nexpect class ImageCompressUtils() {\n\n    fun compress(bytes: ByteArray, targetSize: StorageSize): CompressResult\n}\n\ndata class CompressResult(\n    val bytes: ByteArray,\n    val ratio: AspectRatio?,\n) {\n\n    override fun equals(other: Any?): Boolean {\n        if (this === other) return true\n        if (other == null || this::class != other::class) return false\n\n        other as CompressResult\n\n        if (!bytes.contentEquals(other.bytes)) return false\n        if (ratio != other.ratio) return false\n\n        return true\n    }\n\n    override fun hashCode(): Int {\n        var result = bytes.contentHashCode()\n        result = 31 * result + ratio.hashCode()\n        return result\n    }\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/utils/IntExt.kt",
    "content": "package com.zhangke.framework.utils\n\nimport com.ionspin.kotlin.bignum.decimal.toBigDecimal\n\nfun Long.formatToHumanReadable(): String {\n    return if (this >= 1_000_000) {\n        (this / 1_000_000F).decimal(1)\n            .removeSuffix(\".0\")\n            .plus(\"M\")\n    } else if (this >= 1000) {\n        (this / 1000F).decimal(1)\n            .removeSuffix(\".0\")\n            .plus(\"K\")\n    } else {\n        this.toString()\n    }\n}\n\nfun Int.formatToHumanReadable(): String {\n    return this.toLong().formatToHumanReadable()\n}\n\nfun Float.decimal(digits: Int): String {\n    return this.toBigDecimal()\n        .scale(digits.toLong())\n        .toStringExpanded()\n}\n\nfun Double.decimal(digits: Int): String {\n    return this.toBigDecimal()\n        .scale(digits.toLong())\n        .toStringExpanded()\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/utils/LanguageUtils.kt",
    "content": "package com.zhangke.framework.utils\n\nexpect object LanguageUtils {\n    fun getAllLanguages(): List<Locale>\n}\n\nexpect class Locale\n\nexpect val Locale.languageCode: String\n\nexpect val Locale.isO3LanguageCode: String\n\nexpect fun Locale.getDisplayName(displayLocale: Locale): String\n\nexpect fun initLocale(language: String): Locale\n\nexpect fun getDefaultLocale(): Locale\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/utils/LinkPreviewUtils.kt",
    "content": "package com.zhangke.framework.utils\n\nimport com.fleeksoft.ksoup.Ksoup\nimport com.fleeksoft.ksoup.nodes.Document\n\nobject LinkPreviewUtils {\n\n    fun fetchPreviewInfo(url: String, html: String): LinkPreviewInfo? {\n        val document = runCatching { Ksoup.parse(html) }.getOrNull() ?: return null\n        val pageUrl = resolveUrl(\n            rawUrl = document.selectFirst(\"base[href]\")?.attr(\"href\"),\n            articleUrl = url,\n        ) ?: url\n        val previewUrl = resolveUrl(\n            rawUrl = firstNotBlank(\n                document.metaContent(\"meta[property=og:url]\"),\n                document.metaContent(\"meta[name=og:url]\"),\n                document.linkHref(\"link[rel=canonical]\"),\n                document.linkHref(\"link[rel=alternate][hreflang=x-default]\"),\n            ),\n            articleUrl = pageUrl,\n        ) ?: url\n        val title = firstNotBlank(\n            document.metaContent(\"meta[property=og:title]\"),\n            document.metaContent(\"meta[name=og:title]\"),\n            document.metaContent(\"meta[name=twitter:title]\"),\n            document.metaContent(\"meta[property=twitter:title]\"),\n            document.title().normalizeText(),\n            document.selectFirst(\"h1\")?.text().normalizeText(),\n        ) ?: return null\n        val description = firstNotBlank(\n            document.metaContent(\"meta[property=og:description]\"),\n            document.metaContent(\"meta[name=og:description]\"),\n            document.metaContent(\"meta[name=twitter:description]\"),\n            document.metaContent(\"meta[property=twitter:description]\"),\n            document.metaContent(\"meta[name=description]\"),\n            document.metaContent(\"meta[property=description]\"),\n        )\n        val image = resolveUrl(\n            rawUrl = firstNotBlank(\n                document.metaContent(\"meta[property=og:image]\"),\n                document.metaContent(\"meta[property=og:image:url]\"),\n                document.metaContent(\"meta[name=twitter:image]\"),\n                document.metaContent(\"meta[property=twitter:image]\"),\n                document.metaContent(\"meta[itemprop=image]\"),\n                document.linkHref(\"link[rel=image_src]\"),\n                document.linkHref(\"link[rel=apple-touch-icon]\"),\n            ),\n            articleUrl = pageUrl,\n        )\n        val siteName = firstNotBlank(\n            document.metaContent(\"meta[property=og:site_name]\"),\n            document.metaContent(\"meta[name=application-name]\"),\n            document.metaContent(\"meta[name=apple-mobile-web-app-title]\"),\n        )\n        return LinkPreviewInfo(\n            title = title,\n            description = description,\n            image = image,\n            url = previewUrl,\n            siteName = siteName,\n        )\n    }\n\n    private fun Document.metaContent(selector: String): String? {\n        return selectFirst(selector)\n            ?.attr(\"content\")\n            .normalizeText()\n    }\n\n    private fun Document.linkHref(selector: String): String? {\n        return selectFirst(selector)\n            ?.attr(\"href\")\n            .normalizeText()\n    }\n\n    private fun String?.normalizeText(): String? {\n        return this\n            ?.replace(whitespaceRegex, \" \")\n            ?.trim()\n            ?.takeIf { it.isNotEmpty() }\n    }\n\n    private fun firstNotBlank(vararg values: String?): String? {\n        return values.firstNotNullOfOrNull { it.normalizeText() }\n    }\n\n    private fun resolveUrl(rawUrl: String?, articleUrl: String?): String? {\n        val value = rawUrl.normalizeText() ?: return null\n        if (value.startsWith(\"data:\", ignoreCase = true)) return null\n        if (value.startsWith(\"blob:\", ignoreCase = true)) return null\n        if (value.startsWith(\"javascript:\", ignoreCase = true)) return null\n        if (value.startsWith(\"http://\", ignoreCase = true) ||\n            value.startsWith(\"https://\", ignoreCase = true)\n        ) {\n            return value\n        }\n        if (value.startsWith(\"//\")) {\n            val scheme = articleUrl\n                ?.substringBefore(\"://\", \"\")\n                ?.takeIf { it.isNotBlank() }\n                ?: \"https\"\n            return \"$scheme:$value\"\n        }\n        val baseUrl = articleUrl\n            ?.takeIf {\n                it.startsWith(\"http://\", ignoreCase = true) ||\n                        it.startsWith(\"https://\", ignoreCase = true)\n            }\n            ?: return null\n        val origin = baseUrl.origin() ?: return null\n        if (value.startsWith(\"/\")) {\n            return origin + normalizePath(value)\n        }\n        val sanitizedBase = baseUrl.substringBefore('#').substringBefore('?')\n        val directory = if (sanitizedBase.endsWith(\"/\")) {\n            sanitizedBase\n        } else {\n            sanitizedBase.substringBeforeLast(\"/\", \"$origin/\")\n        }\n        val relativePath = directory.substringAfter(origin, \"/\").trimEnd('/')\n        val normalizedPath = normalizePath(\"$relativePath/$value\")\n        return origin + normalizedPath\n    }\n\n    private fun String.origin(): String? {\n        val schemeIndex = indexOf(\"://\")\n        if (schemeIndex <= 0) return null\n        val hostStart = schemeIndex + 3\n        val hostEnd = indexOf('/', startIndex = hostStart).takeIf { it >= 0 } ?: length\n        return substring(0, hostEnd)\n    }\n\n    private fun normalizePath(pathWithQuery: String): String {\n        val fragment = pathWithQuery.substringAfter('#', \"\")\n        val pathWithoutFragment = pathWithQuery.substringBefore('#')\n        val query = pathWithoutFragment.substringAfter('?', \"\")\n        val rawPath = pathWithoutFragment.substringBefore('?')\n        val stack = mutableListOf<String>()\n        rawPath.split('/').forEach { segment ->\n            when (segment) {\n                \"\", \".\" -> Unit\n                \"..\" -> if (stack.isNotEmpty()) {\n                    stack.removeAt(stack.lastIndex)\n                }\n\n                else -> stack += segment\n            }\n        }\n        val normalizedPath = \"/\" + stack.joinToString(\"/\")\n        return buildString {\n            append(normalizedPath)\n            if (query.isNotEmpty()) {\n                append('?')\n                append(query)\n            }\n            if (fragment.isNotEmpty()) {\n                append('#')\n                append(fragment)\n            }\n        }\n    }\n}\n\nprivate val whitespaceRegex = \"\\\\s+\".toRegex()\n\ndata class LinkPreviewInfo(\n    val title: String,\n    val description: String?,\n    val image: String?,\n    val url: String,\n    val siteName: String?,\n)\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/utils/LoadState.kt",
    "content": "package com.zhangke.framework.utils\n\nimport com.zhangke.framework.composable.TextString\n\nsealed interface LoadState {\n\n    val loading: Boolean\n        get() = this is Loading\n\n    data object Idle : LoadState\n\n    data object Loading : LoadState\n\n    data class Failed(val message: TextString?) : LoadState\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/utils/Log.kt",
    "content": "package com.zhangke.framework.utils\n\nimport co.touchlab.kermit.Logger\nimport co.touchlab.kermit.loggerConfigInit\nimport co.touchlab.kermit.platformLogWriter\n\nobject Log {\n\n    val log = Logger(\n        loggerConfigInit(platformLogWriter()),\n        \"Fread\",\n    )\n\n    inline fun d(tag: String, message: () -> String) {\n        log.d(tag = tag, message = message)\n    }\n\n    inline fun i(tag: String, message: () -> String) {\n        log.i(tag = tag, message = message)\n    }\n}"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/utils/Parcelize.kt",
    "content": "package com.zhangke.framework.utils\n\n@Target(AnnotationTarget.CLASS)\n@Retention(AnnotationRetention.BINARY)\nannotation class Parcelize\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/utils/PlatformIgnoredOnParcel.kt",
    "content": "package com.zhangke.framework.utils\n\nexpect annotation class PlatformIgnoredOnParcel()"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/utils/PlatformParcelable.kt",
    "content": "package com.zhangke.framework.utils\n\nexpect interface PlatformParcelable"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/utils/PlatformSerializable.kt",
    "content": "package com.zhangke.framework.utils\n\nexpect interface PlatformSerializable"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/utils/PlatformTransient.kt",
    "content": "package com.zhangke.framework.utils\n\n@Target(AnnotationTarget.PROPERTY)\nexpect annotation class PlatformTransient()"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/utils/PlatformUri.kt",
    "content": "package com.zhangke.framework.utils\n\nimport com.eygraber.uri.Uri\n\ntypealias PlatformUri = Uri\n\nfun String.toPlatformUri(): PlatformUri {\n    return Uri.parse(this)\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/utils/RegexFactory.kt",
    "content": "package com.zhangke.framework.utils\n\nobject RegexFactory {\n\n    val domainRegex =\n        \"^(?=^.{3,255}$)[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(\\\\.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+$\".toRegex()\n\n    val didRegex = \"^did:[a-z0-9]+:[a-zA-Z0-9._%-]+$\".toRegex()\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/utils/ResultExt.kt",
    "content": "package com.zhangke.framework.utils\n\nfun <T> List<Result<List<T>>>.collect(): Result<List<T>> {\n    if (isNotEmpty() && any { it.isSuccess }.not()) {\n        return first()\n    }\n    return mapNotNull {\n        it.getOrNull()\n    }.reduce { list1, list2 ->\n        mutableListOf<T>().apply {\n            addAll(list1)\n            addAll(list2)\n        }\n    }.let { Result.success(it) }\n}\n\nfun <T> Result<T>.exceptionOrThrow(): Throwable {\n    return exceptionOrNull() ?: throw IllegalStateException(\"Result is success!\")\n}\n\nfun Result<*>.ignoreContent(): Result<Unit> = map {}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/utils/Rfc822InstantParser.kt",
    "content": "@file:OptIn(ExperimentalTime::class)\n\npackage com.zhangke.framework.utils\n\nimport kotlinx.datetime.Instant\nimport kotlinx.datetime.LocalDateTime\nimport kotlinx.datetime.TimeZone\nimport kotlinx.datetime.UtcOffset\nimport kotlinx.datetime.asTimeZone\nimport kotlinx.datetime.toDeprecatedInstant\nimport kotlinx.datetime.toInstant\nimport kotlin.time.ExperimentalTime\n\n/**\n * RFC 822 date/time format to [Instant] parser.\n *\n * See https://www.rfc-editor.org/rfc/rfc822#section-5\n */\nobject Rfc822InstantParser {\n\n    private enum class Month {\n        Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec,\n        ;\n    }\n\n    /**\n     * Parse RFC 822 date/time format to [Instant]\n     *\n     * @throws IllegalArgumentException in case input string cannot be parsed as RFC 822\n     */\n    fun parse(input: String): Instant {\n        /**\n         * Groups:\n         *\n         * 1 = day of month (1-31)\n         * 2 = month (Jan, Feb, Mar, ...)\n         * 3 = year (2022)\n         * 4 = hour (00-23)\n         * 5 = minute (00-59)\n         * 6 = OPTIONAL: second (00-59)\n         * 7 = time zone (+/-hhmm or letters)\n         */\n        val regex =\n            Regex(\"^(?:\\\\w{3}, )?(\\\\d{1,2}) (\\\\w{3}) (\\\\d{4}) (\\\\d{2}):(\\\\d{2})(?::(\\\\d{2}))? ([+-]?\\\\w+)\\$\")\n\n        val result = regex.matchEntire(input)\n\n        if (result == null || result.groups.size != 8) {\n            throw IllegalArgumentException(\"Unexpected RFC 822 date/time format\")\n        }\n\n        try {\n            val dayOfMonth = result.groupValues[1].toInt()\n            val month = Month.valueOf(result.groupValues[2])\n            val year = result.groupValues[3].toInt()\n            val hour = result.groupValues[4].toInt()\n            val minute = result.groupValues[5].toInt()\n            val second = result.groupValues[6].ifEmpty { \"00\" }.toInt()\n            val timeZone = result.groupValues[7]\n\n            val dateTime = LocalDateTime(\n                year = year,\n                monthNumber = month.ordinal + 1,\n                dayOfMonth = dayOfMonth,\n                hour = hour,\n                minute = minute,\n                second = second,\n                nanosecond = 0,\n            )\n\n            val tz = parseTimeZone(timeZone)\n            return dateTime.toInstant(tz).toDeprecatedInstant()\n        } catch (e: Exception) {\n            throw IllegalArgumentException(\"Unexpected RFC 822 date/time format\", e)\n        }\n    }\n\n    /**\n     * @see parse\n     */\n    operator fun invoke(input: String): Instant = parse(input)\n\n    private fun parseTimeZone(timeZone: String): TimeZone {\n        val startsWithPlus = timeZone.startsWith('+')\n        val startsWithMinus = timeZone.startsWith('-')\n\n        val (hours, minutes) = when {\n            startsWithPlus || startsWithMinus -> {\n                val hour = timeZone.substring(1..2).toInt()\n                val minute = timeZone.substring(3..4).toInt()\n\n                if (startsWithMinus) {\n                    Pair(-hour, -minute)\n                } else {\n                    Pair(hour, minute)\n                }\n            }\n            // Time zones\n            else -> when (timeZone) {\n                \"Z\", \"UT\", \"GMT\" -> 0.hours\n                \"EST\" -> (-5).hours\n                \"EDT\" -> (-4).hours\n                \"CST\" -> (-6).hours\n                \"CDT\" -> (-5).hours\n                \"MST\" -> (-7).hours\n                \"MDT\" -> (-6).hours\n                \"PST\" -> (-8).hours\n                \"PDT\" -> (-7).hours\n\n                // Military\n                \"A\" -> (-1).hours\n                \"M\" -> (-12).hours\n                \"N\" -> 1.hours\n                \"Y\" -> 12.hours\n                else -> throw IllegalArgumentException(\"Unexpected time zone format\")\n            }\n        }\n\n        val offset = UtcOffset(hours = hours, minutes = minutes, seconds = 0)\n        return offset.asTimeZone()\n    }\n\n    private val Int.hours\n        get() = Pair(this, 0)\n}"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/utils/SizeUtils.kt",
    "content": "package com.zhangke.framework.utils\n\nimport androidx.compose.ui.geometry.Size\nimport androidx.compose.ui.unit.Constraints\n\nfun Constraints.asSize(): Size {\n    return Size(\n        width = maxWidth.toFloat(),\n        height = maxHeight.toFloat(),\n    )\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/utils/Standard.kt",
    "content": "package com.zhangke.framework.utils\n\ninline fun <T> T.maybe(predication: Boolean, block: (T) -> T): T {\n    return if (predication) block(this) else this\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/utils/StorageSize.kt",
    "content": "package com.zhangke.framework.utils\n\nimport kotlin.jvm.JvmInline\n\n@Suppress(\"PropertyName\")\n@JvmInline\nvalue class StorageSize(val bytes: Long) {\n\n    val KB: Double get() = bytes.toDouble() / 1024\n\n    val MB: Double get() = KB / 1024\n\n    val GB: Double get() = MB / 1024\n\n    operator fun compareTo(other: StorageSize): Int = bytes.compareTo(other.bytes)\n}\n\nval Int.KB: StorageSize get() = StorageSize(this.toLong() * 1024)\n\nval Int.MB: StorageSize get() = StorageSize(this.toLong() * 1024 * 1024)\n\nval Int.GB: StorageSize get() = StorageSize(this.toLong() * 1024 * 1024 * 1024)\n\nval StorageSize.prettyString: String\n    get() {\n        if (GB >= 1) {\n            return \"${GB.decimal(2)} GB\"\n        }\n        if (MB >= 1) {\n            return \"${MB.decimal(2)} MB\"\n        }\n        return \"${KB.decimal(2)} KB\"\n    }\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/utils/TextFieldUtils.kt",
    "content": "package com.zhangke.framework.utils\n\nimport androidx.compose.material3.TextFieldColors\nimport androidx.compose.material3.TextFieldDefaults\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.text.TextRange\nimport androidx.compose.ui.text.input.TextFieldValue\n\nobject TextFieldUtils {\n\n    fun insertText(value: TextFieldValue, insertText: String): TextFieldValue {\n        val selection = value.selection\n        val oldText = value.text\n        if (oldText.isEmpty()) {\n            return TextFieldValue(insertText, TextRange(insertText.length))\n        }\n        val selectedCount = (selection.end - selection.start).coerceAtLeast(0)\n        if (selectedCount == 0) {\n            val charList = oldText.toMutableList()\n            val newTextList = insertText.toList()\n            charList.addAll(selection.start, newTextList)\n            val newText = charList.joinToString(\"\")\n            return TextFieldValue(newText, TextRange(selection.start + newTextList.size))\n        }\n        val charList = oldText.toMutableList()\n        repeat(selectedCount) {\n            charList.removeAt(selection.start)\n        }\n        val newTextField = TextFieldValue(charList.joinToString(\"\"), TextRange(selection.start))\n        return insertText(newTextField, insertText)\n    }\n}\n\nval TextFieldDefaults.transparentIndicatorColors: TextFieldColors\n    @Composable get() = colors(\n        focusedIndicatorColor = Color.Transparent,\n        unfocusedIndicatorColor = Color.Transparent,\n        disabledIndicatorColor = Color.Transparent,\n        errorIndicatorColor = Color.Transparent,\n    )\n\nval TextFieldDefaults.transparentIndicatorAndContainerColors: TextFieldColors\n    @Composable get() = colors(\n        focusedIndicatorColor = Color.Transparent,\n        unfocusedIndicatorColor = Color.Transparent,\n        disabledIndicatorColor = Color.Transparent,\n        errorIndicatorColor = Color.Transparent,\n        errorContainerColor = Color.Transparent,\n        focusedContainerColor = Color.Transparent,\n        unfocusedContainerColor = Color.Transparent,\n        disabledContainerColor = Color.Transparent,\n    )\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/utils/ThrowableUtils.kt",
    "content": "package com.zhangke.framework.utils\n\nfun Throwable.mapForMessage(newMessage: String): Throwable {\n    return if (message.isNullOrEmpty()) {\n        val typeName = this::class.simpleName\n        RuntimeException(\"$newMessage-$typeName\", this)\n    } else {\n        this\n    }\n}\n\nfun <T> Result<T>.mapForErrorMessage(newErrorMessage: String): Result<T> {\n    if (this.isSuccess) return this\n    val exception =\n        this.exceptionOrNull() ?: return Result.failure(RuntimeException(newErrorMessage))\n    return Result.failure(exception.mapForMessage(newErrorMessage))\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/utils/UriUtils.kt",
    "content": "package com.zhangke.framework.utils\n\nfun uriString(\n    scheme: String,\n    host: String,\n    path: String,\n    queries: Map<String, String>,\n    encode: Boolean = true,\n): String {\n    val builder = StringBuilder()\n    if (scheme.isNotEmpty()) {\n        builder.append(scheme)\n        builder.append(\"://\")\n    }\n    builder.append(host)\n    var fixedPath = path\n    if (builder.endsWith(\"/\") && path.startsWith(\"/\")) {\n        fixedPath = fixedPath.removePrefix(\"/\")\n    }\n    if (fixedPath.endsWith(\"/\") && queries.isNotEmpty()) {\n        fixedPath = fixedPath.removeSuffix(\"/\")\n    }\n    builder.append(fixedPath)\n    if (queries.isNotEmpty()) {\n        val query = queries.entries\n            .joinToString(prefix = \"?\", separator = \"&\") {\n                val value = if (encode) {\n                    UrlEncoder.encode(it.value)\n                } else {\n                    it.value\n                }\n                \"${it.key}=$value\"\n            }\n        builder.append(query)\n    }\n    return builder.toString()\n}\n\nfun String.decodeAsUri(): String {\n    return UrlEncoder.decode(this)\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/utils/UrlEncoder.kt",
    "content": "package com.zhangke.framework.utils\n\nimport kotlin.Char.Companion.MIN_HIGH_SURROGATE\nimport kotlin.Char.Companion.MIN_LOW_SURROGATE\n\n/**\n * Most defensive approach to URL encoding and decoding.\n *\n * - Rules determined by combining the unreserved character set from\n * [RFC 3986](https://www.rfc-editor.org/rfc/rfc3986#page-13) with the percent-encode set from\n * [application/x-www-form-urlencoded](https://url.spec.whatwg.org/#application-x-www-form-urlencoded-percent-encode-set).\n *\n * - Both specs above support percent decoding of two hexadecimal digits to a binary octet, however their unreserved\n * set of characters differs and `application/x-www-form-urlencoded` adds conversion of space to `+`, which has the\n * potential to be misunderstood.\n *\n * - This library encodes with rules that will be decoded correctly in either case.\n *\n * @author Geert Bevin (gbevin(remove) at uwyn dot com)\n * @author Erik C. Thauvin (erik@thauvin.net)\n **/\nobject UrlEncoder {\n    private val hexDigits = \"0123456789ABCDEF\".toCharArray()\n\n    /**\n     * A [BooleanArray] with entries for the [character codes][Char.code] of\n     *\n     * * `0-9`,\n     * * `A-Z`,\n     * * `a-z`\n     *\n     * set to `true`.\n     */\n    private val unreservedChars = BooleanArray('z'.code + 1).apply {\n        set('-'.code, true)\n        set('.'.code, true)\n        set('_'.code, true)\n        for (c in '0'..'9') {\n            set(c.code, true)\n        }\n        for (c in 'A'..'Z') {\n            set(c.code, true)\n        }\n        for (c in 'a'..'z') {\n            set(c.code, true)\n        }\n    }\n\n    // see https://www.rfc-editor.org/rfc/rfc3986#page-13\n    // and https://url.spec.whatwg.org/#application-x-www-form-urlencoded-percent-encode-set\n    private fun Char.isUnreserved(): Boolean {\n        return this <= 'z' && unreservedChars[code]\n    }\n\n    private fun StringBuilder.appendEncodedDigit(digit: Int) {\n        this.append(hexDigits[digit and 0x0F])\n    }\n\n    private fun StringBuilder.appendEncodedByte(ch: Int) {\n        this.append(\"%\")\n        this.appendEncodedDigit(ch shr 4)\n        this.appendEncodedDigit(ch)\n    }\n\n    /**\n     * Transforms a provided [String] into a new string, containing decoded URL characters in the UTF-8\n     * encoding.\n     */\n    fun decode(source: String, plusToSpace: Boolean = false): String {\n        if (source.isEmpty()) {\n            return source\n        }\n\n        val length = source.length\n        val out = StringBuilder(length)\n        var bytesBuffer: ByteArray? = null\n        var bytesPos = 0\n        var i = 0\n        var started = false\n        while (i < length) {\n            val ch = source[i]\n            if (ch == '%') {\n                if (!started) {\n                    out.append(source, 0, i)\n                    started = true\n                }\n                if (bytesBuffer == null) {\n                    // the remaining characters divided by the length of the encoding format %xx, is the maximum number\n                    // of bytes that can be extracted\n                    bytesBuffer = ByteArray((length - i) / 3)\n                }\n                i++\n                require(length >= i + 2) { \"Incomplete trailing escape ($ch) pattern\" }\n                try {\n                    val v = source.substring(i, i + 2).toInt(16)\n                    require(v in 0..0xFF) { \"Illegal escape value\" }\n                    bytesBuffer[bytesPos++] = v.toByte()\n                    i += 2\n                } catch (e: NumberFormatException) {\n                    throw IllegalArgumentException(\n                        \"Illegal characters in escape sequence: $e.message\",\n                        e\n                    )\n                }\n            } else {\n                if (bytesBuffer != null) {\n                    out.append(bytesBuffer.decodeToString(0, bytesPos))\n                    started = true\n                    bytesBuffer = null\n                    bytesPos = 0\n                }\n                if (plusToSpace && ch == '+') {\n                    if (!started) {\n                        out.append(source, 0, i)\n                        started = true\n                    }\n                    out.append(\" \")\n                } else if (started) {\n                    out.append(ch)\n                }\n                i++\n            }\n        }\n\n        if (bytesBuffer != null) {\n            out.append(bytesBuffer.decodeToString(0, bytesPos))\n        }\n\n        return if (!started) source else out.toString()\n    }\n\n    /**\n     * Transforms a provided [String] object into a new string, containing only valid URL\n     * characters in the UTF-8 encoding.\n     *\n     * - Letters, numbers, unreserved (`_-!.'()*`) and allowed characters are left intact.\n     */\n    fun encode(source: String, allow: String = \"\", spaceToPlus: Boolean = false): String {\n        if (source.isEmpty()) {\n            return source\n        }\n        var out: StringBuilder? = null\n        var i = 0\n        while (i < source.length) {\n            val ch = source[i]\n            if (ch.isUnreserved() || ch in allow) {\n                out?.append(ch)\n                i++\n            } else {\n                if (out == null) {\n                    out = StringBuilder(source.length)\n                    out.append(source, 0, i)\n                }\n                val cp = source.codePointAt(i)\n                when {\n                    cp < 0x80 -> {\n                        if (spaceToPlus && ch == ' ') {\n                            out.append('+')\n                        } else {\n                            out.appendEncodedByte(cp)\n                        }\n                        i++\n                    }\n\n                    Character.isBmpCodePoint(cp) -> {\n                        for (b in ch.toString().encodeToByteArray()) {\n                            out.appendEncodedByte(b.toInt())\n                        }\n                        i++\n                    }\n\n                    Character.isSupplementaryCodePoint(cp) -> {\n                        val high = Character.highSurrogateOf(cp)\n                        val low = Character.lowSurrogateOf(cp)\n                        for (b in charArrayOf(high, low).concatToString().encodeToByteArray()) {\n                            out.appendEncodedByte(b.toInt())\n                        }\n                        i += 2\n                    }\n                }\n            }\n        }\n\n        return out?.toString() ?: source\n    }\n\n    /**\n     * Returns the Unicode code point at the specified index.\n     *\n     * The `index` parameter is the regular `CharSequence` index, i.e. the number of `Char`s from the start of the character\n     * sequence.\n     *\n     * If the code point at the specified index is part of the Basic Multilingual Plane (BMP), its value can be represented\n     * using a single `Char` and this method will behave exactly like [CharSequence.get].\n     * Code points outside the BMP are encoded using a surrogate pair – a `Char` containing a value in the high surrogate\n     * range followed by a `Char` containing a value in the low surrogate range. Together these two `Char`s encode a single\n     * code point in one of the supplementary planes. This method will do the necessary decoding and return the value of\n     * that single code point.\n     *\n     * In situations where surrogate characters are encountered that don't form a valid surrogate pair starting at `index`,\n     * this method will return the surrogate code point itself, behaving like [CharSequence.get].\n     *\n     * If the `index` is out of bounds of this character sequence, this method throws an [IndexOutOfBoundsException].\n     *\n     * ```kotlin\n     * // Text containing code points outside the BMP (encoded as a surrogate pairs)\n     * val text = \"\\uD83E\\uDD95\\uD83E\\uDD96\"\n     *\n     * var index = 0\n     * while (index < text.length) {\n     *     val codePoint = text.codePointAt(index)\n     *     // (Do something with codePoint...)\n     *     index += CodePoints.charCount(codePoint)\n     * }\n     * ```\n     */\n    private fun CharSequence.codePointAt(index: Int): Int {\n        if (index !in indices) throw IndexOutOfBoundsException(\"index $index was not in range $indices\")\n\n        val firstChar = this[index]\n        if (firstChar.isHighSurrogate()) {\n            val nextChar = getOrNull(index + 1)\n            if (nextChar?.isLowSurrogate() == true) {\n                return Character.toCodePoint(firstChar, nextChar)\n            }\n        }\n\n        return firstChar.code\n    }\n\n\n    /**\n     * Kotlin Multiplatform equivalent for `java.lang.Character`\n     *\n     * @author <a href=\"https://github.com/aSemy\">aSemy</a>\n     */\n\n    private object Character {\n\n        /**\n         * See https://www.tutorialspoint.com/java/lang/character_issupplementarycodepoint.htm\n         *\n         * Determines whether the specified character (Unicode code point) is in the supplementary character range.\n         * The supplementary character range in the Unicode system falls in `U+10000` to `U+10FFFF`.\n         *\n         * The Unicode code points are divided into two categories:\n         * Basic Multilingual Plane (BMP) code points and Supplementary code points.\n         * BMP code points are present in the range U+0000 to U+FFFF.\n         *\n         * Whereas, supplementary characters are rare characters that are not represented using the original 16-bit Unicode.\n         * For example, these type of characters are used in Chinese or Japanese scripts and hence, are required by the\n         * applications used in these countries.\n         *\n         * @returns `true` if the specified code point falls in the range of supplementary code points\n         * ([MIN_SUPPLEMENTARY_CODE_POINT] to [MAX_CODE_POINT], inclusive), `false` otherwise.\n         */\n        internal fun isSupplementaryCodePoint(codePoint: Int): Boolean =\n            codePoint in MIN_SUPPLEMENTARY_CODE_POINT..MAX_CODE_POINT\n\n        internal fun toCodePoint(highSurrogate: Char, lowSurrogate: Char): Int =\n            (highSurrogate.code shl 10) + lowSurrogate.code + SURROGATE_DECODE_OFFSET\n\n        /** Basic Multilingual Plane (BMP) */\n        internal fun isBmpCodePoint(codePoint: Int): Boolean = codePoint ushr 16 == 0\n\n        internal fun highSurrogateOf(codePoint: Int): Char =\n            ((codePoint ushr 10) + HIGH_SURROGATE_ENCODE_OFFSET.code).toChar()\n\n        internal fun lowSurrogateOf(codePoint: Int): Char =\n            ((codePoint and 0x3FF) + MIN_LOW_SURROGATE.code).toChar()\n\n        //    private const val MIN_CODE_POINT: Int = 0x000000\n        private const val MAX_CODE_POINT: Int = 0x10FFFF\n\n        private const val MIN_SUPPLEMENTARY_CODE_POINT: Int = 0x10000\n\n        private const val SURROGATE_DECODE_OFFSET: Int =\n            MIN_SUPPLEMENTARY_CODE_POINT -\n                    (MIN_HIGH_SURROGATE.code shl 10) -\n                    MIN_LOW_SURROGATE.code\n\n        private const val HIGH_SURROGATE_ENCODE_OFFSET: Char =\n            MIN_HIGH_SURROGATE - (MIN_SUPPLEMENTARY_CODE_POINT ushr 10)\n    }\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/utils/VideoUtils.kt",
    "content": "package com.zhangke.framework.utils\n\nexpect class VideoUtils() {\n\n    fun getVideoAspect(uri: String): AspectRatio?\n}\n"
  },
  {
    "path": "framework/src/commonMain/kotlin/com/zhangke/framework/utils/WebFinger.kt",
    "content": "package com.zhangke.framework.utils\n\nimport com.zhangke.framework.network.FormalBaseUrl\nimport com.zhangke.framework.network.HttpScheme\nimport kotlinx.serialization.Serializable\n\n/**\n * Supported:\n * - jw@jakewharton.com\n * - @jw@jakewharton.com\n * - acct:@jw@jakewharton.com\n * - https://m.cmx.im/@jw@jakewharton.com\n * - https://m.cmx.im/@AtomZ\n * - m.cmx.im/@jw@jakewharton.com\n * - jakewharton.com/@jw\n *\n * For Bluesky Workaround: @bsky@did\n * name is bsky\n * host is did\n */\n@Parcelize\n@Serializable\nclass WebFinger private constructor(\n    val name: String,\n    val host: String,\n) : PlatformParcelable, PlatformSerializable {\n\n    val did: String? = if (name == NAME_DID) host else null\n\n    override fun toString(): String {\n        return \"@$name@$host\"\n    }\n\n    override fun hashCode(): Int {\n        var result = name.hashCode()\n        result = 31 * result + host.hashCode()\n        return result\n    }\n\n    override fun equals(other: Any?): Boolean {\n        if (other === this) return true\n        if (other == null) return false\n        if (other !is WebFinger) return false\n        return (other.name == name) && (other.host == host)\n    }\n\n    fun equalsDomain(other: WebFinger): Boolean {\n        if (this == other) return true\n        if (this.name != other.name) return false\n        if (this.host.endsWith(other.host)) return true\n        if (other.host.endsWith(this.host)) return true\n        return false\n    }\n\n    companion object {\n\n        private const val NAME_DID = \"did\"\n\n        fun create(content: String, baseUrl: FormalBaseUrl? = null): WebFinger? {\n            if (content.isBlank()) return null\n            return createAsAcct(content, baseUrl) ?: createAsUrl(content)\n        }\n\n        fun build(name: String, host: String): WebFinger {\n            return WebFinger(name, host)\n        }\n\n        fun createFromDid(did: String): WebFinger {\n            return WebFinger(NAME_DID, did)\n        }\n\n        private fun createAsAcct(content: String, baseUrl: FormalBaseUrl? = null): WebFinger? {\n            val fixedAcct = content.removePrefix(\"acct:\").removePrefix(\"@\")\n            val name: String\n            val host: String\n            val split = fixedAcct.split('@')\n            if (split.size == 1 && baseUrl != null) {\n                name = split[0]\n                host = baseUrl.host\n            } else if (split.size == 2) {\n                name = split[0]\n                host = split[1]\n            } else {\n                return null\n            }\n            if (!hostValidate(host)) return null\n            return WebFinger(name, host)\n        }\n\n        private fun createAsUrl(content: String): WebFinger? {\n            val maybeUrl = content\n                .removePrefix(HttpScheme.HTTP)\n                .removePrefix(HttpScheme.HTTPS)\n            val split = maybeUrl.split('/')\n            if (split.size < 2) return null\n            val urlHost = split[0]\n            if (!hostValidate(urlHost)) return null\n            val maybeAcct = split.last().removePrefix(\"@\")\n            return if (maybeAcct.contains('@')) {\n                createAsAcct(maybeAcct)\n            } else {\n                createAsAcct(\"$maybeAcct@$urlHost\")\n            }\n        }\n\n        private fun hostValidate(host: String): Boolean {\n            return DomainValidator.validate(host)\n        }\n    }\n}\n"
  },
  {
    "path": "framework/src/commonMain/res/values/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"purple_200\">#FFBB86FC</color>\n    <color name=\"purple_500\">#FF6200EE</color>\n    <color name=\"purple_700\">#FF3700B3</color>\n    <color name=\"teal_200\">#FF03DAC5</color>\n    <color name=\"teal_700\">#FF018786</color>\n    <color name=\"black\">#FF000000</color>\n    <color name=\"white\">#FFFFFFFF</color>\n</resources>"
  },
  {
    "path": "framework/src/commonMain/res/values/ids.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <item name=\"status_bar_view\" type=\"id\" />\n</resources>"
  },
  {
    "path": "framework/src/commonTest/kotlin/com/zhangke/framework/network/SimpleUriTest.kt",
    "content": "package com.zhangke.framework.network\n\nimport kotlin.test.Test\nimport kotlin.test.assertEquals\nimport kotlin.test.assertNotNull\nimport kotlin.test.assertNull\n\nclass SimpleUriTest {\n\n    @Test\n    fun `should return null when empty`() {\n        assertNull(SimpleUri.parse(\"\"))\n    }\n\n    @Test\n    fun `should return host when given host`() {\n        val host = \"example.com\"\n        val uri = SimpleUri.parse(\"https://$host\")\n        assertEquals(host, uri!!.host)\n    }\n\n    @Test\n    fun `should return scheme when given scheme`() {\n        val scheme = \"ftp\"\n        val uri = SimpleUri.parse(\"${scheme}://example.com\")\n        assertEquals(scheme, uri!!.scheme)\n    }\n\n    @Test\n    fun `should has path when given path`() {\n        val uri = SimpleUri.parse(\"http://example.com/a/b/c?name=we\")\n        assertEquals(\"/a/b/c\", uri!!.path)\n    }\n\n    @Test\n    fun `should return query when given path is empty`() {\n        val uri = SimpleUri.parse(\"https://a.b/?a=1&b=2\")\n        assertEquals(\"1\", uri!!.queries[\"a\"])\n        assertEquals(\"2\", uri.queries[\"b\"])\n    }\n\n    @Test\n    fun `should return query when given query`() {\n        val uri = SimpleUri.parse(\"https://a.b/path?a=1&b=2\")\n        assertEquals(\"1\", uri!!.queries[\"a\"])\n        assertEquals(\"2\", uri.queries[\"b\"])\n    }\n\n    @Test\n    fun testParse() {\n        val simpleUri = SimpleUri.parse(\"fread://activitypub/platform/detail?baseUrl=https://mastodon.social\")\n        assertNotNull(simpleUri)\n        assertEquals(\"fread\", simpleUri.scheme)\n        assertEquals(\"activitypub\", simpleUri.host)\n        assertEquals(\"/platform/detail\", simpleUri.path)\n        assertEquals(\"https://mastodon.social\", simpleUri.queries[\"baseUrl\"])\n    }\n}"
  },
  {
    "path": "framework/src/commonTest/kotlin/com/zhangke/framework/opml/OpmlParserTest.kt",
    "content": "package com.zhangke.framework.opml\n\nimport kotlin.test.Test\nimport kotlin.test.assertEquals\n\nclass OpmlParserTest {\n    @Test\n    fun testParse() {\n        val xml = \"\"\"\n            <?xml version=\"1.0\" encoding=\"UTF-8\"?>\n            <opml version=\"1.0\">\n            <head><title>中文独立博客列表</title></head>\n            <body>\n                <outline text=\"透明创业实验\" title=\"透明创业实验\" type=\"rss\" xmlUrl=\"https://blog.t9t.io/atom.xml\" htmlUrl=\"https://blog.t9t.io\"/>\n                <outline text=\"阮一峰的网络日志\" title=\"阮一峰的网络日志\" type=\"rss\" xmlUrl=\"http://feeds.feedburner.com/ruanyifeng\" htmlUrl=\"https://www.ruanyifeng.com/blog/\"/>\n                <outline text=\"酷 壳 – CoolShell\" title=\"酷 壳 – CoolShell\" type=\"rss\" xmlUrl=\"http://coolshell.cn/feed\" htmlUrl=\"https://coolshell.cn\"/>\n                <outline text=\"张鑫旭-鑫空间-鑫生活\" title=\"张鑫旭-鑫空间-鑫生活\" type=\"rss\" xmlUrl=\"http://www.zhangxinxu.com/wordpress/?feed=rss2\" htmlUrl=\"https://www.zhangxinxu.com/\"/>\n                <outline text=\"Alili丶前端大爆炸\" title=\"Alili丶前端大爆炸\" type=\"rss\" xmlUrl=\"https://alili.tech/index.xml\" htmlUrl=\"https://alili.tech\"/>\n                <outline text=\"蚊子前端博客\" title=\"蚊子前端博客\" type=\"rss\" xmlUrl=\"https://www.xiabingbao.com/atom.xml\" htmlUrl=\"https://www.xiabingbao.com\"/>\n                <outline text=\"DIYGod - 写代码是热爱，写到世界充满爱!\" title=\"DIYGod - 写代码是热爱，写到世界充满爱!\" type=\"rss\" xmlUrl=\"https://diygod.me/atom.xml\" htmlUrl=\"https://diygod.me\"/>\n                <outline text=\"MacTalk-池建强的随想录\" title=\"MacTalk-池建强的随想录\" type=\"rss\" xmlUrl=\"http://macshuo.com/?feed=rss2\" htmlUrl=\"http://macshuo.com\"/>\n                <outline text=\"ShrekShao\" title=\"ShrekShao\" type=\"rss\" xmlUrl=\"http://shrekshao.github.io/feed.xml\" htmlUrl=\"https://shrekshao.github.io/\"/>\n                <outline text=\"云风的 BLOG\" title=\"云风的 BLOG\" type=\"rss\" xmlUrl=\"http://blog.codingnow.com/atom.xml\" htmlUrl=\"https://blog.codingnow.com\"/>\n                <outline text=\"ZDDHUB 的博客\" title=\"ZDDHUB 的博客\" type=\"rss\" xmlUrl=\"https://zddhub.com/feed\" htmlUrl=\"https://zddhub.com/\"/>\n                <outline text=\"全栈应用开发:精益实践\" title=\"全栈应用开发:精益实践\" type=\"rss\" xmlUrl=\"https://www.phodal.com/blog/feeds/rss/\" htmlUrl=\"https://www.phodal.com\">\n                    <outline text=\"前端之巅\" title=\"前端之巅\" type=\"rss\" xmlUrl=\"https://www.phodal.com/blog/feeds/rss/\" htmlUrl=\"https://www.phodal.com\"/>\n                    <outline text=\"云风的 BLOG\" title=\"云风的 BLOG\" type=\"rss\" xmlUrl=\"http://blog.codingnow.com/atom.xml\" htmlUrl=\"https://blog.codingnow.com\"/>\n                </outline>\n                <outline text=\"Reorx’s Forge\" title=\"Reorx’s Forge\" type=\"rss\" xmlUrl=\"https://reorx.com/feed.xml\" htmlUrl=\"https://reorx.com/\"/>\n            </body>\n            </opml>\n        \"\"\".trimIndent()\n        val result = OpmlParser.parse(xml)\n        assertEquals(13, result.size)\n        assertEquals(\"透明创业实验\", result[0].title)\n        assertEquals(\"全栈应用开发:精益实践\", result[11].title)\n        assertEquals(\"前端之巅\", result[11].children[0].title)\n        assertEquals(\"Reorx’s Forge\", result[12].title)\n    }\n}"
  },
  {
    "path": "framework/src/commonTest/kotlin/com/zhangke/framework/security/Md5Test.kt",
    "content": "package com.zhangke.framework.security\n\nimport kotlin.test.Test\nimport kotlin.test.assertEquals\n\nclass Md5Test {\n    @Test\n    fun testMd5() {\n        val md5 = Md5.md5(\"123456\")\n        assertEquals(\"e10adc3949ba59abbe56e057f20f883e\", md5)\n    }\n}"
  },
  {
    "path": "framework/src/commonTest/kotlin/com/zhangke/framework/utils/ExtractUrlFromTextUtilsTest.kt",
    "content": "package com.zhangke.framework.utils\n\nimport kotlin.test.Test\nimport kotlin.test.assertContentEquals\n\nclass ExtractUrlFromTextUtilsTest {\n\n    @Test\n    fun `should extract urls from text`() {\n        assertContentEquals(\n            expected = listOf(\n                \"https://example.com/path?a=1\",\n                \"example.org/hello\",\n            ),\n            actual = ExtractUrlFromTextUtils.extract(\n                \"Visit https://example.com/path?a=1 and example.org/hello now.\"\n            ),\n        )\n    }\n\n    @Test\n    fun `should trim trailing punctuation`() {\n        assertContentEquals(\n            expected = listOf(\n                \"https://example.com/path(test)\",\n                \"https://another.example.com/demo\",\n                \"https://third.example.com\",\n            ),\n            actual = ExtractUrlFromTextUtils.extract(\n                \"(https://example.com/path(test)), https://another.example.com/demo. [https://third.example.com]\"\n            ),\n        )\n    }\n\n    @Test\n    fun `should ignore invalid urls`() {\n        assertContentEquals(\n            expected = emptyList(),\n            actual = ExtractUrlFromTextUtils.extract(\n                \"localhost http://-example.com example invalid_domain\"\n            ),\n        )\n    }\n}\n"
  },
  {
    "path": "framework/src/commonTest/kotlin/com/zhangke/framework/utils/IntExtTest.kt",
    "content": "package com.zhangke.framework.utils\n\nimport kotlin.test.Test\nimport kotlin.test.assertEquals\n\nclass IntExtTest {\n    @Test\n    fun testIntFormatAsCount() {\n        assertEquals(\"1M\", 1_000_000.formatToHumanReadable())\n        assertEquals(\"1.1M\", 1_100_000.formatToHumanReadable())\n        assertEquals(\"1.9M\", 1_900_000.formatToHumanReadable())\n        assertEquals(\"1.5M\", 1_500_000.formatToHumanReadable())\n        assertEquals(\"1M\", 1_009_000.formatToHumanReadable())\n        assertEquals(\"1.1M\", 1_090_000.formatToHumanReadable())\n        assertEquals(\"10M\", 10_000_000.formatToHumanReadable())\n        assertEquals(\"1K\", 1000.formatToHumanReadable())\n        assertEquals(\"10K\", 10000.formatToHumanReadable())\n        assertEquals(\"89K\", 89000.formatToHumanReadable())\n        assertEquals(\"99K\", 99000.formatToHumanReadable())\n        assertEquals(\"999\", 999.formatToHumanReadable())\n    }\n}\n"
  },
  {
    "path": "framework/src/commonTest/kotlin/com/zhangke/framework/utils/WebFingerTest.kt",
    "content": "package com.zhangke.framework.utils\n\nimport kotlin.test.Test\n\nclass WebFingerTest {\n\n    @Test\n    fun testCrashedCase() {\n    }\n}\n"
  },
  {
    "path": "framework/src/iosMain/kotlin/com/zhangke/framework/architect/http/HttpClientEngine.ios.kt",
    "content": "package com.zhangke.framework.architect.http\n\nimport io.ktor.client.engine.HttpClientEngine\nimport io.ktor.client.engine.darwin.Darwin\n\nactual fun createHttpClientEngine(): HttpClientEngine {\n    return Darwin.create()\n}"
  },
  {
    "path": "framework/src/iosMain/kotlin/com/zhangke/framework/architect/theme/FreadTheme.ios.kt",
    "content": "package com.zhangke.framework.architect.theme\n\nimport androidx.compose.runtime.Composable\n\n@Composable\ninternal actual fun FreadPlatformTheme(darkTheme: Boolean) {\n}"
  },
  {
    "path": "framework/src/iosMain/kotlin/com/zhangke/framework/blurhash/BitmapExt.ios.kt",
    "content": "package com.zhangke.framework.blurhash\n\nimport androidx.compose.ui.graphics.ImageBitmap\nimport androidx.compose.ui.graphics.asComposeImageBitmap\nimport org.jetbrains.skia.Bitmap\nimport org.jetbrains.skia.ColorAlphaType\nimport org.jetbrains.skia.ImageInfo\n\nactual fun bitmapFromBuffer(buffer: IntArray, width: Int, height: Int): ImageBitmap {\n    val byteArray = getBytes(width, height, buffer)\n    return Bitmap().apply {\n        allocPixels(ImageInfo.makeS32(width, height, ColorAlphaType.PREMUL))\n        installPixels(imageInfo, byteArray, width * 4)\n    }.asComposeImageBitmap()\n}\n\nprivate fun getBytes(width: Int, height: Int, buffer: IntArray): ByteArray {\n    val pixels = ByteArray(width * height * 4)\n\n    var index = 0\n    for (y in 0 until height) {\n        for (x in 0 until width) {\n            val pixel = buffer[y * width + x]\n            pixels[index++] = ((pixel and 0xFF)).toByte() // Blue component\n            pixels[index++] = (((pixel shr 8) and 0xFF)).toByte() // Green component\n            pixels[index++] = (((pixel shr 16) and 0xFF)).toByte() // Red component\n            pixels[index++] = (((pixel shr 24) and 0xFF)).toByte() // Alpha component\n        }\n    }\n\n    return pixels\n}"
  },
  {
    "path": "framework/src/iosMain/kotlin/com/zhangke/framework/composable/TextString.ios.kt",
    "content": "package com.zhangke.framework.composable\n\nimport androidx.compose.runtime.Composable\n\n@Composable\nactual fun stringResource(resId: Int, vararg formatArgs: Any): String {\n    error(\"stringResource(resId, formatArgs) Not supported on iOS\")\n}\n\nactual suspend fun TextString.getString(): String {\n    return when (this) {\n        is TextString.StringText -> string\n        is TextString.ComposeResourceText -> org.jetbrains.compose.resources.getString(res)\n        is TextString.ResourceText -> error(\"ResourceText Not supported on iOS\")\n    }\n}"
  },
  {
    "path": "framework/src/iosMain/kotlin/com/zhangke/framework/composable/pick/PickVisualMediaLauncherContainer.ios.kt",
    "content": "package com.zhangke.framework.composable.pick\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport com.zhangke.framework.utils.PlatformUri\n\n@Composable\nactual fun PickVisualMediaLauncherContainer(\n    onResult: (List<PlatformUri>) -> Unit,\n    maxItems: Int,\n    content: @Composable PickVisualMediaLauncherContainerScope.() -> Unit,\n) {\n    val scope = remember {\n        PickVisualMediaLauncherContainerScope()\n    }\n    with(scope) {\n        content()\n    }\n}\n\nactual class PickVisualMediaLauncherContainerScope {\n    actual fun launchImage() {\n        TODO(\"Not yet implemented\")\n    }\n\n    actual fun launchMedia() {\n        TODO(\"Not yet implemented\")\n    }\n\n    actual fun launchVideo(){\n        TODO(\"Not yet implemented\")\n    }\n\n    actual fun launchImageFile() {\n        TODO(\"Not yet implemented\")\n    }\n\n    actual fun launchVideoFile() {\n        TODO(\"Not yet implemented\")\n    }\n}\n"
  },
  {
    "path": "framework/src/iosMain/kotlin/com/zhangke/framework/date/InstantFormater.ios.kt",
    "content": "@file:OptIn(ExperimentalTime::class)\n\npackage com.zhangke.framework.date\n\nimport kotlinx.datetime.Instant\nimport kotlinx.datetime.LocalDateTime\nimport kotlinx.datetime.TimeZone\nimport kotlinx.datetime.format\nimport kotlinx.datetime.format.char\nimport kotlinx.datetime.toLocalDateTime\nimport kotlin.time.ExperimentalTime\n\nactual class InstantFormater {\n\n    actual fun formatToMediumDate(instant: Instant): String {\n        return instant.toLocalDateTime(TimeZone.currentSystemDefault())\n            .format(\n                LocalDateTime.Format {\n                    year()\n                    char('-')\n                    monthNumber()\n                    char('-')\n                    dayOfMonth()\n                    char(' ')\n                    hour()\n                    char(':')\n                    minute()\n                    char(':')\n                    second()\n                }\n            )\n    }\n\n    actual fun formatToMediumDateWithoutTime(instant: Instant): String {\n        return instant.toLocalDateTime(TimeZone.currentSystemDefault())\n            .format(\n                LocalDateTime.Format {\n                    year()\n                    char('-')\n                    monthNumber()\n                    char('-')\n                    dayOfMonth()\n                }\n            )\n    }\n}\n"
  },
  {
    "path": "framework/src/iosMain/kotlin/com/zhangke/framework/datetime/Instant.ios.kt",
    "content": "package com.zhangke.framework.datetime\n\nimport com.zhangke.framework.serialize.TimestampAsInstantSerializer\nimport com.zhangke.framework.utils.Parcelize\nimport kotlinx.serialization.Serializable\n\n//@Parcelize\n//@Serializable(with = TimestampAsInstantSerializer::class)\n//actual class Instant actual (actual val instant: kotlinx.datetime.Instant)"
  },
  {
    "path": "framework/src/iosMain/kotlin/com/zhangke/framework/permission/RequireLocalStoragePermission.ios.kt",
    "content": "package com.zhangke.framework.permission\n\nimport androidx.compose.runtime.Composable\n\n@Composable\nactual fun RequireLocalStoragePermission(\n    onPermissionGranted: suspend () -> Unit,\n    onPermissionDenied: suspend () -> Unit\n) {\n    TODO(\"Not yet implemented\")\n}\n"
  },
  {
    "path": "framework/src/iosMain/kotlin/com/zhangke/framework/toast/Toast.ios.kt",
    "content": "package com.zhangke.framework.toast\n\nactual fun toast(message: String?) {\n}"
  },
  {
    "path": "framework/src/iosMain/kotlin/com/zhangke/framework/utils/IDNUtils.ios.kt",
    "content": "package com.zhangke.framework.utils\n\nactual class IDNUtils {\n\n    actual fun toASCII(input: String): String {\n        throw UnsupportedOperationException(\"IDNUtils is not supported on this platform.\")\n    }\n}\n"
  },
  {
    "path": "framework/src/iosMain/kotlin/com/zhangke/framework/utils/ImageCompressUtils.kt",
    "content": "package com.zhangke.framework.utils\n\nactual class ImageCompressUtils {\n\n    actual fun compress(bytes: ByteArray, targetSize: StorageSize): CompressResult {\n        return CompressResult(bytes, null)\n    }\n}"
  },
  {
    "path": "framework/src/iosMain/kotlin/com/zhangke/framework/utils/LanguageUtils.ios.kt",
    "content": "package com.zhangke.framework.utils\n\nimport platform.Foundation.NSLocale\nimport platform.Foundation.NSLocaleLanguageCode\nimport platform.Foundation.availableLocaleIdentifiers\nimport platform.Foundation.languageIdentifier\nimport platform.Foundation.systemLocale\n\nactual object LanguageUtils {\n    actual fun getAllLanguages(): List<Locale> {\n        return NSLocale.availableLocaleIdentifiers().map {\n            NSLocale(it as String)\n        }\n    }\n}\n\nactual typealias Locale = NSLocale\n\nactual val Locale.languageCode: String\n    get() = objectForKey(NSLocaleLanguageCode) as? String ?: \"\"\n\nactual val Locale.isO3LanguageCode: String\n    get() = languageIdentifier()\n\nactual fun Locale.getDisplayName(displayLocale: Locale): String {\n    return displayNameForKey(NSLocaleLanguageCode, displayLocale).orEmpty()\n}\n\nactual fun initLocale(language: String): Locale {\n    return NSLocale(language)\n}\n\nactual fun getDefaultLocale(): Locale {\n    return NSLocale.systemLocale()\n}\n"
  },
  {
    "path": "framework/src/iosMain/kotlin/com/zhangke/framework/utils/PlatformIgnoredOnParcel.ios.kt",
    "content": "package com.zhangke.framework.utils\n\nactual annotation class PlatformIgnoredOnParcel()"
  },
  {
    "path": "framework/src/iosMain/kotlin/com/zhangke/framework/utils/PlatformParcelable.ios.kt",
    "content": "package com.zhangke.framework.utils\n\nactual interface PlatformParcelable"
  },
  {
    "path": "framework/src/iosMain/kotlin/com/zhangke/framework/utils/PlatformTransient.ios.kt",
    "content": "package com.zhangke.framework.utils\n\n@Target(AnnotationTarget.PROPERTY)\nactual annotation class PlatformTransient\n"
  },
  {
    "path": "framework/src/iosMain/kotlin/com/zhangke/framework/utils/PlatformUri.ios.kt",
    "content": "package com.zhangke.framework.utils\n\n"
  },
  {
    "path": "framework/src/iosMain/kotlin/com/zhangke/framework/utils/Serializable.ios.kt",
    "content": "package com.zhangke.framework.utils\n\nactual interface PlatformSerializable"
  },
  {
    "path": "framework/src/iosMain/kotlin/com/zhangke/framework/utils/VideoUtils.ios.kt",
    "content": "package com.zhangke.framework.utils\n\nactual class VideoUtils {\n\n    actual fun getVideoAspect(uri: String): AspectRatio? {\n        return null\n    }\n}\n"
  },
  {
    "path": "gradle/libs.versions.toml",
    "content": "[versions]\nkotlin = \"2.3.20\"\nksp = \"2.3.6\"\nkotlinCoroutine = \"1.10.2\"\nkotlinCoroutineTest = \"1.10.2\"\nkotlinx-serialization = \"1.10.0\"\nkotlinx-datetime = \"0.7.1-0.6.x-compat\"\n\narrow = \"2.1.2\"\n\ncompose-multiplatform = \"1.10.3\"\n\nandroidxCore = \"1.18.0\"\nandroidxFragment = \"1.8.9\"\nandroidxAppcompat = \"1.7.1\"\nandroidx-annotation = \"1.9.1\"\nandroidxActivity = \"1.13.0\"\nandroidx-preference = \"1.2.1\"\nandroidx-collection = \"1.3.0\"\nandroidx-sqlite = \"2.6.2\"\nandroidx-room = \"2.8.4\"\nandroidx-datastore = \"1.1.7\"\nandroidx-browser = \"1.4.0\"\nandroidx-paging = \"3.3.6\"\nandroidx-media3 = \"1.5.1\"\n\nnav3Core = \"1.0.0\"\njetbrainsNav3 = \"1.0.0-alpha06\"\nlifecycleViewmodelNav3 = \"2.10.0-alpha07\"\n\ncompose-runtime-tracing = \"1.10.1\"\n\njetbrains-lifecycle = \"2.10.0\"\n\nkoin = \"4.2.0\"\n\naccompanist = \"0.37.3\"\n\nokhttp3 = \"4.12.0\"\nktor = \"3.3.0\"\nktorfit = \"2.2.0\"\n\nauto_service = \"1.0.1\"\n\nkrouter = \"1.3.7\"\n\nmultiplatformsettings = \"1.3.0\"\n\nandroidGradlePlugin = \"8.13.2\"\n\nmockk = \"1.13.8\"\njunit = \"4.13.2\"\nandroidx-test-ext-junit = \"1.1.5\"\nespresso-core = \"3.5.1\"\nmaterial = \"1.13.0\"\n\nrssparser = \"6.0.12\"\n\nktml = \"0.0.6\"\n\njsoup = \"1.15.3\"\n\ngoogle-service-version = \"4.4.2\"\nfirebaseKmp = \"2.1.0\"\n\ngoolgePlayReview = \"2.0.2\"\ngoolgePlayReviewKtx = \"2.0.1\"\n\nbluesky = \"0.3.3\"\nactivity-pub-client = \"1.0.0\"\n\nhaze = \"1.7.2\"\ncompose-media-player = \"0.8.7\"\n\n[bundles]\nandroidx-fragment = [\"androidx.fragment\", \"androidx.fragment.ktx\"]\nandroidx-activity = [\"androidx.activity\", \"androidx.activity.ktx\", \"androidx.activity.compose\"]\nandroidx-preference = [\"androidx.preference\", \"androidx.preference.ktx\"]\nandroidx-datastore = [\"androidx-datastore\", \"androidx.datastore-preferences\"]\nandroidx-collection = [\"androidx.collection\", \"androidx.collection.ktx\"]\n\nandroidx-media3 = [\"androidx.media3.exoplayer\", \"androidx.media3.datasource.okhttp\", \"androidx.media3.ui\", \"androidx.media3.session\", \"androidx.media3.database\", \"androidx.media3.decoder\", \"androidx.media3.datasource\", \"androidx.media3.common\", \"androidx-media3-hls\"]\n\nkotlin = [\"kotlin.stdlib\", \"kotlin.coroutine.core\", \"kotlin.coroutine.android\"]\n\ngooglePlayReview = [\"googlePlayReview\", \"googlePlayReviewKtx\"]\n\nandroidx-nav3 = [\"androidx-nav3-runtime\", \"jetbrains-nav3-ui\", \"lifecycle-viewmodel-nav3\"]\n\n[libraries]\nandroidx-core = { group = \"androidx.core\", name = \"core\", version.ref = \"androidxCore\" }\nandroidx-core-ktx = { group = \"androidx.core\", name = \"core-ktx\", version.ref = \"androidxCore\" }\nandroidx-appcompat = { group = \"androidx.appcompat\", name = \"appcompat\", version.ref = \"androidxAppcompat\" }\nandroidx-annotation = { group = \"androidx.annotation\", name = \"annotation\", version.ref = \"androidx-annotation\" }\nandroidx-fragment = { group = \"androidx.fragment\", name = \"fragment\", version.ref = \"androidxFragment\" }\nandroidx-fragment-ktx = { group = \"androidx.fragment\", name = \"fragment-ktx\", version.ref = \"androidxFragment\" }\nandroidx-activity = { group = \"androidx.activity\", name = \"activity\", version.ref = \"androidxActivity\" }\nandroidx-activity-ktx = { group = \"androidx.activity\", name = \"activity-ktx\", version.ref = \"androidxActivity\" }\nandroidx-activity-compose = { group = \"androidx.activity\", name = \"activity-compose\", version.ref = \"androidxActivity\" }\nandroidx-preference = { group = \"androidx.preference\", name = \"preference\", version.ref = \"androidx.preference\" }\nandroidx-preference-ktx = { group = \"androidx.preference\", name = \"preference-ktx\", version.ref = \"androidx.preference\" }\nandroidx-collection = { group = \"androidx.collection\", name = \"collection\", version.ref = \"androidx.collection\" }\nandroidx-collection-ktx = { group = \"androidx.collection\", name = \"collection-ktx\", version.ref = \"androidx.collection\" }\nandroidx-sqlite-bundled = { module = \"androidx.sqlite:sqlite-bundled\", version.ref = \"androidx-sqlite\" }\nandroidx-room = { group = \"androidx.room\", name = \"room-runtime\", version.ref = \"androidx-room\" }\nandroidx-room-compiler = { group = \"androidx.room\", name = \"room-compiler\", version.ref = \"androidx-room\" }\nandroidx-datastore = { group = \"androidx.datastore\", name = \"datastore\", version.ref = \"androidx-datastore\" }\nandroidx-datastore-preferences = { group = \"androidx.datastore\", name = \"datastore-preferences\", version.ref = \"androidx-datastore\" }\nandroidx-browser = { group = \"androidx.browser\", name = \"browser\", version.ref = \"androidx.browser\" }\nandroidx-constraintlayout-compose-kmp = { module = \"tech.annexflow.compose:constraintlayout-compose-multiplatform\", version = \"0.6.0\" }\nandroidx-paging-common = { group = \"androidx.paging\", name = \"paging-common\", version.ref = \"androidx-paging\" }\n\njetbrains-lifecycle-runtime = { group = \"org.jetbrains.androidx.lifecycle\", name = \"lifecycle-runtime\", version.ref = \"jetbrains-lifecycle\" }\njetbrains-lifecycle-viewmodel = { group = \"org.jetbrains.androidx.lifecycle\", name = \"lifecycle-viewmodel\", version.ref = \"jetbrains-lifecycle\" }\njetbrains-lifecycle-viewmodel-compose = { group = \"org.jetbrains.androidx.lifecycle\", name = \"lifecycle-viewmodel-compose\", version.ref = \"jetbrains-lifecycle\" }\n\nandroidx-media3-exoplayer = { group = \"androidx.media3\", name = \"media3-exoplayer\", version.ref = \"androidx.media3\" }\nandroidx-media3-datasource-okhttp = { group = \"androidx.media3\", name = \"media3-datasource-okhttp\", version.ref = \"androidx.media3\" }\nandroidx-media3-ui = { group = \"androidx.media3\", name = \"media3-ui\", version.ref = \"androidx.media3\" }\nandroidx-media3-session = { group = \"androidx.media3\", name = \"media3-session\", version.ref = \"androidx.media3\" }\nandroidx-media3-database = { group = \"androidx.media3\", name = \"media3-database\", version.ref = \"androidx.media3\" }\nandroidx-media3-decoder = { group = \"androidx.media3\", name = \"media3-decoder\", version.ref = \"androidx.media3\" }\nandroidx-media3-datasource = { group = \"androidx.media3\", name = \"media3-datasource\", version.ref = \"androidx.media3\" }\nandroidx-media3-common = { group = \"androidx.media3\", name = \"media3-common\", version.ref = \"androidx.media3\" }\nandroidx-media3-hls = { group = \"androidx.media3\", name = \"media3-exoplayer-hls\", version.ref = \"androidx.media3\" }\n\nlifecycle-viewmodel-nav3 = { module = \"org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-navigation3\", version.ref = \"lifecycleViewmodelNav3\" }\nandroidx-nav3-runtime = { module = \"androidx.navigation3:navigation3-runtime\", version.ref = \"nav3Core\" }\nandroidx-nav3-ui = { module = \"androidx.navigation3:navigation3-ui\", version.ref = \"nav3Core\" }\njetbrains-nav3-ui = { module = \"org.jetbrains.androidx.navigation3:navigation3-ui\", version.ref = \"jetbrainsNav3\" }\n\naccompanist-permissions = { group = \"com.google.accompanist\", name = \"accompanist-permissions\", version.ref = \"accompanist\" }\n\nfirebase-kmp-analytics = { module = \"dev.gitlive:firebase-analytics\", version.ref = \"firebaseKmp\" }\nfirebase-kmp-crashlytics = { module = \"dev.gitlive:firebase-crashlytics\", version.ref = \"firebaseKmp\" }\nfirebase-kmp-messaging = { module = \"dev.gitlive:firebase-messaging\", version.ref = \"firebaseKmp\" }\n\nkotlin-stdlib = { group = \"org.jetbrains.kotlin\", name = \"kotlin-stdlib\", version.ref = \"kotlin\" }\nkotlin-coroutine-core = { group = \"org.jetbrains.kotlinx\", name = \"kotlinx-coroutines-core\", version.ref = \"kotlinCoroutine\" }\nkotlin-coroutine-android = { group = \"org.jetbrains.kotlinx\", name = \"kotlinx-coroutines-android\", version.ref = \"kotlinCoroutine\" }\nkotlin-coroutine-test = { group = \"org.jetbrains.kotlinx\", name = \"kotlinx-coroutines-test\", version.ref = \"kotlinCoroutineTest\" }\n\nkotlinx-serialization-core = { group = \"org.jetbrains.kotlinx\", name = \"kotlinx-serialization-core\", version.ref = \"kotlinx-serialization\" }\nkotlinx-serialization-json = { group = \"org.jetbrains.kotlinx\", name = \"kotlinx-serialization-json\", version.ref = \"kotlinx-serialization\" }\nkotlinx-datetime = { group = \"org.jetbrains.kotlinx\", name = \"kotlinx-datetime\", version.ref = \"kotlinx-datetime\" }\n\nokhttp3 = { group = \"com.squareup.okhttp3\", name = \"okhttp\", version.ref = \"okhttp3\" }\nokhttp3-logging = { group = \"com.squareup.okhttp3\", name = \"logging-interceptor\", version.ref = \"okhttp3\" }\n\nktor-client-core = { group = \"io.ktor\", name = \"ktor-client-core\", version.ref = \"ktor\" }\nktor-client-content-negotiation = { module = \"io.ktor:ktor-client-content-negotiation\", version.ref = \"ktor\" }\nktor-client-serialization-kotlinx-json = { module = \"io.ktor:ktor-serialization-kotlinx-json\", version.ref = \"ktor\" }\nktor-client-okhttp = { group = \"io.ktor\", name = \"ktor-client-okhttp\", version.ref = \"ktor\" }\nktor-client-darwin = { group = \"io.ktor\", name = \"ktor-client-darwin\", version.ref = \"ktor\" }\nktor-client-logging = { group = \"io.ktor\", name = \"ktor-client-logging\", version.ref = \"ktor\" }\n\nktorfit-lib = { module = \"de.jensklingenberg.ktorfit:ktorfit-lib\", version.ref = \"ktorfit\" }\nktorfit-converters-response = { module = \"de.jensklingenberg.ktorfit:ktorfit-converters-response\", version.ref = \"ktorfit\" }\n\nauto-service-annotations = { group = \"com.google.auto.service\", name = \"auto-service-annotations\", version.ref = \"auto.service\" }\nauto-service-ksp = { module = \"dev.zacsweers.autoservice:auto-service-ksp\", version = \"1.2.0\" }\n\narrow-core = { group = \"io.arrow-kt\", name = \"arrow-core\", version.ref = \"arrow\" }\n\nmockk = { group = \"io.mockk\", name = \"mockk\", version.ref = \"mockk\" }\n\nactivity-pub-client = { group = \"io.github.0xzhangke\", name = \"activity-pub-client\", version.ref = \"activity-pub-client\" }\n\n# Dependencies of the included build-logic\nandroid-gradlePlugin = { group = \"com.android.tools.build\", name = \"gradle\", version.ref = \"androidGradlePlugin\" }\n#firebase-crashlytics-gradlePlugin = { group = \"com.google.firebase\", name = \"firebase-crashlytics-gradle\", version.ref = \"firebaseCrashlyticsPlugin\" }\n#firebase-performance-gradlePlugin = { group = \"com.google.firebase\", name = \"perf-plugin\", version.ref = \"firebasePerfPlugin\" }\nkotlin-gradlePlugin = { group = \"org.jetbrains.kotlin\", name = \"kotlin-gradle-plugin\", version.ref = \"kotlin\" }\nksp-gradlePlugin = { group = \"com.google.devtools.ksp\", name = \"com.google.devtools.ksp.gradle.plugin\", version.ref = \"ksp\" }\ncompose-gradlePlugin = { module = \"org.jetbrains.compose:compose-gradle-plugin\", version.ref = \"compose-multiplatform\" }\ncomposeCompiler-gradlePlugin = { module = \"org.jetbrains.kotlin:compose-compiler-gradle-plugin\", version.ref = \"kotlin\" }\ncompose-jb-backhandler = { group = \"org.jetbrains.compose.ui\", name = \"ui-backhandler\", version.ref = \"compose-multiplatform\" }\n\nkrouter-reducing-compiler = { group = \"io.github.0xzhangke\", name = \"krouter-reducing-compiler\", version.ref = \"krouter\" }\nkrouter-collecting-compiler = { group = \"io.github.0xzhangke\", name = \"krouter-collecting-compiler\", version.ref = \"krouter\" }\nkrouter-runtime = { group = \"io.github.0xzhangke\", name = \"krouter-runtime\", version.ref = \"krouter\" }\nkrouter-annotation = { group = \"io.github.0xzhangke\", name = \"krouter-annotation\", version.ref = \"krouter\" }\n\nmultiplatformsettings-core = { module = \"com.russhwolf:multiplatform-settings\", version.ref = \"multiplatformsettings\" }\nmultiplatformsettings-coroutines = { module = \"com.russhwolf:multiplatform-settings-coroutines\", version.ref = \"multiplatformsettings\" }\nmultiplatformsettings-datastore = { module = \"com.russhwolf:multiplatform-settings-datastore\", version.ref = \"multiplatformsettings\" }\n\njunit = { group = \"junit\", name = \"junit\", version.ref = \"junit\" }\nandroidx-test-ext-junit = { group = \"androidx.test.ext\", name = \"junit\", version.ref = \"androidx-test-ext-junit\" }\nandroidx-test-espresso-core = { group = \"androidx.test.espresso\", name = \"espresso-core\", version.ref = \"espresso-core\" }\nmaterial = { group = \"com.google.android.material\", name = \"material\", version.ref = \"material\" }\n\nrssparser = { group = \"com.prof18.rssparser\", name = \"rssparser\", version.ref = \"rssparser\" }\n\nktml = { group = \"moe.tlaster\", name = \"ktml\", version.ref = \"ktml\" }\nimageLoader = { group = \"io.github.qdsfdhvh\", name = \"image-loader\", version = \"1.10.0\" }\nokio = { group = \"com.squareup.okio\", name = \"okio\", version = \"3.9.0\" }\nksoup = { module = \"com.fleeksoft.ksoup:ksoup-kotlinx\", version = \"0.2.1\" }\nuri-kmp = { module = \"com.eygraber:uri-kmp\", version = \"0.0.18\" }\nbignum = { module = \"com.ionspin.kotlin:bignum\", version = \"0.3.10\" }\nkermit = { module = \"co.touchlab:kermit\", version = \"2.0.4\" }\nplaceholder-material3 = { module = \"com.eygraber:compose-placeholder-material3\", version = \"1.0.8\" }\nleftright = { module = \"io.github.charlietap:leftright\", version = \"0.2.4\" }\n\ngooglePlayReview = { group = \"com.google.android.play\", name = \"review\", version.ref = \"goolgePlayReview\" }\ngooglePlayReviewKtx = { group = \"com.google.android.play\", name = \"review-ktx\", version.ref = \"goolgePlayReview\" }\n\ncomposeRuntimeTracing = { group = \"androidx.compose.runtime\", name = \"runtime-tracing\", version.ref = \"compose-runtime-tracing\" }\n\nbluesky = { module = \"sh.christian.ozone:bluesky\", version.ref = \"bluesky\" }\n\nkoin-core = { module = \"io.insert-koin:koin-core\", version.ref = \"koin\" }\nkoin-core-coroutines = { module = \"io.insert-koin:koin-core-coroutines\", version.ref = \"koin\" }\nkoin-compose = { module = \"io.insert-koin:koin-compose\", version.ref = \"koin\" }\nkoin-compose-viewmodel = { module = \"io.insert-koin:koin-compose-viewmodel\", version.ref = \"koin\" }\nkoin-compose-nav3 = { module = \"io.insert-koin:koin-compose-navigation3\", version = \"4.2.0-alpha3\" }\nkoin-android = { module = \"io.insert-koin:koin-android\", version.ref = \"koin\" }\n\nhaze = { group = \"dev.chrisbanes.haze\", name = \"haze\", version.ref = \"haze\" }\nhaze-materials = { group = \"dev.chrisbanes.haze\", name = \"haze-materials\", version.ref = \"haze\" }\ncompose-media-player = { group = \"io.github.kdroidfilter\", name = \"composemediaplayer\", version.ref = \"compose-media-player\" }\n\n[plugins]\nksp = { id = \"com.google.devtools.ksp\", version.ref = \"ksp\" }\nandroid-application = { id = \"com.android.application\", version.ref = \"androidGradlePlugin\" }\nandroid-library = { id = \"com.android.library\", version.ref = \"androidGradlePlugin\" }\nkotlin-android = { id = \"org.jetbrains.kotlin.android\", version.ref = \"kotlin\" }\nkotlin-jvm = { id = \"org.jetbrains.kotlin.jvm\", version.ref = \"kotlin\" }\nkotlin-multiplatform = { id = \"org.jetbrains.kotlin.multiplatform\", version.ref = \"kotlin\" }\nkotlinx-serialization = { id = \"org.jetbrains.kotlin.plugin.serialization\", version.ref = \"kotlin\" }\njetbrains-compose = { id = \"org.jetbrains.compose\", version.ref = \"compose-multiplatform\" }\njetbrains-compose-compiler = { id = \"org.jetbrains.kotlin.plugin.compose\", version.ref = \"kotlin\" }\ngoogle-service = { id = \"com.google.gms.google-services\", version.ref = \"google.service.version\" }\nfirebase-crashlytics = { id = \"com.google.firebase.crashlytics\", version = \"3.0.2\" }\nktorfit = { id = \"de.jensklingenberg.ktorfit\", version.ref = \"ktorfit\" }\nroom = { id = \"androidx.room\", version.ref = \"androidx-room\" }\n"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "content": "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-8.13-bin.zip\nnetworkTimeout=10000\nvalidateDistributionUrl=true\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\n"
  },
  {
    "path": "gradle.properties",
    "content": "#Gradle\norg.gradle.jvmargs=-Xmx6g -XX:MaxMetaspaceSize=2g -Dfile.encoding=UTF-8\norg.gradle.parallel=true\norg.gradle.caching=true\n#org.gradle.configureondemand=true\n\n#Android\nandroid.useAndroidX=true\nandroid.nonTransitiveRClass=true\nandroid.enableR8.fullMode=false\n\n# Kotlin\nkotlin.code.style=official\n\n# Ksp\nksp.incremental=true\nksp.incremental.log=true\n\n#systemProp.http.proxyHost=\n#systemProp.http.proxyPort=80\n#systemProp.https.proxyHost=\n#systemProp.https.proxyPort=80"
  },
  {
    "path": "gradlew",
    "content": "#!/bin/sh\n\n#\n# Copyright © 2015-2021 the original authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      https://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n# SPDX-License-Identifier: Apache-2.0\n#\n\n##############################################################################\n#\n#   Gradle start up script for POSIX generated by Gradle.\n#\n#   Important for running:\n#\n#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is\n#       noncompliant, but you have some other compliant shell such as ksh or\n#       bash, then to run this script, type that shell name before the whole\n#       command line, like:\n#\n#           ksh Gradle\n#\n#       Busybox and similar reduced shells will NOT work, because this script\n#       requires all of these POSIX shell features:\n#         * functions;\n#         * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,\n#           «${var#prefix}», «${var%suffix}», and «$( cmd )»;\n#         * compound commands having a testable exit status, especially «case»;\n#         * various built-in commands including «command», «set», and «ulimit».\n#\n#   Important for patching:\n#\n#   (2) This script targets any POSIX shell, so it avoids extensions provided\n#       by Bash, Ksh, etc; in particular arrays are avoided.\n#\n#       The \"traditional\" practice of packing multiple parameters into a\n#       space-separated string is a well documented source of bugs and security\n#       problems, so this is (mostly) avoided, by progressively accumulating\n#       options in \"$@\", and eventually passing that to Java.\n#\n#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,\n#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;\n#       see the in-line comments for details.\n#\n#       There are tweaks for specific operating systems such as AIX, CygWin,\n#       Darwin, MinGW, and NonStop.\n#\n#   (3) This script is generated from the Groovy template\n#       https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt\n#       within the Gradle project.\n#\n#       You can find Gradle at https://github.com/gradle/gradle/.\n#\n##############################################################################\n\n# Attempt to set APP_HOME\n\n# Resolve links: $0 may be a link\napp_path=$0\n\n# Need this for daisy-chained symlinks.\nwhile\n    APP_HOME=${app_path%\"${app_path##*/}\"}  # leaves a trailing /; empty if no leading path\n    [ -h \"$app_path\" ]\ndo\n    ls=$( ls -ld \"$app_path\" )\n    link=${ls#*' -> '}\n    case $link in             #(\n      /*)   app_path=$link ;; #(\n      *)    app_path=$APP_HOME$link ;;\n    esac\ndone\n\n# This is normally unused\n# shellcheck disable=SC2034\nAPP_BASE_NAME=${0##*/}\n# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)\nAPP_HOME=$( cd -P \"${APP_HOME:-./}\" > /dev/null && printf '%s\n' \"$PWD\" ) || exit\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=maximum\n\nwarn () {\n    echo \"$*\"\n} >&2\n\ndie () {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n} >&2\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\nnonstop=false\ncase \"$( uname )\" in                #(\n  CYGWIN* )         cygwin=true  ;; #(\n  Darwin* )         darwin=true  ;; #(\n  MSYS* | MINGW* )  msys=true    ;; #(\n  NONSTOP* )        nonstop=true ;;\nesac\n\nCLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar\n\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n        # IBM's JDK on AIX uses strange locations for the executables\n        JAVACMD=$JAVA_HOME/jre/sh/java\n    else\n        JAVACMD=$JAVA_HOME/bin/java\n    fi\n    if [ ! -x \"$JAVACMD\" ] ; then\n        die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nelse\n    JAVACMD=java\n    if ! command -v java >/dev/null 2>&1\n    then\n        die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nfi\n\n# Increase the maximum file descriptors if we can.\nif ! \"$cygwin\" && ! \"$darwin\" && ! \"$nonstop\" ; then\n    case $MAX_FD in #(\n      max*)\n        # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.\n        # shellcheck disable=SC2039,SC3045\n        MAX_FD=$( ulimit -H -n ) ||\n            warn \"Could not query maximum file descriptor limit\"\n    esac\n    case $MAX_FD in  #(\n      '' | soft) :;; #(\n      *)\n        # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.\n        # shellcheck disable=SC2039,SC3045\n        ulimit -n \"$MAX_FD\" ||\n            warn \"Could not set maximum file descriptor limit to $MAX_FD\"\n    esac\nfi\n\n# Collect all arguments for the java command, stacking in reverse order:\n#   * args from the command line\n#   * the main class name\n#   * -classpath\n#   * -D...appname settings\n#   * --module-path (only if needed)\n#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.\n\n# For Cygwin or MSYS, switch paths to Windows format before running java\nif \"$cygwin\" || \"$msys\" ; then\n    APP_HOME=$( cygpath --path --mixed \"$APP_HOME\" )\n    CLASSPATH=$( cygpath --path --mixed \"$CLASSPATH\" )\n\n    JAVACMD=$( cygpath --unix \"$JAVACMD\" )\n\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    for arg do\n        if\n            case $arg in                                #(\n              -*)   false ;;                            # don't mess with options #(\n              /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath\n                    [ -e \"$t\" ] ;;                      #(\n              *)    false ;;\n            esac\n        then\n            arg=$( cygpath --path --ignore --mixed \"$arg\" )\n        fi\n        # Roll the args list around exactly as many times as the number of\n        # args, so each arg winds up back in the position where it started, but\n        # possibly modified.\n        #\n        # NB: a `for` loop captures its iteration list before it begins, so\n        # changing the positional parameters here affects neither the number of\n        # iterations, nor the values presented in `arg`.\n        shift                   # remove old arg\n        set -- \"$@\" \"$arg\"      # push replacement arg\n    done\nfi\n\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS='\"-Xmx64m\" \"-Xms64m\"'\n\n# Collect all arguments for the java command:\n#   * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,\n#     and any embedded shellness will be escaped.\n#   * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be\n#     treated as '${Hostname}' itself on the command line.\n\nset -- \\\n        \"-Dorg.gradle.appname=$APP_BASE_NAME\" \\\n        -classpath \"$CLASSPATH\" \\\n        org.gradle.wrapper.GradleWrapperMain \\\n        \"$@\"\n\n# Stop when \"xargs\" is not available.\nif ! command -v xargs >/dev/null 2>&1\nthen\n    die \"xargs is not available\"\nfi\n\n# Use \"xargs\" to parse quoted args.\n#\n# With -n1 it outputs one arg per line, with the quotes and backslashes removed.\n#\n# In Bash we could simply go:\n#\n#   readarray ARGS < <( xargs -n1 <<<\"$var\" ) &&\n#   set -- \"${ARGS[@]}\" \"$@\"\n#\n# but POSIX shell has neither arrays nor command substitution, so instead we\n# post-process each arg (as a line of input to sed) to backslash-escape any\n# character that might be a shell metacharacter, then use eval to reverse\n# that process (while maintaining the separation between arguments), and wrap\n# the whole thing up as a single \"set\" statement.\n#\n# This will of course break if any of these variables contains a newline or\n# an unmatched quote.\n#\n\neval \"set -- $(\n        printf '%s\\n' \"$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS\" |\n        xargs -n1 |\n        sed ' s~[^-[:alnum:]+,./:=@_]~\\\\&~g; ' |\n        tr '\\n' ' '\n    )\" '\"$@\"'\n\nexec \"$JAVACMD\" \"$@\"\n"
  },
  {
    "path": "gradlew.bat",
    "content": "@rem\r\n@rem Copyright 2015 the original author or authors.\r\n@rem\r\n@rem Licensed under the Apache License, Version 2.0 (the \"License\");\r\n@rem you may not use this file except in compliance with the License.\r\n@rem You may obtain a copy of the License at\r\n@rem\r\n@rem      https://www.apache.org/licenses/LICENSE-2.0\r\n@rem\r\n@rem Unless required by applicable law or agreed to in writing, software\r\n@rem distributed under the License is distributed on an \"AS IS\" BASIS,\r\n@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n@rem See the License for the specific language governing permissions and\r\n@rem limitations under the License.\r\n@rem\r\n@rem SPDX-License-Identifier: Apache-2.0\r\n@rem\r\n\r\n@if \"%DEBUG%\"==\"\" @echo off\r\n@rem ##########################################################################\r\n@rem\r\n@rem  Gradle startup script for Windows\r\n@rem\r\n@rem ##########################################################################\r\n\r\n@rem Set local scope for the variables with windows NT shell\r\nif \"%OS%\"==\"Windows_NT\" setlocal\r\n\r\nset DIRNAME=%~dp0\r\nif \"%DIRNAME%\"==\"\" set DIRNAME=.\r\n@rem This is normally unused\r\nset APP_BASE_NAME=%~n0\r\nset APP_HOME=%DIRNAME%\r\n\r\n@rem Resolve any \".\" and \"..\" in APP_HOME to make it shorter.\r\nfor %%i in (\"%APP_HOME%\") do set APP_HOME=%%~fi\r\n\r\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\r\nset DEFAULT_JVM_OPTS=\"-Xmx64m\" \"-Xms64m\"\r\n\r\n@rem Find java.exe\r\nif defined JAVA_HOME goto findJavaFromJavaHome\r\n\r\nset JAVA_EXE=java.exe\r\n%JAVA_EXE% -version >NUL 2>&1\r\nif %ERRORLEVEL% equ 0 goto execute\r\n\r\necho. 1>&2\r\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2\r\necho. 1>&2\r\necho Please set the JAVA_HOME variable in your environment to match the 1>&2\r\necho location of your Java installation. 1>&2\r\n\r\ngoto fail\r\n\r\n:findJavaFromJavaHome\r\nset JAVA_HOME=%JAVA_HOME:\"=%\r\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\r\n\r\nif exist \"%JAVA_EXE%\" goto execute\r\n\r\necho. 1>&2\r\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2\r\necho. 1>&2\r\necho Please set the JAVA_HOME variable in your environment to match the 1>&2\r\necho location of your Java installation. 1>&2\r\n\r\ngoto fail\r\n\r\n:execute\r\n@rem Setup the command line\r\n\r\nset CLASSPATH=%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\r\n\r\n\r\n@rem Execute Gradle\r\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -classpath \"%CLASSPATH%\" org.gradle.wrapper.GradleWrapperMain %*\r\n\r\n:end\r\n@rem End local scope for the variables with windows NT shell\r\nif %ERRORLEVEL% equ 0 goto mainEnd\r\n\r\n:fail\r\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\r\nrem the _cmd.exe /c_ return code!\r\nset EXIT_CODE=%ERRORLEVEL%\r\nif %EXIT_CODE% equ 0 set EXIT_CODE=1\r\nif not \"\"==\"%GRADLE_EXIT_CONSOLE%\" exit %EXIT_CODE%\r\nexit /b %EXIT_CODE%\r\n\r\n:mainEnd\r\nif \"%OS%\"==\"Windows_NT\" endlocal\r\n\r\n:omega\r\n"
  },
  {
    "path": "iosApp/.gitignore",
    "content": "# Xcode\n.DS_Store\nbuild/\nDerivedData/\n*.pbxuser\n!default.pbxuser\n*.mode1v3\n!default.mode1v3\n*.mode2v3\n!default.mode2v3\n*.perspectivev3\n!default.perspectivev3\nxcuserdata/\n*.xccheckout\n*.moved-aside\n*.xcuserstate\n*.xcscmblueprint\n\n# Swift Package Manager\n.build/\n.swiftpm/xcode/package.xcworkspace/\n.swiftpm/xcode/package.xcworkspace/xcuserdata\n\n# CocoaPods\nPods/\nPodfile.lock\nPodfile.swp\npodfile.swp\npodfile.lock\n\n# Carthage\nCarthage/Build/\n\n# Xcode Patch\n*.xcodeproj/*\n!*.xcodeproj/project.pbxproj\n!*.xcodeproj/xcshareddata/xcschemes/*.xcscheme\n!*.xcworkspace/contents.xcworkspacedata\n!*.xcworkspace/xcshareddata/xcschemes/*.xcscheme\n\n# SPM\nPackages/\nPackage.pins\nPackage.resolved\n.swiftpm/"
  },
  {
    "path": "iosApp/Configuration/Config.xcconfig",
    "content": "TEAM_ID=\nBUNDLE_ID=com.zhangke.fread.Fread\nAPP_NAME=Fread"
  },
  {
    "path": "iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json",
    "content": "{\n  \"colors\" : [\n    {\n      \"idiom\" : \"universal\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}"
  },
  {
    "path": "iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"filename\" : \"app-icon-1024.png\",\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"size\" : \"1024x1024\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "iosApp/iosApp/Assets.xcassets/Contents.json",
    "content": "{\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}"
  },
  {
    "path": "iosApp/iosApp/ContentView.swift",
    "content": "import UIKit\nimport FreadKit\nimport SwiftUI\n\nstruct ContentView: View {\n\n    private let component: IosActivityComponent\n\n    init(component: IosActivityComponent) {\n        self.component = component\n    }\n\n    var body: some View {\n        ComposeView(component: self.component)\n                .ignoresSafeArea(.keyboard) // Compose has own keyboard handler\n    }\n}\n\nstruct ComposeView: UIViewControllerRepresentable {\n\n    private let component: IosActivityComponent\n\n    init(component: IosActivityComponent) {\n        self.component = component\n    }\n\n    func makeUIViewController(context: Context) -> UIViewController {\n        component.uiViewControllerFactory()\n    }\n\n    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}\n}\n\n\n\n\n\n"
  },
  {
    "path": "iosApp/iosApp/GoogleService-Info.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>API_KEY</key>\n\t<string>AIzaSyABobDMExO3CYk-jVOPlz-dJ7z2GidFVgo</string>\n\t<key>GCM_SENDER_ID</key>\n\t<string>1083712488538</string>\n\t<key>PLIST_VERSION</key>\n\t<string>1</string>\n\t<key>BUNDLE_ID</key>\n\t<string>com.zhangke.fread</string>\n\t<key>PROJECT_ID</key>\n\t<string>fread-515c4</string>\n\t<key>STORAGE_BUCKET</key>\n\t<string>fread-515c4.firebasestorage.app</string>\n\t<key>IS_ADS_ENABLED</key>\n\t<false></false>\n\t<key>IS_ANALYTICS_ENABLED</key>\n\t<false></false>\n\t<key>IS_APPINVITE_ENABLED</key>\n\t<true></true>\n\t<key>IS_GCM_ENABLED</key>\n\t<true></true>\n\t<key>IS_SIGNIN_ENABLED</key>\n\t<true></true>\n\t<key>GOOGLE_APP_ID</key>\n\t<string>1:1083712488538:ios:1e6d310f7f93a2ab9d4463</string>\n</dict>\n</plist>"
  },
  {
    "path": "iosApp/iosApp/Info.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>CFBundleDevelopmentRegion</key>\n\t<string>$(DEVELOPMENT_LANGUAGE)</string>\n\t<key>CFBundleExecutable</key>\n\t<string>$(EXECUTABLE_NAME)</string>\n\t<key>CFBundleIdentifier</key>\n\t<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>\n\t<key>CFBundleInfoDictionaryVersion</key>\n\t<string>6.0</string>\n\t<key>CFBundleName</key>\n\t<string>$(PRODUCT_NAME)</string>\n\t<key>CFBundlePackageType</key>\n\t<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>\n\t<key>CFBundleShortVersionString</key>\n\t<string>1.0</string>\n\t<key>CFBundleVersion</key>\n\t<string>1</string>\n\t<key>LSRequiresIPhoneOS</key>\n\t<true/>\n\t<key>CADisableMinimumFrameDurationOnPhone</key>\n\t<true/>\n\t<key>UIApplicationSceneManifest</key>\n\t<dict>\n\t\t<key>UIApplicationSupportsMultipleScenes</key>\n\t\t<false/>\n\t</dict>\n\t<key>UILaunchScreen</key>\n\t<dict/>\n\t<key>UIRequiredDeviceCapabilities</key>\n\t<array>\n\t\t<string>armv7</string>\n\t</array>\n\t<key>UISupportedInterfaceOrientations</key>\n\t<array>\n\t\t<string>UIInterfaceOrientationPortrait</string>\n\t\t<string>UIInterfaceOrientationLandscapeLeft</string>\n\t\t<string>UIInterfaceOrientationLandscapeRight</string>\n\t</array>\n\t<key>UISupportedInterfaceOrientations~ipad</key>\n\t<array>\n\t\t<string>UIInterfaceOrientationPortrait</string>\n\t\t<string>UIInterfaceOrientationPortraitUpsideDown</string>\n\t\t<string>UIInterfaceOrientationLandscapeLeft</string>\n\t\t<string>UIInterfaceOrientationLandscapeRight</string>\n\t</array>\n</dict>\n</plist>\n"
  },
  {
    "path": "iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json",
    "content": "{\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}"
  },
  {
    "path": "iosApp/iosApp/iOSApp.swift",
    "content": "import FreadKit\nimport SwiftUI\nimport FirebaseCore\n\nclass AppDelegate : UIResponder, UIApplicationDelegate {\n\n    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {\n        // Use Firebase library to configure APIs\n        FirebaseApp.configure()\n\n        applicationComponent.startupManager.initialize()\n\n        return true\n    }\n}\n\n@main\nstruct iOSApp: App {\n\n    @UIApplicationDelegateAdaptor(AppDelegate.self) var delegate\n\n    var body: some Scene {\n        WindowGroup {\n            let activityComponent = createActivityComponent(\n                applicationComponent: delegate.applicationComponent\n            )\n            ContentView(component: activityComponent)\n        }\n    }\n}\n\nprivate func createApplicationComponent(\n    appDelegate: AppDelegate\n) -> IosApplicationComponent {\n    return IosApplicationComponent.companion.create(\n        applicationDelegate: appDelegate\n    )\n}\n\nprivate func createActivityComponent(\n    applicationComponent: IosApplicationComponent\n) -> IosActivityComponent {\n    return IosActivityComponent.companion.create(\n        applicationComponent: applicationComponent\n    )\n}\n"
  },
  {
    "path": "iosApp/iosApp.xcodeproj/project.pbxproj",
    "content": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 56;\n\tobjects = {\n\n/* Begin PBXBuildFile section */\n\t\t058557BB273AAA24004C7B11 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557BA273AAA24004C7B11 /* Assets.xcassets */; };\n\t\t058557D9273AAEEB004C7B11 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */; };\n\t\t0B5CFD0C2D1563D600065731 /* FirebaseAnalytics in Frameworks */ = {isa = PBXBuildFile; productRef = 0B5CFD0B2D1563D600065731 /* FirebaseAnalytics */; };\n\t\t0B5CFD0E2D1563D600065731 /* FirebaseCore in Frameworks */ = {isa = PBXBuildFile; productRef = 0B5CFD0D2D1563D600065731 /* FirebaseCore */; };\n\t\t0B5CFD102D1563D600065731 /* FirebaseCrashlytics in Frameworks */ = {isa = PBXBuildFile; productRef = 0B5CFD0F2D1563D600065731 /* FirebaseCrashlytics */; };\n\t\t0B5CFD122D1563D600065731 /* FirebaseMessaging in Frameworks */ = {isa = PBXBuildFile; productRef = 0B5CFD112D1563D600065731 /* FirebaseMessaging */; };\n\t\t0B5CFD142D1564EA00065731 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 0B5CFD132D1564EA00065731 /* GoogleService-Info.plist */; };\n\t\t2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2152FB032600AC8F00CF470E /* iOSApp.swift */; };\n\t\t7555FF83242A565900829871 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7555FF82242A565900829871 /* ContentView.swift */; };\n/* End PBXBuildFile section */\n\n/* Begin PBXFileReference section */\n\t\t058557BA273AAA24004C7B11 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = \"<group>\"; };\n\t\t058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = \"Preview Assets.xcassets\"; sourceTree = \"<group>\"; };\n\t\t0B5CFD132D1564EA00065731 /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = \"GoogleService-Info.plist\"; sourceTree = \"<group>\"; };\n\t\t2152FB032600AC8F00CF470E /* iOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSApp.swift; sourceTree = \"<group>\"; };\n\t\t7555FF7B242A565900829871 /* Fread.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Fread.app; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t7555FF82242A565900829871 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = \"<group>\"; };\n\t\t7555FF8C242A565B00829871 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = \"<group>\"; };\n\t\tAB3632DC29227652001CCB65 /* Config.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = \"<group>\"; };\n/* End PBXFileReference section */\n\n/* Begin PBXFrameworksBuildPhase section */\n\t\tB92378962B6B1156000C7307 /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t0B5CFD122D1563D600065731 /* FirebaseMessaging in Frameworks */,\n\t\t\t\t0B5CFD102D1563D600065731 /* FirebaseCrashlytics in Frameworks */,\n\t\t\t\t0B5CFD0E2D1563D600065731 /* FirebaseCore in Frameworks */,\n\t\t\t\t0B5CFD0C2D1563D600065731 /* FirebaseAnalytics in Frameworks */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXFrameworksBuildPhase section */\n\n/* Begin PBXGroup section */\n\t\t058557D7273AAEEB004C7B11 /* Preview Content */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */,\n\t\t\t);\n\t\t\tpath = \"Preview Content\";\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t42799AB246E5F90AF97AA0EF /* Frameworks */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t);\n\t\t\tname = Frameworks;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t7555FF72242A565900829871 = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tAB1DB47929225F7C00F7AF9C /* Configuration */,\n\t\t\t\t7555FF7D242A565900829871 /* iosApp */,\n\t\t\t\t7555FF7C242A565900829871 /* Products */,\n\t\t\t\t42799AB246E5F90AF97AA0EF /* Frameworks */,\n\t\t\t);\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t7555FF7C242A565900829871 /* Products */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t7555FF7B242A565900829871 /* Fread.app */,\n\t\t\t);\n\t\t\tname = Products;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t7555FF7D242A565900829871 /* iosApp */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t0B5CFD132D1564EA00065731 /* GoogleService-Info.plist */,\n\t\t\t\t058557BA273AAA24004C7B11 /* Assets.xcassets */,\n\t\t\t\t7555FF82242A565900829871 /* ContentView.swift */,\n\t\t\t\t7555FF8C242A565B00829871 /* Info.plist */,\n\t\t\t\t2152FB032600AC8F00CF470E /* iOSApp.swift */,\n\t\t\t\t058557D7273AAEEB004C7B11 /* Preview Content */,\n\t\t\t);\n\t\t\tpath = iosApp;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tAB1DB47929225F7C00F7AF9C /* Configuration */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tAB3632DC29227652001CCB65 /* Config.xcconfig */,\n\t\t\t);\n\t\t\tpath = Configuration;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXGroup section */\n\n/* Begin PBXNativeTarget section */\n\t\t7555FF7A242A565900829871 /* iosApp */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget \"iosApp\" */;\n\t\t\tbuildPhases = (\n\t\t\t\tF36B1CEB2AD83DDC00CB74D5 /* Compile Kotlin Framework */,\n\t\t\t\t7555FF77242A565900829871 /* Sources */,\n\t\t\t\tB92378962B6B1156000C7307 /* Frameworks */,\n\t\t\t\t7555FF79242A565900829871 /* Resources */,\n\t\t\t\t0B5CFD172D15728E00065731 /* Upload dSYM to Firebase Crashlytics */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t);\n\t\t\tname = iosApp;\n\t\t\tpackageProductDependencies = (\n\t\t\t\t0B5CFD0B2D1563D600065731 /* FirebaseAnalytics */,\n\t\t\t\t0B5CFD0D2D1563D600065731 /* FirebaseCore */,\n\t\t\t\t0B5CFD0F2D1563D600065731 /* FirebaseCrashlytics */,\n\t\t\t\t0B5CFD112D1563D600065731 /* FirebaseMessaging */,\n\t\t\t);\n\t\t\tproductName = iosApp;\n\t\t\tproductReference = 7555FF7B242A565900829871 /* Fread.app */;\n\t\t\tproductType = \"com.apple.product-type.application\";\n\t\t};\n/* End PBXNativeTarget section */\n\n/* Begin PBXProject section */\n\t\t7555FF73242A565900829871 /* Project object */ = {\n\t\t\tisa = PBXProject;\n\t\t\tattributes = {\n\t\t\t\tBuildIndependentTargetsInParallel = YES;\n\t\t\t\tLastSwiftUpdateCheck = 1130;\n\t\t\t\tLastUpgradeCheck = 1540;\n\t\t\t\tORGANIZATIONNAME = orgName;\n\t\t\t\tTargetAttributes = {\n\t\t\t\t\t7555FF7A242A565900829871 = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 11.3.1;\n\t\t\t\t\t};\n\t\t\t\t};\n\t\t\t};\n\t\t\tbuildConfigurationList = 7555FF76242A565900829871 /* Build configuration list for PBXProject \"iosApp\" */;\n\t\t\tcompatibilityVersion = \"Xcode 14.0\";\n\t\t\tdevelopmentRegion = en;\n\t\t\thasScannedForEncodings = 0;\n\t\t\tknownRegions = (\n\t\t\t\ten,\n\t\t\t\tBase,\n\t\t\t);\n\t\t\tmainGroup = 7555FF72242A565900829871;\n\t\t\tpackageReferences = (\n\t\t\t\t0B5CFD0A2D1561E900065731 /* XCRemoteSwiftPackageReference \"firebase-ios-sdk\" */,\n\t\t\t);\n\t\t\tproductRefGroup = 7555FF7C242A565900829871 /* Products */;\n\t\t\tprojectDirPath = \"\";\n\t\t\tprojectRoot = \"\";\n\t\t\ttargets = (\n\t\t\t\t7555FF7A242A565900829871 /* iosApp */,\n\t\t\t);\n\t\t};\n/* End PBXProject section */\n\n/* Begin PBXResourcesBuildPhase section */\n\t\t7555FF79242A565900829871 /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t058557D9273AAEEB004C7B11 /* Preview Assets.xcassets in Resources */,\n\t\t\t\t058557BB273AAA24004C7B11 /* Assets.xcassets in Resources */,\n\t\t\t\t0B5CFD142D1564EA00065731 /* GoogleService-Info.plist in Resources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXResourcesBuildPhase section */\n\n/* Begin PBXShellScriptBuildPhase section */\n\t\t0B5CFD172D15728E00065731 /* Upload dSYM to Firebase Crashlytics */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputFileListPaths = (\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t\t\"${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}\",\n\t\t\t\t\"${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${PRODUCT_NAME}\",\n\t\t\t\t\"$(TARGET_BUILD_DIR)/$(UNLOCALIZED_RESOURCES_FOLDER_PATH)/GoogleService-Info.plist\",\n\t\t\t\t\"$(TARGET_BUILD_DIR)/$(EXECUTABLE_PATH)\",\n\t\t\t);\n\t\t\tname = \"Upload dSYM to Firebase Crashlytics\";\n\t\t\toutputFileListPaths = (\n\t\t\t);\n\t\t\toutputPaths = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"# Type a script or drag a script file from your workspace to insert its path.\\n\\\"${BUILD_DIR%/Build/*}/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run\\\"\\n\";\n\t\t};\n\t\tF36B1CEB2AD83DDC00CB74D5 /* Compile Kotlin Framework */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputFileListPaths = (\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t);\n\t\t\tname = \"Compile Kotlin Framework\";\n\t\t\toutputFileListPaths = (\n\t\t\t);\n\t\t\toutputPaths = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"if [ \\\"YES\\\" = \\\"$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED\\\" ]; then\\n  echo \\\"Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \\\\\\\"YES\\\\\\\"\\\"\\n  exit 0\\nfi\\ncd \\\"$SRCROOT/..\\\"\\n./gradlew :app-hosting:embedAndSignAppleFrameworkForXcode\\n\";\n\t\t};\n/* End PBXShellScriptBuildPhase section */\n\n/* Begin PBXSourcesBuildPhase section */\n\t\t7555FF77242A565900829871 /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */,\n\t\t\t\t7555FF83242A565900829871 /* ContentView.swift in Sources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXSourcesBuildPhase section */\n\n/* Begin XCBuildConfiguration section */\n\t\t7555FFA3242A565B00829871 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = AB3632DC29227652001CCB65 /* Config.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++14\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_WEAK = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_COMMA = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_DOCUMENTATION_COMMENTS = YES;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_STRICT_PROTOTYPES = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;\n\t\t\t\tCLANG_WARN_UNREACHABLE_CODE = YES;\n\t\t\t\tCLANG_WARN__DUPLICATE_METHOD_MATCH = YES;\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = \"dwarf-with-dsym\";\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tENABLE_TESTABILITY = YES;\n\t\t\t\tENABLE_USER_SCRIPT_SANDBOXING = NO;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu11;\n\t\t\t\tGCC_DYNAMIC_NO_PIC = NO;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_OPTIMIZATION_LEVEL = 0;\n\t\t\t\tGCC_PREPROCESSOR_DEFINITIONS = (\n\t\t\t\t\t\"DEBUG=1\",\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t);\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNDECLARED_SELECTOR = YES;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 15.3;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;\n\t\t\t\tMTL_FAST_MATH = YES;\n\t\t\t\tONLY_ACTIVE_ARCH = YES;\n\t\t\t\tSDKROOT = iphoneos;\n\t\t\t\tSWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-Onone\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t7555FFA4242A565B00829871 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = AB3632DC29227652001CCB65 /* Config.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++14\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_WEAK = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_COMMA = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_DOCUMENTATION_COMMENTS = YES;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_STRICT_PROTOTYPES = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;\n\t\t\t\tCLANG_WARN_UNREACHABLE_CODE = YES;\n\t\t\t\tCLANG_WARN__DUPLICATE_METHOD_MATCH = YES;\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = \"dwarf-with-dsym\";\n\t\t\t\tENABLE_NS_ASSERTIONS = NO;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tENABLE_USER_SCRIPT_SANDBOXING = NO;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu11;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNDECLARED_SELECTOR = YES;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 15.3;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = NO;\n\t\t\t\tMTL_FAST_MATH = YES;\n\t\t\t\tSDKROOT = iphoneos;\n\t\t\t\tSWIFT_COMPILATION_MODE = wholemodule;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-O\";\n\t\t\t\tVALIDATE_PRODUCT = YES;\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t7555FFA6242A565B00829871 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCODE_SIGN_IDENTITY = \"Apple Development\";\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tDEVELOPMENT_ASSET_PATHS = \"\\\"iosApp/Preview Content\\\"\";\n\t\t\t\tDEVELOPMENT_TEAM = \"${TEAM_ID}\";\n\t\t\t\tENABLE_PREVIEWS = YES;\n\t\t\t\tFRAMEWORK_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"$(SRCROOT)/../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)\\n$(SRCROOT)/../app-hosting/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)\",\n\t\t\t\t);\n\t\t\t\tINFOPLIST_FILE = iosApp/Info.plist;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 15.3;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = \"${BUNDLE_ID}${TEAM_ID}\";\n\t\t\t\tPRODUCT_NAME = \"${APP_NAME}\";\n\t\t\t\tPROVISIONING_PROFILE_SPECIFIER = \"\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t7555FFA7242A565B00829871 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCODE_SIGN_IDENTITY = \"Apple Development\";\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tDEVELOPMENT_ASSET_PATHS = \"\\\"iosApp/Preview Content\\\"\";\n\t\t\t\tDEVELOPMENT_TEAM = \"${TEAM_ID}\";\n\t\t\t\tENABLE_PREVIEWS = YES;\n\t\t\t\tFRAMEWORK_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"$(SRCROOT)/../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)\\n$(SRCROOT)/../app-hosting/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)\",\n\t\t\t\t);\n\t\t\t\tINFOPLIST_FILE = iosApp/Info.plist;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 15.3;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = \"${BUNDLE_ID}${TEAM_ID}\";\n\t\t\t\tPRODUCT_NAME = \"${APP_NAME}\";\n\t\t\t\tPROVISIONING_PROFILE_SPECIFIER = \"\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n/* End XCBuildConfiguration section */\n\n/* Begin XCConfigurationList section */\n\t\t7555FF76242A565900829871 /* Build configuration list for PBXProject \"iosApp\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t7555FFA3242A565B00829871 /* Debug */,\n\t\t\t\t7555FFA4242A565B00829871 /* Release */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget \"iosApp\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t7555FFA6242A565B00829871 /* Debug */,\n\t\t\t\t7555FFA7242A565B00829871 /* Release */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n/* End XCConfigurationList section */\n\n/* Begin XCRemoteSwiftPackageReference section */\n\t\t0B5CFD0A2D1561E900065731 /* XCRemoteSwiftPackageReference \"firebase-ios-sdk\" */ = {\n\t\t\tisa = XCRemoteSwiftPackageReference;\n\t\t\trepositoryURL = \"https://github.com/firebase/firebase-ios-sdk\";\n\t\t\trequirement = {\n\t\t\t\tkind = upToNextMajorVersion;\n\t\t\t\tminimumVersion = 11.6.0;\n\t\t\t};\n\t\t};\n/* End XCRemoteSwiftPackageReference section */\n\n/* Begin XCSwiftPackageProductDependency section */\n\t\t0B5CFD0B2D1563D600065731 /* FirebaseAnalytics */ = {\n\t\t\tisa = XCSwiftPackageProductDependency;\n\t\t\tpackage = 0B5CFD0A2D1561E900065731 /* XCRemoteSwiftPackageReference \"firebase-ios-sdk\" */;\n\t\t\tproductName = FirebaseAnalytics;\n\t\t};\n\t\t0B5CFD0D2D1563D600065731 /* FirebaseCore */ = {\n\t\t\tisa = XCSwiftPackageProductDependency;\n\t\t\tpackage = 0B5CFD0A2D1561E900065731 /* XCRemoteSwiftPackageReference \"firebase-ios-sdk\" */;\n\t\t\tproductName = FirebaseCore;\n\t\t};\n\t\t0B5CFD0F2D1563D600065731 /* FirebaseCrashlytics */ = {\n\t\t\tisa = XCSwiftPackageProductDependency;\n\t\t\tpackage = 0B5CFD0A2D1561E900065731 /* XCRemoteSwiftPackageReference \"firebase-ios-sdk\" */;\n\t\t\tproductName = FirebaseCrashlytics;\n\t\t};\n\t\t0B5CFD112D1563D600065731 /* FirebaseMessaging */ = {\n\t\t\tisa = XCSwiftPackageProductDependency;\n\t\t\tpackage = 0B5CFD0A2D1561E900065731 /* XCRemoteSwiftPackageReference \"firebase-ios-sdk\" */;\n\t\t\tproductName = FirebaseMessaging;\n\t\t};\n/* End XCSwiftPackageProductDependency section */\n\t};\n\trootObject = 7555FF73242A565900829871 /* Project object */;\n}\n"
  },
  {
    "path": "kotlin-inject-relative.md",
    "content": "# Kotlin Inject 依赖注入关系整理\n\n## 1. 组件层级与入口\n- Android 入口组件：`AndroidApplicationComponent`（`app-hosting/src/androidMain/...`），`@Component` + `@ApplicationScope`，由 `HostingApplication` 中 `AndroidApplicationComponent.create(this)` 创建。\n- iOS 入口组件：`IosApplicationComponent`（`app-hosting/src/iosMain/...`），`@Component` + `@ApplicationScope`，由 `IosApplicationComponent.create(...)` 创建。\n- Activity 级组件：\n  - `AndroidActivityComponent`（`@ActivityScope`），通过 `@Component val applicationComponent: AndroidApplicationComponent` 作为父组件。\n  - `IosActivityComponent`（`@ActivityScope`），通过 `@Component val applicationComponent: IosApplicationComponent` 作为父组件。\n- 额外组件：\n  - `BrowserBridgeDialogActivityComponent`（Android-only，`commonbiz/common`），用于 BrowserBridge Dialog 的 Activity 级依赖。\n\n简图（逻辑关系）：\n\n```\nAndroidApplicationComponent (@ApplicationScope)\n  └─ HostingApplicationComponent\n      ├─ CommonComponent + CommonPlatformComponent\n      ├─ SharedScreenModelModule + SharedScreenPlatformModule\n      ├─ ExploreComponent\n      ├─ FeedsComponent\n      ├─ NotificationsComponent + NotificationsComponentPlatform\n      ├─ ProfileComponent\n      ├─ ActivityPubComponent + ActivityPubPlatformComponent\n      ├─ RssComponent + RssPlatformComponent\n      └─ BlueskyComponent + BlueskyPlatformComponent\n\nAndroidActivityComponent (@ActivityScope)\n  └─ parent: AndroidApplicationComponent\n\nIosActivityComponent (@ActivityScope)\n  └─ parent: IosApplicationComponent\n```\n\n## 2. 作用域\n- `@ApplicationScope`：应用级单例（`commonbiz/common/src/commonMain/.../Scopes.kt`）。\n- `@ActivityScope`：Activity 级对象，通常由 ActivityComponent 承载。\n\n## 3. 模块级 DI 结构\n\n### app-hosting（聚合层）\n- `HostingApplicationComponent`（`app-hosting/src/commonMain/...`）\n  - 聚合所有 Feature/Plugin/Common 模块组件（见上图）。\n  - 将 `Set<IStatusProvider>` 组装为 `StatusProvider`（统一内容源入口）。\n  - 注册 `MainViewModel`、`MainDrawerViewModel` 的 ViewModel Map。\n- `AndroidApplicationComponent` / `IosApplicationComponent`\n  - 平台基础依赖（`ApplicationContext`/`UIApplication`、`NSUserDefaults`）。\n  - 提供 `ImageLoader`。\n  - iOS 额外绑定 `KRouterStartup` 到 `ModuleStartup` 集合。\n- `AndroidActivityComponent` / `IosActivityComponent`\n  - 提供 Activity / UIViewController 与 `FreadApp` 容器。\n\n### commonbiz/common\n- `CommonComponent`（`commonbiz/common/src/commonMain/...`）\n  - 提供 `StartupManager`、`DayNightHelper`、`FreadConfigManager`、`StatusProvider` 等核心单例。\n  - 用 `KotlinInjectViewModelProviderFactory` 组装 ViewModel Map（`ViewModelCreator` + `ViewModelFactory`）。\n  - 绑定 `CommonStartup`、`FreadConfigModuleStartup` 到 `ModuleStartup` 集合。\n  - 注册全局 ViewModel（如 `UrlRedirectViewModel`、`SelectAccountForPublishViewModel`）。\n- `CommonPlatformComponent`（expect/actual）\n  - Android：Room DB、`FlowSettings`、`browserInterceptorSet`、`oauthHandler`，并把 `FeedsRepoModuleStartup`、`LanguageModuleStartup` 加入 `ModuleStartup` 集合。\n  - iOS：Room + BundledSQLiteDriver，`FlowSettings` 使用 `NSUserDefaults`。\n- `CommonActivityComponent` + `CommonActivityPlatformComponent`\n  - 提供 `ActivityDayNightHelper`，并为 `SystemBrowserLauncher` 做平台绑定。\n\n### commonbiz/sharedscreen\n- `SharedScreenModelModule`\n  - 注册共享页 ViewModel（`StatusContextViewModel`、`RssBlogDetailViewModel`、`MultiAccountPublishingViewModel`、`SelectAccountOpenStatusViewModel`）。\n  - 提供 `ModuleScreenVisitor`，依赖 `IFeedsScreenVisitor` + `IProfileScreenVisitor`（来自 Feeds/Profile 模块）。\n- `SharedScreenPlatformModule`\n  - 提供 `SelectedAccountPublishingDatabase`（平台路径差异）。\n\n### feature/feeds\n- `FeedsComponent`\n  - 注册 Feeds 管理相关 ViewModel（`ContentHome`、`MixedContent`、`Add/Edit/Import` 等）。\n  - 提供 `IFeedsScreenVisitor`（被 SharedScreenModelModule 使用）。\n\n### feature/profile\n- `ProfileComponent`\n  - 注册 `ProfileHomeViewModel`、`SettingScreenModel`、`AboutViewModel`。\n  - 提供 `IProfileScreenVisitor`（被 SharedScreenModelModule 使用）。\n\n### feature/explore\n- `ExploreComponent`\n  - 注册 Explore/搜索相关 ViewModel（Home、Search、SearchAuthor/Status/Hashtag/Platform 等）。\n\n### feature/notifications\n- `NotificationsComponent`\n  - 注册通知页 ViewModel（Home/Container）。\n- `NotificationsComponentPlatform`\n  - 提供 `NotificationsDatabase`（平台 Room/SQLite 实现差异）。\n\n### plugins/activitypub-app\n- `ActivityPubComponent`\n  - 提供 `ActivityPubLoggedAccountRepo`。\n  - 将 `ActivityPubProvider` 注入 `Set<IStatusProvider>`。\n  - 将 `ActivityPubUrlInterceptor` 注入 `Set<BrowserInterceptor>`。\n  - 绑定 `ActivityPubStartup` 到 `ModuleStartup` 集合。\n  - 注册 ActivityPub 相关 ViewModel（账号、内容流、列表、过滤器、用户、搜索、趋势等）。\n- `ActivityPubPlatformComponent`\n  - 提供 ActivityPub 相关数据库。\n  - Android：额外提供 Push 数据库、`PushInfoRepo`、`PushNotificationManager`。\n  - iOS：Room + BundledSQLiteDriver。\n\n### plugins/rss\n- `RssComponent`\n  - 将 `RssStatusProvider` 注入 `Set<IStatusProvider>`。\n  - 注册 `RssSourceViewModel`。\n- `RssPlatformComponent`\n  - 提供 `RssDatabases` 与 `RssParser`（Android 使用 OkHttp，iOS 使用 NSURLSession）。\n\n### plugins/bluesky\n- `BlueskyComponent`\n  - 将 `BlueskyProvider` 注入 `Set<IStatusProvider>`。\n  - 将 `BskyUrlInterceptor` 注入 `Set<BrowserInterceptor>`。\n  - 绑定 `BskyStartup` 到 `ModuleStartup` 集合。\n  - 注册 Bluesky 相关 ViewModel（home、feeds、user、publish、search 等）。\n- `BlueskyPlatformComponent`\n  - 提供 `BlueskyLoggedAccountDatabase`（平台 Room/SQLite 实现差异）。\n\n## 4. 跨模块依赖关系汇总\n- `HostingApplicationComponent` 统一聚合各模块的 `@Provides`、`@IntoSet`、`@IntoMap` 产物。\n- `StatusProvider` 依赖 `Set<IStatusProvider>`：由 ActivityPub / Rss / Bluesky 三个插件模块贡献。\n- `BrowserInterceptor` 集合在 Android 平台通过 ActivityPub/Bluesky 注入，用于统一 URL 拦截与跳转。\n- `ModuleStartup` 集合由 Common + 平台模块 + ActivityPub + Bluesky + iOS 的 `KRouterStartup` 共同贡献，`StartupManager.initialize()` 统一触发。\n- `SharedScreenModelModule` 依赖 Feeds/Profile 模块的 `IFeedsScreenVisitor`、`IProfileScreenVisitor` 来构建 `ModuleScreenVisitor`。\n- ViewModel Map 汇总在 `CommonComponent`，由各模块以 `@IntoMap` 方式注册，最终由 `ViewModelProvider.Factory` 统一提供。\n\n## 5. 组件访问方式（Android）\n- `HostingApplication` 实现 `ApplicationComponentProvider`，并设置 `commonComponentProvider` 全局入口。\n- `Context.component` -> `AndroidApplicationComponent`\n- `Context.commonComponent` -> `CommonComponent`\n- `Context.activityPubComponent` -> `ActivityPubComponent`\n"
  },
  {
    "path": "localization/.gitignore",
    "content": "/build"
  },
  {
    "path": "localization/build.gradle.kts",
    "content": "plugins {\n    id(\"fread.project.framework.kmp\")\n    id(\"kotlin-parcelize\")\n}\n\nandroid {\n    namespace = \"com.zhangke.fread.localization\"\n    sourceSets {\n        getByName(\"main\") {\n            res.srcDirs(\"src/commonMain/res\")\n            resources.srcDirs(\"src/commonMain/resources\")\n        }\n    }\n}\n\nkotlin {\n    sourceSets {\n        commonMain {\n            dependencies {\n\n                implementation(compose.components.resources)\n\n                implementation(libs.compose.jb.backhandler)\n\n                implementation(libs.androidx.annotation)\n\n                implementation(libs.arrow.core)\n\n                implementation(libs.jetbrains.lifecycle.runtime)\n                implementation(libs.jetbrains.lifecycle.viewmodel)\n\n                implementation(libs.kotlinx.serialization.core)\n                implementation(libs.kotlinx.serialization.json)\n\n                implementation(libs.ktor.client.core)\n                implementation(libs.ktor.client.serialization.kotlinx.json)\n                implementation(libs.ktor.client.content.negotiation)\n\n                implementation(libs.imageLoader)\n                implementation(libs.okio)\n                implementation(libs.ksoup)\n                implementation(libs.uri.kmp)\n                implementation(libs.bignum)\n                implementation(libs.kermit)\n                implementation(libs.placeholder.material3)\n\n                implementation(libs.krouter.runtime)\n            }\n        }\n        commonTest {\n            dependencies {\n                implementation(kotlin(\"test\"))\n            }\n        }\n        androidMain {\n            dependencies {\n                implementation(compose.uiTooling)\n                implementation(compose.preview)\n            }\n        }\n        androidUnitTest {\n            dependencies {\n            }\n        }\n        androidInstrumentedTest {\n            dependencies {\n                implementation(libs.junit)\n                implementation(libs.androidx.test.ext.junit)\n                implementation(libs.androidx.test.espresso.core)\n            }\n        }\n        iosMain {\n            dependencies {\n                implementation(libs.ktor.client.darwin)\n            }\n        }\n    }\n}\n\ncompose {\n    resources {\n        publicResClass = true\n        packageOfResClass = \"com.zhangke.fread.localization\"\n        generateResClass = always\n    }\n}\n"
  },
  {
    "path": "localization/consumer-rules.pro",
    "content": ""
  },
  {
    "path": "localization/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguardFiles setting in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n\n# Uncomment this to preserve the line number information for\n# debugging stack traces.\n#-keepattributes SourceFile,LineNumberTable\n\n# If you keep the line number information, uncomment this to\n# hide the original source file name.\n#-renamesourcefileattribute SourceFile"
  },
  {
    "path": "localization/src/androidMain/kotlin/com/zhangke/fread/localization/LanguageCodeUtils.kt",
    "content": "package com.zhangke.fread.localization\n\nimport java.util.Locale\n\nval LanguageCode.locale: Locale\n    get() =\n        when (this) {\n            LanguageCode.EN_US -> Locale.ENGLISH\n            LanguageCode.DE_DE -> Locale(\"de\", \"DE\")\n            LanguageCode.ES_ES -> Locale(\"es\", \"ES\")\n            LanguageCode.FR_FR -> Locale(\"fr\", \"FR\")\n            LanguageCode.JA_JP -> Locale(\"ja\", \"JP\")\n            LanguageCode.PT_PT -> Locale(\"pt\", \"PT\")\n            LanguageCode.RU_RU -> Locale(\"ru\", \"RU\")\n            LanguageCode.ZH_CN -> Locale.SIMPLIFIED_CHINESE\n            LanguageCode.ZH_HK -> Locale(\"zh\", \"HK\")\n            LanguageCode.ZH_TW -> Locale(\"zh\", \"TW\")\n        }\n"
  },
  {
    "path": "localization/src/commonMain/composeResources/values/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"skip\">Skip</string>\n    <string name=\"alert\">Alert</string>\n    <string name=\"duration_minute\">minute</string>\n    <string name=\"duration_hour\">hour</string>\n    <string name=\"duration_day\">day</string>\n    <string name=\"duration_week\">week</string>\n    <string name=\"cancel\">Cancel</string>\n    <string name=\"ok\">Yes</string>\n    <string name=\"empty\">This space is empty～</string>\n    <string name=\"duration_selector_title\">Set duration</string>\n    <string name=\"retry\">Try Again</string>\n    <string name=\"load_more_error\">Oops! Couldn\\'t load.</string>\n    <string name=\"unknown_error\">Unknown Error</string>\n    <string name=\"network_error\">Network Error</string>\n    <string name=\"resource_not_found\">Resource Not Found</string>\n    <string name=\"mixed_content_subtitle_1\">Mixed Content ·</string>\n    <string name=\"mixed_content_subtitle_2\">Sources</string>\n    <string name=\"date_time_ago\">&#160;ago</string>\n    <string name=\"date_time_ago_prefix\"></string>\n    <string name=\"date_time_ago_suffix\">&#160;ago</string>\n    <string name=\"date_time_day\">day</string>\n    <string name=\"date_time_hour\">hour</string>\n    <string name=\"date_time_minute\">min</string>\n    <string name=\"date_time_second\">sec</string>\n    <string name=\"image_saving\">Saving…</string>\n    <string name=\"image_save_success\">Save successful</string>\n    <string name=\"image_save_failed\">Save failed</string>\n    <string name=\"permission_write_external_permission_denied\">Storage permission is denied, please open it in the settings.</string>\n    <string name=\"feeds_load_previous_page_label\">Loading Previous Page Content</string>\n    <string name=\"feeds_load_previous_page_failed_label\">Load Failed, Click to Retry</string>\n    <string name=\"image\">Image</string>\n    <string name=\"video\">Video</string>\n    <string name=\"login\">Login</string>\n    <string name=\"add\">Add</string>\n    <string name=\"search\">Search</string>\n    <string name=\"auth_success\">Login successful</string>\n    <string name=\"auth_failed\">Login failed</string>\n    <string name=\"select\">Select</string>\n    <string name=\"logout\">Logout</string>\n    <string name=\"settings\">Settings</string>\n    <string name=\"donate\">Donate</string>\n    <string name=\"feeds\">Feeds</string>\n    <string name=\"save\">Save</string>\n    <string name=\"done\">All done!</string>\n    <string name=\"bluesky_name\">Bluesky</string>\n    <string name=\"bluesky_description\">Bluesky is an open network. With one account, you can access both an easy-to-use social network and a shared identity across the entire social internet.</string>\n    <string name=\"mastodon_name\">Mastodon</string>\n    <string name=\"mastodon_description\">Mastodon is a decentralized, not-for-sale social media where users control their timeline, with each server being an independent entity.</string>\n    <string name=\"mixed_content_name\">Mixed Content</string>\n    <string name=\"mixed_content_description\">Mixed Feed brings all your content together. Add sources from Mastodon, Bluesky, RSS and more - Fread combines them into one chronological timeline.</string>\n    <string name=\"status_provider_type_activity_pub\">Mastodon</string>\n    <string name=\"add_content_title\">Add Content</string>\n    <string name=\"add_content_success_with_account\">Added successfully. Current account:</string>\n    <string name=\"add_content_success_with_login_reminder\">Added successfully. Log in now?</string>\n    <string name=\"content_exist_tips\">Oops! This content is already here.</string>\n    <string name=\"content_add_success\">All set! Your content is good to go.</string>\n    <string name=\"add_feeds_page_empty_name_exist\">This username is already taken.</string>\n    <string name=\"edit_profile_name_empty\">Please enter your username</string>\n    <string name=\"edit_profile_label_name\">Name</string>\n    <string name=\"edit_profile_label_note\">Bio</string>\n    <string name=\"edit_profile_input_name_hint\">Please enter your username</string>\n    <string name=\"edit_profile_input_note_hint\">Please enter bio</string>\n    <string name=\"edit_profile_edit_confirm_message\">Are you sure you want to exit editing?</string>\n    <string name=\"add_content_success_snackbar\">Add Success</string>\n    <string name=\"login_dialog_input_hint\">Please input the server to log in to</string>\n    <string name=\"login_dialog_title\">Please select the server to log in to</string>\n    <string name=\"login_dialog_input_tip\">Input Server Host</string>\n    <string name=\"login_dialog_input_title\">Please Input Server Host</string>\n    <string name=\"login_dialog_input_label\">Server Host</string>\n    <string name=\"login_dialog_target_title\">Please log in</string>\n    <string name=\"profile_description\">Fread supports adding multiple accounts. The added accounts can be used to post, like, forward, comment, etc. You can also view the list created by the added accounts.</string>\n    <string name=\"list_content_empty_placeholder\">No content available</string>\n    <string name=\"post_status_scope_public\">Public</string>\n    <string name=\"post_status_scope_unlisted\">Public, but opted-out of discovery feature</string>\n    <string name=\"post_status_scope_follower_only\">Followers only</string>\n    <string name=\"post_status_scope_mentioned_only\">Only people mentioned</string>\n    <string name=\"post_status_content_warning\">Content Warning</string>\n    <string name=\"post_status_success\">Sent successfully</string>\n    <string name=\"post_status_failed\">Post failed: %1$s</string>\n    <string name=\"post_status_exit_dialog_content\">Unsaved changes will be lost. Are you sure you want to exit?</string>\n    <string name=\"post_status_content_is_empty\">Please enter content</string>\n    <string name=\"post_status_part_failed\">Some accounts couldn't be published. Successfully posted accounts have been removed. Please try again with the remaining ones：%1$s</string>\n    <string name=\"select_account_open_status_title\">Select an account</string>\n    <string name=\"select_account_open_status_empty\">No other accounts</string>\n    <string name=\"select_account_open_status_search_in\">Search in %1$s</string>\n    <string name=\"select_account_open_status_search_failed\">Search Failed</string>\n    <string name=\"explorer_search_no_results\">No results found</string>\n    <string name=\"explorer_search_bar_hint\">Search</string>\n    <string name=\"explorer_search_bar_hint_specialize_platform\">Search in %1$s</string>\n    <string name=\"explorer_search_tab_title_author\">Accounts</string>\n    <string name=\"explorer_search_tab_title_status\">Status</string>\n    <string name=\"explorer_search_tab_title_hashtag\">Hashtags</string>\n    <string name=\"explorer_search_tab_title_server\">Server</string>\n    <string name=\"explorer_tab_title\">Explorer</string>\n    <string name=\"explorer_tab_status_title\">Posts</string>\n    <string name=\"explorer_tab_users_title\">Users</string>\n    <string name=\"explorer_tab_hashtag_title\">Hashtags</string>\n    <string name=\"add_provider_page_title\">Add Server</string>\n    <string name=\"add_provider_page_confirm_button\">Confirm Add</string>\n    <string name=\"search_page_title\">Search</string>\n    <string name=\"search_result_not_found\">No search results found</string>\n    <string name=\"feeds_import_page_title\">Import</string>\n    <string name=\"feeds_import_page_hint\">Please select the OPML file to import</string>\n    <string name=\"feeds_import_button\">Import</string>\n    <string name=\"add_feeds_page_title\">Add Content</string>\n    <string name=\"add_feeds_page_feeds_name_label\">Content Name</string>\n    <string name=\"add_feeds_page_feeds_name_hint\">Please enter the content name</string>\n    <string name=\"add_feeds_page_feeds_empty\">Please add a subscription source</string>\n    <string name=\"pre_add_feeds_no_result\">No results found</string>\n    <string name=\"pre_add_feeds_hint\">Instance name, URL, User homepage URL, RSS</string>\n    <string name=\"pre_add_feeds_input_label_1\">Fread supports adding [[Mastodon]]/[[Bluesky]] as well as [[RSS]] feeds, and even [[mixed content]].</string>\n    <string name=\"pre_add_feeds_input_label_2\">Please enter the Mastodon or Bluesky [[NickName]],[[WebFinger\\Handle]] user [[homepage URL]], or the [[RSS]] feed URL here.</string>\n    <string name=\"empty_content_hint_title\">Please add some content first</string>\n    <string name=\"empty_content_hint_desc\">You'll need to add some subscription sources first. Fread supports a variety of content types and content from different platforms, enabling you to build your own information feed.</string>\n    <string name=\"feeds_add_content\">Add Content</string>\n    <string name=\"select_feeds_type_screen_title\">Select Content Type</string>\n    <string name=\"feeds_pre_add_login_dialog_content\">Content Successfully Added. Would You Like to Log In Now?</string>\n    <string name=\"add_feeds_page_empty_name_tips\">Please enter a name</string>\n    <string name=\"add_feeds_page_empty_source_tips\">Please add a subscription source</string>\n    <string name=\"add_feeds_choose_auth_dialog_title\">Please select login server</string>\n    <string name=\"search_feeds_title\">Search</string>\n    <string name=\"search_feeds_title_hint\">Mastodon or Bluesky NickName,WebFinger,URL,RSS</string>\n    <string name=\"feeds_select_account_for_post_status\">Please select an account</string>\n    <string name=\"feeds_mixed_config_edit_new_name_dialog_title\">Please input name</string>\n    <string name=\"feeds_mixed_config_edit_new_name_dialog_label\">Name</string>\n    <string name=\"feeds_mixed_config_edit_delete_content_dialog_message\">Are you sure you want to delete this feeds?</string>\n    <string name=\"feeds_mixed_config_not_found\">Abnormal status, configuration not found.</string>\n    <string name=\"feeds_import_back_dialog_message\">Are you sure you want to exit?</string>\n    <string name=\"feeds_delete_confirm_content\">Are you sure you want to delete?</string>\n    <string name=\"feeds_select_type_screen_title\">Pick a format</string>\n    <string name=\"feeds_select_type_screen_content\">What would you like to add?</string>\n    <string name=\"notification_tab_title\">Notifications</string>\n    <string name=\"notifications_account_empty_tip\">No logged-in account currently</string>\n    <string name=\"notifications_tab_all\">All</string>\n    <string name=\"notifications_tab_mention\">Mentions</string>\n    <string name=\"profile_page_title\">Profile</string>\n    <string name=\"profile_setting_about_title\">About</string>\n    <string name=\"profile_setting_donate_desc\">Buy me a coffee to support this project.</string>\n    <string name=\"profile_setting_inline_video_auto_play\">Inline Video Autoplay</string>\n    <string name=\"profile_setting_inline_video_auto_play_subtitle\">If enabled, videos in the Feeds will autoplay.</string>\n    <string name=\"profile_setting_always_show_sensitive_content\">Sensitive content is displayed by default</string>\n    <string name=\"profile_setting_always_show_sensitive_content_subtitle\">If enabled, sensitive content will be displayed by default (Mastodon only).</string>\n    <string name=\"profile_setting_dark_mode_title\">Dark Mode</string>\n    <string name=\"profile_setting_dark_mode_dark\">Dark Mode</string>\n    <string name=\"profile_setting_dark_mode_light\">Light Mode</string>\n    <string name=\"profile_setting_dark_mode_follow_system\">Follow System</string>\n    <string name=\"profile_setting_open_source_title\">Open Source</string>\n    <string name=\"profile_setting_open_source_desc\">Open Source Licenses of Dependencies Used in This Project</string>\n    <string name=\"profile_setting_open_source_feedback\">Give us feedback</string>\n    <string name=\"profile_setting_open_source_feedback_desc\">Join the Telegram group or through other methods</string>\n    <string name=\"profile_setting_open_source_feedback_telegram\">oin the Telegram group</string>\n    <string name=\"profile_setting_open_source_feedback_github\">Create GitHub issue</string>\n    <string name=\"profile_setting_open_source_feedback_email\">Send email</string>\n    <string name=\"profile_setting_language_title\">Display Language</string>\n    <string name=\"profile_setting_language_zh\">简体中文</string>\n    <string name=\"profile_setting_language_en\">English</string>\n    <string name=\"profile_setting_language_system\">Follow System</string>\n    <string name=\"profile_setting_ratting\">Rating us</string>\n    <string name=\"profile_setting_ratting_desc\">If you like it, please give us a good rating.</string>\n    <string name=\"profile_setting_font_size\">Content Font Size</string>\n    <string name=\"profile_setting_font_size_desc\">Adjust the content's font and icon size</string>\n    <string name=\"profile_setting_font_size_small\">Small</string>\n    <string name=\"profile_setting_font_size_medium\">Medium</string>\n    <string name=\"profile_setting_font_size_large\">Large</string>\n    <string name=\"profile_setting_immersive_nav_bar\">Immersive Navigation Bar</string>\n    <string name=\"profile_setting_immersive_nav_bar_desc\">The bottom navigation bar hides automatically when scrolling on the homepage.</string>\n    <string name=\"profile_setting_timeline_position\">Default timeline position</string>\n    <string name=\"profile_setting_timeline_position_last_read\">Where I left off</string>\n    <string name=\"profile_setting_timeline_position_newest\">Latest posts</string>\n    <string name=\"profile_setting_theme_title\">Theme color</string>\n    <string name=\"profile_setting_theme_default\">Default</string>\n    <string name=\"profile_setting_theme_system\">Follow system</string>\n    <string name=\"profile_about_version\">Version:&#160;</string>\n    <string name=\"profile_about_website\">Website:&#160;</string>\n    <string name=\"profile_about_developer\">Created by&#160;</string>\n    <string name=\"profile_about_contract_us\">Contact me at:&#160;</string>\n    <string name=\"profile_about_telegram\">Telegram Group:&#160;</string>\n    <string name=\"profile_about_privacy_policy\">Privacy Policy:&#160;</string>\n    <string name=\"profile_setting_check_for_update\">Check for update</string>\n    <string name=\"profile_setting_already_latest_version\">Your app is already up to date</string>\n    <string name=\"profile_setting_have_new_version\">New version available, click to update.</string>\n    <string name=\"profile_donate_page_title\">Select how you'd like to donate</string>\n    <string name=\"profile_account_not_login\">Session expired</string>\n    <string name=\"rss_source_detail_screen_title\">Source Attribution</string>\n    <string name=\"rss_source_detail_screen_custom_title\">Custom Title</string>\n    <string name=\"rss_source_detail_screen_url\">Subscription Link</string>\n    <string name=\"rss_source_detail_screen_home_url\">Home Page</string>\n    <string name=\"rss_source_detail_screen_add_date\">Add Date</string>\n    <string name=\"rss_source_detail_screen_last_update_date\">Last Updated</string>\n    <string name=\"bluesky_protocol_name\">Bluesky</string>\n    <string name=\"bsky_add_content_title\">Sign in</string>\n    <string name=\"bsky_add_content_hosting_provider\">Hosting Provider</string>\n    <string name=\"bsky_add_content_user_name\">Username or email address</string>\n    <string name=\"bsky_add_content_password\">Password</string>\n    <string name=\"bsky_add_content_factor_token\">Confirmation code</string>\n    <string name=\"bsky_edit_content_title\">Edit Content</string>\n    <string name=\"bsky_feeds_explorer_more\">Dive into more feeds</string>\n    <string name=\"bsky_feeds_explorer_creator_label\">By %1$s</string>\n    <string name=\"bsky_feeds_explorer_liked_by\">Liked by %1$s users</string>\n    <string name=\"bsky_feeds_detail_creator_prefix\">By</string>\n    <string name=\"bsky_feeds_item_subtitle\">By %1$s · Liked by %2$s users</string>\n    <string name=\"bsky_feeds_following_name\">Following</string>\n    <string name=\"bsky_feeds_user_posts\">Posts</string>\n    <string name=\"bsky_feeds_user_replies\">Replies</string>\n    <string name=\"bsky_feeds_user_medias\">Medias</string>\n    <string name=\"bsky_feeds_user_likes\">Likes</string>\n    <string name=\"bsky_user_detail_action_blocked_list\">Blocked Users</string>\n    <string name=\"bsky_user_detail_action_muted_list\">Muted Users</string>\n    <string name=\"bsky_user_detail_action_mute_user\">Mute %1$s</string>\n    <string name=\"bsky_user_detail_action_unmute_user\">Unmute</string>\n    <string name=\"bsky_user_detail_action_block_user\">Block %1$s</string>\n    <string name=\"bsky_user_detail_action_unblock_user\">Unblock</string>\n    <string name=\"bsky_user_detail_action_block_user_dialog_message\">Do you want to block this user?</string>\n    <string name=\"bsky_user_detail_action_mute_user_dialog_message\">Do you want to mute this user?</string>\n    <string name=\"bsky_edit_profile_title\">Edit Profile</string>\n    <string name=\"input_media_desc_page_title\">Image description text</string>\n    <string name=\"input_media_desc_input_hint\">Please input image description text</string>\n    <string name=\"post_status_page_title\">New Blog</string>\n    <string name=\"post_screen_input_hint\">Please input your blog</string>\n    <string name=\"post_screen_media_descriptor_placeholder\">Description text</string>\n    <string name=\"post_status_poll_item_hint\">Option %1$s</string>\n    <string name=\"post_status_poll_duration\">Voting duration</string>\n    <string name=\"post_status_poll_function_title\">Function</string>\n    <string name=\"post_status_poll_single\">Single</string>\n    <string name=\"post_status_poll_multiple\">Multiple</string>\n    <string name=\"post_status_poll_style_select_dialog_title\">Please select style</string>\n    <string name=\"post_status_poll_is_empty\">Please enter the content for the vote</string>\n    <string name=\"post_status_media_is_not_upload\">Media not uploaded yet</string>\n    <string name=\"authentication_page_normal_title\">This function requires login to use</string>\n    <string name=\"authentication_page_failed_title\">Login status is invalid, please login again.</string>\n    <string name=\"main_drawer_title\">Content List</string>\n    <string name=\"main_drawer_mixed_item_subtitle_1\">Mixed Content ·</string>\n    <string name=\"main_drawer_mixed_item_subtitle_2\">Sources</string>\n    <string name=\"main_drawer_settings\">Settings</string>\n    <string name=\"main_drawer_donate\">Donate</string>\n    <string name=\"main_request_notification_dialog_title\">Notification</string>\n    <string name=\"main_request_notification_dialog_reject_button\">Reject</string>\n    <string name=\"main_request_notification_dialog_allow_button\">Allow</string>\n    <string name=\"main_request_notification_dialog_content\">To receive interaction messages, please allow notification permissions.</string>\n    <string name=\"setting_item_amoled_mode\">AMOLED Mode</string>\n    <string name=\"setting_item_amoled_mode_description\">When enabled, the background will switch to pure black in dark mode to reduce power consumption and improve contrast in dark environments.</string>\n    <string name=\"setting_group_appearance\">Appearance</string>\n    <string name=\"setting_group_appearance_subtitle\">Theme, display and Home style settings</string>\n    <string name=\"setting_group_behavior\">Behavior</string>\n    <string name=\"setting_group_behavior_subtitle\">Content display and interaction behavior settings</string>\n    <string name=\"setting_item_home_tab_next_title\">Show top switch-content button on Home</string>\n    <string name=\"setting_item_home_tab_next_subtitle\">Show the button in the top-right of Home to switch to the next content</string>\n    <string name=\"setting_item_home_tab_refresh_title\">Show top refresh button on Home</string>\n    <string name=\"setting_item_home_tab_refresh_subtitle\">Show the button in the top-right of Home to refresh content</string>\n    <string name=\"setting_item_open_url_by_system_title\">Open links with system browser</string>\n    <string name=\"setting_item_open_url_by_system_subtitle\">When enabled, Fread will open links with the system browser.</string>\n    <string name=\"setting_item_solid_bar_background_title\">Solid bar background</string>\n    <string name=\"setting_item_solid_bar_background_subtitle\">Make the top and bottom bars solid instead of blurred.</string>\n</resources>\n"
  },
  {
    "path": "localization/src/commonMain/composeResources/values/strings_mastodon.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"activity_pub_user_detail_join_date\">Joined on</string>\n    <string name=\"activity_pub_user_detail_tab_about_joined\">Joined on %1$s</string>\n    <string name=\"activity_pub_user_detail_menu_block\">Block %1$s</string>\n    <string name=\"activity_pub_user_detail_menu_block_domain\">Block %1$s</string>\n    <string name=\"activity_pub_user_detail_menu_unblock_domain\">Unblock %1$s</string>\n    <string name=\"activity_pub_user_detail_menu_edit_private_note\">Edit Note</string>\n    <string name=\"activity_pub_user_detail_menu_edit_private_note_dialog_hint\">Enter Private Note</string>\n    <string name=\"activity_pub_user_detail_dialog_content_block\">Do you want to block this user?</string>\n    <string name=\"activity_pub_user_detail_dialog_content_block_domain\">Do you want to block this domain?</string>\n    <string name=\"activity_pub_user_menu_blocked_user_list\">Blocked Users</string>\n    <string name=\"activity_pub_user_menu_muted_user_list\">Muted Users</string>\n    <string name=\"activity_pub_user_detail_menu_mute_user\">Mute %1$s</string>\n    <string name=\"activity_pub_user_detail_menu_unmute_user\">Unmute %1$s</string>\n    <string name=\"activity_pub_mute_user_bottom_sheet_title\">Mute User?</string>\n    <string name=\"activity_pub_mute_user_bottom_sheet_role1\">They won\\'t know they\\'ve been muted.</string>\n    <string name=\"activity_pub_mute_user_bottom_sheet_role2\">They can still see your posts, but you won\\'t see theirs.</string>\n    <string name=\"activity_pub_mute_user_bottom_sheet_role3\">You won\\'t see posts that mention them.</string>\n    <string name=\"activity_pub_mute_user_bottom_sheet_role4\">They can mention and follow you, but you won\\'t see them.</string>\n    <string name=\"activity_pub_mute_user_bottom_sheet_btn_mute\">Mute</string>\n    <string name=\"add_instance_screen_title\">Add Instance</string>\n    <string name=\"add_instance_input_service_hint\">Please enter server address</string>\n    <string name=\"add_instance_input_service_error\">Please enter the correct server address</string>\n    <string name=\"activity_pub_content_tab_home\">Home</string>\n    <string name=\"activity_pub_content_tab_local_timeline\">Local</string>\n    <string name=\"activity_pub_content_tab_public_timeline\">Public</string>\n    <string name=\"activity_pub_content_tab_trending\">Trending</string>\n    <string name=\"activity_pub_hashtag_timeline_description\">%1$s posts · %2$s participants · %3$s post today</string>\n    <string name=\"activity_pub_hashtag_unfollow_dialog_message\">Are you sure you want to unfollow this hashtag?</string>\n    <string name=\"activity_pub_edit_account_info_label_about\">About</string>\n    <string name=\"activity_pub_edit_content_screen_config_not_found\">Config Not Found</string>\n    <string name=\"activity_pub_user_list_empty\">No users for now</string>\n    <string name=\"activity_pub_muted_user_list_empty\">No muted users</string>\n    <string name=\"activity_pub_muted_user_list_unmute\">Unmute</string>\n    <string name=\"activity_pub_blocked_user_list_empty\">No blocked users</string>\n    <string name=\"activity_pub_blocked_user_list_unblock\">Unblock</string>\n    <string name=\"activity_pub_bookmarks_list_title\">Bookmarks</string>\n    <string name=\"activity_pub_favourites_list_title\">Favourites</string>\n    <string name=\"activity_pub_followed_tags_screen_title\">Followed Hashtags</string>\n    <string name=\"activity_pub_filters_list_page_title\">Filters</string>\n    <string name=\"activity_pub_filters_active\">Active</string>\n    <string name=\"activity_pub_filters_expired\">Expired</string>\n    <string name=\"activity_pub_filter_edit_title\">Edit Filter</string>\n    <string name=\"activity_pub_filter_edit_input_title_label\">Title</string>\n    <string name=\"activity_pub_filter_edit_input_title_hint\">Please enter a title</string>\n    <string name=\"activity_pub_filter_edit_duration\">Duration</string>\n    <string name=\"activity_pub_filter_edit_duration_permanent\">Permanent</string>\n    <string name=\"activity_pub_filter_edit_duration_thirty_minutes\">30 minutes</string>\n    <string name=\"activity_pub_filter_edit_duration_one_hour\">1 hour</string>\n    <string name=\"activity_pub_filter_edit_duration_twelve_hours\">12 hours</string>\n    <string name=\"activity_pub_filter_edit_duration_one_day\">1 day</string>\n    <string name=\"activity_pub_filter_edit_duration_three_day\">3 days</string>\n    <string name=\"activity_pub_filter_edit_duration_one_week\">1 week</string>\n    <string name=\"activity_pub_filter_edit_duration_custom\">Custom</string>\n    <string name=\"activity_pub_filter_edit_duration_finish_subtitle\">Expires on %1$s</string>\n    <string name=\"activity_pub_filter_edit_keyword_title\">Hidden Keywords</string>\n    <string name=\"activity_pub_filter_edit_keyword_dialog_title\">Edit Keyword</string>\n    <string name=\"activity_pub_filter_edit_keyword_dialog_hint\">Keyword</string>\n    <string name=\"activity_pub_filter_edit_keyword_remove_keyword_dialog_content\">Are you sure you want to delete this keyword?</string>\n    <string name=\"activity_pub_filter_edit_keyword_list_title\">Muted words</string>\n    <string name=\"activity_pub_filter_edit_keyword_list_desc\">%1$s muted word or phrase</string>\n    <string name=\"activity_pub_filter_edit_context_title\">Mute from</string>\n    <string name=\"activity_pub_filter_edit_context_selector_title\">Please select sources to hide</string>\n    <string name=\"activity_pub_filter_edit_context_home\">Home &amp; List</string>\n    <string name=\"activity_pub_filter_edit_context_notification\">Notifications</string>\n    <string name=\"activity_pub_filter_edit_context_timeline\">Public Timelines</string>\n    <string name=\"activity_pub_filter_edit_context_thread\">Threads &amp; Replies</string>\n    <string name=\"activity_pub_filter_edit_context_account\">Profiles</string>\n    <string name=\"activity_pub_filter_edit_empty_context\">Empty</string>\n    <string name=\"activity_pub_filter_edit_warning_title\">Show with content warning</string>\n    <string name=\"activity_pub_filter_edit_warning_desc\">Still show posts that match this filter, but behind a content warning</string>\n    <string name=\"activity_pub_filter_edit_delete_content\">Are you sure you want to delete?</string>\n    <string name=\"activity_pub_filter_edit_back_dialog\">Are you sure you want to exit?</string>\n    <string name=\"activity_pub_filter_edit_whole_word\">Whole Word</string>\n    <string name=\"activity_pub_explorer_tab_status_title\">Posts</string>\n    <string name=\"activity_pub_explorer_tab_users_title\">Users</string>\n    <string name=\"activity_pub_explorer_tab_hashtag_title\">Hashtags</string>\n    <string name=\"activity_pub_created_list_title\">Lists</string>\n    <string name=\"activity_pub_add_list_title\">Create List</string>\n    <string name=\"activity_pub_add_list_name\">List Name</string>\n    <string name=\"activity_pub_add_list_replies\">Show replies to</string>\n    <string name=\"activity_pub_add_list_replies_non\">No one</string>\n    <string name=\"activity_pub_add_list_replies_list\">Members of the list</string>\n    <string name=\"activity_pub_add_list_replies_followers\">Anyone I follow</string>\n    <string name=\"activity_pub_add_list_accounts\">List members</string>\n    <string name=\"activity_pub_add_list_hide_in_timeline\">Hide members in Following</string>\n    <string name=\"activity_pub_add_list_hide_in_timeline_desc\">If someone is on this list, hide them in your Following timeline to avoid seeing their posts twice.</string>\n    <string name=\"activity_pub_add_list_remove_user_message\">Are you sure you want to remove this user?</string>\n    <string name=\"activity_pub_add_list_name_is_empty\">Please enter your username</string>\n    <string name=\"activity_pub_add_list_back_reminder\">You have unsaved changes. Are you sure you want to exit?</string>\n    <string name=\"activity_pub_search_user_placeholder\">Type to search</string>\n    <string name=\"activity_pub_list_delete_confirm\">Are you sure you want to delete this list?</string>\n    <string name=\"activity_pub_login_exception\">OAuthor state exception!</string>\n    <string name=\"activity_pub_home_timeline\">Home</string>\n    <string name=\"activity_pub_local_timeline\">Local</string>\n    <string name=\"activity_pub_public_timeline\">Public</string>\n    <string name=\"activity_pub_instance_detail_language_label\">Language: %1$s</string>\n    <string name=\"activity_pub_instance_detail_active_month_label\">Active month: %1$s</string>\n    <string name=\"activity_pub_user_detail_tab_post\">Posts</string>\n    <string name=\"activity_pub_user_detail_tab_replies\">Replies</string>\n    <string name=\"activity_pub_user_detail_tab_media\">Media</string>\n    <string name=\"activity_pub_user_detail_tab_about\">About</string>\n    <string name=\"activity_pub_about\">About</string>\n    <string name=\"activity_pub_about_rule_title\">Rules</string>\n    <string name=\"activity_pub_trends_tag\">Trends Tags</string>\n    <string name=\"activity_pub_trends_tag_description\">%1$s people in the past 2 days</string>\n    <string name=\"activity_pub_trends_status\">Hot</string>\n    <string name=\"activity_pub_select_platform_title\">Choose a instance</string>\n    <string name=\"activity_pub_select_platform_text_hint\">Enter instance name or URL</string>\n</resources>\n"
  },
  {
    "path": "localization/src/commonMain/composeResources/values/strings_status_ui.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"status_ui_reposted\">reposted</string>\n    <string name=\"status_ui_repost\">Repost</string>\n    <string name=\"status_ui_quote\">Quote</string>\n    <string name=\"status_ui_like\">Like</string>\n    <string name=\"status_ui_comment\">Comment</string>\n    <string name=\"status_ui_delete\">Delete</string>\n    <string name=\"status_ui_share\">Share</string>\n    <string name=\"status_ui_bookmark\">Bookmark</string>\n    <string name=\"status_ui_unbookmark\">Unbookmark</string>\n    <string name=\"status_ui_reply\">Reply</string>\n    <string name=\"status_ui_follow\">Follow</string>\n    <string name=\"status_ui_unfollow\">Unfollow</string>\n    <string name=\"status_ui_pin\">Pin</string>\n    <string name=\"status_ui_unpin\">Unpin</string>\n    <string name=\"status_ui_edit\">Edit</string>\n    <string name=\"status_ui_sensitive_by_filter\">Matches filter \"%1$s\"</string>\n    <string name=\"status_ui_new_status\">New Content</string>\n    <string name=\"status_ui_delete_status_confirm\">Are you sure you want to delete this content?</string>\n    <string name=\"status_ui_image_sensitive_label\">Media hidden</string>\n    <string name=\"status_ui_image_content_show_hidden_label\">Show more</string>\n    <string name=\"status_ui_image_content_hide_hidden_label\">Hide content</string>\n    <string name=\"status_ui_poll_vote\">Vote</string>\n    <string name=\"status_ui_poll_vote_finished_tip\">%1$s vote · Closed</string>\n    <string name=\"status_ui_poll_votes_finished_tip\">%1$s votes · Closed</string>\n    <string name=\"status_ui_interaction_open_in_browser\">Open In Browser</string>\n    <string name=\"status_ui_interaction_open_original_instance\">Open the other instance</string>\n    <string name=\"status_ui_interaction_copy_url\">Copy Link</string>\n    <string name=\"status_ui_interaction_translate\">Translate</string>\n    <string name=\"status_ui_interaction_open_blog_by_other_account\">Open with another account</string>\n    <string name=\"status_ui_visibility_mentioned_only\">Mentioned Only</string>\n    <string name=\"status_ui_label_pinned\">Pinned</string>\n    <string name=\"status_ui_info_label_edited\">Edited</string>\n    <string name=\"status_ui_interaction_label_favourited_count\">%1$s favorites</string>\n    <string name=\"status_ui_interaction_label_boosted_count\">%1$s boosts</string>\n    <string name=\"status_ui_bottom_label_edited_at\">Edited on %1$s</string>\n    <string name=\"status_ui_translating\">Translating…</string>\n    <string name=\"status_ui_translate_show_original\">Show Original</string>\n    <string name=\"status_ui_edit_content_delete_dialog_content\">Confirm to delete this content?</string>\n    <string name=\"status_ui_edit_content_name_title\">Edit Content Name</string>\n    <string name=\"status_ui_edit_content_name_label\">Edit Content Name</string>\n    <string name=\"status_ui_edit_content_name_hint\">Please enter the name of the content</string>\n    <string name=\"status_ui_edit_content_config_showing_list_title\">Feeds on the Home Page</string>\n    <string name=\"status_ui_edit_content_config_showing_list_description\">Long press and drag to reorder</string>\n    <string name=\"status_ui_edit_content_config_hidden_list_title\">Feeds Can Add to the Home Page</string>\n    <string name=\"status_ui_user_detail_relationship_mutuals\">Mutuals</string>\n    <string name=\"status_ui_user_detail_relationship_blocking\">Blocked</string>\n    <string name=\"status_ui_user_detail_relationship_following\">Following</string>\n    <string name=\"status_ui_user_detail_relationship_not_follow\">Follow</string>\n    <string name=\"status_ui_user_detail_relationship_requested\">Pending</string>\n    <string name=\"status_ui_user_detail_relationship_follow_back\">Follow back</string>\n    <string name=\"status_ui_user_detail_request_by_tip\">They are requesting to follow you</string>\n    <string name=\"status_ui_relationship_btn_dialog_content_cancel_follow\">Do you want to unfollow this user?</string>\n    <string name=\"status_ui_relationship_btn_dialog_content_cancel_follow_request\">Do you want to withdraw your follow request?</string>\n    <string name=\"status_ui_relationship_btn_dialog_content_cancel_blocking\">Are you sure you want to unblock this user?</string>\n    <string name=\"status_ui_user_detail_follows_you\">Follows you</string>\n    <string name=\"status_ui_user_detail_posts\">Posts</string>\n    <string name=\"status_ui_user_detail_follower_info\">Followers</string>\n    <string name=\"status_ui_user_detail_following_info\">Following</string>\n    <string name=\"status_ui_top_label_continued_thread\">Continued thread</string>\n    <string name=\"status_ui_update_dialog_title\">Update</string>\n    <string name=\"status_ui_update_dialog_release_note\">Version %1$s Update Log:\\n%2$s</string>\n    <string name=\"status_ui_switch_account_dialog_title\">Select Account</string>\n    <string name=\"status_ui_likes\">Likes</string>\n    <string name=\"status_ui_bookmarks\">Bookmarks</string>\n    <string name=\"status_ui_edit_profile\">Edit profile</string>\n    <string name=\"status_ui_logout\">Log out</string>\n    <string name=\"status_ui_logout_dialog_content\">Are you sure you want to log out?</string>\n    <string name=\"status_ui_search_account_status_hint\">Search posts</string>\n    <string name=\"shared_notification_unknown_desc\">Unknown Notification</string>\n    <string name=\"shared_notification_favourited_desc\">favourited your post</string>\n    <string name=\"shared_notification_reblog_desc\">reblog your post</string>\n    <string name=\"shared_notification_poll_desc\">View previous voting results</string>\n    <string name=\"shared_notification_poll_count\">%1$s votes · Closed</string>\n    <string name=\"shared_notification_follow_desc\">followed you</string>\n    <string name=\"shared_notification_update_desc\">edited a post</string>\n    <string name=\"shared_notification_follow_request\">You have received a follow request</string>\n    <string name=\"shared_notification_new_status_desc\">post a new blog</string>\n    <string name=\"shared_notification_severed_desc\">has severed ties with you</string>\n    <string name=\"shared_notification_quote_desc\">quoted your post</string>\n    <string name=\"shared_notification_reply_desc\">Replied to your post</string>\n    <string name=\"shared_user_list_title_following\">Following</string>\n    <string name=\"shared_user_list_title_followers\">Followers</string>\n    <string name=\"shared_user_list_title_mutes\">Muted Users</string>\n    <string name=\"shared_user_list_title_blocks\">Blocked Users</string>\n    <string name=\"shared_user_list_title_likes\">Favorited</string>\n    <string name=\"shared_user_list_title_reblog\">Boosted</string>\n    <string name=\"shared_user_list_action_mute\">Mute</string>\n    <string name=\"shared_user_list_action_block\">Block</string>\n    <string name=\"shared_user_list_action_muted\">Muted</string>\n    <string name=\"shared_user_list_action_blocked\">Blocked</string>\n    <string name=\"shared_user_list_action_mute_dialog_message\">Are you sure you want to mute this user?</string>\n    <string name=\"shared_user_list_action_block_dialog_message\">Are you sure you want to block this user?</string>\n    <string name=\"shared_publish_blog_title\">Post</string>\n    <string name=\"shared_publish_blog_text_hint\">Type or paste what`s on your mind</string>\n    <string name=\"shared_publish_reply_input_hint\">Replying to [[%1$s]]</string>\n    <string name=\"shared_publish_interaction_no_limit\">Anybody can interact</string>\n    <string name=\"shared_publish_interaction_limited\">Interaction limited</string>\n    <string name=\"shared_publish_interaction_dialog_title\">Post interaction settings</string>\n    <string name=\"shared_publish_interaction_dialog_subtitle\">Customize who can interact with this post.</string>\n    <string name=\"shared_publish_interaction_dialog_quote_title\">Quote settings</string>\n    <string name=\"shared_publish_interaction_dialog_quote_allow\">Allow quote posts</string>\n    <string name=\"shared_publish_interaction_dialog_reply_title\">Reply settings</string>\n    <string name=\"shared_publish_interaction_dialog_reply_subtitle\">Allow replies from:</string>\n    <string name=\"shared_publish_interaction_dialog_reply_all\">Everybody</string>\n    <string name=\"shared_publish_interaction_dialog_reply_nobody\">Nobody</string>\n    <string name=\"shared_publish_interaction_dialog_reply_combine_title\">Or combine these options:</string>\n    <string name=\"shared_publish_interaction_dialog_mentioned\">Mentioned users</string>\n    <string name=\"shared_publish_interaction_dialog_following\">Users you follow</string>\n    <string name=\"shared_publish_interaction_dialog_follower\">Your followers</string>\n    <string name=\"shared_publish_interaction_dialog_in_list\">Users in \"%1$s\"</string>\n    <string name=\"shared_publish_media_alt_dialog_title\">Add alt text</string>\n    <string name=\"shared_publish_media_alt_dialog_input_tip\">Descriptive alt text</string>\n    <string name=\"shared_publish_media_alt_dialog_input_hint\">Alt text</string>\n    <string name=\"shared_feeds_not_login_title\">Oops! Your login info has expired. Please log back in to continue.</string>\n    <string name=\"shared_feeds_go_to_login\">Sign in</string>\n    <string name=\"shared_publish_select_account_title\">Please select account</string>\n    <string name=\"shared_select_language_title\">Select Language</string>\n    <string name=\"shared_status_context_screen_title\">Detail</string>\n    <string name=\"shared_alt_label\">ALT</string>\n    <string name=\"status_ui_embed_quote_unavailable\">The current quote is unavailable</string>\n    <string name=\"status_ui_action_unforward\">Cancel repost</string>\n    <string name=\"status_ui_quote_approval_public\">Anyone</string>\n    <string name=\"status_ui_quote_approval_follower\">Followers only</string>\n    <string name=\"status_ui_quote_approval_nobody\">Just me</string>\n    <string name=\"status_ui_visibility\">Visibility</string>\n</resources>\n"
  },
  {
    "path": "localization/src/commonMain/composeResources/values-de-rDE/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"skip\">Überspringen</string>\n    <string name=\"alert\">Hinweis</string>\n    <string name=\"duration_minute\">Minute</string>\n    <string name=\"duration_hour\">Stunde</string>\n    <string name=\"duration_day\">Tag</string>\n    <string name=\"duration_week\">Woche</string>\n    <string name=\"cancel\">Abbrechen</string>\n    <string name=\"ok\">Ja</string>\n    <string name=\"empty\">Dieser Bereich ist leer～</string>\n    <string name=\"duration_selector_title\">Dauer festlegen</string>\n    <string name=\"retry\">Erneut versuchen</string>\n    <string name=\"load_more_error\">Ups! Konnte nicht geladen werden.</string>\n    <string name=\"unknown_error\">Unbekannter Fehler</string>\n    <string name=\"network_error\">Netzwerkfehler</string>\n    <string name=\"resource_not_found\">Ressource nicht gefunden</string>\n    <string name=\"mixed_content_subtitle_1\">Gemischte Inhalte ·</string>\n    <string name=\"mixed_content_subtitle_2\">Quellen</string>\n    <string name=\"date_time_ago\">&#160;vor</string>\n    <string name=\"date_time_ago_prefix\"></string>\n    <string name=\"date_time_ago_suffix\">&#160;vor</string>\n    <string name=\"date_time_day\">Tag</string>\n    <string name=\"date_time_hour\">Stunde</string>\n    <string name=\"date_time_minute\">Min</string>\n    <string name=\"date_time_second\">Sek</string>\n    <string name=\"image_saving\">Speichern…</string>\n    <string name=\"image_save_success\">Erfolgreich gespeichert</string>\n    <string name=\"image_save_failed\">Speichern fehlgeschlagen</string>\n    <string name=\"permission_write_external_permission_denied\">Speicherberechtigung verweigert. Bitte in den Einstellungen aktivieren.</string>\n    <string name=\"feeds_load_previous_page_label\">Vorherige Seite wird geladen</string>\n    <string name=\"feeds_load_previous_page_failed_label\">Laden fehlgeschlagen, zum Wiederholen tippen</string>\n    <string name=\"image\">Bild</string>\n    <string name=\"video\">Video</string>\n    <string name=\"login\">Anmelden</string>\n    <string name=\"add\">Hinzufügen</string>\n    <string name=\"search\">Suchen</string>\n    <string name=\"auth_success\">Anmeldung erfolgreich</string>\n    <string name=\"auth_failed\">Anmeldung fehlgeschlagen</string>\n    <string name=\"select\">Auswählen</string>\n    <string name=\"logout\">Abmelden</string>\n    <string name=\"settings\">Einstellungen</string>\n    <string name=\"donate\">Spenden</string>\n    <string name=\"feeds\">Feeds</string>\n    <string name=\"save\">Speichern</string>\n    <string name=\"done\">Fertig!</string>\n    <string name=\"bluesky_name\">Bluesky</string>\n    <string name=\"bluesky_description\">Bluesky ist ein offenes Netzwerk. Mit nur einem Konto nutzt du ein benutzerfreundliches Social Network und behältst eine gemeinsame Identität im gesamten sozialen Internet.</string>\n    <string name=\"mastodon_name\">Mastodon</string>\n    <string name=\"mastodon_description\">Mastodon ist ein dezentrales, nicht-kommerzielles soziales Netzwerk. Nutzerinnen und Nutzer kontrollieren ihre Timeline; jeder Server ist eine eigenständige Instanz.</string>\n    <string name=\"mixed_content_name\">Gemischte Inhalte</string>\n    <string name=\"mixed_content_description\">Mixed Feed bündelt alles an einem Ort. Füge Quellen aus Mastodon, Bluesky, RSS u. v. m. hinzu – Fread kombiniert sie zu einer chronologischen Timeline.</string>\n    <string name=\"status_provider_type_activity_pub\">Mastodon</string>\n    <string name=\"add_content_title\">Inhalt hinzufügen</string>\n    <string name=\"add_content_success_with_account\">Erfolgreich hinzugefügt. Aktuelles Konto:</string>\n    <string name=\"add_content_success_with_login_reminder\">Erfolgreich hinzugefügt. Jetzt anmelden?</string>\n    <string name=\"content_exist_tips\">Ups! Dieser Inhalt ist bereits vorhanden.</string>\n    <string name=\"content_add_success\">Alles bereit! Dein Inhalt ist startklar.</string>\n    <string name=\"add_feeds_page_empty_name_exist\">Dieser Benutzername ist bereits vergeben.</string>\n    <string name=\"edit_profile_name_empty\">Bitte gib deinen Benutzernamen ein</string>\n    <string name=\"edit_profile_label_name\">Name</string>\n    <string name=\"edit_profile_label_note\">Bio</string>\n    <string name=\"edit_profile_input_name_hint\">Bitte gib deinen Benutzernamen ein</string>\n    <string name=\"edit_profile_input_note_hint\">Bitte gib deine Bio ein</string>\n    <string name=\"edit_profile_edit_confirm_message\">Bearbeitung wirklich verlassen?</string>\n    <string name=\"add_content_success_snackbar\">Erfolgreich hinzugefügt</string>\n    <string name=\"login_dialog_input_hint\">Bitte gib den Server zum Anmelden ein</string>\n    <string name=\"login_dialog_title\">Bitte wähle den Anmeldeserver</string>\n    <string name=\"login_dialog_input_tip\">Server-Hostname eingeben</string>\n    <string name=\"login_dialog_input_title\">Bitte Server-Hostname eingeben</string>\n    <string name=\"login_dialog_input_label\">Server-Hostname</string>\n    <string name=\"login_dialog_target_title\">Bitte anmelden</string>\n    <string name=\"profile_description\">Fread unterstützt mehrere Konten. Hinzugefügte Konten kannst du zum Posten, Liken, Reposten und Kommentieren nutzen und die von ihnen erstellten Listen ansehen.</string>\n    <string name=\"list_content_empty_placeholder\">Kein Inhalt verfügbar</string>\n    <string name=\"post_status_scope_public\">Öffentlich</string>\n    <string name=\"post_status_scope_unlisted\">Öffentlich, aber von Entdecken ausgeschlossen</string>\n    <string name=\"post_status_scope_follower_only\">Nur Follower</string>\n    <string name=\"post_status_scope_mentioned_only\">Nur erwähnte Personen</string>\n    <string name=\"post_status_content_warning\">Inhaltswarnung</string>\n    <string name=\"post_status_success\">Erfolgreich gesendet</string>\n    <string name=\"post_status_failed\">Senden fehlgeschlagen: %1$s</string>\n    <string name=\"post_status_exit_dialog_content\">Ungespeicherte Änderungen gehen verloren. Wirklich verlassen?</string>\n    <string name=\"post_status_content_is_empty\">Bitte gib einen Inhalt ein</string>\n    <string name=\"post_status_part_failed\">Einige Konten konnten nicht veröffentlichen. Erfolgreich veröffentlichte wurden entfernt. Bitte mit den übrigen erneut versuchen: %1$s</string>\n    <string name=\"select_account_open_status_title\">Konto auswählen</string>\n    <string name=\"select_account_open_status_empty\">Keine weiteren Konten</string>\n    <string name=\"select_account_open_status_search_in\">In %1$s suchen</string>\n    <string name=\"select_account_open_status_search_failed\">Suche fehlgeschlagen</string>\n    <string name=\"explorer_search_no_results\">Keine Treffer</string>\n    <string name=\"explorer_search_bar_hint\">Suchen</string>\n    <string name=\"explorer_search_bar_hint_specialize_platform\">In %1$s suchen</string>\n    <string name=\"explorer_search_tab_title_author\">Accounts</string>\n    <string name=\"explorer_search_tab_title_status\">Beiträge</string>\n    <string name=\"explorer_search_tab_title_hashtag\">Hashtags</string>\n    <string name=\"explorer_search_tab_title_server\">Server</string>\n    <string name=\"explorer_tab_title\">Entdecken</string>\n    <string name=\"explorer_tab_status_title\">Beiträge</string>\n    <string name=\"explorer_tab_users_title\">Nutzer</string>\n    <string name=\"explorer_tab_hashtag_title\">Hashtags</string>\n    <string name=\"add_provider_page_title\">Server hinzufügen</string>\n    <string name=\"add_provider_page_confirm_button\">Hinzufügen bestätigen</string>\n    <string name=\"search_page_title\">Suche</string>\n    <string name=\"search_result_not_found\">Keine Suchergebnisse</string>\n    <string name=\"feeds_import_page_title\">Importieren</string>\n    <string name=\"feeds_import_page_hint\">Bitte wähle die zu importierende OPML-Datei</string>\n    <string name=\"feeds_import_button\">Importieren</string>\n    <string name=\"add_feeds_page_title\">Inhalt hinzufügen</string>\n    <string name=\"add_feeds_page_feeds_name_label\">Name des Inhalts</string>\n    <string name=\"add_feeds_page_feeds_name_hint\">Bitte gib den Namen des Inhalts ein</string>\n    <string name=\"add_feeds_page_feeds_empty\">Bitte eine Quelle hinzufügen</string>\n    <string name=\"pre_add_feeds_no_result\">Keine Ergebnisse gefunden</string>\n    <string name=\"pre_add_feeds_hint\">Instanzname, URL, Nutzer-Homepage-URL, RSS</string>\n    <string name=\"pre_add_feeds_input_label_1\">Fread unterstützt [[Mastodon]]/[[Bluesky]] sowie [[RSS]]-Feeds – und sogar [[gemischte Inhalte]].</string>\n    <string name=\"pre_add_feeds_input_label_2\">Bitte gib hier den [[NickName]] von Mastodon/Bluesky, den [[WebFinger\\Handle]], die [[Homepage-URL]] des Nutzers oder die [[RSS]]-Feed-URL ein.</string>\n    <string name=\"empty_content_hint_title\">Füge zuerst Inhalte hinzu</string>\n    <string name=\"empty_content_hint_desc\">Lege zunächst ein paar Quellen an. Fread unterstützt verschiedene Inhaltstypen und Plattformen – so baust du dir deinen eigenen Feed.</string>\n    <string name=\"feeds_add_content\">Inhalt hinzufügen</string>\n    <string name=\"select_feeds_type_screen_title\">Inhaltstyp auswählen</string>\n    <string name=\"feeds_pre_add_login_dialog_content\">Inhalt hinzugefügt. Jetzt anmelden?</string>\n    <string name=\"add_feeds_page_empty_name_tips\">Bitte einen Namen eingeben</string>\n    <string name=\"add_feeds_page_empty_source_tips\">Bitte eine Quelle hinzufügen</string>\n    <string name=\"add_feeds_choose_auth_dialog_title\">Bitte Anmeldeserver wählen</string>\n    <string name=\"search_feeds_title\">Suchen</string>\n    <string name=\"search_feeds_title_hint\">Mastodon/Bluesky NickName, WebFinger, URL, RSS</string>\n    <string name=\"feeds_select_account_for_post_status\">Bitte ein Konto auswählen</string>\n    <string name=\"feeds_mixed_config_edit_new_name_dialog_title\">Bitte Namen eingeben</string>\n    <string name=\"feeds_mixed_config_edit_new_name_dialog_label\">Name</string>\n    <string name=\"feeds_mixed_config_edit_delete_content_dialog_message\">Möchtest du diesen Feed wirklich löschen?</string>\n    <string name=\"feeds_mixed_config_not_found\">Abnormaler Status: Konfiguration nicht gefunden.</string>\n    <string name=\"feeds_import_back_dialog_message\">Möchtest du wirklich beenden?</string>\n    <string name=\"feeds_delete_confirm_content\">Möchtest du das wirklich löschen?</string>\n    <string name=\"feeds_select_type_screen_title\">Format auswählen</string>\n    <string name=\"feeds_select_type_screen_content\">Was möchtest du hinzufügen?</string>\n    <string name=\"notification_tab_title\">Benachrichtigungen</string>\n    <string name=\"notifications_account_empty_tip\">Derzeit kein Konto angemeldet</string>\n    <string name=\"notifications_tab_all\">Alle</string>\n    <string name=\"notifications_tab_mention\">Erwähnungen</string>\n    <string name=\"profile_page_title\">Profil</string>\n    <string name=\"profile_setting_about_title\">Über</string>\n    <string name=\"profile_setting_donate_desc\">Spendiere mir einen Kaffee zur Unterstützung des Projekts.</string>\n    <string name=\"profile_setting_inline_video_auto_play\">Inline-Video automatisch abspielen</string>\n    <string name=\"profile_setting_inline_video_auto_play_subtitle\">Wenn aktiviert, werden Videos im Feed automatisch abgespielt.</string>\n    <string name=\"profile_setting_always_show_sensitive_content\">Sensible Inhalte standardmäßig anzeigen</string>\n    <string name=\"profile_setting_always_show_sensitive_content_subtitle\">Wenn aktiviert, werden sensible Inhalte standardmäßig angezeigt (nur Mastodon).</string>\n    <string name=\"profile_setting_dark_mode_title\">Dunkelmodus</string>\n    <string name=\"profile_setting_dark_mode_dark\">Dunkelmodus</string>\n    <string name=\"profile_setting_dark_mode_light\">Hellmodus</string>\n    <string name=\"profile_setting_dark_mode_follow_system\">Systemeinstellung folgen</string>\n    <string name=\"profile_setting_open_source_title\">Open Source</string>\n    <string name=\"profile_setting_open_source_desc\">Open-Source-Lizenzen der in diesem Projekt verwendeten Abhängigkeiten</string>\n    <string name=\"profile_setting_open_source_feedback\">Feedback geben</string>\n    <string name=\"profile_setting_open_source_feedback_desc\">Der Telegram-Gruppe beitreten oder andere Wege nutzen</string>\n    <string name=\"profile_setting_open_source_feedback_telegram\">Der Telegram-Gruppe beitreten</string>\n    <string name=\"profile_setting_open_source_feedback_github\">GitHub-Issue erstellen</string>\n    <string name=\"profile_setting_open_source_feedback_email\">E-Mail senden</string>\n    <string name=\"profile_setting_language_title\">Anzeigesprache</string>\n    <string name=\"profile_setting_language_zh\">Vereinfachtes Chinesisch</string>\n    <string name=\"profile_setting_language_en\">Englisch</string>\n    <string name=\"profile_setting_language_system\">Systemeinstellung folgen</string>\n    <string name=\"profile_setting_ratting\">Bewerte uns</string>\n    <string name=\"profile_setting_ratting_desc\">Wenn dir die App gefällt, gib uns bitte eine gute Bewertung.</string>\n    <string name=\"profile_setting_font_size\">Schriftgröße der Inhalte</string>\n    <string name=\"profile_setting_font_size_desc\">Schrift- und Symbolgröße der Inhalte anpassen</string>\n    <string name=\"profile_setting_font_size_small\">Klein</string>\n    <string name=\"profile_setting_font_size_medium\">Mittel</string>\n    <string name=\"profile_setting_font_size_large\">Groß</string>\n    <string name=\"profile_setting_immersive_nav_bar\">Immersive Navigationsleiste</string>\n    <string name=\"profile_setting_immersive_nav_bar_desc\">Die untere Navigationsleiste wird auf der Startseite beim Scrollen automatisch ausgeblendet.</string>\n    <string name=\"profile_setting_timeline_position\">Standardposition der Timeline</string>\n    <string name=\"profile_setting_timeline_position_last_read\">Letzter Lesestand</string>\n    <string name=\"profile_setting_timeline_position_newest\">Neueste Beiträge</string>\n    <string name=\"profile_setting_theme_title\">Themenfarbe</string>\n    <string name=\"profile_setting_theme_default\">Standard</string>\n    <string name=\"profile_setting_theme_system\">System folgen</string>\n    <string name=\"profile_about_version\">Version:&#160;</string>\n    <string name=\"profile_about_website\">Website:&#160;</string>\n    <string name=\"profile_about_developer\">Erstellt von&#160;</string>\n    <string name=\"profile_about_contract_us\">Kontakt:&#160;</string>\n    <string name=\"profile_about_telegram\">Telegram-Gruppe:&#160;</string>\n    <string name=\"profile_about_privacy_policy\">Datenschutzrichtlinie:&#160;</string>\n    <string name=\"profile_setting_check_for_update\">Nach Updates suchen</string>\n    <string name=\"profile_setting_already_latest_version\">Deine App ist auf dem neuesten Stand</string>\n    <string name=\"profile_setting_have_new_version\">Neue Version verfügbar – zum Aktualisieren tippen.</string>\n    <string name=\"profile_donate_page_title\">Wähle deine bevorzugte Spendenart</string>\n    <string name=\"profile_account_not_login\">Sitzung abgelaufen</string>\n    <string name=\"rss_source_detail_screen_title\">Quellenangabe</string>\n    <string name=\"rss_source_detail_screen_custom_title\">Eigener Titel</string>\n    <string name=\"rss_source_detail_screen_url\">Abo-Link</string>\n    <string name=\"rss_source_detail_screen_home_url\">Startseite</string>\n    <string name=\"rss_source_detail_screen_add_date\">Hinzugefügt am</string>\n    <string name=\"rss_source_detail_screen_last_update_date\">Zuletzt aktualisiert</string>\n    <string name=\"bluesky_protocol_name\">Bluesky</string>\n    <string name=\"bsky_add_content_title\">Anmelden</string>\n    <string name=\"bsky_add_content_hosting_provider\">Hosting-Anbieter</string>\n    <string name=\"bsky_add_content_user_name\">Benutzername oder E-Mail-Adresse</string>\n    <string name=\"bsky_add_content_password\">Passwort</string>\n    <string name=\"bsky_add_content_factor_token\">Bestätigungscode</string>\n    <string name=\"bsky_edit_content_title\">Inhalt bearbeiten</string>\n    <string name=\"bsky_feeds_explorer_more\">Weitere Feeds entdecken</string>\n    <string name=\"bsky_feeds_explorer_creator_label\">Von %1$s</string>\n    <string name=\"bsky_feeds_explorer_liked_by\">Gefällt %1$s Nutzerinnen und Nutzern</string>\n    <string name=\"bsky_feeds_detail_creator_prefix\">Von</string>\n    <string name=\"bsky_feeds_item_subtitle\">Von %1$s · Gefällt %2$s Personen</string>\n    <string name=\"bsky_feeds_following_name\">Folge ich</string>\n    <string name=\"bsky_feeds_user_posts\">Beiträge</string>\n    <string name=\"bsky_feeds_user_replies\">Antworten</string>\n    <string name=\"bsky_feeds_user_medias\">Medien</string>\n    <string name=\"bsky_feeds_user_likes\">Likes</string>\n    <string name=\"bsky_user_detail_action_blocked_list\">Blockierte Nutzer</string>\n    <string name=\"bsky_user_detail_action_muted_list\">Stummgeschaltete Nutzer</string>\n    <string name=\"bsky_user_detail_action_mute_user\">%1$s stummschalten</string>\n    <string name=\"bsky_user_detail_action_unmute_user\">Stummschaltung aufheben</string>\n    <string name=\"bsky_user_detail_action_block_user\">%1$s blockieren</string>\n    <string name=\"bsky_user_detail_action_unblock_user\">Blockierung aufheben</string>\n    <string name=\"bsky_user_detail_action_block_user_dialog_message\">Möchtest du diesen Nutzer blockieren?</string>\n    <string name=\"bsky_user_detail_action_mute_user_dialog_message\">Möchtest du diesen Nutzer stummschalten?</string>\n    <string name=\"bsky_edit_profile_title\">Profil bearbeiten</string>\n    <string name=\"input_media_desc_page_title\">Bildbeschreibung</string>\n    <string name=\"input_media_desc_input_hint\">Bitte Bildbeschreibung eingeben</string>\n    <string name=\"post_status_page_title\">Neuer Beitrag</string>\n    <string name=\"post_screen_input_hint\">Bitte gib deinen Beitrag ein</string>\n    <string name=\"post_screen_media_descriptor_placeholder\">Beschreibungstext</string>\n    <string name=\"post_status_poll_item_hint\">Option %1$s</string>\n    <string name=\"post_status_poll_duration\">Abstimmungsdauer</string>\n    <string name=\"post_status_poll_function_title\">Funktion</string>\n    <string name=\"post_status_poll_single\">Einzelauswahl</string>\n    <string name=\"post_status_poll_multiple\">Mehrfachauswahl</string>\n    <string name=\"post_status_poll_style_select_dialog_title\">Bitte Stil wählen</string>\n    <string name=\"post_status_poll_is_empty\">Bitte den Inhalt der Abstimmung eingeben</string>\n    <string name=\"post_status_media_is_not_upload\">Medien noch nicht hochgeladen</string>\n    <string name=\"authentication_page_normal_title\">Für diese Funktion ist eine Anmeldung erforderlich</string>\n    <string name=\"authentication_page_failed_title\">Anmeldestatus ungültig. Bitte erneut anmelden.</string>\n    <string name=\"main_drawer_title\">Inhaltsliste</string>\n    <string name=\"main_drawer_mixed_item_subtitle_1\">Gemischte Inhalte ·</string>\n    <string name=\"main_drawer_mixed_item_subtitle_2\">Quellen</string>\n    <string name=\"main_drawer_settings\">Einstellungen</string>\n    <string name=\"main_drawer_donate\">Spenden</string>\n    <string name=\"main_request_notification_dialog_title\">Benachrichtigungen</string>\n    <string name=\"main_request_notification_dialog_reject_button\">Ablehnen</string>\n    <string name=\"main_request_notification_dialog_allow_button\">Zulassen</string>\n    <string name=\"main_request_notification_dialog_content\">Um Interaktions-Benachrichtigungen zu erhalten, bitte Berechtigung für Benachrichtigungen erteilen.</string>\n    <string name=\"setting_item_amoled_mode\">AMOLED-Modus</string>\n    <string name=\"setting_item_amoled_mode_description\">Wenn aktiviert, wechselt der Hintergrund im Dunkelmodus zu reinem Schwarz, um Energie zu sparen und den Kontrast in dunklen Umgebungen zu erhöhen.</string>\n    <string name=\"setting_group_appearance\">Aussehen</string>\n    <string name=\"setting_group_appearance_subtitle\">Einstellungen für Design, Anzeige und Startseitenstil</string>\n    <string name=\"setting_group_behavior\">Verhalten</string>\n    <string name=\"setting_group_behavior_subtitle\">Einstellungen für Inhaltsanzeige und Interaktionsverhalten</string>\n    <string name=\"setting_item_home_tab_next_title\">Schaltfläche zum Wechseln des Inhalts oben auf der Startseite anzeigen</string>\n    <string name=\"setting_item_home_tab_next_subtitle\">Oben rechts auf der Startseite die Schaltfläche zum Wechseln zum nächsten Inhalt anzeigen</string>\n    <string name=\"setting_item_home_tab_refresh_title\">Aktualisierungsschaltfläche oben auf der Startseite anzeigen</string>\n    <string name=\"setting_item_home_tab_refresh_subtitle\">Oben rechts auf der Startseite die Schaltfläche zum Aktualisieren von Inhalten anzeigen</string>\n    <string name=\"setting_item_open_url_by_system_title\">Links im Systembrowser öffnen</string>\n    <string name=\"setting_item_open_url_by_system_subtitle\">Wenn aktiviert, öffnet Fread Links im Systembrowser.</string>\n    <string name=\"setting_item_solid_bar_background_title\">Solider Leistenhintergrund</string>\n    <string name=\"setting_item_solid_bar_background_subtitle\">Macht die obere und untere Leiste einfarbig statt weichgezeichnet.</string>\n</resources>\n"
  },
  {
    "path": "localization/src/commonMain/composeResources/values-de-rDE/strings_mastodon.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"activity_pub_user_detail_join_date\">Beigetreten am</string>\n    <string name=\"activity_pub_user_detail_tab_about_joined\">Beigetreten am %1$s</string>\n    <string name=\"activity_pub_user_detail_menu_block\">%1$s blockieren</string>\n    <string name=\"activity_pub_user_detail_menu_block_domain\">Domain %1$s blockieren</string>\n    <string name=\"activity_pub_user_detail_menu_unblock_domain\">Blockierung von %1$s aufheben</string>\n    <string name=\"activity_pub_user_detail_menu_edit_private_note\">Notiz bearbeiten</string>\n    <string name=\"activity_pub_user_detail_menu_edit_private_note_dialog_hint\">Private Notiz eingeben</string>\n    <string name=\"activity_pub_user_detail_dialog_content_block\">Möchtest du diesen Nutzer blockieren?</string>\n    <string name=\"activity_pub_user_detail_dialog_content_block_domain\">Möchtest du diese Domain blockieren?</string>\n    <string name=\"activity_pub_user_menu_blocked_user_list\">Blockierte Nutzer</string>\n    <string name=\"activity_pub_user_menu_muted_user_list\">Stummgeschaltete Nutzer</string>\n    <string name=\"activity_pub_user_detail_menu_mute_user\">%1$s stummschalten</string>\n    <string name=\"activity_pub_user_detail_menu_unmute_user\">Stummschaltung für %1$s aufheben</string>\n    <string name=\"activity_pub_mute_user_bottom_sheet_title\">Nutzer stummschalten?</string>\n    <string name=\"activity_pub_mute_user_bottom_sheet_role1\">Die Person erfährt nicht, dass sie stummgeschaltet wurde.</string>\n    <string name=\"activity_pub_mute_user_bottom_sheet_role2\">Sie können deine Beiträge weiterhin sehen, aber du siehst ihre nicht.</string>\n    <string name=\"activity_pub_mute_user_bottom_sheet_role3\">Du siehst keine Beiträge, in denen sie erwähnt werden.</string>\n    <string name=\"activity_pub_mute_user_bottom_sheet_role4\">Sie können dich erwähnen und dir folgen, aber du siehst sie nicht.</string>\n    <string name=\"activity_pub_mute_user_bottom_sheet_btn_mute\">Stummschalten</string>\n    <string name=\"add_instance_screen_title\">Instanz hinzufügen</string>\n    <string name=\"add_instance_input_service_hint\">Bitte Serveradresse eingeben</string>\n    <string name=\"add_instance_input_service_error\">Bitte die korrekte Serveradresse eingeben</string>\n    <string name=\"activity_pub_content_tab_home\">Startseite</string>\n    <string name=\"activity_pub_content_tab_local_timeline\">Lokal</string>\n    <string name=\"activity_pub_content_tab_public_timeline\">Öffentlich</string>\n    <string name=\"activity_pub_content_tab_trending\">Trends</string>\n    <string name=\"activity_pub_hashtag_timeline_description\">%1$s Beiträge · %2$s Teilnehmende · Heute %3$s Beiträge</string>\n    <string name=\"activity_pub_hashtag_unfollow_dialog_message\">Möchtest du dieses Hashtag wirklich nicht mehr folgen?</string>\n    <string name=\"activity_pub_edit_account_info_label_about\">Über</string>\n    <string name=\"activity_pub_edit_content_screen_config_not_found\">Konfiguration nicht gefunden</string>\n    <string name=\"activity_pub_user_list_empty\">Derzeit keine Nutzer</string>\n    <string name=\"activity_pub_muted_user_list_empty\">Keine stummgeschalteten Nutzer</string>\n    <string name=\"activity_pub_muted_user_list_unmute\">Stummschaltung aufheben</string>\n    <string name=\"activity_pub_blocked_user_list_empty\">Keine blockierten Nutzer</string>\n    <string name=\"activity_pub_blocked_user_list_unblock\">Blockierung aufheben</string>\n    <string name=\"activity_pub_bookmarks_list_title\">Lesezeichen</string>\n    <string name=\"activity_pub_favourites_list_title\">Favoriten</string>\n    <string name=\"activity_pub_followed_tags_screen_title\">Gefolgte Hashtags</string>\n    <string name=\"activity_pub_filters_list_page_title\">Filter</string>\n    <string name=\"activity_pub_filters_active\">Aktiv</string>\n    <string name=\"activity_pub_filters_expired\">Abgelaufen</string>\n    <string name=\"activity_pub_filter_edit_title\">Filter bearbeiten</string>\n    <string name=\"activity_pub_filter_edit_input_title_label\">Titel</string>\n    <string name=\"activity_pub_filter_edit_input_title_hint\">Bitte einen Titel eingeben</string>\n    <string name=\"activity_pub_filter_edit_duration\">Dauer</string>\n    <string name=\"activity_pub_filter_edit_duration_permanent\">Dauerhaft</string>\n    <string name=\"activity_pub_filter_edit_duration_thirty_minutes\">30 Minuten</string>\n    <string name=\"activity_pub_filter_edit_duration_one_hour\">1 Stunde</string>\n    <string name=\"activity_pub_filter_edit_duration_twelve_hours\">12 Stunden</string>\n    <string name=\"activity_pub_filter_edit_duration_one_day\">1 Tag</string>\n    <string name=\"activity_pub_filter_edit_duration_three_day\">3 Tage</string>\n    <string name=\"activity_pub_filter_edit_duration_one_week\">1 Woche</string>\n    <string name=\"activity_pub_filter_edit_duration_custom\">Benutzerdefiniert</string>\n    <string name=\"activity_pub_filter_edit_duration_finish_subtitle\">Läuft ab am %1$s</string>\n    <string name=\"activity_pub_filter_edit_keyword_title\">Ausgeblendete Schlüsselwörter</string>\n    <string name=\"activity_pub_filter_edit_keyword_dialog_title\">Schlüsselwort bearbeiten</string>\n    <string name=\"activity_pub_filter_edit_keyword_dialog_hint\">Schlüsselwort</string>\n    <string name=\"activity_pub_filter_edit_keyword_remove_keyword_dialog_content\">Möchtest du dieses Schlüsselwort wirklich löschen?</string>\n    <string name=\"activity_pub_filter_edit_keyword_list_title\">Ausgeblendete Wörter</string>\n    <string name=\"activity_pub_filter_edit_keyword_list_desc\">%1$s ausgeblendetes Wort oder Ausdruck</string>\n    <string name=\"activity_pub_filter_edit_context_title\">Ausblenden aus</string>\n    <string name=\"activity_pub_filter_edit_context_selector_title\">Bitte auszublendende Quellen auswählen</string>\n    <string name=\"activity_pub_filter_edit_context_home\">Startseite &amp; Listen</string>\n    <string name=\"activity_pub_filter_edit_context_notification\">Benachrichtigungen</string>\n    <string name=\"activity_pub_filter_edit_context_timeline\">Öffentliche Timelines</string>\n    <string name=\"activity_pub_filter_edit_context_thread\">Threads &amp; Antworten</string>\n    <string name=\"activity_pub_filter_edit_context_account\">Profile</string>\n    <string name=\"activity_pub_filter_edit_empty_context\">Leer</string>\n    <string name=\"activity_pub_filter_edit_warning_title\">Mit Inhaltswarnung anzeigen</string>\n    <string name=\"activity_pub_filter_edit_warning_desc\">Beiträge, die diesem Filter entsprechen, weiterhin anzeigen – jedoch hinter einer Inhaltswarnung.</string>\n    <string name=\"activity_pub_filter_edit_delete_content\">Möchtest du das wirklich löschen?</string>\n    <string name=\"activity_pub_filter_edit_back_dialog\">Möchtest du wirklich beenden?</string>\n    <string name=\"activity_pub_filter_edit_whole_word\">Ganzes Wort</string>\n    <string name=\"activity_pub_explorer_tab_status_title\">Beiträge</string>\n    <string name=\"activity_pub_explorer_tab_users_title\">Nutzer</string>\n    <string name=\"activity_pub_explorer_tab_hashtag_title\">Hashtags</string>\n    <string name=\"activity_pub_created_list_title\">Listen</string>\n    <string name=\"activity_pub_add_list_title\">Liste erstellen</string>\n    <string name=\"activity_pub_add_list_name\">Listenname</string>\n    <string name=\"activity_pub_add_list_replies\">Antworten anzeigen von</string>\n    <string name=\"activity_pub_add_list_replies_non\">Niemand</string>\n    <string name=\"activity_pub_add_list_replies_list\">Mitglieder der Liste</string>\n    <string name=\"activity_pub_add_list_replies_followers\">Alle, denen ich folge</string>\n    <string name=\"activity_pub_add_list_accounts\">Listenmitglieder</string>\n    <string name=\"activity_pub_add_list_hide_in_timeline\">Mitglieder in „Following“ ausblenden</string>\n    <string name=\"activity_pub_add_list_hide_in_timeline_desc\">Wenn jemand in dieser Liste ist, in deiner Following-Timeline ausblenden, um doppelte Beiträge zu vermeiden.</string>\n    <string name=\"activity_pub_add_list_remove_user_message\">Möchtest du diesen Nutzer wirklich entfernen?</string>\n    <string name=\"activity_pub_add_list_name_is_empty\">Bitte einen Listennamen eingeben</string>\n    <string name=\"activity_pub_add_list_back_reminder\">Du hast ungespeicherte Änderungen. Wirklich verlassen?</string>\n    <string name=\"activity_pub_search_user_placeholder\">Zum Suchen tippen</string>\n    <string name=\"activity_pub_list_delete_confirm\">Möchtest du diese Liste wirklich löschen?</string>\n    <string name=\"activity_pub_login_exception\">OAuth-Statusfehler!</string>\n    <string name=\"activity_pub_home_timeline\">Startseite</string>\n    <string name=\"activity_pub_local_timeline\">Lokal</string>\n    <string name=\"activity_pub_public_timeline\">Öffentlich</string>\n    <string name=\"activity_pub_instance_detail_language_label\">Sprache: %1$s</string>\n    <string name=\"activity_pub_instance_detail_active_month_label\">Monatlich aktiv: %1$s</string>\n    <string name=\"activity_pub_user_detail_tab_post\">Beiträge</string>\n    <string name=\"activity_pub_user_detail_tab_replies\">Antworten</string>\n    <string name=\"activity_pub_user_detail_tab_media\">Medien</string>\n    <string name=\"activity_pub_user_detail_tab_about\">Über</string>\n    <string name=\"activity_pub_about\">Über</string>\n    <string name=\"activity_pub_about_rule_title\">Regeln</string>\n    <string name=\"activity_pub_trends_tag\">Trend-Tags</string>\n    <string name=\"activity_pub_trends_tag_description\">%1$s Personen in den letzten 2 Tagen</string>\n    <string name=\"activity_pub_trends_status\">Beliebt</string>\n    <string name=\"activity_pub_select_platform_title\">Instanz auswählen</string>\n    <string name=\"activity_pub_select_platform_text_hint\">Instanzname oder URL eingeben</string>\n</resources>\n"
  },
  {
    "path": "localization/src/commonMain/composeResources/values-de-rDE/strings_status_ui.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"status_ui_reposted\">geboostet</string>\n    <string name=\"status_ui_repost\">Boost</string>\n    <string name=\"status_ui_quote\">Zitieren</string>\n    <string name=\"status_ui_like\">Gefällt mir</string>\n    <string name=\"status_ui_comment\">Kommentieren</string>\n    <string name=\"status_ui_delete\">Löschen</string>\n    <string name=\"status_ui_share\">Teilen</string>\n    <string name=\"status_ui_bookmark\">Lesezeichen</string>\n    <string name=\"status_ui_unbookmark\">Lesezeichen entfernen</string>\n    <string name=\"status_ui_reply\">Antworten</string>\n    <string name=\"status_ui_follow\">Folgen</string>\n    <string name=\"status_ui_unfollow\">Entfolgen</string>\n    <string name=\"status_ui_pin\">Anheften</string>\n    <string name=\"status_ui_unpin\">Loslösen</string>\n    <string name=\"status_ui_edit\">Bearbeiten</string>\n    <string name=\"status_ui_sensitive_by_filter\">Entspricht Filter „%1$s“</string>\n    <string name=\"status_ui_new_status\">Neuer Inhalt</string>\n    <string name=\"status_ui_delete_status_confirm\">Möchtest du diesen Inhalt wirklich löschen?</string>\n    <string name=\"status_ui_image_sensitive_label\">Medien ausgeblendet</string>\n    <string name=\"status_ui_image_content_show_hidden_label\">Mehr anzeigen</string>\n    <string name=\"status_ui_image_content_hide_hidden_label\">Inhalt ausblenden</string>\n    <string name=\"status_ui_poll_vote\">Abstimmen</string>\n    <string name=\"status_ui_poll_vote_finished_tip\">%1$s Stimme · Geschlossen</string>\n    <string name=\"status_ui_poll_votes_finished_tip\">%1$s Stimmen · Geschlossen</string>\n    <string name=\"status_ui_interaction_open_in_browser\">Im Browser öffnen</string>\n    <string name=\"status_ui_interaction_open_original_instance\">Andere Instanz öffnen</string>\n    <string name=\"status_ui_interaction_copy_url\">Link kopieren</string>\n    <string name=\"status_ui_interaction_translate\">Übersetzen</string>\n    <string name=\"status_ui_interaction_open_blog_by_other_account\">Mit anderem Konto öffnen</string>\n    <string name=\"status_ui_visibility_mentioned_only\">Nur Erwähnte</string>\n    <string name=\"status_ui_label_pinned\">Angeheftet</string>\n    <string name=\"status_ui_info_label_edited\">Bearbeitet</string>\n    <string name=\"status_ui_interaction_label_favourited_count\">%1$s Favoriten</string>\n    <string name=\"status_ui_interaction_label_boosted_count\">%1$s Boosts</string>\n    <string name=\"status_ui_bottom_label_edited_at\">Bearbeitet am %1$s</string>\n    <string name=\"status_ui_translating\">Übersetze…</string>\n    <string name=\"status_ui_translate_show_original\">Original anzeigen</string>\n    <string name=\"status_ui_edit_content_delete_dialog_content\">Diesen Inhalt wirklich löschen?</string>\n    <string name=\"status_ui_edit_content_name_title\">Inhaltsnamen bearbeiten</string>\n    <string name=\"status_ui_edit_content_name_label\">Inhaltsnamen bearbeiten</string>\n    <string name=\"status_ui_edit_content_name_hint\">Bitte den Namen des Inhalts eingeben</string>\n    <string name=\"status_ui_edit_content_config_showing_list_title\">Feeds auf der Startseite</string>\n    <string name=\"status_ui_edit_content_config_showing_list_description\">Zum Sortieren lange drücken und ziehen</string>\n    <string name=\"status_ui_edit_content_config_hidden_list_title\">Feeds, die zur Startseite hinzugefügt werden können</string>\n    <string name=\"status_ui_user_detail_relationship_mutuals\">Gegenseitig</string>\n    <string name=\"status_ui_user_detail_relationship_blocking\">Blockiert</string>\n    <string name=\"status_ui_user_detail_relationship_following\">Folge ich</string>\n    <string name=\"status_ui_user_detail_relationship_not_follow\">Folgen</string>\n    <string name=\"status_ui_user_detail_relationship_requested\">Ausstehend</string>\n    <string name=\"status_ui_user_detail_relationship_follow_back\">Zurückfolgen</string>\n    <string name=\"status_ui_user_detail_request_by_tip\">Diese Person möchte dir folgen</string>\n    <string name=\"status_ui_relationship_btn_dialog_content_cancel_follow\">Möchtest du dieser Person nicht mehr folgen?</string>\n    <string name=\"status_ui_relationship_btn_dialog_content_cancel_follow_request\">Möchtest du deine Folgeanfrage zurückziehen?</string>\n    <string name=\"status_ui_relationship_btn_dialog_content_cancel_blocking\">Möchtest du diesen Nutzer wirklich entblocken?</string>\n    <string name=\"status_ui_user_detail_follows_you\">Folgt dir</string>\n    <string name=\"status_ui_user_detail_posts\">Beiträge</string>\n    <string name=\"status_ui_user_detail_follower_info\">Follower</string>\n    <string name=\"status_ui_user_detail_following_info\">Folgt</string>\n    <string name=\"status_ui_top_label_continued_thread\">Gesamten Thread anzeigen</string>\n    <string name=\"status_ui_update_dialog_title\">Aktualisieren</string>\n    <string name=\"status_ui_update_dialog_release_note\">Versionshinweise %1$s:\\n%2$s</string>\n    <string name=\"status_ui_switch_account_dialog_title\">Konto auswählen</string>\n    <string name=\"status_ui_likes\">Likes</string>\n    <string name=\"status_ui_bookmarks\">Lesezeichen</string>\n    <string name=\"status_ui_edit_profile\">Profil bearbeiten</string>\n    <string name=\"status_ui_logout\">Abmelden</string>\n    <string name=\"status_ui_logout_dialog_content\">Möchtest du dich wirklich abmelden?</string>\n    <string name=\"status_ui_search_account_status_hint\">Beiträge suchen</string>\n    <string name=\"shared_notification_unknown_desc\">Unbekannte Benachrichtigung</string>\n    <string name=\"shared_notification_favourited_desc\">hat deinen Beitrag favorisiert</string>\n    <string name=\"shared_notification_reblog_desc\">hat deinen Beitrag geboostet</string>\n    <string name=\"shared_notification_poll_desc\">Vorherige Abstimmungsergebnisse anzeigen</string>\n    <string name=\"shared_notification_poll_count\">%1$s Stimmen · Geschlossen</string>\n    <string name=\"shared_notification_follow_desc\">folgt dir</string>\n    <string name=\"shared_notification_update_desc\">hat einen Beitrag bearbeitet</string>\n    <string name=\"shared_notification_follow_request\">Du hast eine Folgeanfrage erhalten</string>\n    <string name=\"shared_notification_new_status_desc\">hat einen neuen Beitrag veröffentlicht</string>\n    <string name=\"shared_notification_severed_desc\">hat die Verbindung zu dir beendet</string>\n    <string name=\"shared_notification_quote_desc\">hat deinen Beitrag zitiert</string>\n    <string name=\"shared_notification_reply_desc\">hat auf deinen Beitrag geantwortet</string>\n    <string name=\"shared_user_list_title_following\">Folge ich</string>\n    <string name=\"shared_user_list_title_followers\">Follower</string>\n    <string name=\"shared_user_list_title_mutes\">Stummgeschaltete Nutzer</string>\n    <string name=\"shared_user_list_title_blocks\">Blockierte Nutzer</string>\n    <string name=\"shared_user_list_title_likes\">Favorisiert</string>\n    <string name=\"shared_user_list_title_reblog\">Geboostet</string>\n    <string name=\"shared_user_list_action_mute\">Stummschalten</string>\n    <string name=\"shared_user_list_action_block\">Blockieren</string>\n    <string name=\"shared_user_list_action_muted\">Stummgeschaltet</string>\n    <string name=\"shared_user_list_action_blocked\">Blockiert</string>\n    <string name=\"shared_user_list_action_mute_dialog_message\">Möchtest du diesen Nutzer wirklich stummschalten?</string>\n    <string name=\"shared_user_list_action_block_dialog_message\">Möchtest du diesen Nutzer wirklich blockieren?</string>\n    <string name=\"shared_publish_blog_title\">Posten</string>\n    <string name=\"shared_publish_blog_text_hint\">Schreibe, was dir durch den Kopf geht</string>\n    <string name=\"shared_publish_reply_input_hint\">Antwort an [[%1$s]]</string>\n    <string name=\"shared_publish_interaction_no_limit\">Jeder kann interagieren</string>\n    <string name=\"shared_publish_interaction_limited\">Interaktion eingeschränkt</string>\n    <string name=\"shared_publish_interaction_dialog_title\">Interaktionseinstellungen</string>\n    <string name=\"shared_publish_interaction_dialog_subtitle\">Bestimme, wer mit diesem Beitrag interagieren kann.</string>\n    <string name=\"shared_publish_interaction_dialog_quote_title\">Zitat-Einstellungen</string>\n    <string name=\"shared_publish_interaction_dialog_quote_allow\">Zitieren erlauben</string>\n    <string name=\"shared_publish_interaction_dialog_reply_title\">Antwort-Einstellungen</string>\n    <string name=\"shared_publish_interaction_dialog_reply_subtitle\">Antworten erlauben von:</string>\n    <string name=\"shared_publish_interaction_dialog_reply_all\">Alle</string>\n    <string name=\"shared_publish_interaction_dialog_reply_nobody\">Niemand</string>\n    <string name=\"shared_publish_interaction_dialog_reply_combine_title\">Oder kombiniere diese Optionen:</string>\n    <string name=\"shared_publish_interaction_dialog_mentioned\">Erwähnte Nutzer</string>\n    <string name=\"shared_publish_interaction_dialog_following\">Nutzer, denen du folgst</string>\n    <string name=\"shared_publish_interaction_dialog_follower\">Deine Follower</string>\n    <string name=\"shared_publish_interaction_dialog_in_list\">Nutzer in „%1$s“</string>\n    <string name=\"shared_publish_media_alt_dialog_title\">Alt-Text hinzufügen</string>\n    <string name=\"shared_publish_media_alt_dialog_input_tip\">Beschreibender Alt-Text</string>\n    <string name=\"shared_publish_media_alt_dialog_input_hint\">Alt-Text</string>\n    <string name=\"shared_feeds_not_login_title\">Hoppla! Deine Anmeldedaten sind abgelaufen. Bitte erneut anmelden.</string>\n    <string name=\"shared_feeds_go_to_login\">Anmelden</string>\n    <string name=\"shared_publish_select_account_title\">Bitte Konto auswählen</string>\n    <string name=\"shared_select_language_title\">Sprache auswählen</string>\n    <string name=\"shared_status_context_screen_title\">Details</string>\n    <string name=\"shared_alt_label\">ALT</string>\n    <string name=\"status_ui_embed_quote_unavailable\">Das aktuelle Zitat ist nicht verfügbar</string>\n    <string name=\"status_ui_action_unforward\">Weiterleitung aufheben</string>\n    <string name=\"status_ui_quote_approval_public\">Jeder</string>\n    <string name=\"status_ui_quote_approval_follower\">Nur Follower</string>\n    <string name=\"status_ui_quote_approval_nobody\">Nur ich</string>\n    <string name=\"status_ui_visibility\">Sichtbarkeit</string>\n</resources>\n"
  },
  {
    "path": "localization/src/commonMain/composeResources/values-es-rES/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"skip\">Saltar</string>\n    <string name=\"alert\">Alerta</string>\n    <string name=\"duration_minute\">minuto</string>\n    <string name=\"duration_hour\">hora</string>\n    <string name=\"duration_day\">día</string>\n    <string name=\"duration_week\">semana</string>\n    <string name=\"cancel\">Cancelar</string>\n    <string name=\"ok\">Sí</string>\n    <string name=\"empty\">Este espacio está vacío～</string>\n    <string name=\"duration_selector_title\">Establecer duración</string>\n    <string name=\"retry\">Intentar de nuevo</string>\n    <string name=\"load_more_error\">¡Ups! No se pudo cargar.</string>\n    <string name=\"unknown_error\">Error desconocido</string>\n    <string name=\"network_error\">Error de red</string>\n    <string name=\"resource_not_found\">Recurso no encontrado</string>\n    <string name=\"mixed_content_subtitle_1\">Contenido mixto ·</string>\n    <string name=\"mixed_content_subtitle_2\">Fuentes</string>\n    <string name=\"date_time_ago\">&#160;hace</string>\n    <string name=\"date_time_ago_prefix\">Hace&#160;</string>\n    <string name=\"date_time_ago_suffix\"></string>\n    <string name=\"date_time_day\">día</string>\n    <string name=\"date_time_hour\">hora</string>\n    <string name=\"date_time_minute\">min</string>\n    <string name=\"date_time_second\">seg</string>\n    <string name=\"image_saving\">Guardando…</string>\n    <string name=\"image_save_success\">Guardado correctamente</string>\n    <string name=\"image_save_failed\">Error al guardar</string>\n    <string name=\"permission_write_external_permission_denied\">Permiso de almacenamiento denegado. Actívalo en los ajustes.</string>\n    <string name=\"feeds_load_previous_page_label\">Cargando contenido de la página anterior</string>\n    <string name=\"feeds_load_previous_page_failed_label\">Error al cargar, toca para reintentar</string>\n    <string name=\"image\">Imagen</string>\n    <string name=\"video\">Vídeo</string>\n    <string name=\"login\">Iniciar sesión</string>\n    <string name=\"add\">Añadir</string>\n    <string name=\"search\">Buscar</string>\n    <string name=\"auth_success\">Inicio de sesión correcto</string>\n    <string name=\"auth_failed\">Error de inicio de sesión</string>\n    <string name=\"select\">Seleccionar</string>\n    <string name=\"logout\">Cerrar sesión</string>\n    <string name=\"settings\">Ajustes</string>\n    <string name=\"donate\">Donar</string>\n    <string name=\"feeds\">Feeds</string>\n    <string name=\"save\">Guardar</string>\n    <string name=\"done\">¡Todo listo!</string>\n    <string name=\"bluesky_name\">Bluesky</string>\n    <string name=\"bluesky_description\">Bluesky es una red abierta. Con una sola cuenta accedes a una red social fácil de usar y mantienes una identidad compartida en todo el internet social.</string>\n    <string name=\"mastodon_name\">Mastodon</string>\n    <string name=\"mastodon_description\">Mastodon es una red social descentralizada y sin fines de lucro donde las personas controlan su cronología; cada servidor es una entidad independiente.</string>\n    <string name=\"mixed_content_name\">Contenido mixto</string>\n    <string name=\"mixed_content_description\">Mixed Feed reúne todo tu contenido. Añade fuentes de Mastodon, Bluesky, RSS y más: Fread las combina en una única línea temporal cronológica.</string>\n    <string name=\"status_provider_type_activity_pub\">Mastodon</string>\n    <string name=\"add_content_title\">Añadir contenido</string>\n    <string name=\"add_content_success_with_account\">Añadido correctamente. Cuenta actual:</string>\n    <string name=\"add_content_success_with_login_reminder\">Añadido correctamente. ¿Quieres iniciar sesión ahora?</string>\n    <string name=\"content_exist_tips\">¡Ups! Este contenido ya existe.</string>\n    <string name=\"content_add_success\">¡Listo! Tu contenido está preparado.</string>\n    <string name=\"add_feeds_page_empty_name_exist\">Este nombre de usuario ya está en uso.</string>\n    <string name=\"edit_profile_name_empty\">Introduce tu nombre de usuario</string>\n    <string name=\"edit_profile_label_name\">Nombre</string>\n    <string name=\"edit_profile_label_note\">Bio</string>\n    <string name=\"edit_profile_input_name_hint\">Introduce tu nombre de usuario</string>\n    <string name=\"edit_profile_input_note_hint\">Introduce tu bio</string>\n    <string name=\"edit_profile_edit_confirm_message\">¿Seguro que quieres salir de la edición?</string>\n    <string name=\"add_content_success_snackbar\">Añadido correctamente</string>\n    <string name=\"login_dialog_input_hint\">Introduce el servidor al que iniciar sesión</string>\n    <string name=\"login_dialog_title\">Selecciona el servidor para iniciar sesión</string>\n    <string name=\"login_dialog_input_tip\">Introduce el host del servidor</string>\n    <string name=\"login_dialog_input_title\">Introduce el host del servidor</string>\n    <string name=\"login_dialog_input_label\">Host del servidor</string>\n    <string name=\"login_dialog_target_title\">Inicia sesión</string>\n    <string name=\"profile_description\">Fread permite añadir varias cuentas. Con ellas puedes publicar, dar me gusta, reenviar, comentar, etc. También puedes ver las listas creadas por esas cuentas.</string>\n    <string name=\"list_content_empty_placeholder\">No hay contenido disponible</string>\n    <string name=\"post_status_scope_public\">Público</string>\n    <string name=\"post_status_scope_unlisted\">Público, pero excluido del descubrimiento</string>\n    <string name=\"post_status_scope_follower_only\">Solo seguidores</string>\n    <string name=\"post_status_scope_mentioned_only\">Solo personas mencionadas</string>\n    <string name=\"post_status_content_warning\">Aviso de contenido</string>\n    <string name=\"post_status_success\">Enviado correctamente</string>\n    <string name=\"post_status_failed\">Error al publicar: %1$s</string>\n    <string name=\"post_status_exit_dialog_content\">Se perderán los cambios no guardados. ¿Seguro que quieres salir?</string>\n    <string name=\"post_status_content_is_empty\">Introduce contenido</string>\n    <string name=\"post_status_part_failed\">Algunas cuentas no pudieron publicar. Las que sí se publicaron se han excluido. Inténtalo de nuevo con las restantes: %1$s</string>\n    <string name=\"select_account_open_status_title\">Selecciona una cuenta</string>\n    <string name=\"select_account_open_status_empty\">No hay otras cuentas</string>\n    <string name=\"select_account_open_status_search_in\">Buscar en %1$s</string>\n    <string name=\"select_account_open_status_search_failed\">Error de búsqueda</string>\n    <string name=\"explorer_search_no_results\">No se encontraron resultados</string>\n    <string name=\"explorer_search_bar_hint\">Buscar</string>\n    <string name=\"explorer_search_bar_hint_specialize_platform\">Buscar en %1$s</string>\n    <string name=\"explorer_search_tab_title_author\">Cuentas</string>\n    <string name=\"explorer_search_tab_title_status\">Estados</string>\n    <string name=\"explorer_search_tab_title_hashtag\">Hashtags</string>\n    <string name=\"explorer_search_tab_title_server\">Servidor</string>\n    <string name=\"explorer_tab_title\">Explorar</string>\n    <string name=\"explorer_tab_status_title\">Publicaciones</string>\n    <string name=\"explorer_tab_users_title\">Usuarios</string>\n    <string name=\"explorer_tab_hashtag_title\">Hashtags</string>\n    <string name=\"add_provider_page_title\">Añadir servidor</string>\n    <string name=\"add_provider_page_confirm_button\">Confirmar añadido</string>\n    <string name=\"search_page_title\">Buscar</string>\n    <string name=\"search_result_not_found\">No se encontraron resultados</string>\n    <string name=\"feeds_import_page_title\">Importar</string>\n    <string name=\"feeds_import_page_hint\">Selecciona el archivo OPML a importar</string>\n    <string name=\"feeds_import_button\">Importar</string>\n    <string name=\"add_feeds_page_title\">Añadir contenido</string>\n    <string name=\"add_feeds_page_feeds_name_label\">Nombre del contenido</string>\n    <string name=\"add_feeds_page_feeds_name_hint\">Introduce el nombre del contenido</string>\n    <string name=\"add_feeds_page_feeds_empty\">Añade una fuente de suscripción</string>\n    <string name=\"pre_add_feeds_no_result\">No se encontraron resultados</string>\n    <string name=\"pre_add_feeds_hint\">Nombre de instancia, URL, URL de inicio del usuario, RSS</string>\n    <string name=\"pre_add_feeds_input_label_1\">Fread admite [[Mastodon]]/[[Bluesky]] así como fuentes [[RSS]] e incluso [[contenido mixto]].</string>\n    <string name=\"pre_add_feeds_input_label_2\">Introduce aquí el [[NickName]] de Mastodon o Bluesky, el [[WebFinger\\Handle]], la [[URL de inicio]] del usuario o la URL del feed [[RSS]].</string>\n    <string name=\"empty_content_hint_title\">Añade contenido primero</string>\n    <string name=\"empty_content_hint_desc\">Primero debes añadir algunas fuentes. Fread admite distintos tipos de contenido y de plataformas para que construyas tu propio feed.</string>\n    <string name=\"feeds_add_content\">Añadir contenido</string>\n    <string name=\"select_feeds_type_screen_title\">Seleccionar tipo de contenido</string>\n    <string name=\"feeds_pre_add_login_dialog_content\">Contenido añadido. ¿Quieres iniciar sesión ahora?</string>\n    <string name=\"add_feeds_page_empty_name_tips\">Introduce un nombre</string>\n    <string name=\"add_feeds_page_empty_source_tips\">Añade una fuente de suscripción</string>\n    <string name=\"add_feeds_choose_auth_dialog_title\">Selecciona el servidor de inicio de sesión</string>\n    <string name=\"search_feeds_title\">Buscar</string>\n    <string name=\"search_feeds_title_hint\">NickName de Mastodon o Bluesky, WebFinger, URL, RSS</string>\n    <string name=\"feeds_select_account_for_post_status\">Selecciona una cuenta</string>\n    <string name=\"feeds_mixed_config_edit_new_name_dialog_title\">Introduce el nombre</string>\n    <string name=\"feeds_mixed_config_edit_new_name_dialog_label\">Nombre</string>\n    <string name=\"feeds_mixed_config_edit_delete_content_dialog_message\">¿Seguro que quieres eliminar este feed?</string>\n    <string name=\"feeds_mixed_config_not_found\">Estado anómalo, configuración no encontrada.</string>\n    <string name=\"feeds_import_back_dialog_message\">¿Seguro que quieres salir?</string>\n    <string name=\"feeds_delete_confirm_content\">¿Seguro que quieres eliminarlo?</string>\n    <string name=\"feeds_select_type_screen_title\">Elige un formato</string>\n    <string name=\"feeds_select_type_screen_content\">¿Qué te gustaría añadir?</string>\n    <string name=\"notification_tab_title\">Notificaciones</string>\n    <string name=\"notifications_account_empty_tip\">No hay ninguna cuenta iniciada</string>\n    <string name=\"notifications_tab_all\">Todas</string>\n    <string name=\"notifications_tab_mention\">Menciones</string>\n    <string name=\"profile_page_title\">Perfil</string>\n    <string name=\"profile_setting_about_title\">Acerca de</string>\n    <string name=\"profile_setting_donate_desc\">Invítame a un café para apoyar este proyecto.</string>\n    <string name=\"profile_setting_inline_video_auto_play\">Reproducción automática en línea</string>\n    <string name=\"profile_setting_inline_video_auto_play_subtitle\">Si está activado, los vídeos del feed se reproducen automáticamente.</string>\n    <string name=\"profile_setting_always_show_sensitive_content\">Mostrar contenido sensible por defecto</string>\n    <string name=\"profile_setting_always_show_sensitive_content_subtitle\">Si está activado, el contenido sensible se mostrará por defecto (solo Mastodon).</string>\n    <string name=\"profile_setting_dark_mode_title\">Modo oscuro</string>\n    <string name=\"profile_setting_dark_mode_dark\">Modo oscuro</string>\n    <string name=\"profile_setting_dark_mode_light\">Modo claro</string>\n    <string name=\"profile_setting_dark_mode_follow_system\">Seguir el sistema</string>\n    <string name=\"profile_setting_open_source_title\">Código abierto</string>\n    <string name=\"profile_setting_open_source_desc\">Licencias de código abierto de las dependencias usadas en este proyecto</string>\n    <string name=\"profile_setting_open_source_feedback\">Envíanos tus comentarios</string>\n    <string name=\"profile_setting_open_source_feedback_desc\">Únete al grupo de Telegram u otros medios</string>\n    <string name=\"profile_setting_open_source_feedback_telegram\">Únete al grupo de Telegram</string>\n    <string name=\"profile_setting_open_source_feedback_github\">Crear issue en GitHub</string>\n    <string name=\"profile_setting_open_source_feedback_email\">Enviar correo</string>\n    <string name=\"profile_setting_language_title\">Idioma de visualización</string>\n    <string name=\"profile_setting_language_zh\">Chino simplificado</string>\n    <string name=\"profile_setting_language_en\">Inglés</string>\n    <string name=\"profile_setting_language_system\">Seguir el sistema</string>\n    <string name=\"profile_setting_ratting\">Califícanos</string>\n    <string name=\"profile_setting_ratting_desc\">Si te gusta, danos una buena valoración.</string>\n    <string name=\"profile_setting_font_size\">Tamaño de fuente del contenido</string>\n    <string name=\"profile_setting_font_size_desc\">Ajusta el tamaño de la fuente y de los iconos del contenido</string>\n    <string name=\"profile_setting_font_size_small\">Pequeño</string>\n    <string name=\"profile_setting_font_size_medium\">Mediano</string>\n    <string name=\"profile_setting_font_size_large\">Grande</string>\n    <string name=\"profile_setting_immersive_nav_bar\">Barra de navegación inmersiva</string>\n    <string name=\"profile_setting_immersive_nav_bar_desc\">La barra de navegación inferior se oculta automáticamente al desplazarte por la página principal.</string>\n    <string name=\"profile_setting_timeline_position\">Posición predeterminada de la cronología</string>\n    <string name=\"profile_setting_timeline_position_last_read\">Donde me quedé</string>\n    <string name=\"profile_setting_timeline_position_newest\">Publicaciones más recientes</string>\n    <string name=\"profile_setting_theme_title\">Color del tema</string>\n    <string name=\"profile_setting_theme_default\">Predeterminado</string>\n    <string name=\"profile_setting_theme_system\">Seguir el sistema</string>\n    <string name=\"profile_about_version\">Versión:&#160;</string>\n    <string name=\"profile_about_website\">Sitio web:&#160;</string>\n    <string name=\"profile_about_developer\">Creado por&#160;</string>\n    <string name=\"profile_about_contract_us\">Contáctame en:&#160;</string>\n    <string name=\"profile_about_telegram\">Grupo de Telegram:&#160;</string>\n    <string name=\"profile_about_privacy_policy\">Política de privacidad:&#160;</string>\n    <string name=\"profile_setting_check_for_update\">Buscar actualizaciones</string>\n    <string name=\"profile_setting_already_latest_version\">Tu app ya está actualizada</string>\n    <string name=\"profile_setting_have_new_version\">Nueva versión disponible. Pulsa para actualizar.</string>\n    <string name=\"profile_donate_page_title\">Selecciona cómo te gustaría donar</string>\n    <string name=\"profile_account_not_login\">Sesión caducada</string>\n    <string name=\"rss_source_detail_screen_title\">Atribución de la fuente</string>\n    <string name=\"rss_source_detail_screen_custom_title\">Título personalizado</string>\n    <string name=\"rss_source_detail_screen_url\">Enlace de suscripción</string>\n    <string name=\"rss_source_detail_screen_home_url\">Página de inicio</string>\n    <string name=\"rss_source_detail_screen_add_date\">Fecha de añadido</string>\n    <string name=\"rss_source_detail_screen_last_update_date\">Última actualización</string>\n    <string name=\"bluesky_protocol_name\">Bluesky</string>\n    <string name=\"bsky_add_content_title\">Iniciar sesión</string>\n    <string name=\"bsky_add_content_hosting_provider\">Proveedor de alojamiento</string>\n    <string name=\"bsky_add_content_user_name\">Nombre de usuario o correo electrónico</string>\n    <string name=\"bsky_add_content_password\">Contraseña</string>\n    <string name=\"bsky_add_content_factor_token\">Código de confirmación</string>\n    <string name=\"bsky_edit_content_title\">Editar contenido</string>\n    <string name=\"bsky_feeds_explorer_more\">Descubre más feeds</string>\n    <string name=\"bsky_feeds_explorer_creator_label\">Por %1$s</string>\n    <string name=\"bsky_feeds_explorer_liked_by\">Le gusta a %1$s usuarios</string>\n    <string name=\"bsky_feeds_detail_creator_prefix\">Por</string>\n    <string name=\"bsky_feeds_item_subtitle\">Por %1$s · Le gusta a %2$s usuarios</string>\n    <string name=\"bsky_feeds_following_name\">Siguiendo</string>\n    <string name=\"bsky_feeds_user_posts\">Publicaciones</string>\n    <string name=\"bsky_feeds_user_replies\">Respuestas</string>\n    <string name=\"bsky_feeds_user_medias\">Medios</string>\n    <string name=\"bsky_feeds_user_likes\">Me gusta</string>\n    <string name=\"bsky_user_detail_action_blocked_list\">Usuarios bloqueados</string>\n    <string name=\"bsky_user_detail_action_muted_list\">Usuarios silenciados</string>\n    <string name=\"bsky_user_detail_action_mute_user\">Silenciar a %1$s</string>\n    <string name=\"bsky_user_detail_action_unmute_user\">Quitar silencio</string>\n    <string name=\"bsky_user_detail_action_block_user\">Bloquear a %1$s</string>\n    <string name=\"bsky_user_detail_action_unblock_user\">Desbloquear</string>\n    <string name=\"bsky_user_detail_action_block_user_dialog_message\">¿Quieres bloquear a este usuario?</string>\n    <string name=\"bsky_user_detail_action_mute_user_dialog_message\">¿Quieres silenciar a este usuario?</string>\n    <string name=\"bsky_edit_profile_title\">Editar perfil</string>\n    <string name=\"input_media_desc_page_title\">Descripción de la imagen</string>\n    <string name=\"input_media_desc_input_hint\">Introduce la descripción de la imagen</string>\n    <string name=\"post_status_page_title\">Nueva publicación</string>\n    <string name=\"post_screen_input_hint\">Escribe tu publicación</string>\n    <string name=\"post_screen_media_descriptor_placeholder\">Texto descriptivo</string>\n    <string name=\"post_status_poll_item_hint\">Opción %1$s</string>\n    <string name=\"post_status_poll_duration\">Duración de la votación</string>\n    <string name=\"post_status_poll_function_title\">Función</string>\n    <string name=\"post_status_poll_single\">Selección única</string>\n    <string name=\"post_status_poll_multiple\">Selección múltiple</string>\n    <string name=\"post_status_poll_style_select_dialog_title\">Selecciona el estilo</string>\n    <string name=\"post_status_poll_is_empty\">Introduce el contenido de la votación</string>\n    <string name=\"post_status_media_is_not_upload\">Los medios aún no se han subido</string>\n    <string name=\"authentication_page_normal_title\">Esta función requiere inicio de sesión</string>\n    <string name=\"authentication_page_failed_title\">Estado de inicio de sesión no válido. Vuelve a iniciar sesión.</string>\n    <string name=\"main_drawer_title\">Lista de contenidos</string>\n    <string name=\"main_drawer_mixed_item_subtitle_1\">Contenido mixto ·</string>\n    <string name=\"main_drawer_mixed_item_subtitle_2\">Fuentes</string>\n    <string name=\"main_drawer_settings\">Ajustes</string>\n    <string name=\"main_drawer_donate\">Donar</string>\n    <string name=\"main_request_notification_dialog_title\">Notificaciones</string>\n    <string name=\"main_request_notification_dialog_reject_button\">Rechazar</string>\n    <string name=\"main_request_notification_dialog_allow_button\">Permitir</string>\n    <string name=\"main_request_notification_dialog_content\">Para recibir mensajes de interacción, permite las notificaciones.</string>\n    <string name=\"setting_item_amoled_mode\">Modo AMOLED</string>\n    <string name=\"setting_item_amoled_mode_description\">Al activarlo, el fondo cambiará a negro puro en modo oscuro para reducir el consumo de energía y mejorar el contraste en entornos oscuros.</string>\n    <string name=\"setting_group_appearance\">Apariencia</string>\n    <string name=\"setting_group_appearance_subtitle\">Ajustes de tema, visualización y estilo de Inicio</string>\n    <string name=\"setting_group_behavior\">Comportamiento</string>\n    <string name=\"setting_group_behavior_subtitle\">Ajustes de visualización de contenido y comportamiento de interacción</string>\n    <string name=\"setting_item_home_tab_next_title\">Mostrar el botón para cambiar de contenido en la parte superior de Inicio</string>\n    <string name=\"setting_item_home_tab_next_subtitle\">Mostrar en la esquina superior derecha de Inicio el botón para cambiar al siguiente contenido</string>\n    <string name=\"setting_item_home_tab_refresh_title\">Mostrar el botón de actualizar en la parte superior de Inicio</string>\n    <string name=\"setting_item_home_tab_refresh_subtitle\">Mostrar en la esquina superior derecha de Inicio el botón para actualizar el contenido</string>\n    <string name=\"setting_item_open_url_by_system_title\">Abrir enlaces con el navegador del sistema</string>\n    <string name=\"setting_item_open_url_by_system_subtitle\">Al activarlo, Fread abrirá los enlaces con el navegador del sistema.</string>\n    <string name=\"setting_item_solid_bar_background_title\">Fondo sólido para las barras</string>\n    <string name=\"setting_item_solid_bar_background_subtitle\">Hace que las barras superior e inferior sean sólidas en lugar de desenfocadas.</string>\n</resources>\n"
  },
  {
    "path": "localization/src/commonMain/composeResources/values-es-rES/strings_mastodon.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"activity_pub_user_detail_join_date\">Se unió el</string>\n    <string name=\"activity_pub_user_detail_tab_about_joined\">Se unió el %1$s</string>\n    <string name=\"activity_pub_user_detail_menu_block\">Bloquear a %1$s</string>\n    <string name=\"activity_pub_user_detail_menu_block_domain\">Bloquear %1$s</string>\n    <string name=\"activity_pub_user_detail_menu_unblock_domain\">Desbloquear %1$s</string>\n    <string name=\"activity_pub_user_detail_menu_edit_private_note\">Editar nota</string>\n    <string name=\"activity_pub_user_detail_menu_edit_private_note_dialog_hint\">Introduce una nota privada</string>\n    <string name=\"activity_pub_user_detail_dialog_content_block\">¿Quieres bloquear a este usuario?</string>\n    <string name=\"activity_pub_user_detail_dialog_content_block_domain\">¿Quieres bloquear este dominio?</string>\n    <string name=\"activity_pub_user_menu_blocked_user_list\">Usuarios bloqueados</string>\n    <string name=\"activity_pub_user_menu_muted_user_list\">Usuarios silenciados</string>\n    <string name=\"activity_pub_user_detail_menu_mute_user\">Silenciar a %1$s</string>\n    <string name=\"activity_pub_user_detail_menu_unmute_user\">Quitar silencio a %1$s</string>\n    <string name=\"activity_pub_mute_user_bottom_sheet_title\">¿Silenciar usuario?</string>\n    <string name=\"activity_pub_mute_user_bottom_sheet_role1\">No sabrán que han sido silenciados.</string>\n    <string name=\"activity_pub_mute_user_bottom_sheet_role2\">Podrán ver tus publicaciones, pero tú no verás las suyas.</string>\n    <string name=\"activity_pub_mute_user_bottom_sheet_role3\">No verás publicaciones que los mencionen.</string>\n    <string name=\"activity_pub_mute_user_bottom_sheet_role4\">Podrán mencionarte y seguirte, pero no los verás.</string>\n    <string name=\"activity_pub_mute_user_bottom_sheet_btn_mute\">Silenciar</string>\n    <string name=\"add_instance_screen_title\">Añadir instancia</string>\n    <string name=\"add_instance_input_service_hint\">Introduce la dirección del servidor</string>\n    <string name=\"add_instance_input_service_error\">Introduce la dirección correcta del servidor</string>\n    <string name=\"activity_pub_content_tab_home\">Inicio</string>\n    <string name=\"activity_pub_content_tab_local_timeline\">Local</string>\n    <string name=\"activity_pub_content_tab_public_timeline\">Público</string>\n    <string name=\"activity_pub_content_tab_trending\">Tendencias</string>\n    <string name=\"activity_pub_hashtag_timeline_description\">%1$s publicaciones · %2$s participantes · %3$s publicación hoy</string>\n    <string name=\"activity_pub_hashtag_unfollow_dialog_message\">¿Seguro que quieres dejar de seguir este hashtag?</string>\n    <string name=\"activity_pub_edit_account_info_label_about\">Acerca de</string>\n    <string name=\"activity_pub_edit_content_screen_config_not_found\">Configuración no encontrada</string>\n    <string name=\"activity_pub_user_list_empty\">Sin usuarios por ahora</string>\n    <string name=\"activity_pub_muted_user_list_empty\">No hay usuarios silenciados</string>\n    <string name=\"activity_pub_muted_user_list_unmute\">Quitar silencio</string>\n    <string name=\"activity_pub_blocked_user_list_empty\">No hay usuarios bloqueados</string>\n    <string name=\"activity_pub_blocked_user_list_unblock\">Desbloquear</string>\n    <string name=\"activity_pub_bookmarks_list_title\">Marcadores</string>\n    <string name=\"activity_pub_favourites_list_title\">Favoritos</string>\n    <string name=\"activity_pub_followed_tags_screen_title\">Hashtags seguidos</string>\n    <string name=\"activity_pub_filters_list_page_title\">Filtros</string>\n    <string name=\"activity_pub_filters_active\">Activos</string>\n    <string name=\"activity_pub_filters_expired\">Caducados</string>\n    <string name=\"activity_pub_filter_edit_title\">Editar filtro</string>\n    <string name=\"activity_pub_filter_edit_input_title_label\">Título</string>\n    <string name=\"activity_pub_filter_edit_input_title_hint\">Introduce un título</string>\n    <string name=\"activity_pub_filter_edit_duration\">Duración</string>\n    <string name=\"activity_pub_filter_edit_duration_permanent\">Permanente</string>\n    <string name=\"activity_pub_filter_edit_duration_thirty_minutes\">30 minutos</string>\n    <string name=\"activity_pub_filter_edit_duration_one_hour\">1 hora</string>\n    <string name=\"activity_pub_filter_edit_duration_twelve_hours\">12 horas</string>\n    <string name=\"activity_pub_filter_edit_duration_one_day\">1 día</string>\n    <string name=\"activity_pub_filter_edit_duration_three_day\">3 días</string>\n    <string name=\"activity_pub_filter_edit_duration_one_week\">1 semana</string>\n    <string name=\"activity_pub_filter_edit_duration_custom\">Personalizada</string>\n    <string name=\"activity_pub_filter_edit_duration_finish_subtitle\">Expira el %1$s</string>\n    <string name=\"activity_pub_filter_edit_keyword_title\">Palabras clave ocultas</string>\n    <string name=\"activity_pub_filter_edit_keyword_dialog_title\">Editar palabra clave</string>\n    <string name=\"activity_pub_filter_edit_keyword_dialog_hint\">Palabra clave</string>\n    <string name=\"activity_pub_filter_edit_keyword_remove_keyword_dialog_content\">¿Seguro que quieres eliminar esta palabra clave?</string>\n    <string name=\"activity_pub_filter_edit_keyword_list_title\">Palabras silenciadas</string>\n    <string name=\"activity_pub_filter_edit_keyword_list_desc\">%1$s palabra o frase silenciada</string>\n    <string name=\"activity_pub_filter_edit_context_title\">Silenciar de</string>\n    <string name=\"activity_pub_filter_edit_context_selector_title\">Selecciona las fuentes a ocultar</string>\n    <string name=\"activity_pub_filter_edit_context_home\">Inicio y listas</string>\n    <string name=\"activity_pub_filter_edit_context_notification\">Notificaciones</string>\n    <string name=\"activity_pub_filter_edit_context_timeline\">Cronologías públicas</string>\n    <string name=\"activity_pub_filter_edit_context_thread\">Hilos y respuestas</string>\n    <string name=\"activity_pub_filter_edit_context_account\">Perfiles</string>\n    <string name=\"activity_pub_filter_edit_empty_context\">Ninguna</string>\n    <string name=\"activity_pub_filter_edit_warning_title\">Mostrar con advertencia de contenido</string>\n    <string name=\"activity_pub_filter_edit_warning_desc\">Seguir mostrando las publicaciones que coincidan con este filtro, pero con una advertencia de contenido.</string>\n    <string name=\"activity_pub_filter_edit_delete_content\">¿Seguro que quieres eliminar?</string>\n    <string name=\"activity_pub_filter_edit_back_dialog\">¿Seguro que quieres salir?</string>\n    <string name=\"activity_pub_filter_edit_whole_word\">Palabra completa</string>\n    <string name=\"activity_pub_explorer_tab_status_title\">Publicaciones</string>\n    <string name=\"activity_pub_explorer_tab_users_title\">Usuarios</string>\n    <string name=\"activity_pub_explorer_tab_hashtag_title\">Hashtags</string>\n    <string name=\"activity_pub_created_list_title\">Listas</string>\n    <string name=\"activity_pub_add_list_title\">Crear lista</string>\n    <string name=\"activity_pub_add_list_name\">Nombre de la lista</string>\n    <string name=\"activity_pub_add_list_replies\">Mostrar respuestas a</string>\n    <string name=\"activity_pub_add_list_replies_non\">Nadie</string>\n    <string name=\"activity_pub_add_list_replies_list\">Miembros de la lista</string>\n    <string name=\"activity_pub_add_list_replies_followers\">Cualquiera a quien sigo</string>\n    <string name=\"activity_pub_add_list_accounts\">Miembros de la lista</string>\n    <string name=\"activity_pub_add_list_hide_in_timeline\">Ocultar miembros en “Siguiendo”</string>\n    <string name=\"activity_pub_add_list_hide_in_timeline_desc\">Si alguien está en esta lista, escóndelo en tu cronología de Siguiendo para evitar ver sus publicaciones dos veces.</string>\n    <string name=\"activity_pub_add_list_remove_user_message\">¿Seguro que quieres eliminar a este usuario?</string>\n    <string name=\"activity_pub_add_list_name_is_empty\">Introduce tu nombre de usuario</string>\n    <string name=\"activity_pub_add_list_back_reminder\">Tienes cambios sin guardar. ¿Seguro que quieres salir?</string>\n    <string name=\"activity_pub_search_user_placeholder\">Escribe para buscar</string>\n    <string name=\"activity_pub_list_delete_confirm\">¿Seguro que quieres eliminar esta lista?</string>\n    <string name=\"activity_pub_login_exception\">¡Excepción de estado de OAuth!</string>\n    <string name=\"activity_pub_home_timeline\">Inicio</string>\n    <string name=\"activity_pub_local_timeline\">Local</string>\n    <string name=\"activity_pub_public_timeline\">Público</string>\n    <string name=\"activity_pub_instance_detail_language_label\">Idioma: %1$s</string>\n    <string name=\"activity_pub_instance_detail_active_month_label\">Activos mensuales: %1$s</string>\n    <string name=\"activity_pub_user_detail_tab_post\">Publicaciones</string>\n    <string name=\"activity_pub_user_detail_tab_replies\">Respuestas</string>\n    <string name=\"activity_pub_user_detail_tab_media\">Medios</string>\n    <string name=\"activity_pub_user_detail_tab_about\">Acerca de</string>\n    <string name=\"activity_pub_about\">Acerca de</string>\n    <string name=\"activity_pub_about_rule_title\">Reglas</string>\n    <string name=\"activity_pub_trends_tag\">Etiquetas en tendencia</string>\n    <string name=\"activity_pub_trends_tag_description\">%1$s personas en los últimos 2 días</string>\n    <string name=\"activity_pub_trends_status\">Popular</string>\n    <string name=\"activity_pub_select_platform_title\">Elige una instancia</string>\n    <string name=\"activity_pub_select_platform_text_hint\">Introduce el nombre de la instancia o la URL</string>\n</resources>\n"
  },
  {
    "path": "localization/src/commonMain/composeResources/values-es-rES/strings_status_ui.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"status_ui_reposted\">impulsado</string>\n    <string name=\"status_ui_repost\">Impulsar</string>\n    <string name=\"status_ui_quote\">Citar</string>\n    <string name=\"status_ui_like\">Me gusta</string>\n    <string name=\"status_ui_comment\">Comentar</string>\n    <string name=\"status_ui_delete\">Eliminar</string>\n    <string name=\"status_ui_share\">Compartir</string>\n    <string name=\"status_ui_bookmark\">Guardar</string>\n    <string name=\"status_ui_unbookmark\">Quitar de guardados</string>\n    <string name=\"status_ui_reply\">Responder</string>\n    <string name=\"status_ui_follow\">Seguir</string>\n    <string name=\"status_ui_unfollow\">Dejar de seguir</string>\n    <string name=\"status_ui_pin\">Fijar</string>\n    <string name=\"status_ui_unpin\">Quitar fijado</string>\n    <string name=\"status_ui_edit\">Editar</string>\n    <string name=\"status_ui_sensitive_by_filter\">Coincide con el filtro \"%1$s\"</string>\n    <string name=\"status_ui_new_status\">Nuevo contenido</string>\n    <string name=\"status_ui_delete_status_confirm\">¿Seguro que quieres eliminar este contenido?</string>\n    <string name=\"status_ui_image_sensitive_label\">Contenido oculto</string>\n    <string name=\"status_ui_image_content_show_hidden_label\">Mostrar más</string>\n    <string name=\"status_ui_image_content_hide_hidden_label\">Ocultar contenido</string>\n    <string name=\"status_ui_poll_vote\">Votar</string>\n    <string name=\"status_ui_poll_vote_finished_tip\">%1$s voto · Cerrado</string>\n    <string name=\"status_ui_poll_votes_finished_tip\">%1$s votos · Cerrado</string>\n    <string name=\"status_ui_interaction_open_in_browser\">Abrir en navegador</string>\n    <string name=\"status_ui_interaction_open_original_instance\">Abrir la otra instancia</string>\n    <string name=\"status_ui_interaction_copy_url\">Copiar enlace</string>\n    <string name=\"status_ui_interaction_translate\">Traducir</string>\n    <string name=\"status_ui_interaction_open_blog_by_other_account\">Abrir con otra cuenta</string>\n    <string name=\"status_ui_visibility_mentioned_only\">Solo mencionados</string>\n    <string name=\"status_ui_label_pinned\">Fijado</string>\n    <string name=\"status_ui_info_label_edited\">Editado</string>\n    <string name=\"status_ui_interaction_label_favourited_count\">%1$s me gusta</string>\n    <string name=\"status_ui_interaction_label_boosted_count\">%1$s impulsos</string>\n    <string name=\"status_ui_bottom_label_edited_at\">Editado el %1$s</string>\n    <string name=\"status_ui_translating\">Traduciendo…</string>\n    <string name=\"status_ui_translate_show_original\">Mostrar original</string>\n    <string name=\"status_ui_edit_content_delete_dialog_content\">¿Confirmas eliminar este contenido?</string>\n    <string name=\"status_ui_edit_content_name_title\">Editar nombre del contenido</string>\n    <string name=\"status_ui_edit_content_name_label\">Editar nombre del contenido</string>\n    <string name=\"status_ui_edit_content_name_hint\">Introduce el nombre del contenido</string>\n    <string name=\"status_ui_edit_content_config_showing_list_title\">Feeds en la página de inicio</string>\n    <string name=\"status_ui_edit_content_config_showing_list_description\">Mantén pulsado y arrastra para reordenar</string>\n    <string name=\"status_ui_edit_content_config_hidden_list_title\">Feeds que se pueden añadir a inicio</string>\n    <string name=\"status_ui_user_detail_relationship_mutuals\">Mutuos</string>\n    <string name=\"status_ui_user_detail_relationship_blocking\">Bloqueado</string>\n    <string name=\"status_ui_user_detail_relationship_following\">Siguiendo</string>\n    <string name=\"status_ui_user_detail_relationship_not_follow\">Seguir</string>\n    <string name=\"status_ui_user_detail_relationship_requested\">Pendiente</string>\n    <string name=\"status_ui_user_detail_relationship_follow_back\">Seguir de vuelta</string>\n    <string name=\"status_ui_user_detail_request_by_tip\">Han solicitado seguirte</string>\n    <string name=\"status_ui_relationship_btn_dialog_content_cancel_follow\">¿Quieres dejar de seguir a este usuario?</string>\n    <string name=\"status_ui_relationship_btn_dialog_content_cancel_follow_request\">¿Quieres cancelar tu solicitud de seguimiento?</string>\n    <string name=\"status_ui_relationship_btn_dialog_content_cancel_blocking\">¿Seguro que quieres desbloquear a este usuario?</string>\n    <string name=\"status_ui_user_detail_follows_you\">Te sigue</string>\n    <string name=\"status_ui_user_detail_posts\">Publicaciones</string>\n    <string name=\"status_ui_user_detail_follower_info\">Seguidores</string>\n    <string name=\"status_ui_user_detail_following_info\">Siguiendo</string>\n    <string name=\"status_ui_top_label_continued_thread\">Ver conversación</string>\n    <string name=\"status_ui_update_dialog_title\">Actualizar</string>\n    <string name=\"status_ui_update_dialog_release_note\">Registro de cambios de la versión %1$s:\\n%2$s</string>\n    <string name=\"status_ui_switch_account_dialog_title\">Seleccionar cuenta</string>\n    <string name=\"status_ui_likes\">Me gusta</string>\n    <string name=\"status_ui_bookmarks\">Guardados</string>\n    <string name=\"status_ui_edit_profile\">Editar perfil</string>\n    <string name=\"status_ui_logout\">Cerrar sesión</string>\n    <string name=\"status_ui_logout_dialog_content\">¿Seguro que quieres cerrar sesión?</string>\n    <string name=\"status_ui_search_account_status_hint\">Buscar publicaciones</string>\n    <string name=\"shared_notification_unknown_desc\">Notificación desconocida</string>\n    <string name=\"shared_notification_favourited_desc\">le ha dado me gusta a tu publicación</string>\n    <string name=\"shared_notification_reblog_desc\">ha compartido tu publicación</string>\n    <string name=\"shared_notification_poll_desc\">Ver resultados de la votación</string>\n    <string name=\"shared_notification_poll_count\">%1$s votos · Cerrado</string>\n    <string name=\"shared_notification_follow_desc\">te ha seguido</string>\n    <string name=\"shared_notification_update_desc\">ha editado una publicación</string>\n    <string name=\"shared_notification_follow_request\">Has recibido una solicitud de seguimiento</string>\n    <string name=\"shared_notification_new_status_desc\">ha publicado un nuevo blog</string>\n    <string name=\"shared_notification_severed_desc\">ha roto la conexión contigo</string>\n    <string name=\"shared_notification_quote_desc\">citó tu publicación</string>\n    <string name=\"shared_notification_reply_desc\">respondió a tu publicación</string>\n    <string name=\"shared_user_list_title_following\">Siguiendo</string>\n    <string name=\"shared_user_list_title_followers\">Seguidores</string>\n    <string name=\"shared_user_list_title_mutes\">Usuarios silenciados</string>\n    <string name=\"shared_user_list_title_blocks\">Usuarios bloqueados</string>\n    <string name=\"shared_user_list_title_likes\">A quienes les gustó</string>\n    <string name=\"shared_user_list_title_reblog\">Quienes compartieron</string>\n    <string name=\"shared_user_list_action_mute\">Silenciar</string>\n    <string name=\"shared_user_list_action_block\">Bloquear</string>\n    <string name=\"shared_user_list_action_muted\">Silenciado</string>\n    <string name=\"shared_user_list_action_blocked\">Bloqueado</string>\n    <string name=\"shared_user_list_action_mute_dialog_message\">¿Seguro que quieres silenciar a este usuario?</string>\n    <string name=\"shared_user_list_action_block_dialog_message\">¿Seguro que quieres bloquear a este usuario?</string>\n    <string name=\"shared_publish_blog_title\">Publicar</string>\n    <string name=\"shared_publish_blog_text_hint\">Escribe o pega lo que piensas</string>\n    <string name=\"shared_publish_reply_input_hint\">Respondiendo a [[%1$s]]</string>\n    <string name=\"shared_publish_interaction_no_limit\">Cualquiera puede interactuar</string>\n    <string name=\"shared_publish_interaction_limited\">Interacción limitada</string>\n    <string name=\"shared_publish_interaction_dialog_title\">Ajustes de interacción de la publicación</string>\n    <string name=\"shared_publish_interaction_dialog_subtitle\">Personaliza quién puede interactuar con esta publicación.</string>\n    <string name=\"shared_publish_interaction_dialog_quote_title\">Opciones de cita</string>\n    <string name=\"shared_publish_interaction_dialog_quote_allow\">Permitir citar publicaciones</string>\n    <string name=\"shared_publish_interaction_dialog_reply_title\">Opciones de respuesta</string>\n    <string name=\"shared_publish_interaction_dialog_reply_subtitle\">Permitir respuestas de:</string>\n    <string name=\"shared_publish_interaction_dialog_reply_all\">Todos</string>\n    <string name=\"shared_publish_interaction_dialog_reply_nobody\">Nadie</string>\n    <string name=\"shared_publish_interaction_dialog_reply_combine_title\">O combina estas opciones:</string>\n    <string name=\"shared_publish_interaction_dialog_mentioned\">Usuarios mencionados</string>\n    <string name=\"shared_publish_interaction_dialog_following\">Usuarios que sigues</string>\n    <string name=\"shared_publish_interaction_dialog_follower\">Tus seguidores</string>\n    <string name=\"shared_publish_interaction_dialog_in_list\">Usuarios en \"%1$s\"</string>\n    <string name=\"shared_publish_media_alt_dialog_title\">Añadir texto alternativo</string>\n    <string name=\"shared_publish_media_alt_dialog_input_tip\">Texto alternativo descriptivo</string>\n    <string name=\"shared_publish_media_alt_dialog_input_hint\">Texto alternativo</string>\n    <string name=\"shared_feeds_not_login_title\">¡Ups! Tu sesión ha caducado. Inicia sesión nuevamente para continuar.</string>\n    <string name=\"shared_feeds_go_to_login\">Iniciar sesión</string>\n    <string name=\"shared_publish_select_account_title\">Selecciona una cuenta</string>\n    <string name=\"shared_select_language_title\">Seleccionar idioma</string>\n    <string name=\"shared_status_context_screen_title\">Detalle</string>\n    <string name=\"shared_alt_label\">ALT</string>\n    <string name=\"status_ui_embed_quote_unavailable\">La cita actual no está disponible</string>\n    <string name=\"status_ui_action_unforward\">Cancelar reenvío</string>\n    <string name=\"status_ui_quote_approval_public\">Cualquiera</string>\n    <string name=\"status_ui_quote_approval_follower\">Solo seguidores</string>\n    <string name=\"status_ui_quote_approval_nobody\">Solo yo</string>\n    <string name=\"status_ui_visibility\">Visibilidad</string>\n</resources>\n"
  },
  {
    "path": "localization/src/commonMain/composeResources/values-fr-rFR/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"skip\">Passer</string>\n    <string name=\"alert\">Alerte</string>\n    <string name=\"duration_minute\">minute</string>\n    <string name=\"duration_hour\">heure</string>\n    <string name=\"duration_day\">jour</string>\n    <string name=\"duration_week\">semaine</string>\n    <string name=\"cancel\">Annuler</string>\n    <string name=\"ok\">Oui</string>\n    <string name=\"empty\">Cet espace est vide～</string>\n    <string name=\"duration_selector_title\">Définir la durée</string>\n    <string name=\"retry\">Réessayer</string>\n    <string name=\"load_more_error\">Oups ! Chargement impossible.</string>\n    <string name=\"unknown_error\">Erreur inconnue</string>\n    <string name=\"network_error\">Erreur réseau</string>\n    <string name=\"resource_not_found\">Ressource introuvable</string>\n    <string name=\"mixed_content_subtitle_1\">Contenu mixte ·</string>\n    <string name=\"mixed_content_subtitle_2\">Sources</string>\n    <string name=\"date_time_ago\">&#160;plus tôt</string>\n    <string name=\"date_time_ago_prefix\"></string>\n    <string name=\"date_time_ago_suffix\">&#160;plus tôt</string>\n    <string name=\"date_time_day\">jour</string>\n    <string name=\"date_time_hour\">heure</string>\n    <string name=\"date_time_minute\">min</string>\n    <string name=\"date_time_second\">s</string>\n    <string name=\"image_saving\">Enregistrement…</string>\n    <string name=\"image_save_success\">Enregistré avec succès</string>\n    <string name=\"image_save_failed\">Échec de l’enregistrement</string>\n    <string name=\"permission_write_external_permission_denied\">L’autorisation de stockage est refusée, veuillez l’activer dans les réglages.</string>\n    <string name=\"feeds_load_previous_page_label\">Chargement du contenu de la page précédente</string>\n    <string name=\"feeds_load_previous_page_failed_label\">Échec du chargement, touchez pour réessayer</string>\n    <string name=\"image\">Image</string>\n    <string name=\"video\">Vidéo</string>\n    <string name=\"login\">Se connecter</string>\n    <string name=\"add\">Ajouter</string>\n    <string name=\"search\">Rechercher</string>\n    <string name=\"auth_success\">Connexion réussie</string>\n    <string name=\"auth_failed\">Échec de la connexion</string>\n    <string name=\"select\">Sélectionner</string>\n    <string name=\"logout\">Se déconnecter</string>\n    <string name=\"settings\">Réglages</string>\n    <string name=\"donate\">Faire un don</string>\n    <string name=\"feeds\">Flux</string>\n    <string name=\"save\">Enregistrer</string>\n    <string name=\"done\">C’est fait !</string>\n    <string name=\"bluesky_name\">Bluesky</string>\n    <string name=\"bluesky_description\">Bluesky est un réseau ouvert. Avec un seul compte, vous accédez à un réseau social simple d’usage et à une identité partagée sur tout l’internet social.</string>\n    <string name=\"mastodon_name\">Mastodon</string>\n    <string name=\"mastodon_description\">Mastodon est un média social décentralisé et non commercial où les utilisateurs contrôlent leur fil, chaque serveur étant une entité indépendante.</string>\n    <string name=\"mixed_content_name\">Contenu mixte</string>\n    <string name=\"mixed_content_description\">Le flux mixte rassemble tout votre contenu. Ajoutez des sources depuis Mastodon, Bluesky, RSS et plus encore – Fread les combine en une timeline chronologique.</string>\n    <string name=\"status_provider_type_activity_pub\">Mastodon</string>\n    <string name=\"add_content_title\">Ajouter du contenu</string>\n    <string name=\"add_content_success_with_account\">Ajout réussi. Compte actuel :</string>\n    <string name=\"add_content_success_with_login_reminder\">Ajout réussi. Se connecter maintenant ?</string>\n    <string name=\"content_exist_tips\">Oups ! Ce contenu est déjà présent.</string>\n    <string name=\"content_add_success\">Parfait ! Votre contenu est prêt.</string>\n    <string name=\"add_feeds_page_empty_name_exist\">Ce nom d’utilisateur est déjà pris.</string>\n    <string name=\"edit_profile_name_empty\">Veuillez saisir votre nom d’utilisateur</string>\n    <string name=\"edit_profile_label_name\">Nom</string>\n    <string name=\"edit_profile_label_note\">Bio</string>\n    <string name=\"edit_profile_input_name_hint\">Veuillez saisir votre nom d’utilisateur</string>\n    <string name=\"edit_profile_input_note_hint\">Veuillez saisir votre bio</string>\n    <string name=\"edit_profile_edit_confirm_message\">Voulez-vous vraiment quitter l’édition ?</string>\n    <string name=\"add_content_success_snackbar\">Ajout réussi</string>\n    <string name=\"login_dialog_input_hint\">Veuillez indiquer le serveur sur lequel vous connecter</string>\n    <string name=\"login_dialog_title\">Veuillez choisir le serveur de connexion</string>\n    <string name=\"login_dialog_input_tip\">Saisir l’hôte du serveur</string>\n    <string name=\"login_dialog_input_title\">Veuillez saisir l’hôte du serveur</string>\n    <string name=\"login_dialog_input_label\">Hôte du serveur</string>\n    <string name=\"login_dialog_target_title\">Veuillez vous connecter</string>\n    <string name=\"profile_description\">Fread permet d’ajouter plusieurs comptes. Les comptes ajoutés peuvent publier, aimer, partager, commenter, etc. Vous pouvez aussi voir les listes créées par ces comptes.</string>\n    <string name=\"list_content_empty_placeholder\">Aucun contenu disponible</string>\n    <string name=\"post_status_scope_public\">Public</string>\n    <string name=\"post_status_scope_unlisted\">Public, mais exclu de la découverte</string>\n    <string name=\"post_status_scope_follower_only\">Abonnés uniquement</string>\n    <string name=\"post_status_scope_mentioned_only\">Uniquement les personnes mentionnées</string>\n    <string name=\"post_status_content_warning\">Alerte de contenu</string>\n    <string name=\"post_status_success\">Envoyé avec succès</string>\n    <string name=\"post_status_failed\">Échec de la publication : %1$s</string>\n    <string name=\"post_status_exit_dialog_content\">Les modifications non enregistrées seront perdues. Quitter quand même ?</string>\n    <string name=\"post_status_content_is_empty\">Veuillez saisir du contenu</string>\n    <string name=\"post_status_part_failed\">Certaines publications de comptes ont échoué. Les comptes publiés avec succès ont été retirés. Réessayez avec les comptes restants : %1$s</string>\n    <string name=\"select_account_open_status_title\">Sélectionner un compte</string>\n    <string name=\"select_account_open_status_empty\">Aucun autre compte</string>\n    <string name=\"select_account_open_status_search_in\">Rechercher dans %1$s</string>\n    <string name=\"select_account_open_status_search_failed\">Échec de la recherche</string>\n    <string name=\"explorer_search_no_results\">Aucun résultat</string>\n    <string name=\"explorer_search_bar_hint\">Rechercher</string>\n    <string name=\"explorer_search_bar_hint_specialize_platform\">Rechercher dans %1$s</string>\n    <string name=\"explorer_search_tab_title_author\">Comptes</string>\n    <string name=\"explorer_search_tab_title_status\">Statuts</string>\n    <string name=\"explorer_search_tab_title_hashtag\">Hashtags</string>\n    <string name=\"explorer_search_tab_title_server\">Serveur</string>\n    <string name=\"explorer_tab_title\">Explorer</string>\n    <string name=\"explorer_tab_status_title\">Publications</string>\n    <string name=\"explorer_tab_users_title\">Utilisateurs</string>\n    <string name=\"explorer_tab_hashtag_title\">Hashtags</string>\n    <string name=\"add_provider_page_title\">Ajouter un serveur</string>\n    <string name=\"add_provider_page_confirm_button\">Confirmer l’ajout</string>\n    <string name=\"search_page_title\">Recherche</string>\n    <string name=\"search_result_not_found\">Aucun résultat trouvé</string>\n    <string name=\"feeds_import_page_title\">Importer</string>\n    <string name=\"feeds_import_page_hint\">Veuillez sélectionner le fichier OPML à importer</string>\n    <string name=\"feeds_import_button\">Importer</string>\n    <string name=\"add_feeds_page_title\">Ajouter du contenu</string>\n    <string name=\"add_feeds_page_feeds_name_label\">Nom du contenu</string>\n    <string name=\"add_feeds_page_feeds_name_hint\">Veuillez saisir le nom du contenu</string>\n    <string name=\"add_feeds_page_feeds_empty\">Veuillez ajouter une source d’abonnement</string>\n    <string name=\"pre_add_feeds_no_result\">Aucun résultat trouvé</string>\n    <string name=\"pre_add_feeds_hint\">Nom d’instance, URL, page d’accueil de l’utilisateur, RSS</string>\n    <string name=\"pre_add_feeds_input_label_1\">Fread permet d’ajouter des contenus [[Mastodon]]/[[Bluesky]] ainsi que des flux [[RSS]], voire du [[contenu mixte]].</string>\n    <string name=\"pre_add_feeds_input_label_2\">Veuillez saisir ici le [[NickName]] Mastodon ou Bluesky, le [[WebFinger\\Handle]], l’[[URL de la page d’accueil]] de l’utilisateur, ou l’URL du flux [[RSS]].</string>\n    <string name=\"empty_content_hint_title\">Veuillez d’abord ajouter du contenu</string>\n    <string name=\"empty_content_hint_desc\">Vous devez d’abord ajouter des sources d’abonnement. Fread prend en charge différents types de contenu et de plateformes pour créer votre propre fil d’informations.</string>\n    <string name=\"feeds_add_content\">Ajouter du contenu</string>\n    <string name=\"select_feeds_type_screen_title\">Sélectionner le type de contenu</string>\n    <string name=\"feeds_pre_add_login_dialog_content\">Contenu ajouté avec succès. Souhaitez-vous vous connecter maintenant ?</string>\n    <string name=\"add_feeds_page_empty_name_tips\">Veuillez saisir un nom</string>\n    <string name=\"add_feeds_page_empty_source_tips\">Veuillez ajouter une source d’abonnement</string>\n    <string name=\"add_feeds_choose_auth_dialog_title\">Veuillez sélectionner le serveur de connexion</string>\n    <string name=\"search_feeds_title\">Recherche</string>\n    <string name=\"search_feeds_title_hint\">Pseudo Mastodon ou Bluesky, WebFinger, URL, RSS</string>\n    <string name=\"feeds_select_account_for_post_status\">Veuillez sélectionner un compte</string>\n    <string name=\"feeds_mixed_config_edit_new_name_dialog_title\">Veuillez saisir un nom</string>\n    <string name=\"feeds_mixed_config_edit_new_name_dialog_label\">Nom</string>\n    <string name=\"feeds_mixed_config_edit_delete_content_dialog_message\">Voulez-vous vraiment supprimer ce flux ?</string>\n    <string name=\"feeds_mixed_config_not_found\">État anormal, configuration introuvable.</string>\n    <string name=\"feeds_import_back_dialog_message\">Voulez-vous vraiment quitter ?</string>\n    <string name=\"feeds_delete_confirm_content\">Voulez-vous vraiment supprimer ?</string>\n    <string name=\"feeds_select_type_screen_title\">Choisir un format</string>\n    <string name=\"feeds_select_type_screen_content\">Qu’aimeriez-vous ajouter ?</string>\n    <string name=\"notification_tab_title\">Notifications</string>\n    <string name=\"notifications_account_empty_tip\">Aucun compte connecté pour le moment</string>\n    <string name=\"notifications_tab_all\">Tous</string>\n    <string name=\"notifications_tab_mention\">Mentions</string>\n    <string name=\"profile_page_title\">Profil</string>\n    <string name=\"profile_setting_about_title\">À propos</string>\n    <string name=\"profile_setting_donate_desc\">Offrez-moi un café pour soutenir ce projet.</string>\n    <string name=\"profile_setting_inline_video_auto_play\">Lecture auto des vidéos intégrées</string>\n    <string name=\"profile_setting_inline_video_auto_play_subtitle\">Si activé, les vidéos dans les flux se lanceront automatiquement.</string>\n    <string name=\"profile_setting_always_show_sensitive_content\">Afficher par défaut le contenu sensible</string>\n    <string name=\"profile_setting_always_show_sensitive_content_subtitle\">Si activé, le contenu sensible sera affiché par défaut (Mastodon uniquement).</string>\n    <string name=\"profile_setting_dark_mode_title\">Mode sombre</string>\n    <string name=\"profile_setting_dark_mode_dark\">Mode sombre</string>\n    <string name=\"profile_setting_dark_mode_light\">Mode clair</string>\n    <string name=\"profile_setting_dark_mode_follow_system\">Suivre le système</string>\n    <string name=\"profile_setting_open_source_title\">Open source</string>\n    <string name=\"profile_setting_open_source_desc\">Licences open source des dépendances utilisées dans ce projet</string>\n    <string name=\"profile_setting_open_source_feedback\">Donnez-nous votre avis</string>\n    <string name=\"profile_setting_open_source_feedback_desc\">Rejoignez le groupe Telegram ou utilisez d’autres moyens</string>\n    <string name=\"profile_setting_open_source_feedback_telegram\">Rejoindre le groupe Telegram</string>\n    <string name=\"profile_setting_open_source_feedback_github\">Créer une issue GitHub</string>\n    <string name=\"profile_setting_open_source_feedback_email\">Envoyer un e-mail</string>\n    <string name=\"profile_setting_language_title\">Langue d’affichage</string>\n    <string name=\"profile_setting_language_zh\">简体中文</string>\n    <string name=\"profile_setting_language_en\">English</string>\n    <string name=\"profile_setting_language_system\">Suivre le système</string>\n    <string name=\"profile_setting_ratting\">Nous évaluer</string>\n    <string name=\"profile_setting_ratting_desc\">Si l’appli vous plaît, laissez-nous une bonne note.</string>\n    <string name=\"profile_setting_font_size\">Taille de police du contenu</string>\n    <string name=\"profile_setting_font_size_desc\">Ajuster la taille de la police et des icônes du contenu</string>\n    <string name=\"profile_setting_font_size_small\">Petite</string>\n    <string name=\"profile_setting_font_size_medium\">Moyenne</string>\n    <string name=\"profile_setting_font_size_large\">Grande</string>\n    <string name=\"profile_setting_immersive_nav_bar\">Barre de navigation immersive</string>\n    <string name=\"profile_setting_immersive_nav_bar_desc\">La barre de navigation inférieure se masque automatiquement lors du défilement sur la page d’accueil.</string>\n    <string name=\"profile_setting_timeline_position\">Position par défaut sur le fil</string>\n    <string name=\"profile_setting_timeline_position_last_read\">Là où je me suis arrêté</string>\n    <string name=\"profile_setting_timeline_position_newest\">Dernières publications</string>\n    <string name=\"profile_setting_theme_title\">Couleur du thème</string>\n    <string name=\"profile_setting_theme_default\">Par défaut</string>\n    <string name=\"profile_setting_theme_system\">Suivre le système</string>\n    <string name=\"profile_about_version\">Version :&#160;</string>\n    <string name=\"profile_about_website\">Site :&#160;</string>\n    <string name=\"profile_about_developer\">Créé par&#160;</string>\n    <string name=\"profile_about_contract_us\">Contact :&#160;</string>\n    <string name=\"profile_about_telegram\">Groupe Telegram :&#160;</string>\n    <string name=\"profile_about_privacy_policy\">Politique de confidentialité :&#160;</string>\n    <string name=\"profile_setting_check_for_update\">Rechercher une mise à jour</string>\n    <string name=\"profile_setting_already_latest_version\">Votre application est à jour</string>\n    <string name=\"profile_setting_have_new_version\">Nouvelle version disponible, touchez pour mettre à jour.</string>\n    <string name=\"profile_donate_page_title\">Choisissez votre mode de don</string>\n    <string name=\"profile_account_not_login\">Session expirée</string>\n    <string name=\"rss_source_detail_screen_title\">Attribution de la source</string>\n    <string name=\"rss_source_detail_screen_custom_title\">Titre personnalisé</string>\n    <string name=\"rss_source_detail_screen_url\">Lien d’abonnement</string>\n    <string name=\"rss_source_detail_screen_home_url\">Page d’accueil</string>\n    <string name=\"rss_source_detail_screen_add_date\">Date d’ajout</string>\n    <string name=\"rss_source_detail_screen_last_update_date\">Dernière mise à jour</string>\n    <string name=\"bluesky_protocol_name\">Bluesky</string>\n    <string name=\"bsky_add_content_title\">Se connecter</string>\n    <string name=\"bsky_add_content_hosting_provider\">Fournisseur d’hébergement</string>\n    <string name=\"bsky_add_content_user_name\">Nom d’utilisateur ou e-mail</string>\n    <string name=\"bsky_add_content_password\">Mot de passe</string>\n    <string name=\"bsky_add_content_factor_token\">Code de confirmation</string>\n    <string name=\"bsky_edit_content_title\">Modifier le contenu</string>\n    <string name=\"bsky_feeds_explorer_more\">Explorer plus de flux</string>\n    <string name=\"bsky_feeds_explorer_creator_label\">Par %1$s</string>\n    <string name=\"bsky_feeds_explorer_liked_by\">Aimé par %1$s utilisateurs</string>\n    <string name=\"bsky_feeds_detail_creator_prefix\">Par</string>\n    <string name=\"bsky_feeds_item_subtitle\">Par %1$s · Aimé par %2$s utilisateurs</string>\n    <string name=\"bsky_feeds_following_name\">Abonnements</string>\n    <string name=\"bsky_feeds_user_posts\">Publications</string>\n    <string name=\"bsky_feeds_user_replies\">Réponses</string>\n    <string name=\"bsky_feeds_user_medias\">Médias</string>\n    <string name=\"bsky_feeds_user_likes\">Mentions « J’aime »</string>\n    <string name=\"bsky_user_detail_action_blocked_list\">Utilisateurs bloqués</string>\n    <string name=\"bsky_user_detail_action_muted_list\">Utilisateurs masqués</string>\n    <string name=\"bsky_user_detail_action_mute_user\">Masquer %1$s</string>\n    <string name=\"bsky_user_detail_action_unmute_user\">Rétablir</string>\n    <string name=\"bsky_user_detail_action_block_user\">Bloquer %1$s</string>\n    <string name=\"bsky_user_detail_action_unblock_user\">Débloquer</string>\n    <string name=\"bsky_user_detail_action_block_user_dialog_message\">Voulez-vous bloquer cet utilisateur ?</string>\n    <string name=\"bsky_user_detail_action_mute_user_dialog_message\">Voulez-vous masquer cet utilisateur ?</string>\n    <string name=\"bsky_edit_profile_title\">Modifier le profil</string>\n    <string name=\"input_media_desc_page_title\">Texte de description de l’image</string>\n    <string name=\"input_media_desc_input_hint\">Veuillez saisir le texte de description de l’image</string>\n    <string name=\"post_status_page_title\">Nouveau billet</string>\n    <string name=\"post_screen_input_hint\">Veuillez saisir votre billet</string>\n    <string name=\"post_screen_media_descriptor_placeholder\">Texte descriptif</string>\n    <string name=\"post_status_poll_item_hint\">Option %1$s</string>\n    <string name=\"post_status_poll_duration\">Durée du vote</string>\n    <string name=\"post_status_poll_function_title\">Fonction</string>\n    <string name=\"post_status_poll_single\">Unique</string>\n    <string name=\"post_status_poll_multiple\">Multiple</string>\n    <string name=\"post_status_poll_style_select_dialog_title\">Veuillez choisir un style</string>\n    <string name=\"post_status_poll_is_empty\">Veuillez saisir le contenu du vote</string>\n    <string name=\"post_status_media_is_not_upload\">Média non encore téléversé</string>\n    <string name=\"authentication_page_normal_title\">Cette fonctionnalité nécessite une connexion</string>\n    <string name=\"authentication_page_failed_title\">Session invalide, veuillez vous reconnecter.</string>\n    <string name=\"main_drawer_title\">Liste de contenu</string>\n    <string name=\"main_drawer_mixed_item_subtitle_1\">Contenu mixte ·</string>\n    <string name=\"main_drawer_mixed_item_subtitle_2\">Sources</string>\n    <string name=\"main_drawer_settings\">Réglages</string>\n    <string name=\"main_drawer_donate\">Faire un don</string>\n    <string name=\"main_request_notification_dialog_title\">Notifications</string>\n    <string name=\"main_request_notification_dialog_reject_button\">Refuser</string>\n    <string name=\"main_request_notification_dialog_allow_button\">Autoriser</string>\n    <string name=\"main_request_notification_dialog_content\">Pour recevoir les messages d’interaction, veuillez autoriser les notifications.</string>\n    <string name=\"setting_item_amoled_mode\">Mode AMOLED</string>\n    <string name=\"setting_item_amoled_mode_description\">Une fois activé, l’arrière-plan devient noir pur en mode sombre afin de réduire la consommation d’énergie et d’améliorer le contraste dans les environnements sombres.</string>\n    <string name=\"setting_group_appearance\">Apparence</string>\n    <string name=\"setting_group_appearance_subtitle\">Paramètres du thème, de l’affichage et du style de l’accueil</string>\n    <string name=\"setting_group_behavior\">Comportement</string>\n    <string name=\"setting_group_behavior_subtitle\">Paramètres d’affichage du contenu et de comportement d’interaction</string>\n    <string name=\"setting_item_home_tab_next_title\">Afficher le bouton de changement de contenu en haut de l’accueil</string>\n    <string name=\"setting_item_home_tab_next_subtitle\">Afficher en haut à droite de l’accueil le bouton pour passer au contenu suivant</string>\n    <string name=\"setting_item_home_tab_refresh_title\">Afficher le bouton d’actualisation en haut de l’accueil</string>\n    <string name=\"setting_item_home_tab_refresh_subtitle\">Afficher en haut à droite de l’accueil le bouton pour actualiser le contenu</string>\n    <string name=\"setting_item_open_url_by_system_title\">Ouvrir les liens avec le navigateur du système</string>\n    <string name=\"setting_item_open_url_by_system_subtitle\">Lorsqu’elle est activée, Fread ouvrira les liens avec le navigateur du système.</string>\n    <string name=\"setting_item_solid_bar_background_title\">Fond solide des barres</string>\n    <string name=\"setting_item_solid_bar_background_subtitle\">Rend les barres supérieure et inférieure en fond uni au lieu d’un effet flou.</string>\n</resources>\n"
  },
  {
    "path": "localization/src/commonMain/composeResources/values-fr-rFR/strings_mastodon.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"activity_pub_user_detail_join_date\">Inscrit le</string>\n    <string name=\"activity_pub_user_detail_tab_about_joined\">Inscrit le %1$s</string>\n    <string name=\"activity_pub_user_detail_menu_block\">Bloquer %1$s</string>\n    <string name=\"activity_pub_user_detail_menu_block_domain\">Bloquer %1$s</string>\n    <string name=\"activity_pub_user_detail_menu_unblock_domain\">Débloquer %1$s</string>\n    <string name=\"activity_pub_user_detail_menu_edit_private_note\">Modifier la note</string>\n    <string name=\"activity_pub_user_detail_menu_edit_private_note_dialog_hint\">Saisir une note privée</string>\n    <string name=\"activity_pub_user_detail_dialog_content_block\">Voulez-vous bloquer cet utilisateur ?</string>\n    <string name=\"activity_pub_user_detail_dialog_content_block_domain\">Voulez-vous bloquer ce domaine ?</string>\n    <string name=\"activity_pub_user_menu_blocked_user_list\">Utilisateurs bloqués</string>\n    <string name=\"activity_pub_user_menu_muted_user_list\">Utilisateurs masqués</string>\n    <string name=\"activity_pub_user_detail_menu_mute_user\">Masquer %1$s</string>\n    <string name=\"activity_pub_user_detail_menu_unmute_user\">Rétablir %1$s</string>\n    <string name=\"activity_pub_mute_user_bottom_sheet_title\">Masquer cet utilisateur ?</string>\n    <string name=\"activity_pub_mute_user_bottom_sheet_role1\">Ils ne sauront pas qu’ils ont été masqués.</string>\n    <string name=\"activity_pub_mute_user_bottom_sheet_role2\">Ils peuvent toujours voir vos publications, mais vous ne verrez pas les leurs.</string>\n    <string name=\"activity_pub_mute_user_bottom_sheet_role3\">Vous ne verrez pas les publications qui les mentionnent.</string>\n    <string name=\"activity_pub_mute_user_bottom_sheet_role4\">Ils peuvent vous mentionner et vous suivre, mais vous ne les verrez pas.</string>\n    <string name=\"activity_pub_mute_user_bottom_sheet_btn_mute\">Masquer</string>\n    <string name=\"add_instance_screen_title\">Ajouter une instance</string>\n    <string name=\"add_instance_input_service_hint\">Veuillez saisir l’adresse du serveur</string>\n    <string name=\"add_instance_input_service_error\">Veuillez saisir l’adresse du serveur correcte</string>\n    <string name=\"activity_pub_content_tab_home\">Accueil</string>\n    <string name=\"activity_pub_content_tab_local_timeline\">Local</string>\n    <string name=\"activity_pub_content_tab_public_timeline\">Public</string>\n    <string name=\"activity_pub_content_tab_trending\">Tendances</string>\n    <string name=\"activity_pub_hashtag_timeline_description\">%1$s publications · %2$s participants · %3$s publication aujourd’hui</string>\n    <string name=\"activity_pub_hashtag_unfollow_dialog_message\">Voulez-vous vraiment vous désabonner de ce hashtag ?</string>\n    <string name=\"activity_pub_edit_account_info_label_about\">À propos</string>\n    <string name=\"activity_pub_edit_content_screen_config_not_found\">Configuration introuvable</string>\n    <string name=\"activity_pub_user_list_empty\">Aucun utilisateur pour le moment</string>\n    <string name=\"activity_pub_muted_user_list_empty\">Aucun utilisateur masqué</string>\n    <string name=\"activity_pub_muted_user_list_unmute\">Rétablir</string>\n    <string name=\"activity_pub_blocked_user_list_empty\">Aucun utilisateur bloqué</string>\n    <string name=\"activity_pub_blocked_user_list_unblock\">Débloquer</string>\n    <string name=\"activity_pub_bookmarks_list_title\">Signets</string>\n    <string name=\"activity_pub_favourites_list_title\">Favoris</string>\n    <string name=\"activity_pub_followed_tags_screen_title\">Hashtags suivis</string>\n    <string name=\"activity_pub_filters_list_page_title\">Filtres</string>\n    <string name=\"activity_pub_filters_active\">Actif</string>\n    <string name=\"activity_pub_filters_expired\">Expiré</string>\n    <string name=\"activity_pub_filter_edit_title\">Modifier le filtre</string>\n    <string name=\"activity_pub_filter_edit_input_title_label\">Titre</string>\n    <string name=\"activity_pub_filter_edit_input_title_hint\">Veuillez saisir un titre</string>\n    <string name=\"activity_pub_filter_edit_duration\">Durée</string>\n    <string name=\"activity_pub_filter_edit_duration_permanent\">Permanent</string>\n    <string name=\"activity_pub_filter_edit_duration_thirty_minutes\">30 minutes</string>\n    <string name=\"activity_pub_filter_edit_duration_one_hour\">1 heure</string>\n    <string name=\"activity_pub_filter_edit_duration_twelve_hours\">12 heures</string>\n    <string name=\"activity_pub_filter_edit_duration_one_day\">1 jour</string>\n    <string name=\"activity_pub_filter_edit_duration_three_day\">3 jours</string>\n    <string name=\"activity_pub_filter_edit_duration_one_week\">1 semaine</string>\n    <string name=\"activity_pub_filter_edit_duration_custom\">Personnalisé</string>\n    <string name=\"activity_pub_filter_edit_duration_finish_subtitle\">Expire le %1$s</string>\n    <string name=\"activity_pub_filter_edit_keyword_title\">Mots-clés masqués</string>\n    <string name=\"activity_pub_filter_edit_keyword_dialog_title\">Modifier le mot-clé</string>\n    <string name=\"activity_pub_filter_edit_keyword_dialog_hint\">Mot-clé</string>\n    <string name=\"activity_pub_filter_edit_keyword_remove_keyword_dialog_content\">Voulez-vous vraiment supprimer ce mot-clé ?</string>\n    <string name=\"activity_pub_filter_edit_keyword_list_title\">Mots masqués</string>\n    <string name=\"activity_pub_filter_edit_keyword_list_desc\">%1$s mot ou expression masqué(e)</string>\n    <string name=\"activity_pub_filter_edit_context_title\">Masquer depuis</string>\n    <string name=\"activity_pub_filter_edit_context_selector_title\">Veuillez sélectionner les sources à masquer</string>\n    <string name=\"activity_pub_filter_edit_context_home\">Accueil &amp; Liste</string>\n    <string name=\"activity_pub_filter_edit_context_notification\">Notifications</string>\n    <string name=\"activity_pub_filter_edit_context_timeline\">Fils publics</string>\n    <string name=\"activity_pub_filter_edit_context_thread\">Discussions &amp; réponses</string>\n    <string name=\"activity_pub_filter_edit_context_account\">Profils</string>\n    <string name=\"activity_pub_filter_edit_empty_context\">Aucun</string>\n    <string name=\"activity_pub_filter_edit_warning_title\">Afficher avec avertissement de contenu</string>\n    <string name=\"activity_pub_filter_edit_warning_desc\">Afficher quand même les publications correspondant à ce filtre, mais derrière un avertissement de contenu</string>\n    <string name=\"activity_pub_filter_edit_delete_content\">Voulez-vous vraiment supprimer ?</string>\n    <string name=\"activity_pub_filter_edit_back_dialog\">Voulez-vous vraiment quitter ?</string>\n    <string name=\"activity_pub_filter_edit_whole_word\">Mot entier</string>\n    <string name=\"activity_pub_explorer_tab_status_title\">Publications</string>\n    <string name=\"activity_pub_explorer_tab_users_title\">Utilisateurs</string>\n    <string name=\"activity_pub_explorer_tab_hashtag_title\">Hashtags</string>\n    <string name=\"activity_pub_created_list_title\">Listes</string>\n    <string name=\"activity_pub_add_list_title\">Créer une liste</string>\n    <string name=\"activity_pub_add_list_name\">Nom de la liste</string>\n    <string name=\"activity_pub_add_list_replies\">Afficher les réponses de</string>\n    <string name=\"activity_pub_add_list_replies_non\">Personne</string>\n    <string name=\"activity_pub_add_list_replies_list\">Membres de la liste</string>\n    <string name=\"activity_pub_add_list_replies_followers\">Toute personne que je suis</string>\n    <string name=\"activity_pub_add_list_accounts\">Membres de la liste</string>\n    <string name=\"activity_pub_add_list_hide_in_timeline\">Masquer les membres dans Abonnements</string>\n    <string name=\"activity_pub_add_list_hide_in_timeline_desc\">Si quelqu’un figure dans cette liste, masquez-le dans votre fil Abonnements pour éviter les doublons.</string>\n    <string name=\"activity_pub_add_list_remove_user_message\">Voulez-vous vraiment retirer cet utilisateur ?</string>\n    <string name=\"activity_pub_add_list_name_is_empty\">Veuillez saisir votre nom d’utilisateur</string>\n    <string name=\"activity_pub_add_list_back_reminder\">Des modifications ne sont pas enregistrées. Voulez-vous vraiment quitter ?</string>\n    <string name=\"activity_pub_search_user_placeholder\">Tapez pour rechercher</string>\n    <string name=\"activity_pub_list_delete_confirm\">Voulez-vous vraiment supprimer cette liste ?</string>\n    <string name=\"activity_pub_login_exception\">Anomalie d’état OAuth !</string>\n    <string name=\"activity_pub_home_timeline\">Accueil</string>\n    <string name=\"activity_pub_local_timeline\">Local</string>\n    <string name=\"activity_pub_public_timeline\">Public</string>\n    <string name=\"activity_pub_instance_detail_language_label\">Langue : %1$s</string>\n    <string name=\"activity_pub_instance_detail_active_month_label\">Mois actifs : %1$s</string>\n    <string name=\"activity_pub_user_detail_tab_post\">Publications</string>\n    <string name=\"activity_pub_user_detail_tab_replies\">Réponses</string>\n    <string name=\"activity_pub_user_detail_tab_media\">Médias</string>\n    <string name=\"activity_pub_user_detail_tab_about\">À propos</string>\n    <string name=\"activity_pub_about\">À propos</string>\n    <string name=\"activity_pub_about_rule_title\">Règles</string>\n    <string name=\"activity_pub_trends_tag\">Sujets tendance</string>\n    <string name=\"activity_pub_trends_tag_description\">%1$s personnes au cours des 2 derniers jours</string>\n    <string name=\"activity_pub_trends_status\">Populaire</string>\n    <string name=\"activity_pub_select_platform_title\">Choisir une instance</string>\n    <string name=\"activity_pub_select_platform_text_hint\">Saisissez le nom de l’instance ou l’URL</string>\n</resources>\n"
  },
  {
    "path": "localization/src/commonMain/composeResources/values-fr-rFR/strings_status_ui.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"status_ui_reposted\">partagé</string>\n    <string name=\"status_ui_repost\">Partager</string>\n    <string name=\"status_ui_quote\">Citer</string>\n    <string name=\"status_ui_like\">J’aime</string>\n    <string name=\"status_ui_comment\">Commenter</string>\n    <string name=\"status_ui_delete\">Supprimer</string>\n    <string name=\"status_ui_share\">Partager</string>\n    <string name=\"status_ui_bookmark\">Ajouter aux favoris</string>\n    <string name=\"status_ui_unbookmark\">Retirer des favoris</string>\n    <string name=\"status_ui_reply\">Répondre</string>\n    <string name=\"status_ui_follow\">Suivre</string>\n    <string name=\"status_ui_unfollow\">Ne plus suivre</string>\n    <string name=\"status_ui_pin\">Épingler</string>\n    <string name=\"status_ui_unpin\">Désépingler</string>\n    <string name=\"status_ui_edit\">Modifier</string>\n    <string name=\"status_ui_sensitive_by_filter\">Correspond au filtre « %1$s »</string>\n    <string name=\"status_ui_new_status\">Nouveau contenu</string>\n    <string name=\"status_ui_delete_status_confirm\">Voulez-vous vraiment supprimer ce contenu ?</string>\n    <string name=\"status_ui_image_sensitive_label\">Média masqué</string>\n    <string name=\"status_ui_image_content_show_hidden_label\">Voir plus</string>\n    <string name=\"status_ui_image_content_hide_hidden_label\">Masquer le contenu</string>\n    <string name=\"status_ui_poll_vote\">Voter</string>\n    <string name=\"status_ui_poll_vote_finished_tip\">%1$s vote · Clos</string>\n    <string name=\"status_ui_poll_votes_finished_tip\">%1$s votes · Clos</string>\n    <string name=\"status_ui_interaction_open_in_browser\">Ouvrir dans le navigateur</string>\n    <string name=\"status_ui_interaction_open_original_instance\">Ouvrir l’autre instance</string>\n    <string name=\"status_ui_interaction_copy_url\">Copier le lien</string>\n    <string name=\"status_ui_interaction_translate\">Traduire</string>\n    <string name=\"status_ui_interaction_open_blog_by_other_account\">Ouvrir avec un autre compte</string>\n    <string name=\"status_ui_visibility_mentioned_only\">Mentions uniquement</string>\n    <string name=\"status_ui_label_pinned\">Épinglé</string>\n    <string name=\"status_ui_info_label_edited\">Modifié</string>\n    <string name=\"status_ui_interaction_label_favourited_count\">%1$s favoris</string>\n    <string name=\"status_ui_interaction_label_boosted_count\">%1$s partages</string>\n    <string name=\"status_ui_bottom_label_edited_at\">Modifié le %1$s</string>\n    <string name=\"status_ui_translating\">Traduction…</string>\n    <string name=\"status_ui_translate_show_original\">Afficher l’original</string>\n    <string name=\"status_ui_edit_content_delete_dialog_content\">Confirmer la suppression de ce contenu ?</string>\n    <string name=\"status_ui_edit_content_name_title\">Modifier le nom du contenu</string>\n    <string name=\"status_ui_edit_content_name_label\">Nom du contenu</string>\n    <string name=\"status_ui_edit_content_name_hint\">Veuillez saisir le nom du contenu</string>\n    <string name=\"status_ui_edit_content_config_showing_list_title\">Flux sur la page d’accueil</string>\n    <string name=\"status_ui_edit_content_config_showing_list_description\">Appuyez longuement et faites glisser pour réorganiser</string>\n    <string name=\"status_ui_edit_content_config_hidden_list_title\">Flux pouvant être ajoutés à la page d’accueil</string>\n    <string name=\"status_ui_user_detail_relationship_mutuals\">Abonnements mutuels</string>\n    <string name=\"status_ui_user_detail_relationship_blocking\">Bloqué</string>\n    <string name=\"status_ui_user_detail_relationship_following\">Abonnements</string>\n    <string name=\"status_ui_user_detail_relationship_not_follow\">Suivre</string>\n    <string name=\"status_ui_user_detail_relationship_requested\">En attente</string>\n    <string name=\"status_ui_user_detail_relationship_follow_back\">Suivre en retour</string>\n    <string name=\"status_ui_user_detail_request_by_tip\">Cet utilisateur souhaite vous suivre</string>\n    <string name=\"status_ui_relationship_btn_dialog_content_cancel_follow\">Voulez-vous arrêter de suivre cet utilisateur ?</string>\n    <string name=\"status_ui_relationship_btn_dialog_content_cancel_follow_request\">Voulez-vous annuler votre demande de suivi ?</string>\n    <string name=\"status_ui_relationship_btn_dialog_content_cancel_blocking\">Voulez-vous vraiment débloquer cet utilisateur ?</string>\n    <string name=\"status_ui_user_detail_follows_you\">Vous suit</string>\n    <string name=\"status_ui_user_detail_posts\">Publications</string>\n    <string name=\"status_ui_user_detail_follower_info\">Abonnés</string>\n    <string name=\"status_ui_user_detail_following_info\">Abonnements</string>\n    <string name=\"status_ui_top_label_continued_thread\">Fil continué</string>\n    <string name=\"status_ui_update_dialog_title\">Mise à jour</string>\n    <string name=\"status_ui_update_dialog_release_note\">Journal de mise à jour version %1$s :\\n%2$s</string>\n    <string name=\"status_ui_switch_account_dialog_title\">Sélectionner un compte</string>\n    <string name=\"status_ui_likes\">J’aime</string>\n    <string name=\"status_ui_bookmarks\">Favoris</string>\n    <string name=\"status_ui_edit_profile\">Modifier le profil</string>\n    <string name=\"status_ui_logout\">Se déconnecter</string>\n    <string name=\"status_ui_logout_dialog_content\">Voulez-vous vraiment vous déconnecter ?</string>\n    <string name=\"status_ui_search_account_status_hint\">Rechercher des publications</string>\n    <string name=\"shared_notification_unknown_desc\">Notification inconnue</string>\n    <string name=\"shared_notification_favourited_desc\">a ajouté votre publication aux favoris</string>\n    <string name=\"shared_notification_reblog_desc\">a partagé votre publication</string>\n    <string name=\"shared_notification_poll_desc\">Voir les résultats précédents du vote</string>\n    <string name=\"shared_notification_poll_count\">%1$s votes · Clos</string>\n    <string name=\"shared_notification_follow_desc\">vous suit</string>\n    <string name=\"shared_notification_update_desc\">a modifié une publication</string>\n    <string name=\"shared_notification_follow_request\">Vous avez reçu une demande de suivi</string>\n    <string name=\"shared_notification_new_status_desc\">a publié un nouveau billet</string>\n    <string name=\"shared_notification_severed_desc\">a rompu les liens avec vous</string>\n    <string name=\"shared_notification_quote_desc\">a cité votre publication</string>\n    <string name=\"shared_notification_reply_desc\">a répondu à votre publication</string>\n    <string name=\"shared_user_list_title_following\">Abonnements</string>\n    <string name=\"shared_user_list_title_followers\">Abonnés</string>\n    <string name=\"shared_user_list_title_mutes\">Utilisateurs masqués</string>\n    <string name=\"shared_user_list_title_blocks\">Utilisateurs bloqués</string>\n    <string name=\"shared_user_list_title_likes\">Favoris</string>\n    <string name=\"shared_user_list_title_reblog\">Partages</string>\n    <string name=\"shared_user_list_action_mute\">Masquer</string>\n    <string name=\"shared_user_list_action_block\">Bloquer</string>\n    <string name=\"shared_user_list_action_muted\">Masqué</string>\n    <string name=\"shared_user_list_action_blocked\">Bloqué</string>\n    <string name=\"shared_user_list_action_mute_dialog_message\">Voulez-vous vraiment masquer cet utilisateur ?</string>\n    <string name=\"shared_user_list_action_block_dialog_message\">Voulez-vous vraiment bloquer cet utilisateur ?</string>\n    <string name=\"shared_publish_blog_title\">Publier</string>\n    <string name=\"shared_publish_blog_text_hint\">Écrivez ou collez ce que vous pensez</string>\n    <string name=\"shared_publish_reply_input_hint\">Réponse à [[%1$s]]</string>\n    <string name=\"shared_publish_interaction_no_limit\">Tout le monde peut interagir</string>\n    <string name=\"shared_publish_interaction_limited\">Interaction limitée</string>\n    <string name=\"shared_publish_interaction_dialog_title\">Paramètres d’interaction de la publication</string>\n    <string name=\"shared_publish_interaction_dialog_subtitle\">Personnalisez qui peut interagir avec cette publication.</string>\n    <string name=\"shared_publish_interaction_dialog_quote_title\">Paramètres de citation</string>\n    <string name=\"shared_publish_interaction_dialog_quote_allow\">Autoriser les citations</string>\n    <string name=\"shared_publish_interaction_dialog_reply_title\">Paramètres de réponse</string>\n    <string name=\"shared_publish_interaction_dialog_reply_subtitle\">Autoriser les réponses de :</string>\n    <string name=\"shared_publish_interaction_dialog_reply_all\">Tout le monde</string>\n    <string name=\"shared_publish_interaction_dialog_reply_nobody\">Personne</string>\n    <string name=\"shared_publish_interaction_dialog_reply_combine_title\">Ou combiner ces options :</string>\n    <string name=\"shared_publish_interaction_dialog_mentioned\">Utilisateurs mentionnés</string>\n    <string name=\"shared_publish_interaction_dialog_following\">Utilisateurs que vous suivez</string>\n    <string name=\"shared_publish_interaction_dialog_follower\">Vos abonnés</string>\n    <string name=\"shared_publish_interaction_dialog_in_list\">Utilisateurs dans « %1$s »</string>\n    <string name=\"shared_publish_media_alt_dialog_title\">Ajouter un texte alternatif</string>\n    <string name=\"shared_publish_media_alt_dialog_input_tip\">Texte descriptif alternatif</string>\n    <string name=\"shared_publish_media_alt_dialog_input_hint\">Texte alternatif</string>\n    <string name=\"shared_feeds_not_login_title\">Oups ! Votre session a expiré. Veuillez vous reconnecter pour continuer.</string>\n    <string name=\"shared_feeds_go_to_login\">Se connecter</string>\n    <string name=\"shared_publish_select_account_title\">Veuillez sélectionner un compte</string>\n    <string name=\"shared_select_language_title\">Sélectionner la langue</string>\n    <string name=\"shared_status_context_screen_title\">Détail</string>\n    <string name=\"shared_alt_label\">ALT</string>\n    <string name=\"status_ui_embed_quote_unavailable\">La citation actuelle n’est pas disponible</string>\n    <string name=\"status_ui_action_unforward\">Annuler le partage</string>\n    <string name=\"status_ui_quote_approval_public\">Tout le monde</string>\n    <string name=\"status_ui_quote_approval_follower\">Abonnés uniquement</string>\n    <string name=\"status_ui_quote_approval_nobody\">Moi seulement</string>\n    <string name=\"status_ui_visibility\">Visibilité</string>\n</resources>\n"
  },
  {
    "path": "localization/src/commonMain/composeResources/values-ja-rJP/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"skip\">スキップ</string>\n    <string name=\"alert\">アラート</string>\n    <string name=\"duration_minute\">分</string>\n    <string name=\"duration_hour\">時間</string>\n    <string name=\"duration_day\">日</string>\n    <string name=\"duration_week\">週</string>\n    <string name=\"cancel\">キャンセル</string>\n    <string name=\"ok\">OK</string>\n    <string name=\"empty\">ここには何もありません～</string>\n    <string name=\"duration_selector_title\">期間を設定</string>\n    <string name=\"retry\">再試行</string>\n    <string name=\"load_more_error\">おっと！読み込みに失敗しました。</string>\n    <string name=\"unknown_error\">不明なエラー</string>\n    <string name=\"network_error\">ネットワークエラー</string>\n    <string name=\"resource_not_found\">リソースが見つかりません</string>\n    <string name=\"mixed_content_subtitle_1\">ミックスコンテンツ ·</string>\n    <string name=\"mixed_content_subtitle_2\">件のフィード</string>\n    <string name=\"date_time_ago\">前</string>\n    <string name=\"date_time_ago_prefix\"></string>\n    <string name=\"date_time_ago_suffix\">前</string>\n    <string name=\"date_time_day\">日</string>\n    <string name=\"date_time_hour\">時間</string>\n    <string name=\"date_time_minute\">分</string>\n    <string name=\"date_time_second\">秒</string>\n    <string name=\"image_saving\">保存中…</string>\n    <string name=\"image_save_success\">保存しました</string>\n    <string name=\"image_save_failed\">保存に失敗しました</string>\n    <string name=\"permission_write_external_permission_denied\">ストレージ権限が拒否されました。設定で有効にしてください。</string>\n    <string name=\"feeds_load_previous_page_label\">前のページを読み込み中</string>\n    <string name=\"feeds_load_previous_page_failed_label\">読み込みに失敗しました。タップして再試行</string>\n    <string name=\"image\">画像</string>\n    <string name=\"video\">動画</string>\n    <string name=\"login\">ログイン</string>\n    <string name=\"add\">追加</string>\n    <string name=\"search\">検索</string>\n    <string name=\"auth_success\">ログインに成功しました</string>\n    <string name=\"auth_failed\">ログインに失敗しました</string>\n    <string name=\"select\">選択</string>\n    <string name=\"logout\">ログアウト</string>\n    <string name=\"settings\">設定</string>\n    <string name=\"donate\">寄付</string>\n    <string name=\"feeds\">フィード</string>\n    <string name=\"save\">保存</string>\n    <string name=\"done\">完了</string>\n    <string name=\"bluesky_name\">Bluesky</string>\n    <string name=\"bluesky_description\">Bluesky はオープンなネットワークです。1 つのアカウントで使いやすいソーシャルネットワークにアクセスでき、ソーシャルインターネット全体で共有アイデンティティを持てます。</string>\n    <string name=\"mastodon_name\">マストドン</string>\n    <string name=\"mastodon_description\">Mastodon は分散型・非商業的なソーシャルメディアで、ユーザーが自分のタイムラインを管理でき、各サーバーは独立した存在です。</string>\n    <string name=\"mixed_content_name\">ミックスコンテンツ</string>\n    <string name=\"mixed_content_description\">ミックスコンテンツは Fread が提供するカスタム統合フィードです。マストドン、Bluesky、RSS などのプラットフォームからソースを追加でき、これらのコンテンツを新しい順の一つのフィードにまとめます。</string>\n    <string name=\"status_provider_type_activity_pub\">マストドン</string>\n    <string name=\"add_content_title\">コンテンツを追加</string>\n    <string name=\"add_content_success_with_account\">追加しました。現在のアカウント：</string>\n    <string name=\"add_content_success_with_login_reminder\">追加しました。ログインしますか？</string>\n    <string name=\"content_exist_tips\">このコンテンツはすでに存在します</string>\n    <string name=\"content_add_success\">コンテンツを追加しました</string>\n    <string name=\"add_feeds_page_empty_name_exist\">この名前は既に使用されています</string>\n    <string name=\"edit_profile_name_empty\">ユーザー名を入力してください</string>\n    <string name=\"edit_profile_label_name\">名前</string>\n    <string name=\"edit_profile_label_note\">プロフィール</string>\n    <string name=\"edit_profile_input_name_hint\">ユーザー名を入力</string>\n    <string name=\"edit_profile_input_note_hint\">プロフィールを入力</string>\n    <string name=\"edit_profile_edit_confirm_message\">編集を終了しますか？</string>\n    <string name=\"add_content_success_snackbar\">追加しました</string>\n    <string name=\"login_dialog_input_hint\">ログインするサーバーを入力してください</string>\n    <string name=\"login_dialog_title\">ログイン先のサーバーを選択してください</string>\n    <string name=\"login_dialog_input_tip\">サーバーアドレスを入力してください</string>\n    <string name=\"login_dialog_input_title\">サーバーアドレスを入力してください</string>\n    <string name=\"login_dialog_input_label\">サーバーアドレス</string>\n    <string name=\"login_dialog_target_title\">ログインしてください</string>\n    <string name=\"profile_description\">Fread は複数アカウントの追加に対応しています。追加したアカウントで投稿・いいね・ブースト・コメントなどができ、追加済みアカウントが作成したリストも閲覧できます。</string>\n    <string name=\"list_content_empty_placeholder\">コンテンツはありません</string>\n    <string name=\"post_status_scope_public\">公開</string>\n    <string name=\"post_status_scope_unlisted\">公開（探索に表示しない）</string>\n    <string name=\"post_status_scope_follower_only\">フォロワーのみ</string>\n    <string name=\"post_status_scope_mentioned_only\">メンションした相手のみ</string>\n    <string name=\"post_status_content_warning\">コンテンツ警告</string>\n    <string name=\"post_status_success\">送信しました</string>\n    <string name=\"post_status_failed\">送信に失敗しました：%1$s</string>\n    <string name=\"post_status_exit_dialog_content\">編集内容は保存されません。終了しますか？</string>\n    <string name=\"post_status_content_is_empty\">内容を入力してください</string>\n    <string name=\"post_status_part_failed\">一部のアカウントで投稿に失敗しました。成功したアカウントはキューから削除しました。残りで再試行してください：%1$s</string>\n    <string name=\"select_account_open_status_title\">アカウントを選択</string>\n    <string name=\"select_account_open_status_empty\">他のアカウントはありません</string>\n    <string name=\"select_account_open_status_search_in\">%1$s 内を検索</string>\n    <string name=\"select_account_open_status_search_failed\">検索に失敗しました</string>\n    <string name=\"explorer_search_no_results\">結果が見つかりません</string>\n    <string name=\"explorer_search_bar_hint\">検索</string>\n    <string name=\"explorer_search_bar_hint_specialize_platform\">%1$s を検索</string>\n    <string name=\"explorer_search_tab_title_author\">ユーザー</string>\n    <string name=\"explorer_search_tab_title_status\">投稿</string>\n    <string name=\"explorer_search_tab_title_hashtag\">ハッシュタグ</string>\n    <string name=\"explorer_search_tab_title_server\">サーバー</string>\n    <string name=\"explorer_tab_title\">みつける</string>\n    <string name=\"explorer_tab_status_title\">投稿</string>\n    <string name=\"explorer_tab_users_title\">ユーザー</string>\n    <string name=\"explorer_tab_hashtag_title\">トピック</string>\n    <string name=\"add_provider_page_title\">サービスを追加</string>\n    <string name=\"add_provider_page_confirm_button\">追加を確定</string>\n    <string name=\"search_page_title\">検索</string>\n    <string name=\"search_result_not_found\">該当する検索結果はありません</string>\n    <string name=\"feeds_import_page_title\">インポート</string>\n    <string name=\"feeds_import_page_hint\">インポートする OPML ファイルを選択してください</string>\n    <string name=\"feeds_import_button\">インポート</string>\n    <string name=\"add_feeds_page_title\">コンテンツを追加</string>\n    <string name=\"add_feeds_page_feeds_name_label\">コンテンツ名</string>\n    <string name=\"add_feeds_page_feeds_name_hint\">コンテンツ名を入力してください</string>\n    <string name=\"add_feeds_page_feeds_empty\">購読元を追加してください</string>\n    <string name=\"pre_add_feeds_no_result\">結果が見つかりません</string>\n    <string name=\"pre_add_feeds_hint\">インスタンス名・URL・ユーザーアドレス・RSS</string>\n    <string name=\"pre_add_feeds_input_label_1\">Fread は [[マストドン]]、[[Bluesky]]、[[RSS]] のコンテンツ、さらには [[ミックスコンテンツ]] の追加に対応しています。</string>\n    <string name=\"pre_add_feeds_input_label_2\">ここにマストドンまたは Bluesky の[[ユーザー名]]、[[WebFinger\\Handle]]、ユーザーの[[ホームページURL]]、または [[RSS]] の URL を入力してください。</string>\n    <string name=\"empty_content_hint_title\">まずコンテンツを追加してください</string>\n    <string name=\"empty_content_hint_desc\">先に購読元を追加する必要があります。Fread は多様なコンテンツタイプや異なるプラットフォームのミックスに対応し、あなた専用の情報フィードを作成できます。</string>\n    <string name=\"feeds_add_content\">コンテンツを追加</string>\n    <string name=\"select_feeds_type_screen_title\">コンテンツタイプを選択</string>\n    <string name=\"feeds_pre_add_login_dialog_content\">追加しました。ログインしますか？</string>\n    <string name=\"add_feeds_page_empty_name_tips\">名前を入力してください</string>\n    <string name=\"add_feeds_page_empty_source_tips\">情報源を追加してください</string>\n    <string name=\"add_feeds_choose_auth_dialog_title\">ログイン先のサーバーを選択してください</string>\n    <string name=\"search_feeds_title\">検索</string>\n    <string name=\"search_feeds_title_hint\">マストドン/Bluesky のユーザー名、WebFinger、URL、RSS</string>\n    <string name=\"feeds_select_account_for_post_status\">アカウントを選択してください</string>\n    <string name=\"feeds_mixed_config_edit_new_name_dialog_title\">コンテンツ名を入力してください</string>\n    <string name=\"feeds_mixed_config_edit_new_name_dialog_label\">名前</string>\n    <string name=\"feeds_mixed_config_edit_delete_content_dialog_message\">このコンテンツを削除しますか？</string>\n    <string name=\"feeds_mixed_config_not_found\">異常が発生しました。設定が見つかりません。</string>\n    <string name=\"feeds_import_back_dialog_message\">終了してもよろしいですか？</string>\n    <string name=\"feeds_delete_confirm_content\">削除してもよろしいですか？</string>\n    <string name=\"feeds_select_type_screen_title\">コンテンツタイプを選択</string>\n    <string name=\"feeds_select_type_screen_content\">追加するコンテンツタイプを選択してください</string>\n    <string name=\"notification_tab_title\">通知</string>\n    <string name=\"notifications_account_empty_tip\">現在、ログイン中のアカウントはありません</string>\n    <string name=\"notifications_tab_all\">すべて</string>\n    <string name=\"notifications_tab_mention\">メンション</string>\n    <string name=\"profile_page_title\">アカウント</string>\n    <string name=\"profile_setting_about_title\">概要</string>\n    <string name=\"profile_setting_donate_desc\">このプロジェクトの支援にコーヒーをごちそうください</string>\n    <string name=\"profile_setting_inline_video_auto_play\">インライン動画の自動再生</string>\n    <string name=\"profile_setting_inline_video_auto_play_subtitle\">有効にすると、フィード内の動画が自動再生されます。</string>\n    <string name=\"profile_setting_always_show_sensitive_content\">センシティブなコンテンツを常に表示</string>\n    <string name=\"profile_setting_always_show_sensitive_content_subtitle\">有効にすると、センシティブなコンテンツを既定で表示します（Mastodonのみ）。</string>\n    <string name=\"profile_setting_dark_mode_title\">ダークモード</string>\n    <string name=\"profile_setting_dark_mode_dark\">ダークモード</string>\n    <string name=\"profile_setting_dark_mode_light\">ライトモード</string>\n    <string name=\"profile_setting_dark_mode_follow_system\">システムに合わせる</string>\n    <string name=\"profile_setting_open_source_title\">オープンソース表記</string>\n    <string name=\"profile_setting_open_source_desc\">本プロジェクトで使用している依存ライブラリのライセンス表記</string>\n    <string name=\"profile_setting_open_source_feedback\">フィードバックを送る</string>\n    <string name=\"profile_setting_open_source_feedback_desc\">Telegram グループに参加するか、その他の方法でご連絡ください</string>\n    <string name=\"profile_setting_open_source_feedback_telegram\">Telegram グループに参加</string>\n    <string name=\"profile_setting_open_source_feedback_github\">GitHub の issue を作成</string>\n    <string name=\"profile_setting_open_source_feedback_email\">メールを送信</string>\n    <string name=\"profile_setting_language_title\">表示言語</string>\n    <string name=\"profile_setting_language_zh\">簡体字中国語</string>\n    <string name=\"profile_setting_language_en\">英語</string>\n    <string name=\"profile_setting_language_system\">システムに合わせる</string>\n    <string name=\"profile_setting_ratting\">評価する</string>\n    <string name=\"profile_setting_ratting_desc\">気に入っていただけたらストアで高評価をお願いします</string>\n    <string name=\"profile_setting_font_size\">コンテンツの文字サイズ</string>\n    <string name=\"profile_setting_font_size_desc\">コンテンツの文字とアイコンのサイズを調整します</string>\n    <string name=\"profile_setting_font_size_small\">小</string>\n    <string name=\"profile_setting_font_size_medium\">中</string>\n    <string name=\"profile_setting_font_size_large\">大</string>\n    <string name=\"profile_setting_immersive_nav_bar\">イマーシブナビゲーションバー</string>\n    <string name=\"profile_setting_immersive_nav_bar_desc\">有効にすると、ホームをスクロール中に下部のナビゲーションバーが自動的に隠れます。</string>\n    <string name=\"profile_setting_timeline_position\">ホームのタイムライン既定位置</string>\n    <string name=\"profile_setting_timeline_position_last_read\">最後に読んだ位置</string>\n    <string name=\"profile_setting_timeline_position_newest\">最新の投稿</string>\n    <string name=\"profile_setting_theme_title\">テーマカラー</string>\n    <string name=\"profile_setting_theme_default\">既定</string>\n    <string name=\"profile_setting_theme_system\">システム</string>\n    <string name=\"profile_about_version\">バージョン：</string>\n    <string name=\"profile_about_website\">公式サイト：</string>\n    <string name=\"profile_about_developer\">開発者：</string>\n    <string name=\"profile_about_contract_us\">連絡先：</string>\n    <string name=\"profile_about_telegram\">Telegram グループ：</string>\n    <string name=\"profile_about_privacy_policy\">プライバシーポリシー：</string>\n    <string name=\"profile_setting_check_for_update\">アップデートを確認</string>\n    <string name=\"profile_setting_already_latest_version\">最新バージョンを利用中です</string>\n    <string name=\"profile_setting_have_new_version\">新しいバージョンがあります。タップして更新。</string>\n    <string name=\"profile_donate_page_title\">寄付方法を選択してください</string>\n    <string name=\"profile_account_not_login\">ログインが無効です</string>\n    <string name=\"rss_source_detail_screen_title\">情報源のプロパティ</string>\n    <string name=\"rss_source_detail_screen_custom_title\">カスタムタイトル</string>\n    <string name=\"rss_source_detail_screen_url\">購読 URL</string>\n    <string name=\"rss_source_detail_screen_home_url\">ホーム</string>\n    <string name=\"rss_source_detail_screen_add_date\">追加日</string>\n    <string name=\"rss_source_detail_screen_last_update_date\">最終更新日</string>\n    <string name=\"bluesky_protocol_name\">Bluesky</string>\n    <string name=\"bsky_add_content_title\">ログイン</string>\n    <string name=\"bsky_add_content_hosting_provider\">ホスティングプロバイダ</string>\n    <string name=\"bsky_add_content_user_name\">ユーザー名またはメールアドレス</string>\n    <string name=\"bsky_add_content_password\">パスワード</string>\n    <string name=\"bsky_add_content_factor_token\">認証コード</string>\n    <string name=\"bsky_edit_content_title\">編集</string>\n    <string name=\"bsky_feeds_explorer_more\">ほかのフィードをもっと見る</string>\n    <string name=\"bsky_feeds_explorer_creator_label\">作成者 %1$s</string>\n    <string name=\"bsky_feeds_explorer_liked_by\">%1$s 人がいいね</string>\n    <string name=\"bsky_feeds_detail_creator_prefix\">作成者</string>\n    <string name=\"bsky_feeds_item_subtitle\">作成者 %1$s · いいね %2$s 件</string>\n    <string name=\"bsky_feeds_following_name\">フォロー中</string>\n    <string name=\"bsky_feeds_user_posts\">投稿</string>\n    <string name=\"bsky_feeds_user_replies\">返信</string>\n    <string name=\"bsky_feeds_user_medias\">メディア</string>\n    <string name=\"bsky_feeds_user_likes\">いいね</string>\n    <string name=\"bsky_user_detail_action_blocked_list\">ブロックしたユーザー</string>\n    <string name=\"bsky_user_detail_action_muted_list\">ミュートしたユーザー</string>\n    <string name=\"bsky_user_detail_action_mute_user\">%1$s をミュート</string>\n    <string name=\"bsky_user_detail_action_unmute_user\">ミュート解除</string>\n    <string name=\"bsky_user_detail_action_block_user\">%1$s をブロック</string>\n    <string name=\"bsky_user_detail_action_unblock_user\">ブロック解除</string>\n    <string name=\"bsky_user_detail_action_block_user_dialog_message\">このユーザーをブロックしますか？</string>\n    <string name=\"bsky_user_detail_action_mute_user_dialog_message\">このユーザーをミュートしますか？</string>\n    <string name=\"bsky_edit_profile_title\">プロフィールを編集</string>\n    <string name=\"input_media_desc_page_title\">説明文</string>\n    <string name=\"input_media_desc_input_hint\">説明文を入力してください</string>\n    <string name=\"post_status_page_title\">新規ブログ</string>\n    <string name=\"post_screen_input_hint\">ブログ本文を入力してください</string>\n    <string name=\"post_screen_media_descriptor_placeholder\">説明文</string>\n    <string name=\"post_status_poll_item_hint\">選択肢 %1$s</string>\n    <string name=\"post_status_poll_duration\">投票期間</string>\n    <string name=\"post_status_poll_function_title\">機能</string>\n    <string name=\"post_status_poll_single\">単一選択</string>\n    <string name=\"post_status_poll_multiple\">複数選択</string>\n    <string name=\"post_status_poll_style_select_dialog_title\">機能を選択してください</string>\n    <string name=\"post_status_poll_is_empty\">投票内容を入力してください</string>\n    <string name=\"post_status_media_is_not_upload\">メディアがまだアップロードされていません</string>\n    <string name=\"authentication_page_normal_title\">この機能を利用するにはログインが必要です</string>\n    <string name=\"authentication_page_failed_title\">ログイン状態が無効です。再度ログインしてください。</string>\n    <string name=\"main_drawer_title\">コンテンツ一覧</string>\n    <string name=\"main_drawer_mixed_item_subtitle_1\">ミックスコンテンツ ·</string>\n    <string name=\"main_drawer_mixed_item_subtitle_2\">件のフィード</string>\n    <string name=\"main_drawer_settings\">設定</string>\n    <string name=\"main_drawer_donate\">寄付</string>\n    <string name=\"main_request_notification_dialog_title\">通知の許可</string>\n    <string name=\"main_request_notification_dialog_reject_button\">拒否</string>\n    <string name=\"main_request_notification_dialog_allow_button\">許可</string>\n    <string name=\"main_request_notification_dialog_content\">交流に関する通知を受け取るには、通知を許可してください。</string>\n    <string name=\"setting_item_amoled_mode\">AMOLEDモード</string>\n    <string name=\"setting_item_amoled_mode_description\">有効にすると、ダークモードで背景が純黒になり、消費電力を抑えつつ暗い環境でのコントラストが向上します。</string>\n    <string name=\"setting_group_appearance\">外観</string>\n    <string name=\"setting_group_appearance_subtitle\">テーマ、表示、ホーム画面スタイルの設定</string>\n    <string name=\"setting_group_behavior\">動作</string>\n    <string name=\"setting_group_behavior_subtitle\">コンテンツ表示と操作動作の設定</string>\n    <string name=\"setting_item_home_tab_next_title\">ホーム上部のコンテンツ切り替えボタンを表示</string>\n    <string name=\"setting_item_home_tab_next_subtitle\">ホーム右上に次のコンテンツへ切り替えるボタンを表示する</string>\n    <string name=\"setting_item_home_tab_refresh_title\">ホーム上部の更新ボタンを表示</string>\n    <string name=\"setting_item_home_tab_refresh_subtitle\">ホーム右上に内容を更新するボタンを表示する</string>\n    <string name=\"setting_item_open_url_by_system_title\">システムブラウザでリンクを開く</string>\n    <string name=\"setting_item_open_url_by_system_subtitle\">有効にすると、Fread はリンクをシステムブラウザで開きます。</string>\n    <string name=\"setting_item_solid_bar_background_title\">上下バーを不透明背景にする</string>\n    <string name=\"setting_item_solid_bar_background_subtitle\">上部バーと下部バーをぼかしではなく単色にします。</string>\n</resources>\n"
  },
  {
    "path": "localization/src/commonMain/composeResources/values-ja-rJP/strings_mastodon.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"activity_pub_user_detail_join_date\">参加日時</string>\n    <string name=\"activity_pub_user_detail_tab_about_joined\">%1$s に参加</string>\n    <string name=\"activity_pub_user_detail_menu_block\">%1$s をブロック</string>\n    <string name=\"activity_pub_user_detail_menu_block_domain\">%1$s をブロック</string>\n    <string name=\"activity_pub_user_detail_menu_unblock_domain\">%1$s のブロックを解除</string>\n    <string name=\"activity_pub_user_detail_menu_edit_private_note\">メモを編集</string>\n    <string name=\"activity_pub_user_detail_menu_edit_private_note_dialog_hint\">メモを入力してください</string>\n    <string name=\"activity_pub_user_detail_dialog_content_block\">相手をブロックしますか？</string>\n    <string name=\"activity_pub_user_detail_dialog_content_block_domain\">相手のインスタンスをブロックしますか？</string>\n    <string name=\"activity_pub_user_menu_blocked_user_list\">ブロックしたユーザー</string>\n    <string name=\"activity_pub_user_menu_muted_user_list\">ミュートしたユーザー</string>\n    <string name=\"activity_pub_user_detail_menu_mute_user\">%1$s をミュート</string>\n    <string name=\"activity_pub_user_detail_menu_unmute_user\">%1$s のミュートを解除</string>\n    <string name=\"activity_pub_mute_user_bottom_sheet_title\">ユーザーをミュートしますか？</string>\n    <string name=\"activity_pub_mute_user_bottom_sheet_role1\">相手にミュートは通知されません。</string>\n    <string name=\"activity_pub_mute_user_bottom_sheet_role2\">相手はあなたの投稿を見られますが、あなたは相手の投稿を見ません。</string>\n    <string name=\"activity_pub_mute_user_bottom_sheet_role3\">相手が言及された投稿も表示されません。</string>\n    <string name=\"activity_pub_mute_user_bottom_sheet_role4\">相手はあなたに言及・フォローできますが、あなた側では表示されません。</string>\n    <string name=\"activity_pub_mute_user_bottom_sheet_btn_mute\">ミュート</string>\n    <string name=\"add_instance_screen_title\">インスタンスを追加</string>\n    <string name=\"add_instance_input_service_hint\">サーバーアドレスを入力してください</string>\n    <string name=\"add_instance_input_service_error\">正しいサーバーアドレスを入力してください</string>\n    <string name=\"activity_pub_content_tab_home\">ホーム</string>\n    <string name=\"activity_pub_content_tab_local_timeline\">ローカル</string>\n    <string name=\"activity_pub_content_tab_public_timeline\">パブリック</string>\n    <string name=\"activity_pub_content_tab_trending\">トレンド</string>\n    <string name=\"activity_pub_hashtag_timeline_description\">%1$s 件の投稿 · 参加者 %2$s 人 · 本日 %3$s 件の投稿</string>\n    <string name=\"activity_pub_hashtag_unfollow_dialog_message\">この話題のフォローを解除しますか？</string>\n    <string name=\"activity_pub_edit_account_info_label_about\">概要</string>\n    <string name=\"activity_pub_edit_content_screen_config_not_found\">設定が見つかりません</string>\n    <string name=\"activity_pub_user_list_empty\">ユーザーはいません</string>\n    <string name=\"activity_pub_muted_user_list_empty\">ミュート中のユーザーはいません</string>\n    <string name=\"activity_pub_muted_user_list_unmute\">ミュート解除</string>\n    <string name=\"activity_pub_blocked_user_list_empty\">ブロック中のユーザーはいません</string>\n    <string name=\"activity_pub_blocked_user_list_unblock\">ブロック解除</string>\n    <string name=\"activity_pub_bookmarks_list_title\">ブックマーク</string>\n    <string name=\"activity_pub_favourites_list_title\">いいねした内容</string>\n    <string name=\"activity_pub_followed_tags_screen_title\">フォロー中の話題</string>\n    <string name=\"activity_pub_filters_list_page_title\">フィルター</string>\n    <string name=\"activity_pub_filters_active\">有効</string>\n    <string name=\"activity_pub_filters_expired\">期限切れ</string>\n    <string name=\"activity_pub_filter_edit_title\">フィルターを編集</string>\n    <string name=\"activity_pub_filter_edit_input_title_label\">タイトル</string>\n    <string name=\"activity_pub_filter_edit_input_title_hint\">タイトルを入力してください</string>\n    <string name=\"activity_pub_filter_edit_duration\">期間</string>\n    <string name=\"activity_pub_filter_edit_duration_permanent\">永久</string>\n    <string name=\"activity_pub_filter_edit_duration_thirty_minutes\">30 分</string>\n    <string name=\"activity_pub_filter_edit_duration_one_hour\">1 時間</string>\n    <string name=\"activity_pub_filter_edit_duration_twelve_hours\">12 時間</string>\n    <string name=\"activity_pub_filter_edit_duration_one_day\">1 日</string>\n    <string name=\"activity_pub_filter_edit_duration_three_day\">3 日</string>\n    <string name=\"activity_pub_filter_edit_duration_one_week\">1 週間</string>\n    <string name=\"activity_pub_filter_edit_duration_custom\">カスタム</string>\n    <string name=\"activity_pub_filter_edit_duration_finish_subtitle\">%1$s に終了</string>\n    <string name=\"activity_pub_filter_edit_keyword_title\">非表示のキーワード</string>\n    <string name=\"activity_pub_filter_edit_keyword_dialog_title\">キーワードを編集</string>\n    <string name=\"activity_pub_filter_edit_keyword_dialog_hint\">キーワード</string>\n    <string name=\"activity_pub_filter_edit_keyword_remove_keyword_dialog_content\">このキーワードを削除しますか？</string>\n    <string name=\"activity_pub_filter_edit_keyword_list_title\">非表示キーワード</string>\n    <string name=\"activity_pub_filter_edit_keyword_list_desc\">非表示のキーワード %1$s 件</string>\n    <string name=\"activity_pub_filter_edit_context_title\">非表示にする場所</string>\n    <string name=\"activity_pub_filter_edit_context_selector_title\">非表示にする場所を選択してください</string>\n    <string name=\"activity_pub_filter_edit_context_home\">ホーム &amp; リスト</string>\n    <string name=\"activity_pub_filter_edit_context_notification\">通知</string>\n    <string name=\"activity_pub_filter_edit_context_timeline\">公開タイムライン</string>\n    <string name=\"activity_pub_filter_edit_context_thread\">投稿 &amp; 返信</string>\n    <string name=\"activity_pub_filter_edit_context_account\">プロフィール</string>\n    <string name=\"activity_pub_filter_edit_empty_context\">なし</string>\n    <string name=\"activity_pub_filter_edit_warning_title\">コンテンツ警告付きで表示</string>\n    <string name=\"activity_pub_filter_edit_warning_desc\">フィルターに一致する投稿は表示しますが、コンテンツ警告の裏に隠します。</string>\n    <string name=\"activity_pub_filter_edit_delete_content\">削除してもよろしいですか？</string>\n    <string name=\"activity_pub_filter_edit_back_dialog\">終了してもよろしいですか？</string>\n    <string name=\"activity_pub_filter_edit_whole_word\">単語全体</string>\n    <string name=\"activity_pub_explorer_tab_status_title\">投稿</string>\n    <string name=\"activity_pub_explorer_tab_users_title\">ユーザー</string>\n    <string name=\"activity_pub_explorer_tab_hashtag_title\">話題</string>\n    <string name=\"activity_pub_created_list_title\">作成したリスト</string>\n    <string name=\"activity_pub_add_list_title\">リストを作成</string>\n    <string name=\"activity_pub_add_list_name\">リスト名</string>\n    <string name=\"activity_pub_add_list_replies\">返信の表示範囲</string>\n    <string name=\"activity_pub_add_list_replies_non\">表示しない</string>\n    <string name=\"activity_pub_add_list_replies_list\">リストメンバー</string>\n    <string name=\"activity_pub_add_list_replies_followers\">フォロー中のすべての人</string>\n    <string name=\"activity_pub_add_list_accounts\">リストメンバー</string>\n    <string name=\"activity_pub_add_list_hide_in_timeline\">ホームのタイムラインでメンバーを非表示</string>\n    <string name=\"activity_pub_add_list_hide_in_timeline_desc\">重複表示を避けるため、リストのメンバーの投稿をホームのタイムラインで非表示にします。</string>\n    <string name=\"activity_pub_add_list_remove_user_message\">このユーザーを削除しますか？</string>\n    <string name=\"activity_pub_add_list_name_is_empty\">ユーザー名を入力してください</string>\n    <string name=\"activity_pub_add_list_back_reminder\">未保存の変更があります。終了しますか？</string>\n    <string name=\"activity_pub_search_user_placeholder\">検索内容を入力</string>\n    <string name=\"activity_pub_list_delete_confirm\">このリストを削除しますか？</string>\n    <string name=\"activity_pub_login_exception\">認証状態が異常です</string>\n    <string name=\"activity_pub_home_timeline\">ホーム</string>\n    <string name=\"activity_pub_local_timeline\">ローカル</string>\n    <string name=\"activity_pub_public_timeline\">パブリック</string>\n    <string name=\"activity_pub_instance_detail_language_label\">言語: %1$s</string>\n    <string name=\"activity_pub_instance_detail_active_month_label\">月間アクティブ: %1$s</string>\n    <string name=\"activity_pub_user_detail_tab_post\">投稿</string>\n    <string name=\"activity_pub_user_detail_tab_replies\">返信</string>\n    <string name=\"activity_pub_user_detail_tab_media\">メディア</string>\n    <string name=\"activity_pub_user_detail_tab_about\">概要</string>\n    <string name=\"activity_pub_about\">概要</string>\n    <string name=\"activity_pub_about_rule_title\">サイトのルール</string>\n    <string name=\"activity_pub_trends_tag\">話題</string>\n    <string name=\"activity_pub_trends_tag_description\">直近 2 日で %1$s 人が議論中</string>\n    <string name=\"activity_pub_trends_status\">人気</string>\n    <string name=\"activity_pub_select_platform_title\">インスタンスを選択</string>\n    <string name=\"activity_pub_select_platform_text_hint\">インスタンス名または URL を入力してください</string>\n</resources>\n"
  },
  {
    "path": "localization/src/commonMain/composeResources/values-ja-rJP/strings_status_ui.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"status_ui_reposted\">リポスト済み</string>\n    <string name=\"status_ui_repost\">リポスト</string>\n    <string name=\"status_ui_quote\">引用</string>\n    <string name=\"status_ui_like\">いいね</string>\n    <string name=\"status_ui_comment\">コメント</string>\n    <string name=\"status_ui_delete\">削除</string>\n    <string name=\"status_ui_share\">共有</string>\n    <string name=\"status_ui_bookmark\">ブックマーク</string>\n    <string name=\"status_ui_unbookmark\">ブックマーク解除</string>\n    <string name=\"status_ui_reply\">返信</string>\n    <string name=\"status_ui_follow\">フォロー</string>\n    <string name=\"status_ui_unfollow\">フォロー解除</string>\n    <string name=\"status_ui_pin\">固定</string>\n    <string name=\"status_ui_unpin\">固定解除</string>\n    <string name=\"status_ui_edit\">編集</string>\n    <string name=\"status_ui_sensitive_by_filter\">「%1$s」によりフィルターされました</string>\n    <string name=\"status_ui_new_status\">新しい投稿</string>\n    <string name=\"status_ui_delete_status_confirm\">この内容を削除しますか？</string>\n    <string name=\"status_ui_image_sensitive_label\">センシティブな内容</string>\n    <string name=\"status_ui_image_content_show_hidden_label\">もっと見る</string>\n    <string name=\"status_ui_image_content_hide_hidden_label\">非表示にする</string>\n    <string name=\"status_ui_poll_vote\">投票</string>\n    <string name=\"status_ui_poll_vote_finished_tip\">%1$s 票 · 終了</string>\n    <string name=\"status_ui_poll_votes_finished_tip\">%1$s 票 · 終了</string>\n    <string name=\"status_ui_interaction_open_in_browser\">ブラウザで開く</string>\n    <string name=\"status_ui_interaction_open_original_instance\">相手のインスタンスを開く</string>\n    <string name=\"status_ui_interaction_copy_url\">リンクをコピー</string>\n    <string name=\"status_ui_interaction_translate\">翻訳</string>\n    <string name=\"status_ui_interaction_open_blog_by_other_account\">他のアカウントで開く</string>\n    <string name=\"status_ui_visibility_mentioned_only\">メンションされたユーザーのみ</string>\n    <string name=\"status_ui_label_pinned\">固定</string>\n    <string name=\"status_ui_info_label_edited\">編集済み</string>\n    <string name=\"status_ui_interaction_label_favourited_count\">%1$s 件のいいね</string>\n    <string name=\"status_ui_interaction_label_boosted_count\">%1$s 件のブースト</string>\n    <string name=\"status_ui_bottom_label_edited_at\">%1$s に編集</string>\n    <string name=\"status_ui_translating\">翻訳中…</string>\n    <string name=\"status_ui_translate_show_original\">原文を表示</string>\n    <string name=\"status_ui_edit_content_delete_dialog_content\">この内容を削除しますか？</string>\n    <string name=\"status_ui_edit_content_name_title\">内容名を編集</string>\n    <string name=\"status_ui_edit_content_name_label\">内容名を編集</string>\n    <string name=\"status_ui_edit_content_name_hint\">内容の名前を入力してください</string>\n    <string name=\"status_ui_edit_content_config_showing_list_title\">ホームに表示されるリスト</string>\n    <string name=\"status_ui_edit_content_config_showing_list_description\">長押ししてドラッグで並べ替え</string>\n    <string name=\"status_ui_edit_content_config_hidden_list_title\">ホームに追加できるリスト</string>\n    <string name=\"status_ui_user_detail_relationship_mutuals\">相互フォロー</string>\n    <string name=\"status_ui_user_detail_relationship_blocking\">ブロック中</string>\n    <string name=\"status_ui_user_detail_relationship_following\">フォロー中</string>\n    <string name=\"status_ui_user_detail_relationship_not_follow\">フォロー</string>\n    <string name=\"status_ui_user_detail_relationship_requested\">リクエスト中</string>\n    <string name=\"status_ui_user_detail_relationship_follow_back\">フォローバック</string>\n    <string name=\"status_ui_user_detail_request_by_tip\">相手があなたをフォローしようとしています</string>\n    <string name=\"status_ui_relationship_btn_dialog_content_cancel_follow\">フォローを解除しますか？</string>\n    <string name=\"status_ui_relationship_btn_dialog_content_cancel_follow_request\">フォローリクエストを取り消しますか？</string>\n    <string name=\"status_ui_relationship_btn_dialog_content_cancel_blocking\">相手のブロックを解除しますか？</string>\n    <string name=\"status_ui_user_detail_follows_you\">あなたをフォローしました</string>\n    <string name=\"status_ui_user_detail_posts\">投稿</string>\n    <string name=\"status_ui_user_detail_follower_info\">フォロワー</string>\n    <string name=\"status_ui_user_detail_following_info\">フォロー中</string>\n    <string name=\"status_ui_top_label_continued_thread\">スレッドを展開</string>\n    <string name=\"status_ui_update_dialog_title\">アップデート</string>\n    <string name=\"status_ui_update_dialog_release_note\">バージョン %1$s 更新ログ:\\n%2$s</string>\n    <string name=\"status_ui_switch_account_dialog_title\">アカウントを選択</string>\n    <string name=\"status_ui_likes\">いいね</string>\n    <string name=\"status_ui_bookmarks\">ブックマーク</string>\n    <string name=\"status_ui_edit_profile\">プロフィールを編集</string>\n    <string name=\"status_ui_logout\">ログアウト</string>\n    <string name=\"status_ui_logout_dialog_content\">ログアウトしますか？</string>\n    <string name=\"status_ui_search_account_status_hint\">検索する投稿を入力してください</string>\n    <string name=\"shared_notification_unknown_desc\">不明な通知</string>\n    <string name=\"shared_notification_favourited_desc\">あなたの投稿にいいねしました</string>\n    <string name=\"shared_notification_reblog_desc\">あなたの投稿をブーストしました</string>\n    <string name=\"shared_notification_poll_desc\">投票結果を見る</string>\n    <string name=\"shared_notification_poll_count\">%1$s 票 · 終了</string>\n    <string name=\"shared_notification_follow_desc\">あなたをフォローしました</string>\n    <string name=\"shared_notification_update_desc\">投稿を編集しました</string>\n    <string name=\"shared_notification_follow_request\">フォローリクエストが届きました</string>\n    <string name=\"shared_notification_new_status_desc\">新しい投稿を公開しました</string>\n    <string name=\"shared_notification_severed_desc\">関係を解除しました</string>\n    <string name=\"shared_notification_quote_desc\">あなたの投稿を引用しました</string>\n    <string name=\"shared_notification_reply_desc\">あなたの投稿に返信しました</string>\n    <string name=\"shared_user_list_title_following\">フォロー中のユーザー</string>\n    <string name=\"shared_user_list_title_followers\">フォロワー</string>\n    <string name=\"shared_user_list_title_mutes\">ミュートしたユーザー</string>\n    <string name=\"shared_user_list_title_blocks\">ブロックしたユーザー</string>\n    <string name=\"shared_user_list_title_likes\">いいねしたユーザー</string>\n    <string name=\"shared_user_list_title_reblog\">ブーストしたユーザー</string>\n    <string name=\"shared_user_list_action_mute\">ミュート</string>\n    <string name=\"shared_user_list_action_block\">ブロック</string>\n    <string name=\"shared_user_list_action_muted\">ミュート済み</string>\n    <string name=\"shared_user_list_action_blocked\">ブロック済み</string>\n    <string name=\"shared_user_list_action_mute_dialog_message\">このユーザーをミュートしますか？</string>\n    <string name=\"shared_user_list_action_block_dialog_message\">このユーザーをブロックしますか？</string>\n    <string name=\"shared_publish_blog_title\">投稿</string>\n    <string name=\"shared_publish_blog_text_hint\">考えていることを書いてください</string>\n    <string name=\"shared_publish_reply_input_hint\">[[%1$s]] に返信</string>\n    <string name=\"shared_publish_interaction_no_limit\">誰でも参加可能</string>\n    <string name=\"shared_publish_interaction_limited\">制限付き</string>\n    <string name=\"shared_publish_interaction_dialog_title\">投稿の交流設定</string>\n    <string name=\"shared_publish_interaction_dialog_subtitle\">誰がこの投稿と交流できるかを設定します。</string>\n    <string name=\"shared_publish_interaction_dialog_quote_title\">引用設定</string>\n    <string name=\"shared_publish_interaction_dialog_quote_allow\">引用を許可</string>\n    <string name=\"shared_publish_interaction_dialog_reply_title\">返信設定</string>\n    <string name=\"shared_publish_interaction_dialog_reply_subtitle\">返信を許可する範囲:</string>\n    <string name=\"shared_publish_interaction_dialog_reply_all\">全員</string>\n    <string name=\"shared_publish_interaction_dialog_reply_nobody\">自分のみ</string>\n    <string name=\"shared_publish_interaction_dialog_reply_combine_title\">またはこれらを組み合わせ:</string>\n    <string name=\"shared_publish_interaction_dialog_mentioned\">メンションされたユーザー</string>\n    <string name=\"shared_publish_interaction_dialog_following\">フォロー中のユーザー</string>\n    <string name=\"shared_publish_interaction_dialog_follower\">フォロワー</string>\n    <string name=\"shared_publish_interaction_dialog_in_list\">「%1$s」にいるユーザー</string>\n    <string name=\"shared_publish_media_alt_dialog_title\">代替テキストを追加</string>\n    <string name=\"shared_publish_media_alt_dialog_input_tip\">詳しい説明文</string>\n    <string name=\"shared_publish_media_alt_dialog_input_hint\">代替テキスト</string>\n    <string name=\"shared_feeds_not_login_title\">ログイン情報が無効です。再度ログインしてください。</string>\n    <string name=\"shared_feeds_go_to_login\">ログイン</string>\n    <string name=\"shared_publish_select_account_title\">アカウントを選択してください</string>\n    <string name=\"shared_select_language_title\">言語を選択</string>\n    <string name=\"shared_status_context_screen_title\">詳細</string>\n    <string name=\"shared_alt_label\">代替テキスト</string>\n    <string name=\"status_ui_embed_quote_unavailable\">現在の引用は利用できません</string>\n    <string name=\"status_ui_action_unforward\">再共有を取り消す</string>\n    <string name=\"status_ui_quote_approval_public\">誰でも</string>\n    <string name=\"status_ui_quote_approval_follower\">フォロワーのみ</string>\n    <string name=\"status_ui_quote_approval_nobody\">自分のみ</string>\n    <string name=\"status_ui_visibility\">公開範囲</string>\n</resources>\n"
  },
  {
    "path": "localization/src/commonMain/composeResources/values-pt-rPT/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"skip\">Pular</string>\n    <string name=\"alert\">Alerta</string>\n    <string name=\"duration_minute\">minuto</string>\n    <string name=\"duration_hour\">hora</string>\n    <string name=\"duration_day\">dia</string>\n    <string name=\"duration_week\">semana</string>\n    <string name=\"cancel\">Cancelar</string>\n    <string name=\"ok\">Sim</string>\n    <string name=\"empty\">Este espaço está vazio～</string>\n    <string name=\"duration_selector_title\">Definir duração</string>\n    <string name=\"retry\">Tentar novamente</string>\n    <string name=\"load_more_error\">Ops! Não foi possível carregar.</string>\n    <string name=\"unknown_error\">Erro desconhecido</string>\n    <string name=\"network_error\">Erro de rede</string>\n    <string name=\"resource_not_found\">Recurso não encontrado</string>\n    <string name=\"mixed_content_subtitle_1\">Conteúdo misto ·</string>\n    <string name=\"mixed_content_subtitle_2\">Fontes</string>\n    <string name=\"date_time_ago\">&#160;atrás</string>\n    <string name=\"date_time_ago_prefix\"></string>\n    <string name=\"date_time_ago_suffix\">&#160;atrás</string>\n    <string name=\"date_time_day\">dia</string>\n    <string name=\"date_time_hour\">hora</string>\n    <string name=\"date_time_minute\">min</string>\n    <string name=\"date_time_second\">seg</string>\n    <string name=\"image_saving\">Salvando…</string>\n    <string name=\"image_save_success\">Salvo com sucesso</string>\n    <string name=\"image_save_failed\">Falha ao salvar</string>\n    <string name=\"permission_write_external_permission_denied\">A permissão de armazenamento foi negada. Ative-a nas configurações.</string>\n    <string name=\"feeds_load_previous_page_label\">Carregando conteúdo da página anterior</string>\n    <string name=\"feeds_load_previous_page_failed_label\">Falha no carregamento, toque para tentar novamente</string>\n    <string name=\"image\">Imagem</string>\n    <string name=\"video\">Vídeo</string>\n    <string name=\"login\">Entrar</string>\n    <string name=\"add\">Adicionar</string>\n    <string name=\"search\">Pesquisar</string>\n    <string name=\"auth_success\">Login bem-sucedido</string>\n    <string name=\"auth_failed\">Falha no login</string>\n    <string name=\"select\">Selecionar</string>\n    <string name=\"logout\">Sair</string>\n    <string name=\"settings\">Configurações</string>\n    <string name=\"donate\">Doar</string>\n    <string name=\"feeds\">Feeds</string>\n    <string name=\"save\">Salvar</string>\n    <string name=\"done\">Tudo pronto!</string>\n    <string name=\"bluesky_name\">Bluesky</string>\n    <string name=\"bluesky_description\">Bluesky é uma rede aberta. Com uma única conta, você acessa uma rede social fácil de usar e uma identidade compartilhada em toda a internet social.</string>\n    <string name=\"mastodon_name\">Mastodon</string>\n    <string name=\"mastodon_description\">Mastodon é uma mídia social descentralizada e não comercial, onde os usuários controlam sua linha do tempo e cada servidor é uma entidade independente.</string>\n    <string name=\"mixed_content_name\">Conteúdo misto</string>\n    <string name=\"mixed_content_description\">O Feed Misto reúne todo o seu conteúdo. Adicione fontes do Mastodon, Bluesky, RSS e mais — o Fread combina tudo em uma linha do tempo cronológica.</string>\n    <string name=\"status_provider_type_activity_pub\">Mastodon</string>\n    <string name=\"add_content_title\">Adicionar conteúdo</string>\n    <string name=\"add_content_success_with_account\">Adicionado com sucesso. Conta atual:</string>\n    <string name=\"add_content_success_with_login_reminder\">Adicionado com sucesso. Entrar agora?</string>\n    <string name=\"content_exist_tips\">Ops! Este conteúdo já está aqui.</string>\n    <string name=\"content_add_success\">Tudo certo! Seu conteúdo está pronto.</string>\n    <string name=\"add_feeds_page_empty_name_exist\">Este nome de usuário já está em uso.</string>\n    <string name=\"edit_profile_name_empty\">Insira seu nome de usuário</string>\n    <string name=\"edit_profile_label_name\">Nome</string>\n    <string name=\"edit_profile_label_note\">Bio</string>\n    <string name=\"edit_profile_input_name_hint\">Insira seu nome de usuário</string>\n    <string name=\"edit_profile_input_note_hint\">Insira a bio</string>\n    <string name=\"edit_profile_edit_confirm_message\">Tem certeza de que deseja sair da edição?</string>\n    <string name=\"add_content_success_snackbar\">Adicionado com sucesso</string>\n    <string name=\"login_dialog_input_hint\">Insira o servidor no qual deseja entrar</string>\n    <string name=\"login_dialog_title\">Selecione o servidor para entrar</string>\n    <string name=\"login_dialog_input_tip\">Host do servidor</string>\n    <string name=\"login_dialog_input_title\">Insira o host do servidor</string>\n    <string name=\"login_dialog_input_label\">Host do servidor</string>\n    <string name=\"login_dialog_target_title\">Faça login</string>\n    <string name=\"profile_description\">O Fread permite adicionar várias contas. As contas adicionadas podem ser usadas para publicar, curtir, compartilhar, comentar etc. Você também pode ver as listas criadas por essas contas.</string>\n    <string name=\"list_content_empty_placeholder\">Nenhum conteúdo disponível</string>\n    <string name=\"post_status_scope_public\">Público</string>\n    <string name=\"post_status_scope_unlisted\">Público, mas fora da descoberta</string>\n    <string name=\"post_status_scope_follower_only\">Apenas seguidores</string>\n    <string name=\"post_status_scope_mentioned_only\">Somente pessoas mencionadas</string>\n    <string name=\"post_status_content_warning\">Aviso de conteúdo</string>\n    <string name=\"post_status_success\">Enviado com sucesso</string>\n    <string name=\"post_status_failed\">Falha ao publicar: %1$s</string>\n    <string name=\"post_status_exit_dialog_content\">As alterações não salvas serão perdidas. Deseja sair?</string>\n    <string name=\"post_status_content_is_empty\">Insira o conteúdo</string>\n    <string name=\"post_status_part_failed\">Algumas contas não puderam publicar. As contas que publicaram com sucesso foram removidas. Tente novamente com as restantes: %1$s</string>\n    <string name=\"select_account_open_status_title\">Selecionar uma conta</string>\n    <string name=\"select_account_open_status_empty\">Nenhuma outra conta</string>\n    <string name=\"select_account_open_status_search_in\">Pesquisar em %1$s</string>\n    <string name=\"select_account_open_status_search_failed\">Falha na pesquisa</string>\n    <string name=\"explorer_search_no_results\">Nenhum resultado encontrado</string>\n    <string name=\"explorer_search_bar_hint\">Pesquisar</string>\n    <string name=\"explorer_search_bar_hint_specialize_platform\">Pesquisar em %1$s</string>\n    <string name=\"explorer_search_tab_title_author\">Contas</string>\n    <string name=\"explorer_search_tab_title_status\">Publicações</string>\n    <string name=\"explorer_search_tab_title_hashtag\">Hashtags</string>\n    <string name=\"explorer_search_tab_title_server\">Servidor</string>\n    <string name=\"explorer_tab_title\">Explorar</string>\n    <string name=\"explorer_tab_status_title\">Posts</string>\n    <string name=\"explorer_tab_users_title\">Usuários</string>\n    <string name=\"explorer_tab_hashtag_title\">Hashtags</string>\n    <string name=\"add_provider_page_title\">Adicionar servidor</string>\n    <string name=\"add_provider_page_confirm_button\">Confirmar adição</string>\n    <string name=\"search_page_title\">Pesquisar</string>\n    <string name=\"search_result_not_found\">Nenhum resultado encontrado</string>\n    <string name=\"feeds_import_page_title\">Importar</string>\n    <string name=\"feeds_import_page_hint\">Selecione o arquivo OPML para importar</string>\n    <string name=\"feeds_import_button\">Importar</string>\n    <string name=\"add_feeds_page_title\">Adicionar conteúdo</string>\n    <string name=\"add_feeds_page_feeds_name_label\">Nome do conteúdo</string>\n    <string name=\"add_feeds_page_feeds_name_hint\">Insira o nome do conteúdo</string>\n    <string name=\"add_feeds_page_feeds_empty\">Adicione uma fonte de assinatura</string>\n    <string name=\"pre_add_feeds_no_result\">Nenhum resultado encontrado</string>\n    <string name=\"pre_add_feeds_hint\">Nome da instância, URL, página inicial do usuário, RSS</string>\n    <string name=\"pre_add_feeds_input_label_1\">O Fread permite adicionar [[Mastodon]]/[[Bluesky]], além de feeds [[RSS]] e até [[conteúdo misto]].</string>\n    <string name=\"pre_add_feeds_input_label_2\">Insira aqui o [[NickName]] do Mastodon ou Bluesky, o [[WebFinger\\Handle]], a [[URL da página inicial]] do usuário ou o URL do feed [[RSS]].</string>\n    <string name=\"empty_content_hint_title\">Adicione algum conteúdo primeiro</string>\n    <string name=\"empty_content_hint_desc\">Você precisa adicionar fontes de assinatura primeiro. O Fread suporta vários tipos de conteúdo e de diferentes plataformas, permitindo criar seu próprio feed de informações.</string>\n    <string name=\"feeds_add_content\">Adicionar conteúdo</string>\n    <string name=\"select_feeds_type_screen_title\">Selecionar tipo de conteúdo</string>\n    <string name=\"feeds_pre_add_login_dialog_content\">Conteúdo adicionado com sucesso. Deseja entrar agora?</string>\n    <string name=\"add_feeds_page_empty_name_tips\">Insira um nome</string>\n    <string name=\"add_feeds_page_empty_source_tips\">Adicione uma fonte de assinatura</string>\n    <string name=\"add_feeds_choose_auth_dialog_title\">Selecione o servidor de login</string>\n    <string name=\"search_feeds_title\">Pesquisar</string>\n    <string name=\"search_feeds_title_hint\">NickName do Mastodon ou Bluesky, WebFinger, URL, RSS</string>\n    <string name=\"feeds_select_account_for_post_status\">Selecione uma conta</string>\n    <string name=\"feeds_mixed_config_edit_new_name_dialog_title\">Insira o nome</string>\n    <string name=\"feeds_mixed_config_edit_new_name_dialog_label\">Nome</string>\n    <string name=\"feeds_mixed_config_edit_delete_content_dialog_message\">Tem certeza de que deseja excluir estes feeds?</string>\n    <string name=\"feeds_mixed_config_not_found\">Estado anormal, configuração não encontrada.</string>\n    <string name=\"feeds_import_back_dialog_message\">Tem certeza de que deseja sair?</string>\n    <string name=\"feeds_delete_confirm_content\">Tem certeza de que deseja excluir?</string>\n    <string name=\"feeds_select_type_screen_title\">Escolha um formato</string>\n    <string name=\"feeds_select_type_screen_content\">O que você gostaria de adicionar?</string>\n    <string name=\"notification_tab_title\">Notificações</string>\n    <string name=\"notifications_account_empty_tip\">Nenhuma conta conectada no momento</string>\n    <string name=\"notifications_tab_all\">Tudo</string>\n    <string name=\"notifications_tab_mention\">Menções</string>\n    <string name=\"profile_page_title\">Perfil</string>\n    <string name=\"profile_setting_about_title\">Sobre</string>\n    <string name=\"profile_setting_donate_desc\">Pague um café para apoiar este projeto.</string>\n    <string name=\"profile_setting_inline_video_auto_play\">Reprodução automática de vídeos inline</string>\n    <string name=\"profile_setting_inline_video_auto_play_subtitle\">Se ativado, os vídeos nos Feeds serão reproduzidos automaticamente.</string>\n    <string name=\"profile_setting_always_show_sensitive_content\">Conteúdo sensível exibido por padrão</string>\n    <string name=\"profile_setting_always_show_sensitive_content_subtitle\">Se ativado, o conteúdo sensível será exibido por padrão (apenas Mastodon).</string>\n    <string name=\"profile_setting_dark_mode_title\">Modo escuro</string>\n    <string name=\"profile_setting_dark_mode_dark\">Modo escuro</string>\n    <string name=\"profile_setting_dark_mode_light\">Modo claro</string>\n    <string name=\"profile_setting_dark_mode_follow_system\">Seguir o sistema</string>\n    <string name=\"profile_setting_open_source_title\">Código aberto</string>\n    <string name=\"profile_setting_open_source_desc\">Licenças de código aberto das dependências usadas neste projeto</string>\n    <string name=\"profile_setting_open_source_feedback\">Envie seu feedback</string>\n    <string name=\"profile_setting_open_source_feedback_desc\">Entre no grupo do Telegram ou por outros meios</string>\n    <string name=\"profile_setting_open_source_feedback_telegram\">Entrar no grupo do Telegram</string>\n    <string name=\"profile_setting_open_source_feedback_github\">Criar issue no GitHub</string>\n    <string name=\"profile_setting_open_source_feedback_email\">Enviar e-mail</string>\n    <string name=\"profile_setting_language_title\">Idioma de exibição</string>\n    <string name=\"profile_setting_language_zh\">简体中文</string>\n    <string name=\"profile_setting_language_en\">English</string>\n    <string name=\"profile_setting_language_system\">Seguir o sistema</string>\n    <string name=\"profile_setting_ratting\">Avalie-nos</string>\n    <string name=\"profile_setting_ratting_desc\">Se você gostou, deixe-nos uma boa avaliação.</string>\n    <string name=\"profile_setting_font_size\">Tamanho da fonte do conteúdo</string>\n    <string name=\"profile_setting_font_size_desc\">Ajuste o tamanho da fonte e dos ícones do conteúdo</string>\n    <string name=\"profile_setting_font_size_small\">Pequeno</string>\n    <string name=\"profile_setting_font_size_medium\">Médio</string>\n    <string name=\"profile_setting_font_size_large\">Grande</string>\n    <string name=\"profile_setting_immersive_nav_bar\">Barra de navegação imersiva</string>\n    <string name=\"profile_setting_immersive_nav_bar_desc\">A barra de navegação inferior se oculta automaticamente ao rolar na página inicial.</string>\n    <string name=\"profile_setting_timeline_position\">Posição padrão da linha do tempo</string>\n    <string name=\"profile_setting_timeline_position_last_read\">Onde parei</string>\n    <string name=\"profile_setting_timeline_position_newest\">Publicações mais recentes</string>\n    <string name=\"profile_setting_theme_title\">Cor do tema</string>\n    <string name=\"profile_setting_theme_default\">Padrão</string>\n    <string name=\"profile_setting_theme_system\">Seguir o sistema</string>\n    <string name=\"profile_about_version\">Versão:&#160;</string>\n    <string name=\"profile_about_website\">Site:&#160;</string>\n    <string name=\"profile_about_developer\">Criado por&#160;</string>\n    <string name=\"profile_about_contract_us\">Contato:&#160;</string>\n    <string name=\"profile_about_telegram\">Grupo no Telegram:&#160;</string>\n    <string name=\"profile_about_privacy_policy\">Política de privacidade:&#160;</string>\n    <string name=\"profile_setting_check_for_update\">Verificar atualização</string>\n    <string name=\"profile_setting_already_latest_version\">Seu app já está atualizado</string>\n    <string name=\"profile_setting_have_new_version\">Nova versão disponível. Toque para atualizar.</string>\n    <string name=\"profile_donate_page_title\">Selecione como deseja doar</string>\n    <string name=\"profile_account_not_login\">Sessão expirada</string>\n    <string name=\"rss_source_detail_screen_title\">Atribuição de fonte</string>\n    <string name=\"rss_source_detail_screen_custom_title\">Título personalizado</string>\n    <string name=\"rss_source_detail_screen_url\">Link de assinatura</string>\n    <string name=\"rss_source_detail_screen_home_url\">Página inicial</string>\n    <string name=\"rss_source_detail_screen_add_date\">Data de adição</string>\n    <string name=\"rss_source_detail_screen_last_update_date\">Última atualização</string>\n    <string name=\"bluesky_protocol_name\">Bluesky</string>\n    <string name=\"bsky_add_content_title\">Entrar</string>\n    <string name=\"bsky_add_content_hosting_provider\">Provedor de hospedagem</string>\n    <string name=\"bsky_add_content_user_name\">Nome de usuário ou e-mail</string>\n    <string name=\"bsky_add_content_password\">Senha</string>\n    <string name=\"bsky_add_content_factor_token\">Código de confirmação</string>\n    <string name=\"bsky_edit_content_title\">Editar conteúdo</string>\n    <string name=\"bsky_feeds_explorer_more\">Explorar mais feeds</string>\n    <string name=\"bsky_feeds_explorer_creator_label\">Por %1$s</string>\n    <string name=\"bsky_feeds_explorer_liked_by\">Curtido por %1$s usuários</string>\n    <string name=\"bsky_feeds_detail_creator_prefix\">Por</string>\n    <string name=\"bsky_feeds_item_subtitle\">Por %1$s · Curtido por %2$s usuários</string>\n    <string name=\"bsky_feeds_following_name\">Seguindo</string>\n    <string name=\"bsky_feeds_user_posts\">Posts</string>\n    <string name=\"bsky_feeds_user_replies\">Respostas</string>\n    <string name=\"bsky_feeds_user_medias\">Mídias</string>\n    <string name=\"bsky_feeds_user_likes\">Curtidas</string>\n    <string name=\"bsky_user_detail_action_blocked_list\">Usuários bloqueados</string>\n    <string name=\"bsky_user_detail_action_muted_list\">Usuários silenciados</string>\n    <string name=\"bsky_user_detail_action_mute_user\">Silenciar %1$s</string>\n    <string name=\"bsky_user_detail_action_unmute_user\">Reativar</string>\n    <string name=\"bsky_user_detail_action_block_user\">Bloquear %1$s</string>\n    <string name=\"bsky_user_detail_action_unblock_user\">Desbloquear</string>\n    <string name=\"bsky_user_detail_action_block_user_dialog_message\">Deseja bloquear este usuário?</string>\n    <string name=\"bsky_user_detail_action_mute_user_dialog_message\">Deseja silenciar este usuário?</string>\n    <string name=\"bsky_edit_profile_title\">Editar perfil</string>\n    <string name=\"input_media_desc_page_title\">Texto descritivo da imagem</string>\n    <string name=\"input_media_desc_input_hint\">Insira o texto descritivo da imagem</string>\n    <string name=\"post_status_page_title\">Novo blog</string>\n    <string name=\"post_screen_input_hint\">Insira seu blog</string>\n    <string name=\"post_screen_media_descriptor_placeholder\">Texto descritivo</string>\n    <string name=\"post_status_poll_item_hint\">Opção %1$s</string>\n    <string name=\"post_status_poll_duration\">Duração da votação</string>\n    <string name=\"post_status_poll_function_title\">Função</string>\n    <string name=\"post_status_poll_single\">Única</string>\n    <string name=\"post_status_poll_multiple\">Múltipla</string>\n    <string name=\"post_status_poll_style_select_dialog_title\">Selecione o estilo</string>\n    <string name=\"post_status_poll_is_empty\">Insira o conteúdo da votação</string>\n    <string name=\"post_status_media_is_not_upload\">Mídia ainda não enviada</string>\n    <string name=\"authentication_page_normal_title\">Esta função requer login</string>\n    <string name=\"authentication_page_failed_title\">Status de login inválido, entre novamente.</string>\n    <string name=\"main_drawer_title\">Lista de conteúdo</string>\n    <string name=\"main_drawer_mixed_item_subtitle_1\">Conteúdo misto ·</string>\n    <string name=\"main_drawer_mixed_item_subtitle_2\">Fontes</string>\n    <string name=\"main_drawer_settings\">Configurações</string>\n    <string name=\"main_drawer_donate\">Doar</string>\n    <string name=\"main_request_notification_dialog_title\">Notificação</string>\n    <string name=\"main_request_notification_dialog_reject_button\">Recusar</string>\n    <string name=\"main_request_notification_dialog_allow_button\">Permitir</string>\n    <string name=\"main_request_notification_dialog_content\">Para receber mensagens de interação, permita as notificações.</string>\n    <string name=\"setting_item_amoled_mode\">Modo AMOLED</string>\n    <string name=\"setting_item_amoled_mode_description\">Quando ativado, o fundo muda para preto puro no modo escuro para reduzir o consumo de energia e aumentar o contraste em ambientes escuros.</string>\n    <string name=\"setting_group_appearance\">Aparência</string>\n    <string name=\"setting_group_appearance_subtitle\">Configurações de tema, exibição e estilo da Página inicial</string>\n    <string name=\"setting_group_behavior\">Comportamento</string>\n    <string name=\"setting_group_behavior_subtitle\">Configurações de exibição de conteúdo e comportamento de interação</string>\n    <string name=\"setting_item_home_tab_next_title\">Mostrar o botão para mudar de conteúdo no topo da Página inicial</string>\n    <string name=\"setting_item_home_tab_next_subtitle\">Mostrar no canto superior direito da Página inicial o botão para mudar para o conteúdo seguinte</string>\n    <string name=\"setting_item_home_tab_refresh_title\">Mostrar o botão de atualizar no topo da Página inicial</string>\n    <string name=\"setting_item_home_tab_refresh_subtitle\">Mostrar no canto superior direito da Página inicial o botão para atualizar o conteúdo</string>\n    <string name=\"setting_item_open_url_by_system_title\">Abrir links no navegador do sistema</string>\n    <string name=\"setting_item_open_url_by_system_subtitle\">Quando ativado, o Fread abrirá links no navegador do sistema.</string>\n    <string name=\"setting_item_solid_bar_background_title\">Fundo sólido das barras</string>\n    <string name=\"setting_item_solid_bar_background_subtitle\">Torna as barras superior e inferior sólidas em vez de desfocadas.</string>\n</resources>\n"
  },
  {
    "path": "localization/src/commonMain/composeResources/values-pt-rPT/strings_mastodon.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"activity_pub_user_detail_join_date\">Entrou em</string>\n    <string name=\"activity_pub_user_detail_tab_about_joined\">Entrou em %1$s</string>\n    <string name=\"activity_pub_user_detail_menu_block\">Bloquear %1$s</string>\n    <string name=\"activity_pub_user_detail_menu_block_domain\">Bloquear %1$s</string>\n    <string name=\"activity_pub_user_detail_menu_unblock_domain\">Desbloquear %1$s</string>\n    <string name=\"activity_pub_user_detail_menu_edit_private_note\">Editar nota</string>\n    <string name=\"activity_pub_user_detail_menu_edit_private_note_dialog_hint\">Insira a nota privada</string>\n    <string name=\"activity_pub_user_detail_dialog_content_block\">Deseja bloquear este usuário?</string>\n    <string name=\"activity_pub_user_detail_dialog_content_block_domain\">Deseja bloquear este domínio?</string>\n    <string name=\"activity_pub_user_menu_blocked_user_list\">Usuários bloqueados</string>\n    <string name=\"activity_pub_user_menu_muted_user_list\">Usuários silenciados</string>\n    <string name=\"activity_pub_user_detail_menu_mute_user\">Silenciar %1$s</string>\n    <string name=\"activity_pub_user_detail_menu_unmute_user\">Reativar %1$s</string>\n    <string name=\"activity_pub_mute_user_bottom_sheet_title\">Silenciar usuário?</string>\n    <string name=\"activity_pub_mute_user_bottom_sheet_role1\">Eles não saberão que foram silenciados.</string>\n    <string name=\"activity_pub_mute_user_bottom_sheet_role2\">Eles ainda podem ver suas postagens, mas você não verá as deles.</string>\n    <string name=\"activity_pub_mute_user_bottom_sheet_role3\">Você não verá postagens que os mencionem.</string>\n    <string name=\"activity_pub_mute_user_bottom_sheet_role4\">Eles podem mencioná-lo e segui-lo, mas você não os verá.</string>\n    <string name=\"activity_pub_mute_user_bottom_sheet_btn_mute\">Silenciar</string>\n    <string name=\"add_instance_screen_title\">Adicionar instância</string>\n    <string name=\"add_instance_input_service_hint\">Insira o endereço do servidor</string>\n    <string name=\"add_instance_input_service_error\">Insira o endereço correto do servidor</string>\n    <string name=\"activity_pub_content_tab_home\">Início</string>\n    <string name=\"activity_pub_content_tab_local_timeline\">Local</string>\n    <string name=\"activity_pub_content_tab_public_timeline\">Público</string>\n    <string name=\"activity_pub_content_tab_trending\">Em alta</string>\n    <string name=\"activity_pub_hashtag_timeline_description\">%1$s posts · %2$s participantes · %3$s posts hoje</string>\n    <string name=\"activity_pub_hashtag_unfollow_dialog_message\">Tem certeza de que deseja deixar de seguir esta hashtag?</string>\n    <string name=\"activity_pub_edit_account_info_label_about\">Sobre</string>\n    <string name=\"activity_pub_edit_content_screen_config_not_found\">Configuração não encontrada</string>\n    <string name=\"activity_pub_user_list_empty\">Nenhum usuário por enquanto</string>\n    <string name=\"activity_pub_muted_user_list_empty\">Nenhum usuário silenciado</string>\n    <string name=\"activity_pub_muted_user_list_unmute\">Reativar</string>\n    <string name=\"activity_pub_blocked_user_list_empty\">Nenhum usuário bloqueado</string>\n    <string name=\"activity_pub_blocked_user_list_unblock\">Desbloquear</string>\n    <string name=\"activity_pub_bookmarks_list_title\">Itens salvos</string>\n    <string name=\"activity_pub_favourites_list_title\">Favoritos</string>\n    <string name=\"activity_pub_followed_tags_screen_title\">Hashtags seguidas</string>\n    <string name=\"activity_pub_filters_list_page_title\">Filtros</string>\n    <string name=\"activity_pub_filters_active\">Ativos</string>\n    <string name=\"activity_pub_filters_expired\">Expirados</string>\n    <string name=\"activity_pub_filter_edit_title\">Editar filtro</string>\n    <string name=\"activity_pub_filter_edit_input_title_label\">Título</string>\n    <string name=\"activity_pub_filter_edit_input_title_hint\">Insira um título</string>\n    <string name=\"activity_pub_filter_edit_duration\">Duração</string>\n    <string name=\"activity_pub_filter_edit_duration_permanent\">Permanente</string>\n    <string name=\"activity_pub_filter_edit_duration_thirty_minutes\">30 minutos</string>\n    <string name=\"activity_pub_filter_edit_duration_one_hour\">1 hora</string>\n    <string name=\"activity_pub_filter_edit_duration_twelve_hours\">12 horas</string>\n    <string name=\"activity_pub_filter_edit_duration_one_day\">1 dia</string>\n    <string name=\"activity_pub_filter_edit_duration_three_day\">3 dias</string>\n    <string name=\"activity_pub_filter_edit_duration_one_week\">1 semana</string>\n    <string name=\"activity_pub_filter_edit_duration_custom\">Personalizado</string>\n    <string name=\"activity_pub_filter_edit_duration_finish_subtitle\">Expira em %1$s</string>\n    <string name=\"activity_pub_filter_edit_keyword_title\">Palavras-chave ocultas</string>\n    <string name=\"activity_pub_filter_edit_keyword_dialog_title\">Editar palavra-chave</string>\n    <string name=\"activity_pub_filter_edit_keyword_dialog_hint\">Palavra-chave</string>\n    <string name=\"activity_pub_filter_edit_keyword_remove_keyword_dialog_content\">Tem certeza de que deseja excluir esta palavra-chave?</string>\n    <string name=\"activity_pub_filter_edit_keyword_list_title\">Palavras silenciadas</string>\n    <string name=\"activity_pub_filter_edit_keyword_list_desc\">%1$s palavra ou frase silenciada</string>\n    <string name=\"activity_pub_filter_edit_context_title\">Silenciar de</string>\n    <string name=\"activity_pub_filter_edit_context_selector_title\">Selecione as fontes para ocultar</string>\n    <string name=\"activity_pub_filter_edit_context_home\">Início &amp; listas</string>\n    <string name=\"activity_pub_filter_edit_context_notification\">Notificações</string>\n    <string name=\"activity_pub_filter_edit_context_timeline\">Linhas do tempo públicas</string>\n    <string name=\"activity_pub_filter_edit_context_thread\">Tópicos &amp; respostas</string>\n    <string name=\"activity_pub_filter_edit_context_account\">Perfis</string>\n    <string name=\"activity_pub_filter_edit_empty_context\">Vazio</string>\n    <string name=\"activity_pub_filter_edit_warning_title\">Mostrar com aviso de conteúdo</string>\n    <string name=\"activity_pub_filter_edit_warning_desc\">Ainda mostrar postagens que correspondem a este filtro, mas atrás de um aviso de conteúdo</string>\n    <string name=\"activity_pub_filter_edit_delete_content\">Tem certeza de que deseja excluir?</string>\n    <string name=\"activity_pub_filter_edit_back_dialog\">Tem certeza de que deseja sair?</string>\n    <string name=\"activity_pub_filter_edit_whole_word\">Palavra inteira</string>\n    <string name=\"activity_pub_explorer_tab_status_title\">Posts</string>\n    <string name=\"activity_pub_explorer_tab_users_title\">Usuários</string>\n    <string name=\"activity_pub_explorer_tab_hashtag_title\">Hashtags</string>\n    <string name=\"activity_pub_created_list_title\">Listas</string>\n    <string name=\"activity_pub_add_list_title\">Criar lista</string>\n    <string name=\"activity_pub_add_list_name\">Nome da lista</string>\n    <string name=\"activity_pub_add_list_replies\">Mostrar respostas para</string>\n    <string name=\"activity_pub_add_list_replies_non\">Ninguém</string>\n    <string name=\"activity_pub_add_list_replies_list\">Membros da lista</string>\n    <string name=\"activity_pub_add_list_replies_followers\">Qualquer um que eu sigo</string>\n    <string name=\"activity_pub_add_list_accounts\">Membros da lista</string>\n    <string name=\"activity_pub_add_list_hide_in_timeline\">Ocultar membros em Seguindo</string>\n    <string name=\"activity_pub_add_list_hide_in_timeline_desc\">Se alguém estiver nesta lista, oculte-o na sua linha do tempo de Seguindo para evitar ver as postagens duas vezes.</string>\n    <string name=\"activity_pub_add_list_remove_user_message\">Tem certeza de que deseja remover este usuário?</string>\n    <string name=\"activity_pub_add_list_name_is_empty\">Insira seu nome de usuário</string>\n    <string name=\"activity_pub_add_list_back_reminder\">Você tem alterações não salvas. Deseja sair?</string>\n    <string name=\"activity_pub_search_user_placeholder\">Digite para pesquisar</string>\n    <string name=\"activity_pub_list_delete_confirm\">Tem certeza de que deseja excluir esta lista?</string>\n    <string name=\"activity_pub_login_exception\">Exceção de estado do OAuth!</string>\n    <string name=\"activity_pub_home_timeline\">Início</string>\n    <string name=\"activity_pub_local_timeline\">Local</string>\n    <string name=\"activity_pub_public_timeline\">Público</string>\n    <string name=\"activity_pub_instance_detail_language_label\">Idioma: %1$s</string>\n    <string name=\"activity_pub_instance_detail_active_month_label\">Ativos no mês: %1$s</string>\n    <string name=\"activity_pub_user_detail_tab_post\">Posts</string>\n    <string name=\"activity_pub_user_detail_tab_replies\">Respostas</string>\n    <string name=\"activity_pub_user_detail_tab_media\">Mídia</string>\n    <string name=\"activity_pub_user_detail_tab_about\">Sobre</string>\n    <string name=\"activity_pub_about\">Sobre</string>\n    <string name=\"activity_pub_about_rule_title\">Regras</string>\n    <string name=\"activity_pub_trends_tag\">Tags em alta</string>\n    <string name=\"activity_pub_trends_tag_description\">%1$s pessoas nos últimos 2 dias</string>\n    <string name=\"activity_pub_trends_status\">Em alta</string>\n    <string name=\"activity_pub_select_platform_title\">Escolha uma instância</string>\n    <string name=\"activity_pub_select_platform_text_hint\">Insira o nome ou URL da instância</string>\n</resources>\n"
  },
  {
    "path": "localization/src/commonMain/composeResources/values-pt-rPT/strings_status_ui.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"status_ui_reposted\">impulsionado</string>\n    <string name=\"status_ui_repost\">Impulsionar</string>\n    <string name=\"status_ui_quote\">Citar</string>\n    <string name=\"status_ui_like\">Curtir</string>\n    <string name=\"status_ui_comment\">Comentar</string>\n    <string name=\"status_ui_delete\">Excluir</string>\n    <string name=\"status_ui_share\">Compartilhar</string>\n    <string name=\"status_ui_bookmark\">Salvar</string>\n    <string name=\"status_ui_unbookmark\">Remover dos salvos</string>\n    <string name=\"status_ui_reply\">Responder</string>\n    <string name=\"status_ui_follow\">Seguir</string>\n    <string name=\"status_ui_unfollow\">Deixar de seguir</string>\n    <string name=\"status_ui_pin\">Fixar</string>\n    <string name=\"status_ui_unpin\">Desafixar</string>\n    <string name=\"status_ui_edit\">Editar</string>\n    <string name=\"status_ui_sensitive_by_filter\">Corresponde ao filtro \"%1$s\"</string>\n    <string name=\"status_ui_new_status\">Novo conteúdo</string>\n    <string name=\"status_ui_delete_status_confirm\">Tem certeza de que deseja excluir este conteúdo?</string>\n    <string name=\"status_ui_image_sensitive_label\">Mídia oculta</string>\n    <string name=\"status_ui_image_content_show_hidden_label\">Mostrar mais</string>\n    <string name=\"status_ui_image_content_hide_hidden_label\">Ocultar conteúdo</string>\n    <string name=\"status_ui_poll_vote\">Votar</string>\n    <string name=\"status_ui_poll_vote_finished_tip\">%1$s voto · Encerrado</string>\n    <string name=\"status_ui_poll_votes_finished_tip\">%1$s votos · Encerrado</string>\n    <string name=\"status_ui_interaction_open_in_browser\">Abrir no navegador</string>\n    <string name=\"status_ui_interaction_open_original_instance\">Abrir a outra instância</string>\n    <string name=\"status_ui_interaction_copy_url\">Copiar link</string>\n    <string name=\"status_ui_interaction_translate\">Traduzir</string>\n    <string name=\"status_ui_interaction_open_blog_by_other_account\">Abrir com outra conta</string>\n    <string name=\"status_ui_visibility_mentioned_only\">Somente mencionados</string>\n    <string name=\"status_ui_label_pinned\">Fixado</string>\n    <string name=\"status_ui_info_label_edited\">Editado</string>\n    <string name=\"status_ui_interaction_label_favourited_count\">%1$s favoritos</string>\n    <string name=\"status_ui_interaction_label_boosted_count\">%1$s impulsionamentos</string>\n    <string name=\"status_ui_bottom_label_edited_at\">Editado em %1$s</string>\n    <string name=\"status_ui_translating\">Traduzindo…</string>\n    <string name=\"status_ui_translate_show_original\">Mostrar original</string>\n    <string name=\"status_ui_edit_content_delete_dialog_content\">Confirmar exclusão deste conteúdo?</string>\n    <string name=\"status_ui_edit_content_name_title\">Editar nome do conteúdo</string>\n    <string name=\"status_ui_edit_content_name_label\">Editar nome do conteúdo</string>\n    <string name=\"status_ui_edit_content_name_hint\">Digite o nome do conteúdo</string>\n    <string name=\"status_ui_edit_content_config_showing_list_title\">Feeds na página inicial</string>\n    <string name=\"status_ui_edit_content_config_showing_list_description\">Toque e segure para arrastar e reordenar</string>\n    <string name=\"status_ui_edit_content_config_hidden_list_title\">Feeds que podem ser adicionados à página inicial</string>\n    <string name=\"status_ui_user_detail_relationship_mutuals\">Mútuos</string>\n    <string name=\"status_ui_user_detail_relationship_blocking\">Bloqueado</string>\n    <string name=\"status_ui_user_detail_relationship_following\">Seguindo</string>\n    <string name=\"status_ui_user_detail_relationship_not_follow\">Seguir</string>\n    <string name=\"status_ui_user_detail_relationship_requested\">Pendente</string>\n    <string name=\"status_ui_user_detail_relationship_follow_back\">Seguir de volta</string>\n    <string name=\"status_ui_user_detail_request_by_tip\">Este usuário pediu para segui-lo</string>\n    <string name=\"status_ui_relationship_btn_dialog_content_cancel_follow\">Deseja deixar de seguir este usuário?</string>\n    <string name=\"status_ui_relationship_btn_dialog_content_cancel_follow_request\">Deseja cancelar seu pedido de seguir?</string>\n    <string name=\"status_ui_relationship_btn_dialog_content_cancel_blocking\">Tem certeza de que deseja desbloquear este usuário?</string>\n    <string name=\"status_ui_user_detail_follows_you\">Segue você</string>\n    <string name=\"status_ui_user_detail_posts\">Postagens</string>\n    <string name=\"status_ui_user_detail_follower_info\">Seguidores</string>\n    <string name=\"status_ui_user_detail_following_info\">Seguindo</string>\n    <string name=\"status_ui_top_label_continued_thread\">Tópico continuado</string>\n    <string name=\"status_ui_update_dialog_title\">Atualização</string>\n    <string name=\"status_ui_update_dialog_release_note\">Versão %1$s - Registro de alterações:\\n%2$s</string>\n    <string name=\"status_ui_switch_account_dialog_title\">Selecionar conta</string>\n    <string name=\"status_ui_likes\">Curtidas</string>\n    <string name=\"status_ui_bookmarks\">Salvos</string>\n    <string name=\"status_ui_edit_profile\">Editar perfil</string>\n    <string name=\"status_ui_logout\">Sair</string>\n    <string name=\"status_ui_logout_dialog_content\">Tem certeza de que deseja sair?</string>\n    <string name=\"status_ui_search_account_status_hint\">Pesquisar postagens</string>\n    <string name=\"shared_notification_unknown_desc\">Notificação desconhecida</string>\n    <string name=\"shared_notification_favourited_desc\">curtiu sua postagem</string>\n    <string name=\"shared_notification_reblog_desc\">repostou sua postagem</string>\n    <string name=\"shared_notification_poll_desc\">Ver resultados anteriores da votação</string>\n    <string name=\"shared_notification_poll_count\">%1$s votos · Encerrado</string>\n    <string name=\"shared_notification_follow_desc\">começou a seguir você</string>\n    <string name=\"shared_notification_update_desc\">editou uma postagem</string>\n    <string name=\"shared_notification_follow_request\">Você recebeu um pedido de seguir</string>\n    <string name=\"shared_notification_new_status_desc\">publicou um novo blog</string>\n    <string name=\"shared_notification_severed_desc\">rompeu vínculos com você</string>\n    <string name=\"shared_notification_quote_desc\">citou sua postagem</string>\n    <string name=\"shared_notification_reply_desc\">respondeu à sua postagem</string>\n    <string name=\"shared_user_list_title_following\">Seguindo</string>\n    <string name=\"shared_user_list_title_followers\">Seguidores</string>\n    <string name=\"shared_user_list_title_mutes\">Usuários silenciados</string>\n    <string name=\"shared_user_list_title_blocks\">Usuários bloqueados</string>\n    <string name=\"shared_user_list_title_likes\">Curtidas</string>\n    <string name=\"shared_user_list_title_reblog\">Impulsionamentos</string>\n    <string name=\"shared_user_list_action_mute\">Silenciar</string>\n    <string name=\"shared_user_list_action_block\">Bloquear</string>\n    <string name=\"shared_user_list_action_muted\">Silenciado</string>\n    <string name=\"shared_user_list_action_blocked\">Bloqueado</string>\n    <string name=\"shared_user_list_action_mute_dialog_message\">Tem certeza de que deseja silenciar este usuário?</string>\n    <string name=\"shared_user_list_action_block_dialog_message\">Tem certeza de que deseja bloquear este usuário?</string>\n    <string name=\"shared_publish_blog_title\">Postar</string>\n    <string name=\"shared_publish_blog_text_hint\">Escreva ou cole o que está pensando</string>\n    <string name=\"shared_publish_reply_input_hint\">Respondendo a [[%1$s]]</string>\n    <string name=\"shared_publish_interaction_no_limit\">Qualquer pessoa pode interagir</string>\n    <string name=\"shared_publish_interaction_limited\">Interação limitada</string>\n    <string name=\"shared_publish_interaction_dialog_title\">Configurações de interação da postagem</string>\n    <string name=\"shared_publish_interaction_dialog_subtitle\">Personalize quem pode interagir com esta postagem.</string>\n    <string name=\"shared_publish_interaction_dialog_quote_title\">Configurações de citação</string>\n    <string name=\"shared_publish_interaction_dialog_quote_allow\">Permitir citações</string>\n    <string name=\"shared_publish_interaction_dialog_reply_title\">Configurações de resposta</string>\n    <string name=\"shared_publish_interaction_dialog_reply_subtitle\">Permitir respostas de:</string>\n    <string name=\"shared_publish_interaction_dialog_reply_all\">Todos</string>\n    <string name=\"shared_publish_interaction_dialog_reply_nobody\">Ninguém</string>\n    <string name=\"shared_publish_interaction_dialog_reply_combine_title\">Ou combine estas opções:</string>\n    <string name=\"shared_publish_interaction_dialog_mentioned\">Usuários mencionados</string>\n    <string name=\"shared_publish_interaction_dialog_following\">Usuários que você segue</string>\n    <string name=\"shared_publish_interaction_dialog_follower\">Seus seguidores</string>\n    <string name=\"shared_publish_interaction_dialog_in_list\">Usuários na lista \"%1$s\"</string>\n    <string name=\"shared_publish_media_alt_dialog_title\">Adicionar texto alternativo</string>\n    <string name=\"shared_publish_media_alt_dialog_input_tip\">Texto alternativo descritivo</string>\n    <string name=\"shared_publish_media_alt_dialog_input_hint\">Texto alternativo</string>\n    <string name=\"shared_feeds_not_login_title\">Ops! Sua sessão expirou. Faça login novamente para continuar.</string>\n    <string name=\"shared_feeds_go_to_login\">Entrar</string>\n    <string name=\"shared_publish_select_account_title\">Selecione uma conta</string>\n    <string name=\"shared_select_language_title\">Selecionar idioma</string>\n    <string name=\"shared_status_context_screen_title\">Detalhe</string>\n    <string name=\"shared_alt_label\">ALT</string>\n    <string name=\"status_ui_embed_quote_unavailable\">A citação atual não está disponível</string>\n    <string name=\"status_ui_action_unforward\">Cancelar encaminhamento</string>\n    <string name=\"status_ui_quote_approval_public\">Qualquer pessoa</string>\n    <string name=\"status_ui_quote_approval_follower\">Apenas seguidores</string>\n    <string name=\"status_ui_quote_approval_nobody\">Só eu</string>\n    <string name=\"status_ui_visibility\">Visibilidade</string>\n</resources>\n"
  },
  {
    "path": "localization/src/commonMain/composeResources/values-ru-rRU/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"skip\">Пропустить</string>\n    <string name=\"alert\">Оповещение</string>\n    <string name=\"duration_minute\">минута</string>\n    <string name=\"duration_hour\">час</string>\n    <string name=\"duration_day\">день</string>\n    <string name=\"duration_week\">неделя</string>\n    <string name=\"cancel\">Отмена</string>\n    <string name=\"ok\">Да</string>\n    <string name=\"empty\">Здесь пусто～</string>\n    <string name=\"duration_selector_title\">Установить длительность</string>\n    <string name=\"retry\">Повторить</string>\n    <string name=\"load_more_error\">Упс! Не удалось загрузить.</string>\n    <string name=\"unknown_error\">Неизвестная ошибка</string>\n    <string name=\"network_error\">Ошибка сети</string>\n    <string name=\"resource_not_found\">Ресурс не найден</string>\n    <string name=\"mixed_content_subtitle_1\">Смешанный контент ·</string>\n    <string name=\"mixed_content_subtitle_2\">Источники</string>\n    <string name=\"date_time_ago\">&#160;назад</string>\n    <string name=\"date_time_ago_prefix\"></string>\n    <string name=\"date_time_ago_suffix\">&#160;назад</string>\n    <string name=\"date_time_day\">день</string>\n    <string name=\"date_time_hour\">час</string>\n    <string name=\"date_time_minute\">мин</string>\n    <string name=\"date_time_second\">с</string>\n    <string name=\"image_saving\">Сохранение…</string>\n    <string name=\"image_save_success\">Успешно сохранено</string>\n    <string name=\"image_save_failed\">Ошибка сохранения</string>\n    <string name=\"permission_write_external_permission_denied\">Доступ к хранилищу отклонён, пожалуйста, включите его в настройках.</string>\n    <string name=\"feeds_load_previous_page_label\">Загрузка содержимого предыдущей страницы</string>\n    <string name=\"feeds_load_previous_page_failed_label\">Не удалось загрузить, нажмите, чтобы повторить</string>\n    <string name=\"image\">Изображение</string>\n    <string name=\"video\">Видео</string>\n    <string name=\"login\">Войти</string>\n    <string name=\"add\">Добавить</string>\n    <string name=\"search\">Поиск</string>\n    <string name=\"auth_success\">Вход выполнен</string>\n    <string name=\"auth_failed\">Не удалось войти</string>\n    <string name=\"select\">Выбрать</string>\n    <string name=\"logout\">Выйти</string>\n    <string name=\"settings\">Настройки</string>\n    <string name=\"donate\">Пожертвовать</string>\n    <string name=\"feeds\">Ленты</string>\n    <string name=\"save\">Сохранить</string>\n    <string name=\"done\">Готово!</string>\n    <string name=\"bluesky_name\">Bluesky</string>\n    <string name=\"bluesky_description\">Bluesky — это открытая сеть. С одной учётной записью вы получаете доступ к удобной социальной сети и единой идентичности во всей социальной сети Интернет.</string>\n    <string name=\"mastodon_name\">Mastodon</string>\n    <string name=\"mastodon_description\">Mastodon — децентрализированная, некоммерческая социальная сеть, где пользователи управляют своей лентой, а каждый сервер является независимой сущностью.</string>\n    <string name=\"mixed_content_name\">Смешанный контент</string>\n    <string name=\"mixed_content_description\">Смешанная лента объединяет весь ваш контент. Добавляйте источники из Mastodon, Bluesky, RSS и др. — Fread соберёт их в единую хронологическую ленту.</string>\n    <string name=\"status_provider_type_activity_pub\">Mastodon</string>\n    <string name=\"add_content_title\">Добавить контент</string>\n    <string name=\"add_content_success_with_account\">Успешно добавлено. Текущий аккаунт:</string>\n    <string name=\"add_content_success_with_login_reminder\">Успешно добавлено. Войти сейчас?</string>\n    <string name=\"content_exist_tips\">Упс! Этот контент уже добавлен.</string>\n    <string name=\"content_add_success\">Готово! Ваш контент настроен.</string>\n    <string name=\"add_feeds_page_empty_name_exist\">Это имя пользователя уже занято.</string>\n    <string name=\"edit_profile_name_empty\">Пожалуйста, введите имя пользователя</string>\n    <string name=\"edit_profile_label_name\">Имя</string>\n    <string name=\"edit_profile_label_note\">Био</string>\n    <string name=\"edit_profile_input_name_hint\">Пожалуйста, введите имя пользователя</string>\n    <string name=\"edit_profile_input_note_hint\">Пожалуйста, введите био</string>\n    <string name=\"edit_profile_edit_confirm_message\">Вы уверены, что хотите выйти из режима редактирования?</string>\n    <string name=\"add_content_success_snackbar\">Успешно добавлено</string>\n    <string name=\"login_dialog_input_hint\">Укажите сервер для входа</string>\n    <string name=\"login_dialog_title\">Выберите сервер для входа</string>\n    <string name=\"login_dialog_input_tip\">Введите хост сервера</string>\n    <string name=\"login_dialog_input_title\">Введите хост сервера</string>\n    <string name=\"login_dialog_input_label\">Хост сервера</string>\n    <string name=\"login_dialog_target_title\">Выполните вход</string>\n    <string name=\"profile_description\">Fread поддерживает добавление нескольких аккаунтов. С добавленными аккаунтами вы можете публиковать посты, ставить лайки, делиться, комментировать и просматривать списки, созданные этими аккаунтами.</string>\n    <string name=\"list_content_empty_placeholder\">Контент отсутствует</string>\n    <string name=\"post_status_scope_public\">Публично</string>\n    <string name=\"post_status_scope_unlisted\">Публично, но исключено из обнаружения</string>\n    <string name=\"post_status_scope_follower_only\">Только для подписчиков</string>\n    <string name=\"post_status_scope_mentioned_only\">Только упомянутые</string>\n    <string name=\"post_status_content_warning\">Предупреждение о контенте</string>\n    <string name=\"post_status_success\">Отправлено</string>\n    <string name=\"post_status_failed\">Не удалось отправить: %1$s</string>\n    <string name=\"post_status_exit_dialog_content\">Несохранённые изменения будут потеряны. Выйти?</string>\n    <string name=\"post_status_content_is_empty\">Пожалуйста, введите содержимое</string>\n    <string name=\"post_status_part_failed\">Некоторые аккаунты не удалось опубликовать. Успешно опубликованные аккаунты удалены из очереди. Повторите попытку для оставшихся: %1$s</string>\n    <string name=\"select_account_open_status_title\">Выберите аккаунт</string>\n    <string name=\"select_account_open_status_empty\">Других аккаунтов нет</string>\n    <string name=\"select_account_open_status_search_in\">Искать в %1$s</string>\n    <string name=\"select_account_open_status_search_failed\">Поиск не удался</string>\n    <string name=\"explorer_search_no_results\">Ничего не найдено</string>\n    <string name=\"explorer_search_bar_hint\">Поиск</string>\n    <string name=\"explorer_search_bar_hint_specialize_platform\">Поиск в %1$s</string>\n    <string name=\"explorer_search_tab_title_author\">Аккаунты</string>\n    <string name=\"explorer_search_tab_title_status\">Статусы</string>\n    <string name=\"explorer_search_tab_title_hashtag\">Хэштеги</string>\n    <string name=\"explorer_search_tab_title_server\">Сервер</string>\n    <string name=\"explorer_tab_title\">Обзор</string>\n    <string name=\"explorer_tab_status_title\">Посты</string>\n    <string name=\"explorer_tab_users_title\">Пользователи</string>\n    <string name=\"explorer_tab_hashtag_title\">Хэштеги</string>\n    <string name=\"add_provider_page_title\">Добавить сервер</string>\n    <string name=\"add_provider_page_confirm_button\">Подтвердить</string>\n    <string name=\"search_page_title\">Поиск</string>\n    <string name=\"search_result_not_found\">Результатов не найдено</string>\n    <string name=\"feeds_import_page_title\">Импорт</string>\n    <string name=\"feeds_import_page_hint\">Выберите файл OPML для импорта</string>\n    <string name=\"feeds_import_button\">Импорт</string>\n    <string name=\"add_feeds_page_title\">Добавить контент</string>\n    <string name=\"add_feeds_page_feeds_name_label\">Название контента</string>\n    <string name=\"add_feeds_page_feeds_name_hint\">Введите название контента</string>\n    <string name=\"add_feeds_page_feeds_empty\">Добавьте источник подписки</string>\n    <string name=\"pre_add_feeds_no_result\">Ничего не найдено</string>\n    <string name=\"pre_add_feeds_hint\">Имя инстанса, URL, домашняя страница пользователя, RSS</string>\n    <string name=\"pre_add_feeds_input_label_1\">Fread поддерживает добавление лент [[Mastodon]]/[[Bluesky]], а также [[RSS]] и даже [[смешанного контента]].</string>\n    <string name=\"pre_add_feeds_input_label_2\">Введите здесь [[NickName]] Mastodon или Bluesky, [[WebFinger\\Handle]], [[домашнюю страницу]] пользователя или URL ленты [[RSS]].</string>\n    <string name=\"empty_content_hint_title\">Сначала добавьте контент</string>\n    <string name=\"empty_content_hint_desc\">Сначала нужно добавить источники подписки. Fread поддерживает различные типы контента и платформы, позволяя собрать собственную информационную ленту.</string>\n    <string name=\"feeds_add_content\">Добавить контент</string>\n    <string name=\"select_feeds_type_screen_title\">Выберите тип контента</string>\n    <string name=\"feeds_pre_add_login_dialog_content\">Контент добавлен. Войти сейчас?</string>\n    <string name=\"add_feeds_page_empty_name_tips\">Введите имя</string>\n    <string name=\"add_feeds_page_empty_source_tips\">Добавьте источник</string>\n    <string name=\"add_feeds_choose_auth_dialog_title\">Выберите сервер для входа</string>\n    <string name=\"search_feeds_title\">Поиск</string>\n    <string name=\"search_feeds_title_hint\">Mastodon или Bluesky NickName, WebFinger, URL, RSS</string>\n    <string name=\"feeds_select_account_for_post_status\">Пожалуйста, выберите аккаунт</string>\n    <string name=\"feeds_mixed_config_edit_new_name_dialog_title\">Введите имя</string>\n    <string name=\"feeds_mixed_config_edit_new_name_dialog_label\">Имя</string>\n    <string name=\"feeds_mixed_config_edit_delete_content_dialog_message\">Удалить эту ленту?</string>\n    <string name=\"feeds_mixed_config_not_found\">Аномальное состояние: конфигурация не найдена.</string>\n    <string name=\"feeds_import_back_dialog_message\">Выйти?</string>\n    <string name=\"feeds_delete_confirm_content\">Удалить?</string>\n    <string name=\"feeds_select_type_screen_title\">Выберите формат</string>\n    <string name=\"feeds_select_type_screen_content\">Что вы хотите добавить?</string>\n    <string name=\"notification_tab_title\">Уведомления</string>\n    <string name=\"notifications_account_empty_tip\">Нет активных аккаунтов</string>\n    <string name=\"notifications_tab_all\">Все</string>\n    <string name=\"notifications_tab_mention\">Упоминания</string>\n    <string name=\"profile_page_title\">Профиль</string>\n    <string name=\"profile_setting_about_title\">О приложении</string>\n    <string name=\"profile_setting_donate_desc\">Угостите меня кофе, чтобы поддержать проект.</string>\n    <string name=\"profile_setting_inline_video_auto_play\">Автовоспроизведение встроенного видео</string>\n    <string name=\"profile_setting_inline_video_auto_play_subtitle\">Если включено, видео в лентах будет запускаться автоматически.</string>\n    <string name=\"profile_setting_always_show_sensitive_content\">Показывать чувствительный контент по умолчанию</string>\n    <string name=\"profile_setting_always_show_sensitive_content_subtitle\">Если включено, чувствительный контент будет отображаться по умолчанию (только Mastodon).</string>\n    <string name=\"profile_setting_dark_mode_title\">Тёмная тема</string>\n    <string name=\"profile_setting_dark_mode_dark\">Тёмная</string>\n    <string name=\"profile_setting_dark_mode_light\">Светлая</string>\n    <string name=\"profile_setting_dark_mode_follow_system\">Как в системе</string>\n    <string name=\"profile_setting_open_source_title\">Открытый исходный код</string>\n    <string name=\"profile_setting_open_source_desc\">Лицензии открытого ПО, используемого в проекте</string>\n    <string name=\"profile_setting_open_source_feedback\">Оставить отзыв</string>\n    <string name=\"profile_setting_open_source_feedback_desc\">Присоединяйтесь к группе Telegram или свяжитесь иным способом</string>\n    <string name=\"profile_setting_open_source_feedback_telegram\">Присоединиться к группе Telegram</string>\n    <string name=\"profile_setting_open_source_feedback_github\">Создать задачу на GitHub</string>\n    <string name=\"profile_setting_open_source_feedback_email\">Отправить письмо</string>\n    <string name=\"profile_setting_language_title\">Язык отображения</string>\n    <string name=\"profile_setting_language_zh\">简体中文</string>\n    <string name=\"profile_setting_language_en\">English</string>\n    <string name=\"profile_setting_language_system\">Как в системе</string>\n    <string name=\"profile_setting_ratting\">Оцените нас</string>\n    <string name=\"profile_setting_ratting_desc\">Если вам нравится приложение, поставьте, пожалуйста, высокую оценку.</string>\n    <string name=\"profile_setting_font_size\">Размер шрифта контента</string>\n    <string name=\"profile_setting_font_size_desc\">Настроить размер шрифта и значков</string>\n    <string name=\"profile_setting_font_size_small\">Малый</string>\n    <string name=\"profile_setting_font_size_medium\">Средний</string>\n    <string name=\"profile_setting_font_size_large\">Крупный</string>\n    <string name=\"profile_setting_immersive_nav_bar\">Иммерсивная навигационная панель</string>\n    <string name=\"profile_setting_immersive_nav_bar_desc\">При прокрутке главной страницы нижняя панель будет автоматически скрываться.</string>\n    <string name=\"profile_setting_timeline_position\">Позиция ленты по умолчанию</string>\n    <string name=\"profile_setting_timeline_position_last_read\">Где остановился</string>\n    <string name=\"profile_setting_timeline_position_newest\">Самые новые посты</string>\n    <string name=\"profile_setting_theme_title\">Цвет темы</string>\n    <string name=\"profile_setting_theme_default\">По умолчанию</string>\n    <string name=\"profile_setting_theme_system\">Как в системе</string>\n    <string name=\"profile_about_version\">Версия:&#160;</string>\n    <string name=\"profile_about_website\">Сайт:&#160;</string>\n    <string name=\"profile_about_developer\">Создатель:&#160;</string>\n    <string name=\"profile_about_contract_us\">Свяжитесь со мной:&#160;</string>\n    <string name=\"profile_about_telegram\">Группа в Telegram:&#160;</string>\n    <string name=\"profile_about_privacy_policy\">Политика конфиденциальности:&#160;</string>\n    <string name=\"profile_setting_check_for_update\">Проверить обновление</string>\n    <string name=\"profile_setting_already_latest_version\">У вас установлена последняя версия</string>\n    <string name=\"profile_setting_have_new_version\">Доступна новая версия, нажмите для обновления.</string>\n    <string name=\"profile_donate_page_title\">Выберите способ пожертвования</string>\n    <string name=\"profile_account_not_login\">Сессия истекла</string>\n    <string name=\"rss_source_detail_screen_title\">Источник</string>\n    <string name=\"rss_source_detail_screen_custom_title\">Пользовательский заголовок</string>\n    <string name=\"rss_source_detail_screen_url\">Ссылка на подписку</string>\n    <string name=\"rss_source_detail_screen_home_url\">Домашняя страница</string>\n    <string name=\"rss_source_detail_screen_add_date\">Дата добавления</string>\n    <string name=\"rss_source_detail_screen_last_update_date\">Последнее обновление</string>\n    <string name=\"bluesky_protocol_name\">Bluesky</string>\n    <string name=\"bsky_add_content_title\">Войти</string>\n    <string name=\"bsky_add_content_hosting_provider\">Хостинг-провайдер</string>\n    <string name=\"bsky_add_content_user_name\">Имя пользователя или e-mail</string>\n    <string name=\"bsky_add_content_password\">Пароль</string>\n    <string name=\"bsky_add_content_factor_token\">Код подтверждения</string>\n    <string name=\"bsky_edit_content_title\">Редактировать контент</string>\n    <string name=\"bsky_feeds_explorer_more\">Посмотреть больше лент</string>\n    <string name=\"bsky_feeds_explorer_creator_label\">От %1$s</string>\n    <string name=\"bsky_feeds_explorer_liked_by\">Понравилось %1$s пользователям</string>\n    <string name=\"bsky_feeds_detail_creator_prefix\">От</string>\n    <string name=\"bsky_feeds_item_subtitle\">От %1$s · Понравилось %2$s пользователям</string>\n    <string name=\"bsky_feeds_following_name\">Подписки</string>\n    <string name=\"bsky_feeds_user_posts\">Посты</string>\n    <string name=\"bsky_feeds_user_replies\">Ответы</string>\n    <string name=\"bsky_feeds_user_medias\">Медиа</string>\n    <string name=\"bsky_feeds_user_likes\">Лайки</string>\n    <string name=\"bsky_user_detail_action_blocked_list\">Заблокированные пользователи</string>\n    <string name=\"bsky_user_detail_action_muted_list\">Скрытые пользователи</string>\n    <string name=\"bsky_user_detail_action_mute_user\">Скрыть %1$s</string>\n    <string name=\"bsky_user_detail_action_unmute_user\">Отменить скрытие</string>\n    <string name=\"bsky_user_detail_action_block_user\">Заблокировать %1$s</string>\n    <string name=\"bsky_user_detail_action_unblock_user\">Разблокировать</string>\n    <string name=\"bsky_user_detail_action_block_user_dialog_message\">Заблокировать этого пользователя?</string>\n    <string name=\"bsky_user_detail_action_mute_user_dialog_message\">Скрыть этого пользователя?</string>\n    <string name=\"bsky_edit_profile_title\">Редактировать профиль</string>\n    <string name=\"input_media_desc_page_title\">Описание изображения</string>\n    <string name=\"input_media_desc_input_hint\">Введите описание изображения</string>\n    <string name=\"post_status_page_title\">Новый блог</string>\n    <string name=\"post_screen_input_hint\">Введите свой пост</string>\n    <string name=\"post_screen_media_descriptor_placeholder\">Описание</string>\n    <string name=\"post_status_poll_item_hint\">Вариант %1$s</string>\n    <string name=\"post_status_poll_duration\">Длительность голосования</string>\n    <string name=\"post_status_poll_function_title\">Функция</string>\n    <string name=\"post_status_poll_single\">Один вариант</string>\n    <string name=\"post_status_poll_multiple\">Несколько вариантов</string>\n    <string name=\"post_status_poll_style_select_dialog_title\">Выберите стиль</string>\n    <string name=\"post_status_poll_is_empty\">Введите содержание голосования</string>\n    <string name=\"post_status_media_is_not_upload\">Медиа ещё не загружены</string>\n    <string name=\"authentication_page_normal_title\">Для использования этой функции необходимо войти</string>\n    <string name=\"authentication_page_failed_title\">Статус входа недействителен, войдите снова.</string>\n    <string name=\"main_drawer_title\">Список контента</string>\n    <string name=\"main_drawer_mixed_item_subtitle_1\">Смешанный контент ·</string>\n    <string name=\"main_drawer_mixed_item_subtitle_2\">Источники</string>\n    <string name=\"main_drawer_settings\">Настройки</string>\n    <string name=\"main_drawer_donate\">Пожертвовать</string>\n    <string name=\"main_request_notification_dialog_title\">Уведомление</string>\n    <string name=\"main_request_notification_dialog_reject_button\">Отклонить</string>\n    <string name=\"main_request_notification_dialog_allow_button\">Разрешить</string>\n    <string name=\"main_request_notification_dialog_content\">Чтобы получать сообщения об интеракциях, разрешите уведомления.</string>\n    <string name=\"setting_item_amoled_mode\">Режим AMOLED</string>\n    <string name=\"setting_item_amoled_mode_description\">При включении фон в тёмном режиме становится полностью чёрным, что снижает энергопотребление и повышает контраст в тёмных условиях.</string>\n    <string name=\"setting_group_appearance\">Внешний вид</string>\n    <string name=\"setting_group_appearance_subtitle\">Настройки темы, отображения и стиля главной страницы</string>\n    <string name=\"setting_group_behavior\">Поведение</string>\n    <string name=\"setting_group_behavior_subtitle\">Настройки отображения контента и поведения взаимодействия</string>\n    <string name=\"setting_item_home_tab_next_title\">Показывать кнопку переключения контента вверху главной</string>\n    <string name=\"setting_item_home_tab_next_subtitle\">Показывать в правом верхнем углу главной кнопку для перехода к следующему контенту</string>\n    <string name=\"setting_item_home_tab_refresh_title\">Показывать кнопку обновления вверху главной</string>\n    <string name=\"setting_item_home_tab_refresh_subtitle\">Показывать в правом верхнем углу главной кнопку для обновления контента</string>\n    <string name=\"setting_item_open_url_by_system_title\">Открывать ссылки в системном браузере</string>\n    <string name=\"setting_item_open_url_by_system_subtitle\">При включении Fread будет открывать ссылки в системном браузере.</string>\n    <string name=\"setting_item_solid_bar_background_title\">Сплошной фон панелей</string>\n    <string name=\"setting_item_solid_bar_background_subtitle\">Делает верхнюю и нижнюю панели сплошными вместо размытого фона.</string>\n</resources>\n"
  },
  {
    "path": "localization/src/commonMain/composeResources/values-ru-rRU/strings_mastodon.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"activity_pub_user_detail_join_date\">Дата регистрации</string>\n    <string name=\"activity_pub_user_detail_tab_about_joined\">Дата регистрации: %1$s</string>\n    <string name=\"activity_pub_user_detail_menu_block\">Заблокировать %1$s</string>\n    <string name=\"activity_pub_user_detail_menu_block_domain\">Заблокировать %1$s</string>\n    <string name=\"activity_pub_user_detail_menu_unblock_domain\">Разблокировать %1$s</string>\n    <string name=\"activity_pub_user_detail_menu_edit_private_note\">Изменить заметку</string>\n    <string name=\"activity_pub_user_detail_menu_edit_private_note_dialog_hint\">Введите личную заметку</string>\n    <string name=\"activity_pub_user_detail_dialog_content_block\">Заблокировать этого пользователя?</string>\n    <string name=\"activity_pub_user_detail_dialog_content_block_domain\">Заблокировать этот домен?</string>\n    <string name=\"activity_pub_user_menu_blocked_user_list\">Заблокированные пользователи</string>\n    <string name=\"activity_pub_user_menu_muted_user_list\">Скрытые пользователи</string>\n    <string name=\"activity_pub_user_detail_menu_mute_user\">Скрыть %1$s</string>\n    <string name=\"activity_pub_user_detail_menu_unmute_user\">Отменить скрытие %1$s</string>\n    <string name=\"activity_pub_mute_user_bottom_sheet_title\">Скрыть пользователя?</string>\n    <string name=\"activity_pub_mute_user_bottom_sheet_role1\">Они не узнают, что их скрыли.</string>\n    <string name=\"activity_pub_mute_user_bottom_sheet_role2\">Они по-прежнему смогут видеть ваши посты, но вы не будете видеть их.</string>\n    <string name=\"activity_pub_mute_user_bottom_sheet_role3\">Вы не будете видеть посты, в которых их упоминают.</string>\n    <string name=\"activity_pub_mute_user_bottom_sheet_role4\">Они смогут упоминать и подписываться на вас, но вы их не увидите.</string>\n    <string name=\"activity_pub_mute_user_bottom_sheet_btn_mute\">Скрыть</string>\n    <string name=\"add_instance_screen_title\">Добавить инстанс</string>\n    <string name=\"add_instance_input_service_hint\">Введите адрес сервера</string>\n    <string name=\"add_instance_input_service_error\">Введите корректный адрес сервера</string>\n    <string name=\"activity_pub_content_tab_home\">Главная</string>\n    <string name=\"activity_pub_content_tab_local_timeline\">Локальная</string>\n    <string name=\"activity_pub_content_tab_public_timeline\">Публичная</string>\n    <string name=\"activity_pub_content_tab_trending\">В тренде</string>\n    <string name=\"activity_pub_hashtag_timeline_description\">%1$s публикаций · %2$s участников · %3$s публикаций сегодня</string>\n    <string name=\"activity_pub_hashtag_unfollow_dialog_message\">Отписаться от этого хэштега?</string>\n    <string name=\"activity_pub_edit_account_info_label_about\">О себе</string>\n    <string name=\"activity_pub_edit_content_screen_config_not_found\">Конфигурация не найдена</string>\n    <string name=\"activity_pub_user_list_empty\">Пока нет пользователей</string>\n    <string name=\"activity_pub_muted_user_list_empty\">Нет скрытых пользователей</string>\n    <string name=\"activity_pub_muted_user_list_unmute\">Отменить скрытие</string>\n    <string name=\"activity_pub_blocked_user_list_empty\">Нет заблокированных пользователей</string>\n    <string name=\"activity_pub_blocked_user_list_unblock\">Разблокировать</string>\n    <string name=\"activity_pub_bookmarks_list_title\">Закладки</string>\n    <string name=\"activity_pub_favourites_list_title\">Избранное</string>\n    <string name=\"activity_pub_followed_tags_screen_title\">Подписки на хэштеги</string>\n    <string name=\"activity_pub_filters_list_page_title\">Фильтры</string>\n    <string name=\"activity_pub_filters_active\">Активные</string>\n    <string name=\"activity_pub_filters_expired\">Истёкшие</string>\n    <string name=\"activity_pub_filter_edit_title\">Редактировать фильтр</string>\n    <string name=\"activity_pub_filter_edit_input_title_label\">Заголовок</string>\n    <string name=\"activity_pub_filter_edit_input_title_hint\">Введите заголовок</string>\n    <string name=\"activity_pub_filter_edit_duration\">Длительность</string>\n    <string name=\"activity_pub_filter_edit_duration_permanent\">Постоянно</string>\n    <string name=\"activity_pub_filter_edit_duration_thirty_minutes\">30 минут</string>\n    <string name=\"activity_pub_filter_edit_duration_one_hour\">1 час</string>\n    <string name=\"activity_pub_filter_edit_duration_twelve_hours\">12 часов</string>\n    <string name=\"activity_pub_filter_edit_duration_one_day\">1 день</string>\n    <string name=\"activity_pub_filter_edit_duration_three_day\">3 дня</string>\n    <string name=\"activity_pub_filter_edit_duration_one_week\">1 неделя</string>\n    <string name=\"activity_pub_filter_edit_duration_custom\">Другое</string>\n    <string name=\"activity_pub_filter_edit_duration_finish_subtitle\">Действительно до %1$s</string>\n    <string name=\"activity_pub_filter_edit_keyword_title\">Скрытые ключевые слова</string>\n    <string name=\"activity_pub_filter_edit_keyword_dialog_title\">Редактировать ключевое слово</string>\n    <string name=\"activity_pub_filter_edit_keyword_dialog_hint\">Ключевое слово</string>\n    <string name=\"activity_pub_filter_edit_keyword_remove_keyword_dialog_content\">Удалить это ключевое слово?</string>\n    <string name=\"activity_pub_filter_edit_keyword_list_title\">Скрытые слова</string>\n    <string name=\"activity_pub_filter_edit_keyword_list_desc\">%1$s скрытых слов или фраз</string>\n    <string name=\"activity_pub_filter_edit_context_title\">Скрывать в</string>\n    <string name=\"activity_pub_filter_edit_context_selector_title\">Выберите источники для скрытия</string>\n    <string name=\"activity_pub_filter_edit_context_home\">Главная &amp; списки</string>\n    <string name=\"activity_pub_filter_edit_context_notification\">Уведомления</string>\n    <string name=\"activity_pub_filter_edit_context_timeline\">Публичные ленты</string>\n    <string name=\"activity_pub_filter_edit_context_thread\">Темы &amp; ответы</string>\n    <string name=\"activity_pub_filter_edit_context_account\">Профили</string>\n    <string name=\"activity_pub_filter_edit_empty_context\">Пусто</string>\n    <string name=\"activity_pub_filter_edit_warning_title\">Показывать с предупреждением о содержимом</string>\n    <string name=\"activity_pub_filter_edit_warning_desc\">Показывать посты, подходящие под фильтр, но с предупреждением о содержимом</string>\n    <string name=\"activity_pub_filter_edit_delete_content\">Удалить?</string>\n    <string name=\"activity_pub_filter_edit_back_dialog\">Выйти?</string>\n    <string name=\"activity_pub_filter_edit_whole_word\">Целое слово</string>\n    <string name=\"activity_pub_explorer_tab_status_title\">Посты</string>\n    <string name=\"activity_pub_explorer_tab_users_title\">Пользователи</string>\n    <string name=\"activity_pub_explorer_tab_hashtag_title\">Хэштеги</string>\n    <string name=\"activity_pub_created_list_title\">Списки</string>\n    <string name=\"activity_pub_add_list_title\">Создать список</string>\n    <string name=\"activity_pub_add_list_name\">Название списка</string>\n    <string name=\"activity_pub_add_list_replies\">Показывать ответы</string>\n    <string name=\"activity_pub_add_list_replies_non\">Никому</string>\n    <string name=\"activity_pub_add_list_replies_list\">Участникам списка</string>\n    <string name=\"activity_pub_add_list_replies_followers\">Любому, на кого я подписан</string>\n    <string name=\"activity_pub_add_list_accounts\">Участники списка</string>\n    <string name=\"activity_pub_add_list_hide_in_timeline\">Скрывать участников в «Подписках»</string>\n    <string name=\"activity_pub_add_list_hide_in_timeline_desc\">Если кто-то в этом списке, скрывать его посты в ленте «Подписки», чтобы не видеть их дважды.</string>\n    <string name=\"activity_pub_add_list_remove_user_message\">Удалить этого пользователя?</string>\n    <string name=\"activity_pub_add_list_name_is_empty\">Введите имя пользователя</string>\n    <string name=\"activity_pub_add_list_back_reminder\">Есть несохранённые изменения. Выйти?</string>\n    <string name=\"activity_pub_search_user_placeholder\">Введите для поиска</string>\n    <string name=\"activity_pub_list_delete_confirm\">Удалить этот список?</string>\n    <string name=\"activity_pub_login_exception\">Ошибка состояния OAuth!</string>\n    <string name=\"activity_pub_home_timeline\">Главная</string>\n    <string name=\"activity_pub_local_timeline\">Локальная</string>\n    <string name=\"activity_pub_public_timeline\">Публичная</string>\n    <string name=\"activity_pub_instance_detail_language_label\">Язык: %1$s</string>\n    <string name=\"activity_pub_instance_detail_active_month_label\">Активных за месяц: %1$s</string>\n    <string name=\"activity_pub_user_detail_tab_post\">Посты</string>\n    <string name=\"activity_pub_user_detail_tab_replies\">Ответы</string>\n    <string name=\"activity_pub_user_detail_tab_media\">Медиа</string>\n    <string name=\"activity_pub_user_detail_tab_about\">О себе</string>\n    <string name=\"activity_pub_about\">О себе</string>\n    <string name=\"activity_pub_about_rule_title\">Правила</string>\n    <string name=\"activity_pub_trends_tag\">Трендовые теги</string>\n    <string name=\"activity_pub_trends_tag_description\">За последние 2 дня: %1$s человек</string>\n    <string name=\"activity_pub_trends_status\">Популярное</string>\n    <string name=\"activity_pub_select_platform_title\">Выберите инстанс</string>\n    <string name=\"activity_pub_select_platform_text_hint\">Введите имя инстанса или URL</string>\n</resources>\n"
  },
  {
    "path": "localization/src/commonMain/composeResources/values-ru-rRU/strings_status_ui.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"status_ui_reposted\">повторно опубликовано</string>\n    <string name=\"status_ui_repost\">Репост</string>\n    <string name=\"status_ui_quote\">Цитата</string>\n    <string name=\"status_ui_like\">Нравится</string>\n    <string name=\"status_ui_comment\">Комментарий</string>\n    <string name=\"status_ui_delete\">Удалить</string>\n    <string name=\"status_ui_share\">Поделиться</string>\n    <string name=\"status_ui_bookmark\">Закладка</string>\n    <string name=\"status_ui_unbookmark\">Убрать из закладок</string>\n    <string name=\"status_ui_reply\">Ответить</string>\n    <string name=\"status_ui_follow\">Подписаться</string>\n    <string name=\"status_ui_unfollow\">Отписаться</string>\n    <string name=\"status_ui_pin\">Закрепить</string>\n    <string name=\"status_ui_unpin\">Открепить</string>\n    <string name=\"status_ui_edit\">Редактировать</string>\n    <string name=\"status_ui_sensitive_by_filter\">Соответствует фильтру \"%1$s\"</string>\n    <string name=\"status_ui_new_status\">Новый контент</string>\n    <string name=\"status_ui_delete_status_confirm\">Удалить этот контент?</string>\n    <string name=\"status_ui_image_sensitive_label\">Медиа скрыто</string>\n    <string name=\"status_ui_image_content_show_hidden_label\">Показать больше</string>\n    <string name=\"status_ui_image_content_hide_hidden_label\">Скрыть контент</string>\n    <string name=\"status_ui_poll_vote\">Голосовать</string>\n    <string name=\"status_ui_poll_vote_finished_tip\">%1$s голос · Закрыто</string>\n    <string name=\"status_ui_poll_votes_finished_tip\">%1$s голосов · Закрыто</string>\n    <string name=\"status_ui_interaction_open_in_browser\">Открыть в браузере</string>\n    <string name=\"status_ui_interaction_open_original_instance\">Открыть другой инстанс</string>\n    <string name=\"status_ui_interaction_copy_url\">Копировать ссылку</string>\n    <string name=\"status_ui_interaction_translate\">Перевести</string>\n    <string name=\"status_ui_interaction_open_blog_by_other_account\">Открыть с другого аккаунта</string>\n    <string name=\"status_ui_visibility_mentioned_only\">Только упомянутые</string>\n    <string name=\"status_ui_label_pinned\">Закреплено</string>\n    <string name=\"status_ui_info_label_edited\">Отредактировано</string>\n    <string name=\"status_ui_interaction_label_favourited_count\">%1$s отметок «Нравится»</string>\n    <string name=\"status_ui_interaction_label_boosted_count\">%1$s репостов</string>\n    <string name=\"status_ui_bottom_label_edited_at\">Отредактировано %1$s</string>\n    <string name=\"status_ui_translating\">Перевод…</string>\n    <string name=\"status_ui_translate_show_original\">Показать оригинал</string>\n    <string name=\"status_ui_edit_content_delete_dialog_content\">Удалить этот контент?</string>\n    <string name=\"status_ui_edit_content_name_title\">Редактировать название контента</string>\n    <string name=\"status_ui_edit_content_name_label\">Редактировать название</string>\n    <string name=\"status_ui_edit_content_name_hint\">Введите название контента</string>\n    <string name=\"status_ui_edit_content_config_showing_list_title\">Ленты на главной</string>\n    <string name=\"status_ui_edit_content_config_showing_list_description\">Нажмите и удерживайте, затем перетащите для сортировки</string>\n    <string name=\"status_ui_edit_content_config_hidden_list_title\">Ленты для добавления на главную</string>\n    <string name=\"status_ui_user_detail_relationship_mutuals\">Взаимные</string>\n    <string name=\"status_ui_user_detail_relationship_blocking\">Заблокирован</string>\n    <string name=\"status_ui_user_detail_relationship_following\">Подписан</string>\n    <string name=\"status_ui_user_detail_relationship_not_follow\">Подписаться</string>\n    <string name=\"status_ui_user_detail_relationship_requested\">В ожидании</string>\n    <string name=\"status_ui_user_detail_relationship_follow_back\">Подписаться в ответ</string>\n    <string name=\"status_ui_user_detail_request_by_tip\">Этот пользователь хочет подписаться на вас</string>\n    <string name=\"status_ui_relationship_btn_dialog_content_cancel_follow\">Отписаться от этого пользователя?</string>\n    <string name=\"status_ui_relationship_btn_dialog_content_cancel_follow_request\">Отменить запрос на подписку?</string>\n    <string name=\"status_ui_relationship_btn_dialog_content_cancel_blocking\">Разблокировать этого пользователя?</string>\n    <string name=\"status_ui_user_detail_follows_you\">Подписан на вас</string>\n    <string name=\"status_ui_user_detail_posts\">Посты</string>\n    <string name=\"status_ui_user_detail_follower_info\">Подписчики</string>\n    <string name=\"status_ui_user_detail_following_info\">Подписки</string>\n    <string name=\"status_ui_top_label_continued_thread\">Продолжение ветки</string>\n    <string name=\"status_ui_update_dialog_title\">Обновление</string>\n    <string name=\"status_ui_update_dialog_release_note\">Журнал изменений версии %1$s:\\n%2$s</string>\n    <string name=\"status_ui_switch_account_dialog_title\">Выберите аккаунт</string>\n    <string name=\"status_ui_likes\">Лайки</string>\n    <string name=\"status_ui_bookmarks\">Закладки</string>\n    <string name=\"status_ui_edit_profile\">Редактировать профиль</string>\n    <string name=\"status_ui_logout\">Выйти</string>\n    <string name=\"status_ui_logout_dialog_content\">Выйти из аккаунта?</string>\n    <string name=\"status_ui_search_account_status_hint\">Поиск постов</string>\n    <string name=\"shared_notification_unknown_desc\">Неизвестное уведомление</string>\n    <string name=\"shared_notification_favourited_desc\">понравился ваш пост</string>\n    <string name=\"shared_notification_reblog_desc\">поделился вашим постом</string>\n    <string name=\"shared_notification_poll_desc\">Посмотреть результаты голосования</string>\n    <string name=\"shared_notification_poll_count\">%1$s голосов · Закрыто</string>\n    <string name=\"shared_notification_follow_desc\">подписался на вас</string>\n    <string name=\"shared_notification_update_desc\">отредактировал пост</string>\n    <string name=\"shared_notification_follow_request\">Вы получили запрос на подписку</string>\n    <string name=\"shared_notification_new_status_desc\">опубликовал новый пост</string>\n    <string name=\"shared_notification_severed_desc\">разорвал связь с вами</string>\n    <string name=\"shared_notification_quote_desc\">процитировал ваш пост</string>\n    <string name=\"shared_notification_reply_desc\">ответил на ваш пост</string>\n    <string name=\"shared_user_list_title_following\">Подписки</string>\n    <string name=\"shared_user_list_title_followers\">Подписчики</string>\n    <string name=\"shared_user_list_title_mutes\">Скрытые пользователи</string>\n    <string name=\"shared_user_list_title_blocks\">Заблокированные пользователи</string>\n    <string name=\"shared_user_list_title_likes\">Понравилось</string>\n    <string name=\"shared_user_list_title_reblog\">Репосты</string>\n    <string name=\"shared_user_list_action_mute\">Скрыть</string>\n    <string name=\"shared_user_list_action_block\">Заблокировать</string>\n    <string name=\"shared_user_list_action_muted\">Скрыт</string>\n    <string name=\"shared_user_list_action_blocked\">Заблокирован</string>\n    <string name=\"shared_user_list_action_mute_dialog_message\">Скрыть этого пользователя?</string>\n    <string name=\"shared_user_list_action_block_dialog_message\">Заблокировать этого пользователя?</string>\n    <string name=\"shared_publish_blog_title\">Пост</string>\n    <string name=\"shared_publish_blog_text_hint\">Напишите или вставьте, что у вас на уме</string>\n    <string name=\"shared_publish_reply_input_hint\">Ответ для [[%1$s]]</string>\n    <string name=\"shared_publish_interaction_no_limit\">Все могут взаимодействовать</string>\n    <string name=\"shared_publish_interaction_limited\">Взаимодействие ограничено</string>\n    <string name=\"shared_publish_interaction_dialog_title\">Настройки взаимодействия с постом</string>\n    <string name=\"shared_publish_interaction_dialog_subtitle\">Настройте, кто может взаимодействовать с этим постом.</string>\n    <string name=\"shared_publish_interaction_dialog_quote_title\">Настройки цитирования</string>\n    <string name=\"shared_publish_interaction_dialog_quote_allow\">Разрешить цитирование</string>\n    <string name=\"shared_publish_interaction_dialog_reply_title\">Настройки ответов</string>\n    <string name=\"shared_publish_interaction_dialog_reply_subtitle\">Разрешить ответы от:</string>\n    <string name=\"shared_publish_interaction_dialog_reply_all\">Всех</string>\n    <string name=\"shared_publish_interaction_dialog_reply_nobody\">Никого</string>\n    <string name=\"shared_publish_interaction_dialog_reply_combine_title\">Или комбинируйте эти варианты:</string>\n    <string name=\"shared_publish_interaction_dialog_mentioned\">Упомянутые пользователи</string>\n    <string name=\"shared_publish_interaction_dialog_following\">Пользователи, на которых вы подписаны</string>\n    <string name=\"shared_publish_interaction_dialog_follower\">Ваши подписчики</string>\n    <string name=\"shared_publish_interaction_dialog_in_list\">Пользователи из \"%1$s\"</string>\n    <string name=\"shared_publish_media_alt_dialog_title\">Добавить alt-текст</string>\n    <string name=\"shared_publish_media_alt_dialog_input_tip\">Описание для alt-текста</string>\n    <string name=\"shared_publish_media_alt_dialog_input_hint\">Alt-текст</string>\n    <string name=\"shared_feeds_not_login_title\">Ваша сессия истекла. Пожалуйста, войдите снова.</string>\n    <string name=\"shared_feeds_go_to_login\">Войти</string>\n    <string name=\"shared_publish_select_account_title\">Выберите аккаунт</string>\n    <string name=\"shared_select_language_title\">Выберите язык</string>\n    <string name=\"shared_status_context_screen_title\">Детали</string>\n    <string name=\"shared_alt_label\">ALT</string>\n    <string name=\"status_ui_embed_quote_unavailable\">Текущая цитата недоступна</string>\n    <string name=\"status_ui_action_unforward\">Отменить пересылку</string>\n    <string name=\"status_ui_quote_approval_public\">Все</string>\n    <string name=\"status_ui_quote_approval_follower\">Только подписчики</string>\n    <string name=\"status_ui_quote_approval_nobody\">Только я</string>\n    <string name=\"status_ui_visibility\">Видимость</string>\n</resources>\n"
  },
  {
    "path": "localization/src/commonMain/composeResources/values-zh-rCN/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"skip\">跳过</string>\n    <string name=\"alert\">提示</string>\n    <string name=\"duration_minute\">分钟</string>\n    <string name=\"duration_hour\">小时</string>\n    <string name=\"duration_day\">天</string>\n    <string name=\"duration_week\">周</string>\n    <string name=\"cancel\">取消</string>\n    <string name=\"ok\">确定</string>\n    <string name=\"empty\">这里是空的～</string>\n    <string name=\"duration_selector_title\">设置时长</string>\n    <string name=\"retry\">重试</string>\n    <string name=\"load_more_error\">加载失败。</string>\n    <string name=\"unknown_error\">未知错误</string>\n    <string name=\"network_error\">网络错误</string>\n    <string name=\"resource_not_found\">资源未找到</string>\n    <string name=\"mixed_content_subtitle_1\">混合内容 ·</string>\n    <string name=\"mixed_content_subtitle_2\">条订阅源</string>\n    <string name=\"date_time_ago\">前</string>\n    <string name=\"date_time_ago_prefix\"></string>\n    <string name=\"date_time_ago_suffix\">前</string>\n    <string name=\"date_time_day\">天</string>\n    <string name=\"date_time_hour\">小时</string>\n    <string name=\"date_time_minute\">分</string>\n    <string name=\"date_time_second\">秒</string>\n    <string name=\"image_saving\">保存中…</string>\n    <string name=\"image_save_success\">保存成功</string>\n    <string name=\"image_save_failed\">保存失败</string>\n    <string name=\"permission_write_external_permission_denied\">存储权限被拒绝，请到设置中打开。</string>\n    <string name=\"feeds_load_previous_page_label\">正在加载上一页内容</string>\n    <string name=\"feeds_load_previous_page_failed_label\">加载失败，点击重试</string>\n    <string name=\"image\">图片</string>\n    <string name=\"video\">视频</string>\n    <string name=\"login\">登录</string>\n    <string name=\"add\">添加</string>\n    <string name=\"search\">搜索</string>\n    <string name=\"auth_success\">登录成功</string>\n    <string name=\"auth_failed\">登录失败</string>\n    <string name=\"select\">选择</string>\n    <string name=\"logout\">退出登录</string>\n    <string name=\"settings\">设置</string>\n    <string name=\"donate\">捐赠</string>\n    <string name=\"feeds\">Feeds</string>\n    <string name=\"save\">保存</string>\n    <string name=\"done\">完成</string>\n    <string name=\"bluesky_name\">Bluesky</string>\n    <string name=\"bluesky_description\">Bluesky 是一个开放网络。通过一个账户，你既可以访问易用的社交网络，也可以在整个社交互联网上拥有共享身份。</string>\n    <string name=\"mastodon_name\">长毛象</string>\n    <string name=\"mastodon_description\">Mastodon 是一个去中心化、非商业化的社交媒体，用户可以控制自己的时间线，每个服务器都是独立的实体。</string>\n    <string name=\"mixed_content_name\">混合内容</string>\n    <string name=\"mixed_content_description\">混合内容是由 Fread 提供的自定义聚合信息流，你可以添加来自长毛象、Bluesky、RSS 等平台的信息源，来自这些信息源的内容会按照时间倒序形成一个聚合信息流。</string>\n    <string name=\"status_provider_type_activity_pub\">长毛象</string>\n    <string name=\"add_content_title\">添加内容</string>\n    <string name=\"add_content_success_with_account\">添加成功，当前账号：</string>\n    <string name=\"add_content_success_with_login_reminder\">添加成功，是否登录？</string>\n    <string name=\"content_exist_tips\">该内容已存在</string>\n    <string name=\"content_add_success\">内容添加成功</string>\n    <string name=\"add_feeds_page_empty_name_exist\">该名字已存在</string>\n    <string name=\"edit_profile_name_empty\">请输入用户名</string>\n    <string name=\"edit_profile_label_name\">名字</string>\n    <string name=\"edit_profile_label_note\">简介</string>\n    <string name=\"edit_profile_input_name_hint\">请输入用户名</string>\n    <string name=\"edit_profile_input_note_hint\">请输入简介</string>\n    <string name=\"edit_profile_edit_confirm_message\">确定退出编辑吗？</string>\n    <string name=\"add_content_success_snackbar\">添加成功</string>\n    <string name=\"login_dialog_input_hint\">请输入要登录的服务器</string>\n    <string name=\"login_dialog_title\">请选择要登录的服务器</string>\n    <string name=\"login_dialog_input_tip\">请输入服务器地址</string>\n    <string name=\"login_dialog_input_title\">请输入服务器地址</string>\n    <string name=\"login_dialog_input_label\">服务器地址</string>\n    <string name=\"login_dialog_target_title\">请登录</string>\n    <string name=\"profile_description\">Fread 支持添加多个账号，添加进来的账号可以用来发帖、点赞、转发、评论等操作，还可以查看已添加账号创建的列表。</string>\n    <string name=\"list_content_empty_placeholder\">暂无内容</string>\n    <string name=\"post_status_scope_public\">公开</string>\n    <string name=\"post_status_scope_unlisted\">公开，但不在探索功能出现</string>\n    <string name=\"post_status_scope_follower_only\">仅关注者可见</string>\n    <string name=\"post_status_scope_mentioned_only\">仅提及用户可见</string>\n    <string name=\"post_status_content_warning\">警告文本</string>\n    <string name=\"post_status_success\">发送成功</string>\n    <string name=\"post_status_failed\">发送失败：%1$s</string>\n    <string name=\"post_status_exit_dialog_content\">退出后已编辑的内容不会被保存，确定退出吗？</string>\n    <string name=\"post_status_content_is_empty\">请输入内容</string>\n    <string name=\"post_status_part_failed\">部分账号发布失败，已从发布队列移除成功的账号，请重新尝试：%1$s</string>\n    <string name=\"select_account_open_status_title\">请选择账号</string>\n    <string name=\"select_account_open_status_empty\">暂无其它账号</string>\n    <string name=\"select_account_open_status_search_in\">在 %1$s 内查找</string>\n    <string name=\"select_account_open_status_search_failed\">查找失败</string>\n    <string name=\"explorer_search_no_results\">未搜索到结果</string>\n    <string name=\"explorer_search_bar_hint\">搜索</string>\n    <string name=\"explorer_search_bar_hint_specialize_platform\">搜索 %1$s</string>\n    <string name=\"explorer_search_tab_title_author\">用户</string>\n    <string name=\"explorer_search_tab_title_status\">帖子</string>\n    <string name=\"explorer_search_tab_title_hashtag\">标签</string>\n    <string name=\"explorer_search_tab_title_server\">服务器</string>\n    <string name=\"explorer_tab_title\">探索</string>\n    <string name=\"explorer_tab_status_title\">帖子</string>\n    <string name=\"explorer_tab_users_title\">用户</string>\n    <string name=\"explorer_tab_hashtag_title\">话题</string>\n    <string name=\"add_provider_page_title\">添加服务</string>\n    <string name=\"add_provider_page_confirm_button\">确定添加</string>\n    <string name=\"search_page_title\">搜索</string>\n    <string name=\"search_result_not_found\">未搜索到任何结果</string>\n    <string name=\"feeds_import_page_title\">导入</string>\n    <string name=\"feeds_import_page_hint\">请选择要导入的 OPML 文件</string>\n    <string name=\"feeds_import_button\">导入</string>\n    <string name=\"add_feeds_page_title\">添加内容</string>\n    <string name=\"add_feeds_page_feeds_name_label\">内容名</string>\n    <string name=\"add_feeds_page_feeds_name_hint\">请输入内容名</string>\n    <string name=\"add_feeds_page_feeds_empty\">请添加订阅源</string>\n    <string name=\"pre_add_feeds_no_result\">未搜索到任何结果</string>\n    <string name=\"pre_add_feeds_hint\">实例名、实例地址，用户地址、RSS</string>\n    <string name=\"pre_add_feeds_input_label_1\">Fread 支持添加[[长毛象]]、[[Bluesky]]以及[[ RSS ]]内容，甚至是[[混合内容]]。</string>\n    <string name=\"pre_add_feeds_input_label_2\">请直接在此处输入长毛象或 Bluesky [[用户名]]、[[WebFinger\\Handle]]，用户[[首页地址]]或者 [[RSS]] 地址。</string>\n    <string name=\"empty_content_hint_title\">请先添加内容</string>\n    <string name=\"empty_content_hint_desc\">需要先添加内容的订阅源哦，Fread 支持多种内容类型，以及不同平台的混合内容，可以打造您自己的信息流。</string>\n    <string name=\"feeds_add_content\">添加内容</string>\n    <string name=\"select_feeds_type_screen_title\">请选择内容类型</string>\n    <string name=\"feeds_pre_add_login_dialog_content\">添加成功，是否登录？</string>\n    <string name=\"add_feeds_page_empty_name_tips\">请输入一个名字</string>\n    <string name=\"add_feeds_page_empty_source_tips\">请添加信息源</string>\n    <string name=\"add_feeds_choose_auth_dialog_title\">请选择要登录的服务器</string>\n    <string name=\"search_feeds_title\">搜索</string>\n    <string name=\"search_feeds_title_hint\">长毛象或 Bluesky 用户名、WebFinger、URL、RSS</string>\n    <string name=\"feeds_select_account_for_post_status\">请选择一个账号</string>\n    <string name=\"feeds_mixed_config_edit_new_name_dialog_title\">请输入内容名称</string>\n    <string name=\"feeds_mixed_config_edit_new_name_dialog_label\">名称</string>\n    <string name=\"feeds_mixed_config_edit_delete_content_dialog_message\">您确定要删除这个内容吗？</string>\n    <string name=\"feeds_mixed_config_not_found\">状态异常，未找到配置</string>\n    <string name=\"feeds_import_back_dialog_message\">确定要退出吗？</string>\n    <string name=\"feeds_delete_confirm_content\">确定要删除吗？</string>\n    <string name=\"feeds_select_type_screen_title\">请选择内容类型</string>\n    <string name=\"feeds_select_type_screen_content\">请选择您要添加的内容类型</string>\n    <string name=\"notification_tab_title\">通知</string>\n    <string name=\"notifications_account_empty_tip\">当前没有已登录账号</string>\n    <string name=\"notifications_tab_all\">所有</string>\n    <string name=\"notifications_tab_mention\">提及</string>\n    <string name=\"profile_page_title\">账号</string>\n    <string name=\"profile_setting_about_title\">关于</string>\n    <string name=\"profile_setting_donate_desc\">给我买杯咖啡以此支持这个项目</string>\n    <string name=\"profile_setting_inline_video_auto_play\">Inline 视频自动播放</string>\n    <string name=\"profile_setting_inline_video_auto_play_subtitle\">开启后，Feeds 流中的视频将会自动播放。</string>\n    <string name=\"profile_setting_always_show_sensitive_content\">敏感内容默认显示</string>\n    <string name=\"profile_setting_always_show_sensitive_content_subtitle\">开启后，敏感内容默认会显示（仅 Mastodon 平台）。</string>\n    <string name=\"profile_setting_dark_mode_title\">深色模式</string>\n    <string name=\"profile_setting_dark_mode_dark\">深色模式</string>\n    <string name=\"profile_setting_dark_mode_light\">浅色模式</string>\n    <string name=\"profile_setting_dark_mode_follow_system\">跟随系统</string>\n    <string name=\"profile_setting_open_source_title\">开源声明</string>\n    <string name=\"profile_setting_open_source_desc\">本项目使用到的依赖库的开源声明</string>\n    <string name=\"profile_setting_open_source_feedback\">给我们反馈</string>\n    <string name=\"profile_setting_open_source_feedback_desc\">加入 Telegram 群组或者其它方式</string>\n    <string name=\"profile_setting_open_source_feedback_telegram\">加入 Telegram 群组</string>\n    <string name=\"profile_setting_open_source_feedback_github\">创建 GitHub issue</string>\n    <string name=\"profile_setting_open_source_feedback_email\">发送邮件</string>\n    <string name=\"profile_setting_language_title\">显示语言</string>\n    <string name=\"profile_setting_language_zh\">简体中文</string>\n    <string name=\"profile_setting_language_en\">English</string>\n    <string name=\"profile_setting_language_system\">跟随系统</string>\n    <string name=\"profile_setting_ratting\">给我们评价</string>\n    <string name=\"profile_setting_ratting_desc\">如果觉得不错请到应用市场给我们一个好评</string>\n    <string name=\"profile_setting_font_size\">内容字体大小</string>\n    <string name=\"profile_setting_font_size_desc\">调整内容的字体和图标大小</string>\n    <string name=\"profile_setting_font_size_small\">小</string>\n    <string name=\"profile_setting_font_size_medium\">中</string>\n    <string name=\"profile_setting_font_size_large\">大</string>\n    <string name=\"profile_setting_immersive_nav_bar\">沉浸式导航栏</string>\n    <string name=\"profile_setting_immersive_nav_bar_desc\">打开后，首页滑动时会隐藏底部的导航栏。</string>\n    <string name=\"profile_setting_timeline_position\">首页的时间线默认位置</string>\n    <string name=\"profile_setting_timeline_position_last_read\">上次阅读位置</string>\n    <string name=\"profile_setting_timeline_position_newest\">最新位置</string>\n    <string name=\"profile_setting_theme_title\">主题色</string>\n    <string name=\"profile_setting_theme_default\">默认</string>\n    <string name=\"profile_setting_theme_system\">系统</string>\n    <string name=\"profile_about_version\">版本：</string>\n    <string name=\"profile_about_website\">官网：</string>\n    <string name=\"profile_about_developer\">开发者：</string>\n    <string name=\"profile_about_contract_us\">联系我：</string>\n    <string name=\"profile_about_telegram\">Telegram 群组：</string>\n    <string name=\"profile_about_privacy_policy\">隐私协议：</string>\n    <string name=\"profile_setting_check_for_update\">检查更新</string>\n    <string name=\"profile_setting_already_latest_version\">当前版本已经是最新了</string>\n    <string name=\"profile_setting_have_new_version\">有新版本，点击更新。</string>\n    <string name=\"profile_donate_page_title\">请选择捐赠方式</string>\n    <string name=\"profile_account_not_login\">登陆已失效</string>\n    <string name=\"rss_source_detail_screen_title\">信息源属性</string>\n    <string name=\"rss_source_detail_screen_custom_title\">自定义标题</string>\n    <string name=\"rss_source_detail_screen_url\">订阅地址</string>\n    <string name=\"rss_source_detail_screen_home_url\">主页</string>\n    <string name=\"rss_source_detail_screen_add_date\">添加日期</string>\n    <string name=\"rss_source_detail_screen_last_update_date\">上次更新时间</string>\n    <string name=\"bluesky_protocol_name\">Bluesky</string>\n    <string name=\"bsky_add_content_title\">登录</string>\n    <string name=\"bsky_add_content_hosting_provider\">服务商地址</string>\n    <string name=\"bsky_add_content_user_name\">用户名或邮箱</string>\n    <string name=\"bsky_add_content_password\">密码</string>\n    <string name=\"bsky_add_content_factor_token\">验证码</string>\n    <string name=\"bsky_edit_content_title\">编辑</string>\n    <string name=\"bsky_feeds_explorer_more\">浏览更多 Feeds</string>\n    <string name=\"bsky_feeds_explorer_creator_label\">创建者  %1$s</string>\n    <string name=\"bsky_feeds_explorer_liked_by\">%1$s 个用户喜欢</string>\n    <string name=\"bsky_feeds_detail_creator_prefix\">作者</string>\n    <string name=\"bsky_feeds_item_subtitle\">创建者 %1$s · %2$s 个用户喜欢</string>\n    <string name=\"bsky_feeds_following_name\">关注中</string>\n    <string name=\"bsky_feeds_user_posts\">帖子</string>\n    <string name=\"bsky_feeds_user_replies\">回复</string>\n    <string name=\"bsky_feeds_user_medias\">媒体</string>\n    <string name=\"bsky_feeds_user_likes\">喜欢</string>\n    <string name=\"bsky_user_detail_action_blocked_list\">屏蔽的用户</string>\n    <string name=\"bsky_user_detail_action_muted_list\">隐藏的用户</string>\n    <string name=\"bsky_user_detail_action_mute_user\">隐藏 %1$s</string>\n    <string name=\"bsky_user_detail_action_unmute_user\">取消隐藏</string>\n    <string name=\"bsky_user_detail_action_block_user\">屏蔽 %1$s</string>\n    <string name=\"bsky_user_detail_action_unblock_user\">取消屏蔽</string>\n    <string name=\"bsky_user_detail_action_block_user_dialog_message\">您确认要屏蔽对方吗？</string>\n    <string name=\"bsky_user_detail_action_mute_user_dialog_message\">您确认要隐藏对方吗？</string>\n    <string name=\"bsky_edit_profile_title\">编辑个人资料</string>\n    <string name=\"input_media_desc_page_title\">描述文本</string>\n    <string name=\"input_media_desc_input_hint\">请输入描述文本</string>\n    <string name=\"post_status_page_title\">新博客</string>\n    <string name=\"post_screen_input_hint\">请输入你的博客</string>\n    <string name=\"post_screen_media_descriptor_placeholder\">描述文本</string>\n    <string name=\"post_status_poll_item_hint\">选项 %1$s</string>\n    <string name=\"post_status_poll_duration\">投票时长</string>\n    <string name=\"post_status_poll_function_title\">能力</string>\n    <string name=\"post_status_poll_single\">单选</string>\n    <string name=\"post_status_poll_multiple\">多选</string>\n    <string name=\"post_status_poll_style_select_dialog_title\">请选择功能</string>\n    <string name=\"post_status_poll_is_empty\">请输入投票内容</string>\n    <string name=\"post_status_media_is_not_upload\">媒体暂未上传</string>\n    <string name=\"authentication_page_normal_title\">该功能需要登录后才能使用哦</string>\n    <string name=\"authentication_page_failed_title\">登录状态失效，请重新登录。</string>\n    <string name=\"main_drawer_title\">内容列表</string>\n    <string name=\"main_drawer_mixed_item_subtitle_1\">混合内容 ·</string>\n    <string name=\"main_drawer_mixed_item_subtitle_2\">条订阅源</string>\n    <string name=\"main_drawer_settings\">设置</string>\n    <string name=\"main_drawer_donate\">捐赠</string>\n    <string name=\"main_request_notification_dialog_title\">通知权限</string>\n    <string name=\"main_request_notification_dialog_reject_button\">拒绝</string>\n    <string name=\"main_request_notification_dialog_allow_button\">允许</string>\n    <string name=\"main_request_notification_dialog_content\">为了接收互动消息，请允许通知权限。</string>\n    <string name=\"setting_item_amoled_mode\">Amoled 模式</string>\n    <string name=\"setting_item_amoled_mode_description\">启用该功能后，深色模式下会将背景切换为纯黑，以降低能耗并在暗色环境中获得更高对比度。</string>\n    <string name=\"setting_group_appearance\">外观</string>\n    <string name=\"setting_group_appearance_subtitle\">主题、显示与首页样式设置</string>\n    <string name=\"setting_group_behavior\">行为</string>\n    <string name=\"setting_group_behavior_subtitle\">内容展示与交互行为设置</string>\n    <string name=\"setting_item_home_tab_next_title\">首页顶部的切换内容按钮显示</string>\n    <string name=\"setting_item_home_tab_next_subtitle\">首页的右上角是否显示用于切换到下一个内容的按钮</string>\n    <string name=\"setting_item_home_tab_refresh_title\">首页顶部的刷新按钮显示</string>\n    <string name=\"setting_item_home_tab_refresh_subtitle\">首页的右上角是否显示用于刷新内容的按钮</string>\n    <string name=\"setting_item_open_url_by_system_title\">通过系统浏览器打开链接</string>\n    <string name=\"setting_item_open_url_by_system_subtitle\">启用后，Fread 将通过系统浏览器打开链接。</string>\n    <string name=\"setting_item_solid_bar_background_title\">顶部和底部栏纯色背景</string>\n    <string name=\"setting_item_solid_bar_background_subtitle\">将顶部和底部栏改为纯色，而不是高斯模糊。</string>\n</resources>\n"
  },
  {
    "path": "localization/src/commonMain/composeResources/values-zh-rCN/strings_mastodon.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"activity_pub_user_detail_join_date\">加入时间</string>\n    <string name=\"activity_pub_user_detail_tab_about_joined\">%1$s 加入</string>\n    <string name=\"activity_pub_user_detail_menu_block\">屏蔽 %1$s</string>\n    <string name=\"activity_pub_user_detail_menu_block_domain\">屏蔽 %1$s</string>\n    <string name=\"activity_pub_user_detail_menu_unblock_domain\">取消屏蔽 %1$s</string>\n    <string name=\"activity_pub_user_detail_menu_edit_private_note\">编辑备注</string>\n    <string name=\"activity_pub_user_detail_menu_edit_private_note_dialog_hint\">请输入备注</string>\n    <string name=\"activity_pub_user_detail_dialog_content_block\">您确认要屏蔽对方吗？</string>\n    <string name=\"activity_pub_user_detail_dialog_content_block_domain\">您确认要屏蔽对方实例吗？</string>\n    <string name=\"activity_pub_user_menu_blocked_user_list\">屏蔽的用户</string>\n    <string name=\"activity_pub_user_menu_muted_user_list\">隐藏的用户</string>\n    <string name=\"activity_pub_user_detail_menu_mute_user\">隐藏 %1$s</string>\n    <string name=\"activity_pub_user_detail_menu_unmute_user\">取消隐藏 %1$s</string>\n    <string name=\"activity_pub_mute_user_bottom_sheet_title\">隐藏用户？</string>\n    <string name=\"activity_pub_mute_user_bottom_sheet_role1\">他们不会知道自己被隐藏</string>\n    <string name=\"activity_pub_mute_user_bottom_sheet_role2\">他们仍然可以看到你的帖子，但是你不会看到他们的帖子。</string>\n    <string name=\"activity_pub_mute_user_bottom_sheet_role3\">你将不会看到提及他们的帖子。</string>\n    <string name=\"activity_pub_mute_user_bottom_sheet_role4\">他们可以提及和关注你，但是你不会看到他们。</string>\n    <string name=\"activity_pub_mute_user_bottom_sheet_btn_mute\">隐藏</string>\n    <string name=\"add_instance_screen_title\">添加实例</string>\n    <string name=\"add_instance_input_service_hint\">请输入服务器地址</string>\n    <string name=\"add_instance_input_service_error\">请输入正确的服务器地址</string>\n    <string name=\"activity_pub_content_tab_home\">主页</string>\n    <string name=\"activity_pub_content_tab_local_timeline\">本站</string>\n    <string name=\"activity_pub_content_tab_public_timeline\">跨站</string>\n    <string name=\"activity_pub_content_tab_trending\">趋势</string>\n    <string name=\"activity_pub_hashtag_timeline_description\">%1$s 条帖子 · %2$s 参与 · %3$s 条今日帖子</string>\n    <string name=\"activity_pub_hashtag_unfollow_dialog_message\">您确定要取消关注这个话题吗？</string>\n    <string name=\"activity_pub_edit_account_info_label_about\">关于</string>\n    <string name=\"activity_pub_edit_content_screen_config_not_found\">配置未找到</string>\n    <string name=\"activity_pub_user_list_empty\">暂无用户</string>\n    <string name=\"activity_pub_muted_user_list_empty\">暂无隐藏的用户</string>\n    <string name=\"activity_pub_muted_user_list_unmute\">Unmute</string>\n    <string name=\"activity_pub_blocked_user_list_empty\">暂无屏蔽的用户</string>\n    <string name=\"activity_pub_blocked_user_list_unblock\">Unblock</string>\n    <string name=\"activity_pub_bookmarks_list_title\">收藏的内容</string>\n    <string name=\"activity_pub_favourites_list_title\">点赞的内容</string>\n    <string name=\"activity_pub_followed_tags_screen_title\">关注的话题</string>\n    <string name=\"activity_pub_filters_list_page_title\">过滤器</string>\n    <string name=\"activity_pub_filters_active\">生效中</string>\n    <string name=\"activity_pub_filters_expired\">已失效</string>\n    <string name=\"activity_pub_filter_edit_title\">编辑过滤器</string>\n    <string name=\"activity_pub_filter_edit_input_title_label\">标题</string>\n    <string name=\"activity_pub_filter_edit_input_title_hint\">请输入标题</string>\n    <string name=\"activity_pub_filter_edit_duration\">持续时间</string>\n    <string name=\"activity_pub_filter_edit_duration_permanent\">永久</string>\n    <string name=\"activity_pub_filter_edit_duration_thirty_minutes\">三十分钟</string>\n    <string name=\"activity_pub_filter_edit_duration_one_hour\">一小时</string>\n    <string name=\"activity_pub_filter_edit_duration_twelve_hours\">十二小时</string>\n    <string name=\"activity_pub_filter_edit_duration_one_day\">一天</string>\n    <string name=\"activity_pub_filter_edit_duration_three_day\">三天</string>\n    <string name=\"activity_pub_filter_edit_duration_one_week\">一周</string>\n    <string name=\"activity_pub_filter_edit_duration_custom\">自定义</string>\n    <string name=\"activity_pub_filter_edit_duration_finish_subtitle\">结束与 %1$s</string>\n    <string name=\"activity_pub_filter_edit_keyword_title\">已隐藏的关键字</string>\n    <string name=\"activity_pub_filter_edit_keyword_dialog_title\">编辑关键字</string>\n    <string name=\"activity_pub_filter_edit_keyword_dialog_hint\">关键字</string>\n    <string name=\"activity_pub_filter_edit_keyword_remove_keyword_dialog_content\">确定删除关键字吗？</string>\n    <string name=\"activity_pub_filter_edit_keyword_list_title\">已隐藏关键字</string>\n    <string name=\"activity_pub_filter_edit_keyword_list_desc\">%1$s 个已隐藏的关键字</string>\n    <string name=\"activity_pub_filter_edit_context_title\">隐藏来源</string>\n    <string name=\"activity_pub_filter_edit_context_selector_title\">请选择隐藏来源</string>\n    <string name=\"activity_pub_filter_edit_context_home\">主页 &amp; 列表</string>\n    <string name=\"activity_pub_filter_edit_context_notification\">通知</string>\n    <string name=\"activity_pub_filter_edit_context_timeline\">公共时间线</string>\n    <string name=\"activity_pub_filter_edit_context_thread\">嘟文 &amp; 回复</string>\n    <string name=\"activity_pub_filter_edit_context_account\">个人资料</string>\n    <string name=\"activity_pub_filter_edit_empty_context\">无</string>\n    <string name=\"activity_pub_filter_edit_warning_title\">包含有内容警告的内容</string>\n    <string name=\"activity_pub_filter_edit_warning_desc\">仍然显示符合该过滤器的嘟文，但会加上内容警告</string>\n    <string name=\"activity_pub_filter_edit_delete_content\">确定删除吗？</string>\n    <string name=\"activity_pub_filter_edit_back_dialog\">确定退出吗？</string>\n    <string name=\"activity_pub_filter_edit_whole_word\">整个词条</string>\n    <string name=\"activity_pub_explorer_tab_status_title\">嘟文</string>\n    <string name=\"activity_pub_explorer_tab_users_title\">用户</string>\n    <string name=\"activity_pub_explorer_tab_hashtag_title\">话题</string>\n    <string name=\"activity_pub_created_list_title\">创建的列表</string>\n    <string name=\"activity_pub_add_list_title\">创建列表</string>\n    <string name=\"activity_pub_add_list_name\">列表名称</string>\n    <string name=\"activity_pub_add_list_replies\">回复显示范围</string>\n    <string name=\"activity_pub_add_list_replies_non\">不显示</string>\n    <string name=\"activity_pub_add_list_replies_list\">列表成员</string>\n    <string name=\"activity_pub_add_list_replies_followers\">我关注的所有人</string>\n    <string name=\"activity_pub_add_list_accounts\">列表成员</string>\n    <string name=\"activity_pub_add_list_hide_in_timeline\">在主页时间线中隐藏成员</string>\n    <string name=\"activity_pub_add_list_hide_in_timeline_desc\">列表成员的嘟文将不会在你的主页时间线中显示以免重复阅读。</string>\n    <string name=\"activity_pub_add_list_remove_user_message\">确定移除该用户？</string>\n    <string name=\"activity_pub_add_list_name_is_empty\">请输入用户名</string>\n    <string name=\"activity_pub_add_list_back_reminder\">当前还有更改尚未保存，确认退出吗？</string>\n    <string name=\"activity_pub_search_user_placeholder\">请输入搜索内容</string>\n    <string name=\"activity_pub_list_delete_confirm\">确定删除这个列表吗？</string>\n    <string name=\"activity_pub_login_exception\">认证状态异常</string>\n    <string name=\"activity_pub_home_timeline\">主页</string>\n    <string name=\"activity_pub_local_timeline\">本站</string>\n    <string name=\"activity_pub_public_timeline\">跨站</string>\n    <string name=\"activity_pub_instance_detail_language_label\">语言: %1$s</string>\n    <string name=\"activity_pub_instance_detail_active_month_label\">月活: %1$s</string>\n    <string name=\"activity_pub_user_detail_tab_post\">帖子</string>\n    <string name=\"activity_pub_user_detail_tab_replies\">回复</string>\n    <string name=\"activity_pub_user_detail_tab_media\">媒体</string>\n    <string name=\"activity_pub_user_detail_tab_about\">关于</string>\n    <string name=\"activity_pub_about\">关于</string>\n    <string name=\"activity_pub_about_rule_title\">站点规则</string>\n    <string name=\"activity_pub_trends_tag\">话题</string>\n    <string name=\"activity_pub_trends_tag_description\">最近两天有%1$s人在讨论</string>\n    <string name=\"activity_pub_trends_status\">热门</string>\n    <string name=\"activity_pub_select_platform_title\">选择一个实例</string>\n    <string name=\"activity_pub_select_platform_text_hint\">请输入实例名或者实例URL</string>\n</resources>\n"
  },
  {
    "path": "localization/src/commonMain/composeResources/values-zh-rCN/strings_status_ui.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"status_ui_reposted\">已转发</string>\n    <string name=\"status_ui_repost\">转发</string>\n    <string name=\"status_ui_quote\">引用</string>\n    <string name=\"status_ui_like\">喜欢</string>\n    <string name=\"status_ui_comment\">评论</string>\n    <string name=\"status_ui_delete\">删除</string>\n    <string name=\"status_ui_share\">分享</string>\n    <string name=\"status_ui_bookmark\">收藏</string>\n    <string name=\"status_ui_unbookmark\">取消收藏</string>\n    <string name=\"status_ui_reply\">回复</string>\n    <string name=\"status_ui_follow\">关注</string>\n    <string name=\"status_ui_unfollow\">取消关注</string>\n    <string name=\"status_ui_pin\">置顶</string>\n    <string name=\"status_ui_unpin\">取消置顶</string>\n    <string name=\"status_ui_edit\">编辑</string>\n    <string name=\"status_ui_sensitive_by_filter\">内容已被“%1$s”过滤</string>\n    <string name=\"status_ui_new_status\">新内容</string>\n    <string name=\"status_ui_delete_status_confirm\">确定要删除这条内容吗？</string>\n    <string name=\"status_ui_image_sensitive_label\">敏感内容</string>\n    <string name=\"status_ui_image_content_show_hidden_label\">显示更多</string>\n    <string name=\"status_ui_image_content_hide_hidden_label\">隐藏内容</string>\n    <string name=\"status_ui_poll_vote\">投票</string>\n    <string name=\"status_ui_poll_vote_finished_tip\">%1$s 票 · 已关闭</string>\n    <string name=\"status_ui_poll_votes_finished_tip\">%1$s 票 · 已关闭</string>\n    <string name=\"status_ui_interaction_open_in_browser\">在浏览器中打开</string>\n    <string name=\"status_ui_interaction_open_original_instance\">打开对方实例</string>\n    <string name=\"status_ui_interaction_copy_url\">复制链接</string>\n    <string name=\"status_ui_interaction_translate\">翻译</string>\n    <string name=\"status_ui_interaction_open_blog_by_other_account\">使用其它账号打开</string>\n    <string name=\"status_ui_visibility_mentioned_only\">仅提及用户可见</string>\n    <string name=\"status_ui_label_pinned\">置顶</string>\n    <string name=\"status_ui_info_label_edited\">已编辑</string>\n    <string name=\"status_ui_interaction_label_favourited_count\">%1$s 次喜欢</string>\n    <string name=\"status_ui_interaction_label_boosted_count\">%1$s 次转发</string>\n    <string name=\"status_ui_bottom_label_edited_at\">编辑与 %1$s</string>\n    <string name=\"status_ui_translating\">翻译中…</string>\n    <string name=\"status_ui_translate_show_original\">显示原文</string>\n    <string name=\"status_ui_edit_content_delete_dialog_content\">确定删除这个内容？</string>\n    <string name=\"status_ui_edit_content_name_title\">修改内容名字</string>\n    <string name=\"status_ui_edit_content_name_label\">编辑内容名</string>\n    <string name=\"status_ui_edit_content_name_hint\">请输入内容的名字</string>\n    <string name=\"status_ui_edit_content_config_showing_list_title\">显示在首页中的列表</string>\n    <string name=\"status_ui_edit_content_config_showing_list_description\">可以长按拖动排序</string>\n    <string name=\"status_ui_edit_content_config_hidden_list_title\">可添加到首页的列表</string>\n    <string name=\"status_ui_user_detail_relationship_mutuals\">互相关注</string>\n    <string name=\"status_ui_user_detail_relationship_blocking\">已屏蔽</string>\n    <string name=\"status_ui_user_detail_relationship_following\">已关注</string>\n    <string name=\"status_ui_user_detail_relationship_not_follow\">关注</string>\n    <string name=\"status_ui_user_detail_relationship_requested\">请求中</string>\n    <string name=\"status_ui_user_detail_relationship_follow_back\">回关</string>\n    <string name=\"status_ui_user_detail_request_by_tip\">对方正在请求关注你</string>\n    <string name=\"status_ui_relationship_btn_dialog_content_cancel_follow\">您确认要取消关注对方吗？</string>\n    <string name=\"status_ui_relationship_btn_dialog_content_cancel_follow_request\">您确定要撤回关注请求吗？</string>\n    <string name=\"status_ui_relationship_btn_dialog_content_cancel_blocking\">您确定要取消屏蔽对方吗？</string>\n    <string name=\"status_ui_user_detail_follows_you\">关注了你</string>\n    <string name=\"status_ui_user_detail_posts\">帖子</string>\n    <string name=\"status_ui_user_detail_follower_info\">关注者</string>\n    <string name=\"status_ui_user_detail_following_info\">正在关注</string>\n    <string name=\"status_ui_top_label_continued_thread\">展开对话</string>\n    <string name=\"status_ui_update_dialog_title\">更新</string>\n    <string name=\"status_ui_update_dialog_release_note\">%1$s 版本更新日志：\\n%2$s </string>\n    <string name=\"status_ui_switch_account_dialog_title\">选择账号</string>\n    <string name=\"status_ui_likes\">喜欢</string>\n    <string name=\"status_ui_bookmarks\">收藏</string>\n    <string name=\"status_ui_edit_profile\">修改个人资料</string>\n    <string name=\"status_ui_logout\">退出登录</string>\n    <string name=\"status_ui_logout_dialog_content\">你确定要退出登录吗？</string>\n    <string name=\"status_ui_search_account_status_hint\">请输入你要搜索的帖子</string>\n    <string name=\"shared_notification_unknown_desc\">未知通知</string>\n    <string name=\"shared_notification_favourited_desc\">喜欢了你的动态</string>\n    <string name=\"shared_notification_reblog_desc\">转发了你的动态</string>\n    <string name=\"shared_notification_poll_desc\">查看投票结果</string>\n    <string name=\"shared_notification_poll_count\">%1$s 票 · 已结束</string>\n    <string name=\"shared_notification_follow_desc\">关注了你</string>\n    <string name=\"shared_notification_update_desc\">编辑了一条帖子</string>\n    <string name=\"shared_notification_follow_request\">你收到一条关注请求</string>\n    <string name=\"shared_notification_new_status_desc\">发布了新帖子</string>\n    <string name=\"shared_notification_severed_desc\">和你中断了关系</string>\n    <string name=\"shared_notification_quote_desc\">引用了你的帖子</string>\n    <string name=\"shared_notification_reply_desc\">回复了你的帖子</string>\n    <string name=\"shared_user_list_title_following\">正在关注的用户</string>\n    <string name=\"shared_user_list_title_followers\">关注你的用户</string>\n    <string name=\"shared_user_list_title_mutes\">隐藏的用户</string>\n    <string name=\"shared_user_list_title_blocks\">屏蔽的用户</string>\n    <string name=\"shared_user_list_title_likes\">点赞的用户</string>\n    <string name=\"shared_user_list_title_reblog\">转发的用户</string>\n    <string name=\"shared_user_list_action_mute\">隐藏</string>\n    <string name=\"shared_user_list_action_block\">屏蔽</string>\n    <string name=\"shared_user_list_action_muted\">已隐藏</string>\n    <string name=\"shared_user_list_action_blocked\">已屏蔽</string>\n    <string name=\"shared_user_list_action_mute_dialog_message\">您确定要隐藏该用户吗？</string>\n    <string name=\"shared_user_list_action_block_dialog_message\">您确定要屏蔽该用户吗？</string>\n    <string name=\"shared_publish_blog_title\">发帖</string>\n    <string name=\"shared_publish_blog_text_hint\">写下你的想法</string>\n    <string name=\"shared_publish_reply_input_hint\">回复给 [[%1$s]]</string>\n    <string name=\"shared_publish_interaction_no_limit\">任何人都可以参与互动</string>\n    <string name=\"shared_publish_interaction_limited\">已限制互动</string>\n    <string name=\"shared_publish_interaction_dialog_title\">帖子互动选项</string>\n    <string name=\"shared_publish_interaction_dialog_subtitle\">自定义哪些人可以参与这个帖子的互动。</string>\n    <string name=\"shared_publish_interaction_dialog_quote_title\">引用选项</string>\n    <string name=\"shared_publish_interaction_dialog_quote_allow\">允许引用帖子</string>\n    <string name=\"shared_publish_interaction_dialog_reply_title\">回复选项</string>\n    <string name=\"shared_publish_interaction_dialog_reply_subtitle\">允许这些人参与回复：</string>\n    <string name=\"shared_publish_interaction_dialog_reply_all\">所有人</string>\n    <string name=\"shared_publish_interaction_dialog_reply_nobody\">仅限自己</string>\n    <string name=\"shared_publish_interaction_dialog_reply_combine_title\">或者组合选择这些选项：</string>\n    <string name=\"shared_publish_interaction_dialog_mentioned\">被提及的用户</string>\n    <string name=\"shared_publish_interaction_dialog_following\">你关注的用户</string>\n    <string name=\"shared_publish_interaction_dialog_follower\">你的关注者</string>\n    <string name=\"shared_publish_interaction_dialog_in_list\">\\\"%1$s\\\"中的用户</string>\n    <string name=\"shared_publish_media_alt_dialog_title\">添加替代文本</string>\n    <string name=\"shared_publish_media_alt_dialog_input_tip\">扩充描述替代文本</string>\n    <string name=\"shared_publish_media_alt_dialog_input_hint\">替代文本</string>\n    <string name=\"shared_feeds_not_login_title\">登录信息已失效，请重新登陆</string>\n    <string name=\"shared_feeds_go_to_login\">登录</string>\n    <string name=\"shared_publish_select_account_title\">请选择账号</string>\n    <string name=\"shared_select_language_title\">选择语言</string>\n    <string name=\"shared_status_context_screen_title\">详情</string>\n    <string name=\"shared_alt_label\">替代文本</string>\n    <string name=\"status_ui_embed_quote_unavailable\">当前引用不可用</string>\n    <string name=\"status_ui_action_unforward\">取消转发</string>\n    <string name=\"status_ui_quote_approval_public\">任何人</string>\n    <string name=\"status_ui_quote_approval_follower\">仅关注者</string>\n    <string name=\"status_ui_quote_approval_nobody\">仅限自己</string>\n    <string name=\"status_ui_visibility\">可见性</string>\n</resources>\n"
  },
  {
    "path": "localization/src/commonMain/composeResources/values-zh-rHK/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"skip\">跳過</string>\n    <string name=\"alert\">提示</string>\n    <string name=\"duration_minute\">分鐘</string>\n    <string name=\"duration_hour\">小時</string>\n    <string name=\"duration_day\">天</string>\n    <string name=\"duration_week\">週</string>\n    <string name=\"cancel\">取消</string>\n    <string name=\"ok\">確定</string>\n    <string name=\"empty\">這裡是空的～</string>\n    <string name=\"duration_selector_title\">設定時長</string>\n    <string name=\"retry\">重試</string>\n    <string name=\"load_more_error\">載入失敗。</string>\n    <string name=\"unknown_error\">未知錯誤</string>\n    <string name=\"network_error\">網絡錯誤</string>\n    <string name=\"resource_not_found\">資源未找到</string>\n    <string name=\"mixed_content_subtitle_1\">混合內容 ·</string>\n    <string name=\"mixed_content_subtitle_2\">個訂閱源</string>\n    <string name=\"date_time_ago\">前</string>\n    <string name=\"date_time_ago_prefix\"></string>\n    <string name=\"date_time_ago_suffix\">前</string>\n    <string name=\"date_time_day\">天</string>\n    <string name=\"date_time_hour\">小時</string>\n    <string name=\"date_time_minute\">分</string>\n    <string name=\"date_time_second\">秒</string>\n    <string name=\"image_saving\">儲存中…</string>\n    <string name=\"image_save_success\">儲存成功</string>\n    <string name=\"image_save_failed\">儲存失敗</string>\n    <string name=\"permission_write_external_permission_denied\">儲存權限被拒絕，請到設定中開啟。</string>\n    <string name=\"feeds_load_previous_page_label\">正在載入上一頁內容</string>\n    <string name=\"feeds_load_previous_page_failed_label\">載入失敗，點擊重試</string>\n    <string name=\"image\">圖片</string>\n    <string name=\"video\">影片</string>\n    <string name=\"login\">登入</string>\n    <string name=\"add\">新增</string>\n    <string name=\"search\">搜尋</string>\n    <string name=\"auth_success\">登入成功</string>\n    <string name=\"auth_failed\">登入失敗</string>\n    <string name=\"select\">選擇</string>\n    <string name=\"logout\">登出</string>\n    <string name=\"settings\">設定</string>\n    <string name=\"donate\">捐贈</string>\n    <string name=\"feeds\">Feeds</string>\n    <string name=\"save\">儲存</string>\n    <string name=\"done\">完成</string>\n    <string name=\"bluesky_name\">Bluesky</string>\n    <string name=\"bluesky_description\">Bluesky 是一個開放網絡。透過一個帳戶，你既可使用易上手的社交網絡，亦可在整個社交互聯網上擁有共享身分。</string>\n    <string name=\"mastodon_name\">長毛象</string>\n    <string name=\"mastodon_description\">Mastodon 是一個去中心化、非商業化的社交媒體，用戶可以掌控自己的時間線，每個伺服器都是獨立實體。</string>\n    <string name=\"mixed_content_name\">混合內容</string>\n    <string name=\"mixed_content_description\">混合內容是由 Fread 提供的自訂聚合資訊流，你可新增來自長毛象、Bluesky、RSS 等平台的來源，這些來源的內容會按時間倒序聚合成一條資訊流。</string>\n    <string name=\"status_provider_type_activity_pub\">長毛象</string>\n    <string name=\"add_content_title\">新增內容</string>\n    <string name=\"add_content_success_with_account\">新增成功，目前帳戶：</string>\n    <string name=\"add_content_success_with_login_reminder\">新增成功，是否登入？</string>\n    <string name=\"content_exist_tips\">該內容已存在</string>\n    <string name=\"content_add_success\">內容新增成功</string>\n    <string name=\"add_feeds_page_empty_name_exist\">該名稱已存在</string>\n    <string name=\"edit_profile_name_empty\">請輸入用戶名稱</string>\n    <string name=\"edit_profile_label_name\">名稱</string>\n    <string name=\"edit_profile_label_note\">簡介</string>\n    <string name=\"edit_profile_input_name_hint\">請輸入用戶名稱</string>\n    <string name=\"edit_profile_input_note_hint\">請輸入簡介</string>\n    <string name=\"edit_profile_edit_confirm_message\">確定要退出編輯嗎？</string>\n    <string name=\"add_content_success_snackbar\">新增成功</string>\n    <string name=\"login_dialog_input_hint\">請輸入要登入的伺服器</string>\n    <string name=\"login_dialog_title\">請選擇要登入的伺服器</string>\n    <string name=\"login_dialog_input_tip\">請輸入伺服器地址</string>\n    <string name=\"login_dialog_input_title\">請輸入伺服器地址</string>\n    <string name=\"login_dialog_input_label\">伺服器地址</string>\n    <string name=\"login_dialog_target_title\">請登入</string>\n    <string name=\"profile_description\">Fread 支援新增多個帳戶，已新增的帳戶可用於發文、讚好、轉發、留言等操作，亦可查看已新增帳戶建立的清單。</string>\n    <string name=\"list_content_empty_placeholder\">暫無內容</string>\n    <string name=\"post_status_scope_public\">公開</string>\n    <string name=\"post_status_scope_unlisted\">公開，但不在探索中顯示</string>\n    <string name=\"post_status_scope_follower_only\">僅追隨者可見</string>\n    <string name=\"post_status_scope_mentioned_only\">僅被提及用戶可見</string>\n    <string name=\"post_status_content_warning\">警告文字</string>\n    <string name=\"post_status_success\">發送成功</string>\n    <string name=\"post_status_failed\">發送失敗：%1$s</string>\n    <string name=\"post_status_exit_dialog_content\">退出後已編輯的內容不會被儲存，確定要退出嗎？</string>\n    <string name=\"post_status_content_is_empty\">請輸入內容</string>\n    <string name=\"post_status_part_failed\">部分帳戶發佈失敗，已從佇列移除成功的帳戶，請重新嘗試：%1$s</string>\n    <string name=\"select_account_open_status_title\">請選擇帳戶</string>\n    <string name=\"select_account_open_status_empty\">暫無其他帳戶</string>\n    <string name=\"select_account_open_status_search_in\">在 %1$s 內搜尋</string>\n    <string name=\"select_account_open_status_search_failed\">搜尋失敗</string>\n    <string name=\"explorer_search_no_results\">未搜尋到結果</string>\n    <string name=\"explorer_search_bar_hint\">搜尋</string>\n    <string name=\"explorer_search_bar_hint_specialize_platform\">搜尋 %1$s</string>\n    <string name=\"explorer_search_tab_title_author\">用戶</string>\n    <string name=\"explorer_search_tab_title_status\">貼文</string>\n    <string name=\"explorer_search_tab_title_hashtag\">標籤</string>\n    <string name=\"explorer_search_tab_title_server\">伺服器</string>\n    <string name=\"explorer_tab_title\">探索</string>\n    <string name=\"explorer_tab_status_title\">貼文</string>\n    <string name=\"explorer_tab_users_title\">用戶</string>\n    <string name=\"explorer_tab_hashtag_title\">話題</string>\n    <string name=\"add_provider_page_title\">新增服務</string>\n    <string name=\"add_provider_page_confirm_button\">確定新增</string>\n    <string name=\"search_page_title\">搜尋</string>\n    <string name=\"search_result_not_found\">未搜尋到任何結果</string>\n    <string name=\"feeds_import_page_title\">匯入</string>\n    <string name=\"feeds_import_page_hint\">請選擇要匯入的 OPML 檔案</string>\n    <string name=\"feeds_import_button\">匯入</string>\n    <string name=\"add_feeds_page_title\">新增內容</string>\n    <string name=\"add_feeds_page_feeds_name_label\">內容名稱</string>\n    <string name=\"add_feeds_page_feeds_name_hint\">請輸入內容名稱</string>\n    <string name=\"add_feeds_page_feeds_empty\">請新增訂閱源</string>\n    <string name=\"pre_add_feeds_no_result\">未搜尋到任何結果</string>\n    <string name=\"pre_add_feeds_hint\">實例名稱、實例地址、用戶地址、RSS</string>\n    <string name=\"pre_add_feeds_input_label_1\">Fread 支援新增[[長毛象]]、[[Bluesky]]以及[[ RSS ]]內容，甚至是[[混合內容]]。</string>\n    <string name=\"pre_add_feeds_input_label_2\">請直接在此輸入長毛象或 Bluesky 的[[用戶名稱]]、[[WebFinger\\Handle]]、用戶[[主頁地址]]或 [[RSS]] 地址。</string>\n    <string name=\"empty_content_hint_title\">請先新增內容</string>\n    <string name=\"empty_content_hint_desc\">你需要先新增內容的訂閱源。Fread 支援多種內容類型與跨平台混合內容，打造你自己的資訊流。</string>\n    <string name=\"feeds_add_content\">新增內容</string>\n    <string name=\"select_feeds_type_screen_title\">請選擇內容類型</string>\n    <string name=\"feeds_pre_add_login_dialog_content\">新增成功，是否登入？</string>\n    <string name=\"add_feeds_page_empty_name_tips\">請輸入一個名稱</string>\n    <string name=\"add_feeds_page_empty_source_tips\">請新增資訊來源</string>\n    <string name=\"add_feeds_choose_auth_dialog_title\">請選擇要登入的伺服器</string>\n    <string name=\"search_feeds_title\">搜尋</string>\n    <string name=\"search_feeds_title_hint\">長毛象或 Bluesky 用戶名、WebFinger、URL、RSS</string>\n    <string name=\"feeds_select_account_for_post_status\">請選擇一個帳戶</string>\n    <string name=\"feeds_mixed_config_edit_new_name_dialog_title\">請輸入內容名稱</string>\n    <string name=\"feeds_mixed_config_edit_new_name_dialog_label\">名稱</string>\n    <string name=\"feeds_mixed_config_edit_delete_content_dialog_message\">你確定要刪除這個內容嗎？</string>\n    <string name=\"feeds_mixed_config_not_found\">狀態異常，未找到設定</string>\n    <string name=\"feeds_import_back_dialog_message\">確定要退出嗎？</string>\n    <string name=\"feeds_delete_confirm_content\">確定要刪除嗎？</string>\n    <string name=\"feeds_select_type_screen_title\">請選擇內容類型</string>\n    <string name=\"feeds_select_type_screen_content\">請選擇你要新增的內容類型</string>\n    <string name=\"notification_tab_title\">通知</string>\n    <string name=\"notifications_account_empty_tip\">目前沒有已登入帳戶</string>\n    <string name=\"notifications_tab_all\">所有</string>\n    <string name=\"notifications_tab_mention\">提及</string>\n    <string name=\"profile_page_title\">帳戶</string>\n    <string name=\"profile_setting_about_title\">關於</string>\n    <string name=\"profile_setting_donate_desc\">請我飲杯咖啡，以支持此項目</string>\n    <string name=\"profile_setting_inline_video_auto_play\">Inline 影片自動播放</string>\n    <string name=\"profile_setting_inline_video_auto_play_subtitle\">開啟後，Feeds 流中的影片將自動播放。</string>\n    <string name=\"profile_setting_always_show_sensitive_content\">敏感內容預設顯示</string>\n    <string name=\"profile_setting_always_show_sensitive_content_subtitle\">開啟後，敏感內容會預設顯示（僅限 Mastodon 平台）。</string>\n    <string name=\"profile_setting_dark_mode_title\">深色模式</string>\n    <string name=\"profile_setting_dark_mode_dark\">深色模式</string>\n    <string name=\"profile_setting_dark_mode_light\">淺色模式</string>\n    <string name=\"profile_setting_dark_mode_follow_system\">跟隨系統</string>\n    <string name=\"profile_setting_open_source_title\">開源聲明</string>\n    <string name=\"profile_setting_open_source_desc\">本項目所使用依賴庫之開源聲明</string>\n    <string name=\"profile_setting_open_source_feedback\">向我們回饋</string>\n    <string name=\"profile_setting_open_source_feedback_desc\">加入 Telegram 群組或其他方式</string>\n    <string name=\"profile_setting_open_source_feedback_telegram\">加入 Telegram 群組</string>\n    <string name=\"profile_setting_open_source_feedback_github\">建立 GitHub issue</string>\n    <string name=\"profile_setting_open_source_feedback_email\">發送電郵</string>\n    <string name=\"profile_setting_language_title\">顯示語言</string>\n    <string name=\"profile_setting_language_zh\">簡體中文</string>\n    <string name=\"profile_setting_language_en\">English</string>\n    <string name=\"profile_setting_language_system\">跟隨系統</string>\n    <string name=\"profile_setting_ratting\">為我們評分</string>\n    <string name=\"profile_setting_ratting_desc\">如果覺得不錯，請到應用商店給我們好評</string>\n    <string name=\"profile_setting_font_size\">內容字體大小</string>\n    <string name=\"profile_setting_font_size_desc\">調整內容的字體與圖示大小</string>\n    <string name=\"profile_setting_font_size_small\">小</string>\n    <string name=\"profile_setting_font_size_medium\">中</string>\n    <string name=\"profile_setting_font_size_large\">大</string>\n    <string name=\"profile_setting_immersive_nav_bar\">沉浸式導航欄</string>\n    <string name=\"profile_setting_immersive_nav_bar_desc\">開啟後，在主頁捲動時會隱藏底部導航欄。</string>\n    <string name=\"profile_setting_timeline_position\">主頁時間線預設位置</string>\n    <string name=\"profile_setting_timeline_position_last_read\">上次閱讀位置</string>\n    <string name=\"profile_setting_timeline_position_newest\">最新位置</string>\n    <string name=\"profile_setting_theme_title\">主題色</string>\n    <string name=\"profile_setting_theme_default\">預設</string>\n    <string name=\"profile_setting_theme_system\">系統</string>\n    <string name=\"profile_about_version\">版本：</string>\n    <string name=\"profile_about_website\">官網：</string>\n    <string name=\"profile_about_developer\">開發者：</string>\n    <string name=\"profile_about_contract_us\">聯絡我：</string>\n    <string name=\"profile_about_telegram\">Telegram 群組：</string>\n    <string name=\"profile_about_privacy_policy\">私隱政策：</string>\n    <string name=\"profile_setting_check_for_update\">檢查更新</string>\n    <string name=\"profile_setting_already_latest_version\">目前已是最新版本</string>\n    <string name=\"profile_setting_have_new_version\">有新版本，點擊更新。</string>\n    <string name=\"profile_donate_page_title\">請選擇捐贈方式</string>\n    <string name=\"profile_account_not_login\">登入已失效</string>\n    <string name=\"rss_source_detail_screen_title\">資訊源屬性</string>\n    <string name=\"rss_source_detail_screen_custom_title\">自訂標題</string>\n    <string name=\"rss_source_detail_screen_url\">訂閱地址</string>\n    <string name=\"rss_source_detail_screen_home_url\">主頁</string>\n    <string name=\"rss_source_detail_screen_add_date\">新增日期</string>\n    <string name=\"rss_source_detail_screen_last_update_date\">上次更新時間</string>\n    <string name=\"bluesky_protocol_name\">Bluesky</string>\n    <string name=\"bsky_add_content_title\">登入</string>\n    <string name=\"bsky_add_content_hosting_provider\">服務商地址</string>\n    <string name=\"bsky_add_content_user_name\">用戶名或電郵</string>\n    <string name=\"bsky_add_content_password\">密碼</string>\n    <string name=\"bsky_add_content_factor_token\">驗證碼</string>\n    <string name=\"bsky_edit_content_title\">編輯</string>\n    <string name=\"bsky_feeds_explorer_more\">瀏覽更多 Feeds</string>\n    <string name=\"bsky_feeds_explorer_creator_label\">建立者  %1$s</string>\n    <string name=\"bsky_feeds_explorer_liked_by\">%1$s 位用戶讚好</string>\n    <string name=\"bsky_feeds_detail_creator_prefix\">作者</string>\n    <string name=\"bsky_feeds_item_subtitle\">建立者 %1$s · %2$s 位用戶讚好</string>\n    <string name=\"bsky_feeds_following_name\">追蹤中</string>\n    <string name=\"bsky_feeds_user_posts\">貼文</string>\n    <string name=\"bsky_feeds_user_replies\">回覆</string>\n    <string name=\"bsky_feeds_user_medias\">媒體</string>\n    <string name=\"bsky_feeds_user_likes\">讚好</string>\n    <string name=\"bsky_user_detail_action_blocked_list\">已封鎖用戶</string>\n    <string name=\"bsky_user_detail_action_muted_list\">已隱藏用戶</string>\n    <string name=\"bsky_user_detail_action_mute_user\">隱藏 %1$s</string>\n    <string name=\"bsky_user_detail_action_unmute_user\">取消隱藏</string>\n    <string name=\"bsky_user_detail_action_block_user\">封鎖 %1$s</string>\n    <string name=\"bsky_user_detail_action_unblock_user\">取消封鎖</string>\n    <string name=\"bsky_user_detail_action_block_user_dialog_message\">你確認要封鎖對方嗎？</string>\n    <string name=\"bsky_user_detail_action_mute_user_dialog_message\">你確認要隱藏對方嗎？</string>\n    <string name=\"bsky_edit_profile_title\">編輯個人資料</string>\n    <string name=\"input_media_desc_page_title\">描述文字</string>\n    <string name=\"input_media_desc_input_hint\">請輸入描述文字</string>\n    <string name=\"post_status_page_title\">新貼文</string>\n    <string name=\"post_screen_input_hint\">請輸入你的貼文</string>\n    <string name=\"post_screen_media_descriptor_placeholder\">描述文字</string>\n    <string name=\"post_status_poll_item_hint\">選項 %1$s</string>\n    <string name=\"post_status_poll_duration\">投票時長</string>\n    <string name=\"post_status_poll_function_title\">功能</string>\n    <string name=\"post_status_poll_single\">單選</string>\n    <string name=\"post_status_poll_multiple\">多選</string>\n    <string name=\"post_status_poll_style_select_dialog_title\">請選擇功能</string>\n    <string name=\"post_status_poll_is_empty\">請輸入投票內容</string>\n    <string name=\"post_status_media_is_not_upload\">媒體暫未上傳</string>\n    <string name=\"authentication_page_normal_title\">此功能需登入後方可使用</string>\n    <string name=\"authentication_page_failed_title\">登入狀態已失效，請重新登入。</string>\n    <string name=\"main_drawer_title\">內容清單</string>\n    <string name=\"main_drawer_mixed_item_subtitle_1\">混合內容 ·</string>\n    <string name=\"main_drawer_mixed_item_subtitle_2\">個訂閱源</string>\n    <string name=\"main_drawer_settings\">設定</string>\n    <string name=\"main_drawer_donate\">捐贈</string>\n    <string name=\"main_request_notification_dialog_title\">通知權限</string>\n    <string name=\"main_request_notification_dialog_reject_button\">拒絕</string>\n    <string name=\"main_request_notification_dialog_allow_button\">允許</string>\n    <string name=\"main_request_notification_dialog_content\">為接收互動訊息，請允許通知權限。</string>\n    <string name=\"setting_item_amoled_mode\">Amoled 模式</string>\n    <string name=\"setting_item_amoled_mode_description\">啟用此功能後，深色模式下會把背景變成純黑，以減低耗電量，並在昏暗環境中提升對比度。</string>\n    <string name=\"setting_group_appearance\">外觀</string>\n    <string name=\"setting_group_appearance_subtitle\">主題、顯示與首頁樣式設定</string>\n    <string name=\"setting_group_behavior\">行為</string>\n    <string name=\"setting_group_behavior_subtitle\">內容顯示與互動行為設定</string>\n    <string name=\"setting_item_home_tab_next_title\">首頁頂部的切換內容按鈕顯示</string>\n    <string name=\"setting_item_home_tab_next_subtitle\">首頁右上角是否顯示用於切換到下一個內容的按鈕</string>\n    <string name=\"setting_item_home_tab_refresh_title\">首頁頂部的重新整理按鈕顯示</string>\n    <string name=\"setting_item_home_tab_refresh_subtitle\">首頁右上角是否顯示用於重新整理內容的按鈕</string>\n    <string name=\"setting_item_open_url_by_system_title\">透過系統瀏覽器開啟連結</string>\n    <string name=\"setting_item_open_url_by_system_subtitle\">啟用後，Fread 會透過系統瀏覽器開啟連結。</string>\n    <string name=\"setting_item_solid_bar_background_title\">頂部和底部欄純色背景</string>\n    <string name=\"setting_item_solid_bar_background_subtitle\">將頂部和底部欄改為純色，而不是高斯模糊。</string>\n</resources>\n"
  },
  {
    "path": "localization/src/commonMain/composeResources/values-zh-rHK/strings_mastodon.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"activity_pub_user_detail_join_date\">加入時間</string>\n    <string name=\"activity_pub_user_detail_tab_about_joined\">%1$s 加入</string>\n    <string name=\"activity_pub_user_detail_menu_block\">封鎖 %1$s</string>\n    <string name=\"activity_pub_user_detail_menu_block_domain\">封鎖 %1$s</string>\n    <string name=\"activity_pub_user_detail_menu_unblock_domain\">取消封鎖 %1$s</string>\n    <string name=\"activity_pub_user_detail_menu_edit_private_note\">編輯備註</string>\n    <string name=\"activity_pub_user_detail_menu_edit_private_note_dialog_hint\">請輸入備註</string>\n    <string name=\"activity_pub_user_detail_dialog_content_block\">你確認要封鎖對方嗎？</string>\n    <string name=\"activity_pub_user_detail_dialog_content_block_domain\">你確認要封鎖對方實例嗎？</string>\n    <string name=\"activity_pub_user_menu_blocked_user_list\">已封鎖用戶</string>\n    <string name=\"activity_pub_user_menu_muted_user_list\">已隱藏用戶</string>\n    <string name=\"activity_pub_user_detail_menu_mute_user\">隱藏 %1$s</string>\n    <string name=\"activity_pub_user_detail_menu_unmute_user\">取消隱藏 %1$s</string>\n    <string name=\"activity_pub_mute_user_bottom_sheet_title\">隱藏用戶？</string>\n    <string name=\"activity_pub_mute_user_bottom_sheet_role1\">他們不會知道自己被隱藏</string>\n    <string name=\"activity_pub_mute_user_bottom_sheet_role2\">他們仍可看到你的貼文，但你不會看到他們的貼文。</string>\n    <string name=\"activity_pub_mute_user_bottom_sheet_role3\">你將不會看到提及他們的貼文。</string>\n    <string name=\"activity_pub_mute_user_bottom_sheet_role4\">他們可以提及並追蹤你，但你不會看到他們。</string>\n    <string name=\"activity_pub_mute_user_bottom_sheet_btn_mute\">隱藏</string>\n    <string name=\"add_instance_screen_title\">新增實例</string>\n    <string name=\"add_instance_input_service_hint\">請輸入伺服器地址</string>\n    <string name=\"add_instance_input_service_error\">請輸入正確的伺服器地址</string>\n    <string name=\"activity_pub_content_tab_home\">主頁</string>\n    <string name=\"activity_pub_content_tab_local_timeline\">本站</string>\n    <string name=\"activity_pub_content_tab_public_timeline\">跨站</string>\n    <string name=\"activity_pub_content_tab_trending\">趨勢</string>\n    <string name=\"activity_pub_hashtag_timeline_description\">%1$s 則貼文 · %2$s 人參與 · 今日 %3$s 則貼文</string>\n    <string name=\"activity_pub_hashtag_unfollow_dialog_message\">你確定要取消追蹤這個話題嗎？</string>\n    <string name=\"activity_pub_edit_account_info_label_about\">關於</string>\n    <string name=\"activity_pub_edit_content_screen_config_not_found\">找不到設定</string>\n    <string name=\"activity_pub_user_list_empty\">暫無用戶</string>\n    <string name=\"activity_pub_muted_user_list_empty\">暫無已隱藏的用戶</string>\n    <string name=\"activity_pub_muted_user_list_unmute\">取消隱藏</string>\n    <string name=\"activity_pub_blocked_user_list_empty\">暫無已封鎖的用戶</string>\n    <string name=\"activity_pub_blocked_user_list_unblock\">取消封鎖</string>\n    <string name=\"activity_pub_bookmarks_list_title\">收藏的內容</string>\n    <string name=\"activity_pub_favourites_list_title\">讚好的內容</string>\n    <string name=\"activity_pub_followed_tags_screen_title\">追蹤的話題</string>\n    <string name=\"activity_pub_filters_list_page_title\">過濾器</string>\n    <string name=\"activity_pub_filters_active\">生效中</string>\n    <string name=\"activity_pub_filters_expired\">已失效</string>\n    <string name=\"activity_pub_filter_edit_title\">編輯過濾器</string>\n    <string name=\"activity_pub_filter_edit_input_title_label\">標題</string>\n    <string name=\"activity_pub_filter_edit_input_title_hint\">請輸入標題</string>\n    <string name=\"activity_pub_filter_edit_duration\">持續時間</string>\n    <string name=\"activity_pub_filter_edit_duration_permanent\">永久</string>\n    <string name=\"activity_pub_filter_edit_duration_thirty_minutes\">三十分鐘</string>\n    <string name=\"activity_pub_filter_edit_duration_one_hour\">一小時</string>\n    <string name=\"activity_pub_filter_edit_duration_twelve_hours\">十二小時</string>\n    <string name=\"activity_pub_filter_edit_duration_one_day\">一天</string>\n    <string name=\"activity_pub_filter_edit_duration_three_day\">三天</string>\n    <string name=\"activity_pub_filter_edit_duration_one_week\">一週</string>\n    <string name=\"activity_pub_filter_edit_duration_custom\">自訂</string>\n    <string name=\"activity_pub_filter_edit_duration_finish_subtitle\">結束於 %1$s</string>\n    <string name=\"activity_pub_filter_edit_keyword_title\">已隱藏關鍵字</string>\n    <string name=\"activity_pub_filter_edit_keyword_dialog_title\">編輯關鍵字</string>\n    <string name=\"activity_pub_filter_edit_keyword_dialog_hint\">關鍵字</string>\n    <string name=\"activity_pub_filter_edit_keyword_remove_keyword_dialog_content\">確定刪除關鍵字嗎？</string>\n    <string name=\"activity_pub_filter_edit_keyword_list_title\">已隱藏關鍵字</string>\n    <string name=\"activity_pub_filter_edit_keyword_list_desc\">%1$s 個已隱藏的關鍵字</string>\n    <string name=\"activity_pub_filter_edit_context_title\">隱藏來源</string>\n    <string name=\"activity_pub_filter_edit_context_selector_title\">請選擇隱藏來源</string>\n    <string name=\"activity_pub_filter_edit_context_home\">主頁 &amp; 清單</string>\n    <string name=\"activity_pub_filter_edit_context_notification\">通知</string>\n    <string name=\"activity_pub_filter_edit_context_timeline\">公共時間線</string>\n    <string name=\"activity_pub_filter_edit_context_thread\">嘟文 &amp; 回覆</string>\n    <string name=\"activity_pub_filter_edit_context_account\">個人資料</string>\n    <string name=\"activity_pub_filter_edit_empty_context\">無</string>\n    <string name=\"activity_pub_filter_edit_warning_title\">包含內容警告的內容</string>\n    <string name=\"activity_pub_filter_edit_warning_desc\">仍會顯示符合此過濾器的嘟文，但會加上內容警告</string>\n    <string name=\"activity_pub_filter_edit_delete_content\">確定要刪除嗎？</string>\n    <string name=\"activity_pub_filter_edit_back_dialog\">確定要退出嗎？</string>\n    <string name=\"activity_pub_filter_edit_whole_word\">整個詞條</string>\n    <string name=\"activity_pub_explorer_tab_status_title\">嘟文</string>\n    <string name=\"activity_pub_explorer_tab_users_title\">用戶</string>\n    <string name=\"activity_pub_explorer_tab_hashtag_title\">話題</string>\n    <string name=\"activity_pub_created_list_title\">建立的清單</string>\n    <string name=\"activity_pub_add_list_title\">建立清單</string>\n    <string name=\"activity_pub_add_list_name\">清單名稱</string>\n    <string name=\"activity_pub_add_list_replies\">回覆顯示範圍</string>\n    <string name=\"activity_pub_add_list_replies_non\">不顯示</string>\n    <string name=\"activity_pub_add_list_replies_list\">清單成員</string>\n    <string name=\"activity_pub_add_list_replies_followers\">我追蹤的所有人</string>\n    <string name=\"activity_pub_add_list_accounts\">清單成員</string>\n    <string name=\"activity_pub_add_list_hide_in_timeline\">在主頁時間線中隱藏成員</string>\n    <string name=\"activity_pub_add_list_hide_in_timeline_desc\">清單成員的嘟文將不會顯示在你的主頁時間線中，以免重複閱讀。</string>\n    <string name=\"activity_pub_add_list_remove_user_message\">確定移除該用戶？</string>\n    <string name=\"activity_pub_add_list_name_is_empty\">請輸入清單名稱</string>\n    <string name=\"activity_pub_add_list_back_reminder\">目前仍有變更未儲存，確認要退出嗎？</string>\n    <string name=\"activity_pub_search_user_placeholder\">請輸入搜尋內容</string>\n    <string name=\"activity_pub_list_delete_confirm\">確定要刪除這個清單嗎？</string>\n    <string name=\"activity_pub_login_exception\">認證狀態異常</string>\n    <string name=\"activity_pub_home_timeline\">主頁</string>\n    <string name=\"activity_pub_local_timeline\">本站</string>\n    <string name=\"activity_pub_public_timeline\">跨站</string>\n    <string name=\"activity_pub_instance_detail_language_label\">語言：%1$s</string>\n    <string name=\"activity_pub_instance_detail_active_month_label\">月活躍：%1$s</string>\n    <string name=\"activity_pub_user_detail_tab_post\">貼文</string>\n    <string name=\"activity_pub_user_detail_tab_replies\">回覆</string>\n    <string name=\"activity_pub_user_detail_tab_media\">媒體</string>\n    <string name=\"activity_pub_user_detail_tab_about\">關於</string>\n    <string name=\"activity_pub_about\">關於</string>\n    <string name=\"activity_pub_about_rule_title\">站點規則</string>\n    <string name=\"activity_pub_trends_tag\">話題</string>\n    <string name=\"activity_pub_trends_tag_description\">最近兩天有 %1$s 人在討論</string>\n    <string name=\"activity_pub_trends_status\">熱門</string>\n    <string name=\"activity_pub_select_platform_title\">選擇一個實例</string>\n    <string name=\"activity_pub_select_platform_text_hint\">請輸入實例名稱或實例 URL</string>\n</resources>\n"
  },
  {
    "path": "localization/src/commonMain/composeResources/values-zh-rHK/strings_status_ui.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"status_ui_reposted\">已轉貼</string>\n    <string name=\"status_ui_repost\">轉貼</string>\n    <string name=\"status_ui_quote\">引用</string>\n    <string name=\"status_ui_like\">讚好</string>\n    <string name=\"status_ui_comment\">評論</string>\n    <string name=\"status_ui_delete\">刪除</string>\n    <string name=\"status_ui_share\">分享</string>\n    <string name=\"status_ui_bookmark\">收藏</string>\n    <string name=\"status_ui_unbookmark\">取消收藏</string>\n    <string name=\"status_ui_reply\">回覆</string>\n    <string name=\"status_ui_follow\">追蹤</string>\n    <string name=\"status_ui_unfollow\">取消追蹤</string>\n    <string name=\"status_ui_pin\">置頂</string>\n    <string name=\"status_ui_unpin\">取消置頂</string>\n    <string name=\"status_ui_edit\">編輯</string>\n    <string name=\"status_ui_sensitive_by_filter\">內容已被「%1$s」過濾</string>\n    <string name=\"status_ui_new_status\">新內容</string>\n    <string name=\"status_ui_delete_status_confirm\">確定要刪除這則內容嗎？</string>\n    <string name=\"status_ui_image_sensitive_label\">敏感內容</string>\n    <string name=\"status_ui_image_content_show_hidden_label\">顯示更多</string>\n    <string name=\"status_ui_image_content_hide_hidden_label\">隱藏內容</string>\n    <string name=\"status_ui_poll_vote\">投票</string>\n    <string name=\"status_ui_poll_vote_finished_tip\">%1$s 票 · 已關閉</string>\n    <string name=\"status_ui_poll_votes_finished_tip\">%1$s 票 · 已關閉</string>\n    <string name=\"status_ui_interaction_open_in_browser\">在瀏覽器中開啟</string>\n    <string name=\"status_ui_interaction_open_original_instance\">開啟對方實例</string>\n    <string name=\"status_ui_interaction_copy_url\">複製連結</string>\n    <string name=\"status_ui_interaction_translate\">翻譯</string>\n    <string name=\"status_ui_interaction_open_blog_by_other_account\">使用其他帳戶開啟</string>\n    <string name=\"status_ui_visibility_mentioned_only\">僅提及用戶可見</string>\n    <string name=\"status_ui_label_pinned\">置頂</string>\n    <string name=\"status_ui_info_label_edited\">已編輯</string>\n    <string name=\"status_ui_interaction_label_favourited_count\">%1$s 次讚好</string>\n    <string name=\"status_ui_interaction_label_boosted_count\">%1$s 次轉發</string>\n    <string name=\"status_ui_bottom_label_edited_at\">編輯於 %1$s</string>\n    <string name=\"status_ui_translating\">翻譯中…</string>\n    <string name=\"status_ui_translate_show_original\">顯示原文</string>\n    <string name=\"status_ui_edit_content_delete_dialog_content\">確定刪除這個內容？</string>\n    <string name=\"status_ui_edit_content_name_title\">修改內容名稱</string>\n    <string name=\"status_ui_edit_content_name_label\">編輯內容名稱</string>\n    <string name=\"status_ui_edit_content_name_hint\">請輸入內容名稱</string>\n    <string name=\"status_ui_edit_content_config_showing_list_title\">顯示在主頁的清單</string>\n    <string name=\"status_ui_edit_content_config_showing_list_description\">可長按拖動排序</string>\n    <string name=\"status_ui_edit_content_config_hidden_list_title\">可新增到主頁的清單</string>\n    <string name=\"status_ui_user_detail_relationship_mutuals\">互相關注</string>\n    <string name=\"status_ui_user_detail_relationship_blocking\">已封鎖</string>\n    <string name=\"status_ui_user_detail_relationship_following\">已追蹤</string>\n    <string name=\"status_ui_user_detail_relationship_not_follow\">追蹤</string>\n    <string name=\"status_ui_user_detail_relationship_requested\">請求中</string>\n    <string name=\"status_ui_user_detail_relationship_follow_back\">回追</string>\n    <string name=\"status_ui_user_detail_request_by_tip\">對方正在請求追蹤你</string>\n    <string name=\"status_ui_relationship_btn_dialog_content_cancel_follow\">你確認要取消追蹤對方嗎？</string>\n    <string name=\"status_ui_relationship_btn_dialog_content_cancel_follow_request\">你確定要撤回追蹤請求嗎？</string>\n    <string name=\"status_ui_relationship_btn_dialog_content_cancel_blocking\">你確定要取消封鎖對方嗎？</string>\n    <string name=\"status_ui_user_detail_follows_you\">追蹤了你</string>\n    <string name=\"status_ui_user_detail_posts\">貼文</string>\n    <string name=\"status_ui_user_detail_follower_info\">追蹤者</string>\n    <string name=\"status_ui_user_detail_following_info\">正在追蹤</string>\n    <string name=\"status_ui_top_label_continued_thread\">展開對話</string>\n    <string name=\"status_ui_update_dialog_title\">更新</string>\n    <string name=\"status_ui_update_dialog_release_note\">%1$s 版本更新日誌：\\n%2$s </string>\n    <string name=\"status_ui_switch_account_dialog_title\">選擇帳戶</string>\n    <string name=\"status_ui_likes\">讚好</string>\n    <string name=\"status_ui_bookmarks\">收藏</string>\n    <string name=\"status_ui_edit_profile\">修改個人資料</string>\n    <string name=\"status_ui_logout\">登出</string>\n    <string name=\"status_ui_logout_dialog_content\">你確定要登出嗎？</string>\n    <string name=\"status_ui_search_account_status_hint\">請輸入你要搜尋的貼文</string>\n    <string name=\"shared_notification_unknown_desc\">未知通知</string>\n    <string name=\"shared_notification_favourited_desc\">讚好了你的動態</string>\n    <string name=\"shared_notification_reblog_desc\">轉發了你的動態</string>\n    <string name=\"shared_notification_poll_desc\">查看投票結果</string>\n    <string name=\"shared_notification_poll_count\">%1$s 票 · 已結束</string>\n    <string name=\"shared_notification_follow_desc\">追蹤了你</string>\n    <string name=\"shared_notification_update_desc\">編輯了一則貼文</string>\n    <string name=\"shared_notification_follow_request\">你收到一則追蹤請求</string>\n    <string name=\"shared_notification_new_status_desc\">發佈了新貼文</string>\n    <string name=\"shared_notification_severed_desc\">和你中斷了關係</string>\n    <string name=\"shared_notification_quote_desc\">引用了你的貼文</string>\n    <string name=\"shared_notification_reply_desc\">回覆了你的貼文</string>\n    <string name=\"shared_user_list_title_following\">正在追蹤的用戶</string>\n    <string name=\"shared_user_list_title_followers\">追蹤你的用戶</string>\n    <string name=\"shared_user_list_title_mutes\">已隱藏用戶</string>\n    <string name=\"shared_user_list_title_blocks\">已封鎖用戶</string>\n    <string name=\"shared_user_list_title_likes\">讚好的用戶</string>\n    <string name=\"shared_user_list_title_reblog\">轉發的用戶</string>\n    <string name=\"shared_user_list_action_mute\">隱藏</string>\n    <string name=\"shared_user_list_action_block\">封鎖</string>\n    <string name=\"shared_user_list_action_muted\">已隱藏</string>\n    <string name=\"shared_user_list_action_blocked\">已封鎖</string>\n    <string name=\"shared_user_list_action_mute_dialog_message\">你確定要隱藏該用戶嗎？</string>\n    <string name=\"shared_user_list_action_block_dialog_message\">你確定要封鎖該用戶嗎？</string>\n    <string name=\"shared_publish_blog_title\">發文</string>\n    <string name=\"shared_publish_blog_text_hint\">寫下你的想法</string>\n    <string name=\"shared_publish_reply_input_hint\">回覆給 [[%1$s]]</string>\n    <string name=\"shared_publish_interaction_no_limit\">任何人都可以互動</string>\n    <string name=\"shared_publish_interaction_limited\">已限制互動</string>\n    <string name=\"shared_publish_interaction_dialog_title\">貼文互動選項</string>\n    <string name=\"shared_publish_interaction_dialog_subtitle\">自訂哪些人可以互動這則貼文。</string>\n    <string name=\"shared_publish_interaction_dialog_quote_title\">引用選項</string>\n    <string name=\"shared_publish_interaction_dialog_quote_allow\">允許引用貼文</string>\n    <string name=\"shared_publish_interaction_dialog_reply_title\">回覆選項</string>\n    <string name=\"shared_publish_interaction_dialog_reply_subtitle\">允許這些人回覆：</string>\n    <string name=\"shared_publish_interaction_dialog_reply_all\">所有人</string>\n    <string name=\"shared_publish_interaction_dialog_reply_nobody\">僅限自己</string>\n    <string name=\"shared_publish_interaction_dialog_reply_combine_title\">或者組合選擇這些選項：</string>\n    <string name=\"shared_publish_interaction_dialog_mentioned\">被提及的用戶</string>\n    <string name=\"shared_publish_interaction_dialog_following\">你追蹤的用戶</string>\n    <string name=\"shared_publish_interaction_dialog_follower\">你的追蹤者</string>\n    <string name=\"shared_publish_interaction_dialog_in_list\">「%1$s」中的用戶</string>\n    <string name=\"shared_publish_media_alt_dialog_title\">新增替代文字</string>\n    <string name=\"shared_publish_media_alt_dialog_input_tip\">補充描述替代文字</string>\n    <string name=\"shared_publish_media_alt_dialog_input_hint\">替代文字</string>\n    <string name=\"shared_feeds_not_login_title\">登入資訊已失效，請重新登入</string>\n    <string name=\"shared_feeds_go_to_login\">登入</string>\n    <string name=\"shared_publish_select_account_title\">請選擇帳戶</string>\n    <string name=\"shared_select_language_title\">選擇語言</string>\n    <string name=\"shared_status_context_screen_title\">詳情</string>\n    <string name=\"shared_alt_label\">替代文字</string>\n    <string name=\"status_ui_embed_quote_unavailable\">當前引用不可用</string>\n    <string name=\"status_ui_action_unforward\">取消轉發</string>\n    <string name=\"status_ui_quote_approval_public\">任何人</string>\n    <string name=\"status_ui_quote_approval_follower\">只限追蹤者</string>\n    <string name=\"status_ui_quote_approval_nobody\">只限自己</string>\n    <string name=\"status_ui_visibility\">可見性</string>\n</resources>\n"
  },
  {
    "path": "localization/src/commonMain/composeResources/values-zh-rTW/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"skip\">跳過</string>\n    <string name=\"alert\">提示</string>\n    <string name=\"duration_minute\">分鐘</string>\n    <string name=\"duration_hour\">小時</string>\n    <string name=\"duration_day\">天</string>\n    <string name=\"duration_week\">週</string>\n    <string name=\"cancel\">取消</string>\n    <string name=\"ok\">確定</string>\n    <string name=\"empty\">這裡是空的～</string>\n    <string name=\"duration_selector_title\">設定時長</string>\n    <string name=\"retry\">重試</string>\n    <string name=\"load_more_error\">載入失敗。</string>\n    <string name=\"unknown_error\">未知錯誤</string>\n    <string name=\"network_error\">網路錯誤</string>\n    <string name=\"resource_not_found\">資源未找到</string>\n    <string name=\"mixed_content_subtitle_1\">混合內容 ·</string>\n    <string name=\"mixed_content_subtitle_2\">條訂閱源</string>\n    <string name=\"date_time_ago\">前</string>\n    <string name=\"date_time_ago_prefix\"></string>\n    <string name=\"date_time_ago_suffix\">前</string>\n    <string name=\"date_time_day\">天</string>\n    <string name=\"date_time_hour\">小時</string>\n    <string name=\"date_time_minute\">分</string>\n    <string name=\"date_time_second\">秒</string>\n    <string name=\"image_saving\">儲存中…</string>\n    <string name=\"image_save_success\">儲存成功</string>\n    <string name=\"image_save_failed\">儲存失敗</string>\n    <string name=\"permission_write_external_permission_denied\">儲存權限被拒絕，請到設定中開啟。</string>\n    <string name=\"feeds_load_previous_page_label\">正在載入上一頁內容</string>\n    <string name=\"feeds_load_previous_page_failed_label\">載入失敗，點擊重試</string>\n    <string name=\"image\">圖片</string>\n    <string name=\"video\">影片</string>\n    <string name=\"login\">登入</string>\n    <string name=\"add\">新增</string>\n    <string name=\"search\">搜尋</string>\n    <string name=\"auth_success\">登入成功</string>\n    <string name=\"auth_failed\">登入失敗</string>\n    <string name=\"select\">選擇</string>\n    <string name=\"logout\">登出</string>\n    <string name=\"settings\">設定</string>\n    <string name=\"donate\">捐贈</string>\n    <string name=\"feeds\">Feeds</string>\n    <string name=\"save\">儲存</string>\n    <string name=\"done\">完成</string>\n    <string name=\"bluesky_name\">Bluesky</string>\n    <string name=\"bluesky_description\">Bluesky 是一個開放網路。透過一個帳號，你既可以使用易上手的社群網路，也可以在整個社交互聯網上擁有共享身分。</string>\n    <string name=\"mastodon_name\">長毛象</string>\n    <string name=\"mastodon_description\">Mastodon 是一個去中心化、非商業化的社群媒體，用戶可以掌控自己的時間軸，每個伺服器都是獨立的實體。</string>\n    <string name=\"mixed_content_name\">混合內容</string>\n    <string name=\"mixed_content_description\">混合內容是由 Fread 提供的自訂聚合訊息流，你可以新增來自長毛象、Bluesky、RSS 等平台的訊息來源，這些來源的內容會依時間倒序形成一個聚合訊息流。</string>\n    <string name=\"status_provider_type_activity_pub\">長毛象</string>\n    <string name=\"add_content_title\">新增內容</string>\n    <string name=\"add_content_success_with_account\">新增成功，當前帳號：</string>\n    <string name=\"add_content_success_with_login_reminder\">新增成功，是否登入？</string>\n    <string name=\"content_exist_tips\">該內容已存在</string>\n    <string name=\"content_add_success\">內容新增成功</string>\n    <string name=\"add_feeds_page_empty_name_exist\">該名稱已存在</string>\n    <string name=\"edit_profile_name_empty\">請輸入使用者名稱</string>\n    <string name=\"edit_profile_label_name\">名稱</string>\n    <string name=\"edit_profile_label_note\">簡介</string>\n    <string name=\"edit_profile_input_name_hint\">請輸入使用者名稱</string>\n    <string name=\"edit_profile_input_note_hint\">請輸入簡介</string>\n    <string name=\"edit_profile_edit_confirm_message\">確定要退出編輯嗎？</string>\n    <string name=\"add_content_success_snackbar\">新增成功</string>\n    <string name=\"login_dialog_input_hint\">請輸入要登入的伺服器</string>\n    <string name=\"login_dialog_title\">請選擇要登入的伺服器</string>\n    <string name=\"login_dialog_input_tip\">請輸入伺服器位址</string>\n    <string name=\"login_dialog_input_title\">請輸入伺服器位址</string>\n    <string name=\"login_dialog_input_label\">伺服器位址</string>\n    <string name=\"login_dialog_target_title\">請登入</string>\n    <string name=\"profile_description\">Fread 支援新增多個帳號，新增後的帳號可以用來發文、按讚、轉發、留言等操作，還可以查看已新增帳號建立的清單。</string>\n    <string name=\"list_content_empty_placeholder\">暫無內容</string>\n    <string name=\"post_status_scope_public\">公開</string>\n    <string name=\"post_status_scope_unlisted\">公開，但不會出現在探索功能</string>\n    <string name=\"post_status_scope_follower_only\">僅追隨者可見</string>\n    <string name=\"post_status_scope_mentioned_only\">僅被提及用戶可見</string>\n    <string name=\"post_status_content_warning\">警告文字</string>\n    <string name=\"post_status_success\">發送成功</string>\n    <string name=\"post_status_failed\">發送失敗：%1$s</string>\n    <string name=\"post_status_exit_dialog_content\">退出後已編輯的內容不會被儲存，確定要退出嗎？</string>\n    <string name=\"post_status_content_is_empty\">請輸入內容</string>\n    <string name=\"post_status_part_failed\">部分帳號發佈失敗，已從佇列移除成功的帳號，請重新嘗試：%1$s</string>\n    <string name=\"select_account_open_status_title\">請選擇帳號</string>\n    <string name=\"select_account_open_status_empty\">暫無其他帳號</string>\n    <string name=\"select_account_open_status_search_in\">在 %1$s 內搜尋</string>\n    <string name=\"select_account_open_status_search_failed\">搜尋失敗</string>\n    <string name=\"explorer_search_no_results\">未搜尋到結果</string>\n    <string name=\"explorer_search_bar_hint\">搜尋</string>\n    <string name=\"explorer_search_bar_hint_specialize_platform\">搜尋 %1$s</string>\n    <string name=\"explorer_search_tab_title_author\">用戶</string>\n    <string name=\"explorer_search_tab_title_status\">貼文</string>\n    <string name=\"explorer_search_tab_title_hashtag\">標籤</string>\n    <string name=\"explorer_search_tab_title_server\">伺服器</string>\n    <string name=\"explorer_tab_title\">探索</string>\n    <string name=\"explorer_tab_status_title\">貼文</string>\n    <string name=\"explorer_tab_users_title\">用戶</string>\n    <string name=\"explorer_tab_hashtag_title\">話題</string>\n    <string name=\"add_provider_page_title\">新增服務</string>\n    <string name=\"add_provider_page_confirm_button\">確定新增</string>\n    <string name=\"search_page_title\">搜尋</string>\n    <string name=\"search_result_not_found\">未搜尋到任何結果</string>\n    <string name=\"feeds_import_page_title\">匯入</string>\n    <string name=\"feeds_import_page_hint\">請選擇要匯入的 OPML 檔案</string>\n    <string name=\"feeds_import_button\">匯入</string>\n    <string name=\"add_feeds_page_title\">新增內容</string>\n    <string name=\"add_feeds_page_feeds_name_label\">內容名稱</string>\n    <string name=\"add_feeds_page_feeds_name_hint\">請輸入內容名稱</string>\n    <string name=\"add_feeds_page_feeds_empty\">請新增訂閱源</string>\n    <string name=\"pre_add_feeds_no_result\">未搜尋到任何結果</string>\n    <string name=\"pre_add_feeds_hint\">實例名稱、實例位址，用戶位址、RSS</string>\n    <string name=\"pre_add_feeds_input_label_1\">Fread 支援新增[[長毛象]]、[[Bluesky]]以及[[ RSS ]]內容，甚至是[[混合內容]]。</string>\n    <string name=\"pre_add_feeds_input_label_2\">請直接在此處輸入長毛象或 Bluesky [[用戶名稱]]、[[WebFinger\\Handle]]，用戶[[首頁位址]]或 [[RSS]] 位址。</string>\n    <string name=\"empty_content_hint_title\">請先新增內容</string>\n    <string name=\"empty_content_hint_desc\">需要先新增內容的訂閱源哦，Fread 支援多種內容類型，以及不同平台的混合內容，可以打造您自己的訊息流。</string>\n    <string name=\"feeds_add_content\">新增內容</string>\n    <string name=\"select_feeds_type_screen_title\">請選擇內容類型</string>\n    <string name=\"feeds_pre_add_login_dialog_content\">新增成功，是否登入？</string>\n    <string name=\"add_feeds_page_empty_name_tips\">請輸入一個名稱</string>\n    <string name=\"add_feeds_page_empty_source_tips\">請新增訊息源</string>\n    <string name=\"add_feeds_choose_auth_dialog_title\">請選擇要登入的伺服器</string>\n    <string name=\"search_feeds_title\">搜尋</string>\n    <string name=\"search_feeds_title_hint\">長毛象或 Bluesky 用戶名、WebFinger、URL、RSS</string>\n    <string name=\"feeds_select_account_for_post_status\">請選擇一個帳號</string>\n    <string name=\"feeds_mixed_config_edit_new_name_dialog_title\">請輸入內容名稱</string>\n    <string name=\"feeds_mixed_config_edit_new_name_dialog_label\">名稱</string>\n    <string name=\"feeds_mixed_config_edit_delete_content_dialog_message\">您確定要刪除這個內容嗎？</string>\n    <string name=\"feeds_mixed_config_not_found\">狀態異常，未找到設定</string>\n    <string name=\"feeds_import_back_dialog_message\">確定要退出嗎？</string>\n    <string name=\"feeds_delete_confirm_content\">確定要刪除嗎？</string>\n    <string name=\"feeds_select_type_screen_title\">請選擇內容類型</string>\n    <string name=\"feeds_select_type_screen_content\">請選擇您要新增的內容類型</string>\n    <string name=\"notification_tab_title\">通知</string>\n    <string name=\"notifications_account_empty_tip\">當前沒有已登入帳號</string>\n    <string name=\"notifications_tab_all\">所有</string>\n    <string name=\"notifications_tab_mention\">提及</string>\n    <string name=\"profile_page_title\">帳號</string>\n    <string name=\"profile_setting_about_title\">關於</string>\n    <string name=\"profile_setting_donate_desc\">請我喝杯咖啡以支持這個專案</string>\n    <string name=\"profile_setting_inline_video_auto_play\">Inline 影片自動播放</string>\n    <string name=\"profile_setting_inline_video_auto_play_subtitle\">開啟後，Feeds 流中的影片將會自動播放。</string>\n    <string name=\"profile_setting_always_show_sensitive_content\">敏感內容預設顯示</string>\n    <string name=\"profile_setting_always_show_sensitive_content_subtitle\">開啟後，敏感內容會預設顯示（僅限 Mastodon 平台）。</string>\n    <string name=\"profile_setting_dark_mode_title\">深色模式</string>\n    <string name=\"profile_setting_dark_mode_dark\">深色模式</string>\n    <string name=\"profile_setting_dark_mode_light\">淺色模式</string>\n    <string name=\"profile_setting_dark_mode_follow_system\">跟隨系統</string>\n    <string name=\"profile_setting_open_source_title\">開源聲明</string>\n    <string name=\"profile_setting_open_source_desc\">本專案使用到的依賴庫之開源聲明</string>\n    <string name=\"profile_setting_open_source_feedback\">給我們回饋</string>\n    <string name=\"profile_setting_open_source_feedback_desc\">加入 Telegram 群組或其他方式</string>\n    <string name=\"profile_setting_open_source_feedback_telegram\">加入 Telegram 群組</string>\n    <string name=\"profile_setting_open_source_feedback_github\">建立 GitHub issue</string>\n    <string name=\"profile_setting_open_source_feedback_email\">發送郵件</string>\n    <string name=\"profile_setting_language_title\">顯示語言</string>\n    <string name=\"profile_setting_language_zh\">簡體中文</string>\n    <string name=\"profile_setting_language_en\">English</string>\n    <string name=\"profile_setting_language_system\">跟隨系統</string>\n    <string name=\"profile_setting_ratting\">給我們評價</string>\n    <string name=\"profile_setting_ratting_desc\">如果覺得不錯請到應用商店給我們好評</string>\n    <string name=\"profile_setting_font_size\">內容字體大小</string>\n    <string name=\"profile_setting_font_size_desc\">調整內容的字體和圖示大小</string>\n    <string name=\"profile_setting_font_size_small\">小</string>\n    <string name=\"profile_setting_font_size_medium\">中</string>\n    <string name=\"profile_setting_font_size_large\">大</string>\n    <string name=\"profile_setting_immersive_nav_bar\">沉浸式導覽列</string>\n    <string name=\"profile_setting_immersive_nav_bar_desc\">開啟後，首頁滑動時會隱藏底部的導覽列。</string>\n    <string name=\"profile_setting_timeline_position\">首頁的時間軸預設位置</string>\n    <string name=\"profile_setting_timeline_position_last_read\">上次閱讀位置</string>\n    <string name=\"profile_setting_timeline_position_newest\">最新位置</string>\n    <string name=\"profile_setting_theme_title\">主題色</string>\n    <string name=\"profile_setting_theme_default\">預設</string>\n    <string name=\"profile_setting_theme_system\">系統</string>\n    <string name=\"profile_about_version\">版本：</string>\n    <string name=\"profile_about_website\">官網：</string>\n    <string name=\"profile_about_developer\">開發者：</string>\n    <string name=\"profile_about_contract_us\">聯繫我：</string>\n    <string name=\"profile_about_telegram\">Telegram 群組：</string>\n    <string name=\"profile_about_privacy_policy\">隱私政策：</string>\n    <string name=\"profile_setting_check_for_update\">檢查更新</string>\n    <string name=\"profile_setting_already_latest_version\">當前版本已經是最新</string>\n    <string name=\"profile_setting_have_new_version\">有新版本，點擊更新。</string>\n    <string name=\"profile_donate_page_title\">請選擇捐贈方式</string>\n    <string name=\"profile_account_not_login\">登入已失效</string>\n    <string name=\"rss_source_detail_screen_title\">訊息源屬性</string>\n    <string name=\"rss_source_detail_screen_custom_title\">自訂標題</string>\n    <string name=\"rss_source_detail_screen_url\">訂閱位址</string>\n    <string name=\"rss_source_detail_screen_home_url\">主頁</string>\n    <string name=\"rss_source_detail_screen_add_date\">新增日期</string>\n    <string name=\"rss_source_detail_screen_last_update_date\">上次更新時間</string>\n    <string name=\"bluesky_protocol_name\">Bluesky</string>\n    <string name=\"bsky_add_content_title\">登入</string>\n    <string name=\"bsky_add_content_hosting_provider\">服務商位址</string>\n    <string name=\"bsky_add_content_user_name\">使用者名稱或信箱</string>\n    <string name=\"bsky_add_content_password\">密碼</string>\n    <string name=\"bsky_add_content_factor_token\">驗證碼</string>\n    <string name=\"bsky_edit_content_title\">編輯</string>\n    <string name=\"bsky_feeds_explorer_more\">瀏覽更多 Feeds</string>\n    <string name=\"bsky_feeds_explorer_creator_label\">建立者  %1$s</string>\n    <string name=\"bsky_feeds_explorer_liked_by\">%1$s 位用戶喜歡</string>\n    <string name=\"bsky_feeds_detail_creator_prefix\">作者</string>\n    <string name=\"bsky_feeds_item_subtitle\">建立者 %1$s · %2$s 位用戶喜歡</string>\n    <string name=\"bsky_feeds_following_name\">追蹤中</string>\n    <string name=\"bsky_feeds_user_posts\">貼文</string>\n    <string name=\"bsky_feeds_user_replies\">回覆</string>\n    <string name=\"bsky_feeds_user_medias\">媒體</string>\n    <string name=\"bsky_feeds_user_likes\">喜歡</string>\n    <string name=\"bsky_user_detail_action_blocked_list\">已封鎖用戶</string>\n    <string name=\"bsky_user_detail_action_muted_list\">已隱藏用戶</string>\n    <string name=\"bsky_user_detail_action_mute_user\">隱藏 %1$s</string>\n    <string name=\"bsky_user_detail_action_unmute_user\">取消隱藏</string>\n    <string name=\"bsky_user_detail_action_block_user\">封鎖 %1$s</string>\n    <string name=\"bsky_user_detail_action_unblock_user\">取消封鎖</string>\n    <string name=\"bsky_user_detail_action_block_user_dialog_message\">您確認要封鎖對方嗎？</string>\n    <string name=\"bsky_user_detail_action_mute_user_dialog_message\">您確認要隱藏對方嗎？</string>\n    <string name=\"bsky_edit_profile_title\">編輯個人資料</string>\n    <string name=\"input_media_desc_page_title\">描述文字</string>\n    <string name=\"input_media_desc_input_hint\">請輸入描述文字</string>\n    <string name=\"post_status_page_title\">新貼文</string>\n    <string name=\"post_screen_input_hint\">請輸入您的貼文</string>\n    <string name=\"post_screen_media_descriptor_placeholder\">描述文字</string>\n    <string name=\"post_status_poll_item_hint\">選項 %1$s</string>\n    <string name=\"post_status_poll_duration\">投票時長</string>\n    <string name=\"post_status_poll_function_title\">功能</string>\n    <string name=\"post_status_poll_single\">單選</string>\n    <string name=\"post_status_poll_multiple\">多選</string>\n    <string name=\"post_status_poll_style_select_dialog_title\">請選擇功能</string>\n    <string name=\"post_status_poll_is_empty\">請輸入投票內容</string>\n    <string name=\"post_status_media_is_not_upload\">媒體暫未上傳</string>\n    <string name=\"authentication_page_normal_title\">此功能需登入後才能使用哦</string>\n    <string name=\"authentication_page_failed_title\">登入狀態失效，請重新登入。</string>\n    <string name=\"main_drawer_title\">內容清單</string>\n    <string name=\"main_drawer_mixed_item_subtitle_1\">混合內容 ·</string>\n    <string name=\"main_drawer_mixed_item_subtitle_2\">條訂閱源</string>\n    <string name=\"main_drawer_settings\">設定</string>\n    <string name=\"main_drawer_donate\">捐贈</string>\n    <string name=\"main_request_notification_dialog_title\">通知權限</string>\n    <string name=\"main_request_notification_dialog_reject_button\">拒絕</string>\n    <string name=\"main_request_notification_dialog_allow_button\">允許</string>\n    <string name=\"main_request_notification_dialog_content\">為了接收互動訊息，請允許通知權限。</string>\n    <string name=\"setting_item_amoled_mode\">Amoled 模式</string>\n    <string name=\"setting_item_amoled_mode_description\">啟用此功能後，深色模式下會將背景切換為純黑，以降低耗電並在暗色環境中提供更高的對比度。</string>\n    <string name=\"setting_group_appearance\">外觀</string>\n    <string name=\"setting_group_appearance_subtitle\">主題、顯示與首頁樣式設定</string>\n    <string name=\"setting_group_behavior\">行為</string>\n    <string name=\"setting_group_behavior_subtitle\">內容顯示與互動行為設定</string>\n    <string name=\"setting_item_home_tab_next_title\">首頁頂部的切換內容按鈕顯示</string>\n    <string name=\"setting_item_home_tab_next_subtitle\">首頁右上角是否顯示用於切換到下一個內容的按鈕</string>\n    <string name=\"setting_item_home_tab_refresh_title\">首頁頂部的重新整理按鈕顯示</string>\n    <string name=\"setting_item_home_tab_refresh_subtitle\">首頁右上角是否顯示用於重新整理內容的按鈕</string>\n    <string name=\"setting_item_open_url_by_system_title\">透過系統瀏覽器開啟連結</string>\n    <string name=\"setting_item_open_url_by_system_subtitle\">啟用後，Fread 會透過系統瀏覽器開啟連結。</string>\n    <string name=\"setting_item_solid_bar_background_title\">頂部和底部列純色背景</string>\n    <string name=\"setting_item_solid_bar_background_subtitle\">將頂部和底部列改為純色，而不是高斯模糊。</string>\n</resources>\n"
  },
  {
    "path": "localization/src/commonMain/composeResources/values-zh-rTW/strings_mastodon.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"activity_pub_user_detail_join_date\">加入時間</string>\n    <string name=\"activity_pub_user_detail_tab_about_joined\">%1$s 加入</string>\n    <string name=\"activity_pub_user_detail_menu_block\">封鎖 %1$s</string>\n    <string name=\"activity_pub_user_detail_menu_block_domain\">封鎖 %1$s</string>\n    <string name=\"activity_pub_user_detail_menu_unblock_domain\">取消封鎖 %1$s</string>\n    <string name=\"activity_pub_user_detail_menu_edit_private_note\">編輯備註</string>\n    <string name=\"activity_pub_user_detail_menu_edit_private_note_dialog_hint\">請輸入備註</string>\n    <string name=\"activity_pub_user_detail_dialog_content_block\">您確認要封鎖對方嗎？</string>\n    <string name=\"activity_pub_user_detail_dialog_content_block_domain\">您確認要封鎖對方實例嗎？</string>\n    <string name=\"activity_pub_user_menu_blocked_user_list\">已封鎖用戶</string>\n    <string name=\"activity_pub_user_menu_muted_user_list\">已隱藏用戶</string>\n    <string name=\"activity_pub_user_detail_menu_mute_user\">隱藏 %1$s</string>\n    <string name=\"activity_pub_user_detail_menu_unmute_user\">取消隱藏 %1$s</string>\n    <string name=\"activity_pub_mute_user_bottom_sheet_title\">隱藏用戶？</string>\n    <string name=\"activity_pub_mute_user_bottom_sheet_role1\">他們不會知道自己被隱藏</string>\n    <string name=\"activity_pub_mute_user_bottom_sheet_role2\">他們仍可看到你的貼文，但你不會看到他們的貼文。</string>\n    <string name=\"activity_pub_mute_user_bottom_sheet_role3\">你將不會看到提及他們的貼文。</string>\n    <string name=\"activity_pub_mute_user_bottom_sheet_role4\">他們可以提及並追蹤你，但你不會看到他們。</string>\n    <string name=\"activity_pub_mute_user_bottom_sheet_btn_mute\">隱藏</string>\n    <string name=\"add_instance_screen_title\">新增實例</string>\n    <string name=\"add_instance_input_service_hint\">請輸入伺服器位址</string>\n    <string name=\"add_instance_input_service_error\">請輸入正確的伺服器位址</string>\n    <string name=\"activity_pub_content_tab_home\">首頁</string>\n    <string name=\"activity_pub_content_tab_local_timeline\">本站</string>\n    <string name=\"activity_pub_content_tab_public_timeline\">跨站</string>\n    <string name=\"activity_pub_content_tab_trending\">趨勢</string>\n    <string name=\"activity_pub_hashtag_timeline_description\">%1$s 則貼文 · %2$s 人參與 · 今日 %3$s 則貼文</string>\n    <string name=\"activity_pub_hashtag_unfollow_dialog_message\">您確定要取消追蹤這個話題嗎？</string>\n    <string name=\"activity_pub_edit_account_info_label_about\">關於</string>\n    <string name=\"activity_pub_edit_content_screen_config_not_found\">找不到設定</string>\n    <string name=\"activity_pub_user_list_empty\">暫無用戶</string>\n    <string name=\"activity_pub_muted_user_list_empty\">暫無已隱藏的用戶</string>\n    <string name=\"activity_pub_muted_user_list_unmute\">取消隱藏</string>\n    <string name=\"activity_pub_blocked_user_list_empty\">暫無已封鎖的用戶</string>\n    <string name=\"activity_pub_blocked_user_list_unblock\">取消封鎖</string>\n    <string name=\"activity_pub_bookmarks_list_title\">收藏的內容</string>\n    <string name=\"activity_pub_favourites_list_title\">按讚的內容</string>\n    <string name=\"activity_pub_followed_tags_screen_title\">追蹤的話題</string>\n    <string name=\"activity_pub_filters_list_page_title\">過濾器</string>\n    <string name=\"activity_pub_filters_active\">生效中</string>\n    <string name=\"activity_pub_filters_expired\">已失效</string>\n    <string name=\"activity_pub_filter_edit_title\">編輯過濾器</string>\n    <string name=\"activity_pub_filter_edit_input_title_label\">標題</string>\n    <string name=\"activity_pub_filter_edit_input_title_hint\">請輸入標題</string>\n    <string name=\"activity_pub_filter_edit_duration\">持續時間</string>\n    <string name=\"activity_pub_filter_edit_duration_permanent\">永久</string>\n    <string name=\"activity_pub_filter_edit_duration_thirty_minutes\">三十分鐘</string>\n    <string name=\"activity_pub_filter_edit_duration_one_hour\">一小時</string>\n    <string name=\"activity_pub_filter_edit_duration_twelve_hours\">十二小時</string>\n    <string name=\"activity_pub_filter_edit_duration_one_day\">一天</string>\n    <string name=\"activity_pub_filter_edit_duration_three_day\">三天</string>\n    <string name=\"activity_pub_filter_edit_duration_one_week\">一週</string>\n    <string name=\"activity_pub_filter_edit_duration_custom\">自訂</string>\n    <string name=\"activity_pub_filter_edit_duration_finish_subtitle\">結束於 %1$s</string>\n    <string name=\"activity_pub_filter_edit_keyword_title\">已隱藏關鍵字</string>\n    <string name=\"activity_pub_filter_edit_keyword_dialog_title\">編輯關鍵字</string>\n    <string name=\"activity_pub_filter_edit_keyword_dialog_hint\">關鍵字</string>\n    <string name=\"activity_pub_filter_edit_keyword_remove_keyword_dialog_content\">確定刪除關鍵字嗎？</string>\n    <string name=\"activity_pub_filter_edit_keyword_list_title\">已隱藏關鍵字</string>\n    <string name=\"activity_pub_filter_edit_keyword_list_desc\">%1$s 個已隱藏的關鍵字</string>\n    <string name=\"activity_pub_filter_edit_context_title\">隱藏來源</string>\n    <string name=\"activity_pub_filter_edit_context_selector_title\">請選擇隱藏來源</string>\n    <string name=\"activity_pub_filter_edit_context_home\">首頁 &amp; 清單</string>\n    <string name=\"activity_pub_filter_edit_context_notification\">通知</string>\n    <string name=\"activity_pub_filter_edit_context_timeline\">公共時間軸</string>\n    <string name=\"activity_pub_filter_edit_context_thread\">嘟文 &amp; 回覆</string>\n    <string name=\"activity_pub_filter_edit_context_account\">個人資料</string>\n    <string name=\"activity_pub_filter_edit_empty_context\">無</string>\n    <string name=\"activity_pub_filter_edit_warning_title\">包含內容警告的內容</string>\n    <string name=\"activity_pub_filter_edit_warning_desc\">仍會顯示符合此過濾器的嘟文，但會加上內容警告</string>\n    <string name=\"activity_pub_filter_edit_delete_content\">確定要刪除嗎？</string>\n    <string name=\"activity_pub_filter_edit_back_dialog\">確定要退出嗎？</string>\n    <string name=\"activity_pub_filter_edit_whole_word\">整個詞條</string>\n    <string name=\"activity_pub_explorer_tab_status_title\">嘟文</string>\n    <string name=\"activity_pub_explorer_tab_users_title\">用戶</string>\n    <string name=\"activity_pub_explorer_tab_hashtag_title\">話題</string>\n    <string name=\"activity_pub_created_list_title\">建立的清單</string>\n    <string name=\"activity_pub_add_list_title\">建立清單</string>\n    <string name=\"activity_pub_add_list_name\">清單名稱</string>\n    <string name=\"activity_pub_add_list_replies\">回覆顯示範圍</string>\n    <string name=\"activity_pub_add_list_replies_non\">不顯示</string>\n    <string name=\"activity_pub_add_list_replies_list\">清單成員</string>\n    <string name=\"activity_pub_add_list_replies_followers\">我追蹤的所有人</string>\n    <string name=\"activity_pub_add_list_accounts\">清單成員</string>\n    <string name=\"activity_pub_add_list_hide_in_timeline\">在首頁時間軸中隱藏成員</string>\n    <string name=\"activity_pub_add_list_hide_in_timeline_desc\">清單成員的嘟文不會顯示在你的首頁時間軸，避免重複閱讀。</string>\n    <string name=\"activity_pub_add_list_remove_user_message\">確定移除該用戶？</string>\n    <string name=\"activity_pub_add_list_name_is_empty\">請輸入清單名稱</string>\n    <string name=\"activity_pub_add_list_back_reminder\">目前仍有變更未儲存，確認要退出嗎？</string>\n    <string name=\"activity_pub_search_user_placeholder\">請輸入搜尋內容</string>\n    <string name=\"activity_pub_list_delete_confirm\">確定要刪除這個清單嗎？</string>\n    <string name=\"activity_pub_login_exception\">認證狀態異常</string>\n    <string name=\"activity_pub_home_timeline\">首頁</string>\n    <string name=\"activity_pub_local_timeline\">本站</string>\n    <string name=\"activity_pub_public_timeline\">跨站</string>\n    <string name=\"activity_pub_instance_detail_language_label\">語言：%1$s</string>\n    <string name=\"activity_pub_instance_detail_active_month_label\">月活躍：%1$s</string>\n    <string name=\"activity_pub_user_detail_tab_post\">貼文</string>\n    <string name=\"activity_pub_user_detail_tab_replies\">回覆</string>\n    <string name=\"activity_pub_user_detail_tab_media\">媒體</string>\n    <string name=\"activity_pub_user_detail_tab_about\">關於</string>\n    <string name=\"activity_pub_about\">關於</string>\n    <string name=\"activity_pub_about_rule_title\">站點規則</string>\n    <string name=\"activity_pub_trends_tag\">話題</string>\n    <string name=\"activity_pub_trends_tag_description\">最近兩天有 %1$s 人在討論</string>\n    <string name=\"activity_pub_trends_status\">熱門</string>\n    <string name=\"activity_pub_select_platform_title\">選擇一個實例</string>\n    <string name=\"activity_pub_select_platform_text_hint\">請輸入實例名稱或實例 URL</string>\n</resources>\n"
  },
  {
    "path": "localization/src/commonMain/composeResources/values-zh-rTW/strings_status_ui.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"status_ui_reposted\">已轉發</string>\n    <string name=\"status_ui_repost\">轉發</string>\n    <string name=\"status_ui_quote\">引用</string>\n    <string name=\"status_ui_like\">喜歡</string>\n    <string name=\"status_ui_comment\">評論</string>\n    <string name=\"status_ui_delete\">刪除</string>\n    <string name=\"status_ui_share\">分享</string>\n    <string name=\"status_ui_bookmark\">收藏</string>\n    <string name=\"status_ui_unbookmark\">取消收藏</string>\n    <string name=\"status_ui_reply\">回覆</string>\n    <string name=\"status_ui_follow\">追蹤</string>\n    <string name=\"status_ui_unfollow\">取消追蹤</string>\n    <string name=\"status_ui_pin\">置頂</string>\n    <string name=\"status_ui_unpin\">取消置頂</string>\n    <string name=\"status_ui_edit\">編輯</string>\n    <string name=\"status_ui_sensitive_by_filter\">內容已被「%1$s」過濾</string>\n    <string name=\"status_ui_new_status\">新內容</string>\n    <string name=\"status_ui_delete_status_confirm\">確定要刪除這則內容嗎？</string>\n    <string name=\"status_ui_image_sensitive_label\">敏感內容</string>\n    <string name=\"status_ui_image_content_show_hidden_label\">顯示更多</string>\n    <string name=\"status_ui_image_content_hide_hidden_label\">隱藏內容</string>\n    <string name=\"status_ui_poll_vote\">投票</string>\n    <string name=\"status_ui_poll_vote_finished_tip\">%1$s 票 · 已關閉</string>\n    <string name=\"status_ui_poll_votes_finished_tip\">%1$s 票 · 已關閉</string>\n    <string name=\"status_ui_interaction_open_in_browser\">在瀏覽器中開啟</string>\n    <string name=\"status_ui_interaction_open_original_instance\">開啟對方實例</string>\n    <string name=\"status_ui_interaction_copy_url\">複製連結</string>\n    <string name=\"status_ui_interaction_translate\">翻譯</string>\n    <string name=\"status_ui_interaction_open_blog_by_other_account\">使用其他帳號開啟</string>\n    <string name=\"status_ui_visibility_mentioned_only\">僅被提及用戶可見</string>\n    <string name=\"status_ui_label_pinned\">置頂</string>\n    <string name=\"status_ui_info_label_edited\">已編輯</string>\n    <string name=\"status_ui_interaction_label_favourited_count\">%1$s 次喜歡</string>\n    <string name=\"status_ui_interaction_label_boosted_count\">%1$s 次轉發</string>\n    <string name=\"status_ui_bottom_label_edited_at\">編輯於 %1$s</string>\n    <string name=\"status_ui_translating\">翻譯中…</string>\n    <string name=\"status_ui_translate_show_original\">顯示原文</string>\n    <string name=\"status_ui_edit_content_delete_dialog_content\">確定刪除這個內容？</string>\n    <string name=\"status_ui_edit_content_name_title\">修改內容名稱</string>\n    <string name=\"status_ui_edit_content_name_label\">編輯內容名稱</string>\n    <string name=\"status_ui_edit_content_name_hint\">請輸入內容名稱</string>\n    <string name=\"status_ui_edit_content_config_showing_list_title\">顯示在首頁的清單</string>\n    <string name=\"status_ui_edit_content_config_showing_list_description\">可長按拖曳排序</string>\n    <string name=\"status_ui_edit_content_config_hidden_list_title\">可新增到首頁的清單</string>\n    <string name=\"status_ui_user_detail_relationship_mutuals\">互相關注</string>\n    <string name=\"status_ui_user_detail_relationship_blocking\">已封鎖</string>\n    <string name=\"status_ui_user_detail_relationship_following\">已追蹤</string>\n    <string name=\"status_ui_user_detail_relationship_not_follow\">追蹤</string>\n    <string name=\"status_ui_user_detail_relationship_requested\">請求中</string>\n    <string name=\"status_ui_user_detail_relationship_follow_back\">回追</string>\n    <string name=\"status_ui_user_detail_request_by_tip\">對方正在請求追蹤你</string>\n    <string name=\"status_ui_relationship_btn_dialog_content_cancel_follow\">您確認要取消追蹤對方嗎？</string>\n    <string name=\"status_ui_relationship_btn_dialog_content_cancel_follow_request\">您確定要撤回追蹤請求嗎？</string>\n    <string name=\"status_ui_relationship_btn_dialog_content_cancel_blocking\">您確定要取消封鎖對方嗎？</string>\n    <string name=\"status_ui_user_detail_follows_you\">追蹤了你</string>\n    <string name=\"status_ui_user_detail_posts\">貼文</string>\n    <string name=\"status_ui_user_detail_follower_info\">追蹤者</string>\n    <string name=\"status_ui_user_detail_following_info\">正在追蹤</string>\n    <string name=\"status_ui_top_label_continued_thread\">展開對話</string>\n    <string name=\"status_ui_update_dialog_title\">更新</string>\n    <string name=\"status_ui_update_dialog_release_note\">%1$s 版本更新日誌：\\n%2$s </string>\n    <string name=\"status_ui_switch_account_dialog_title\">選擇帳號</string>\n    <string name=\"status_ui_likes\">喜歡</string>\n    <string name=\"status_ui_bookmarks\">收藏</string>\n    <string name=\"status_ui_edit_profile\">修改個人資料</string>\n    <string name=\"status_ui_logout\">登出</string>\n    <string name=\"status_ui_logout_dialog_content\">你確定要登出嗎？</string>\n    <string name=\"status_ui_search_account_status_hint\">請輸入你要搜尋的貼文</string>\n    <string name=\"shared_notification_unknown_desc\">未知通知</string>\n    <string name=\"shared_notification_favourited_desc\">喜歡了你的動態</string>\n    <string name=\"shared_notification_reblog_desc\">轉發了你的動態</string>\n    <string name=\"shared_notification_poll_desc\">查看投票結果</string>\n    <string name=\"shared_notification_poll_count\">%1$s 票 · 已結束</string>\n    <string name=\"shared_notification_follow_desc\">追蹤了你</string>\n    <string name=\"shared_notification_update_desc\">編輯了一則貼文</string>\n    <string name=\"shared_notification_follow_request\">你收到一則追蹤請求</string>\n    <string name=\"shared_notification_new_status_desc\">發佈了新貼文</string>\n    <string name=\"shared_notification_severed_desc\">和你中斷了關係</string>\n    <string name=\"shared_notification_quote_desc\">引用了你的貼文</string>\n    <string name=\"shared_notification_reply_desc\">回覆了你的貼文</string>\n    <string name=\"shared_user_list_title_following\">正在追蹤的用戶</string>\n    <string name=\"shared_user_list_title_followers\">追蹤你的用戶</string>\n    <string name=\"shared_user_list_title_mutes\">已隱藏用戶</string>\n    <string name=\"shared_user_list_title_blocks\">已封鎖用戶</string>\n    <string name=\"shared_user_list_title_likes\">按讚的用戶</string>\n    <string name=\"shared_user_list_title_reblog\">轉發的用戶</string>\n    <string name=\"shared_user_list_action_mute\">隱藏</string>\n    <string name=\"shared_user_list_action_block\">封鎖</string>\n    <string name=\"shared_user_list_action_muted\">已隱藏</string>\n    <string name=\"shared_user_list_action_blocked\">已封鎖</string>\n    <string name=\"shared_user_list_action_mute_dialog_message\">您確定要隱藏該用戶嗎？</string>\n    <string name=\"shared_user_list_action_block_dialog_message\">您確定要封鎖該用戶嗎？</string>\n    <string name=\"shared_publish_blog_title\">發文</string>\n    <string name=\"shared_publish_blog_text_hint\">寫下你的想法</string>\n    <string name=\"shared_publish_reply_input_hint\">回覆給 [[%1$s]]</string>\n    <string name=\"shared_publish_interaction_no_limit\">任何人都可以互動</string>\n    <string name=\"shared_publish_interaction_limited\">已限制互動</string>\n    <string name=\"shared_publish_interaction_dialog_title\">貼文互動選項</string>\n    <string name=\"shared_publish_interaction_dialog_subtitle\">自訂哪些人可以互動這則貼文。</string>\n    <string name=\"shared_publish_interaction_dialog_quote_title\">引用選項</string>\n    <string name=\"shared_publish_interaction_dialog_quote_allow\">允許引用貼文</string>\n    <string name=\"shared_publish_interaction_dialog_reply_title\">回覆選項</string>\n    <string name=\"shared_publish_interaction_dialog_reply_subtitle\">允許這些人回覆：</string>\n    <string name=\"shared_publish_interaction_dialog_reply_all\">所有人</string>\n    <string name=\"shared_publish_interaction_dialog_reply_nobody\">僅限自己</string>\n    <string name=\"shared_publish_interaction_dialog_reply_combine_title\">或組合選擇這些選項：</string>\n    <string name=\"shared_publish_interaction_dialog_mentioned\">被提及的用戶</string>\n    <string name=\"shared_publish_interaction_dialog_following\">你追蹤的用戶</string>\n    <string name=\"shared_publish_interaction_dialog_follower\">你的追蹤者</string>\n    <string name=\"shared_publish_interaction_dialog_in_list\">「%1$s」中的用戶</string>\n    <string name=\"shared_publish_media_alt_dialog_title\">新增替代文字</string>\n    <string name=\"shared_publish_media_alt_dialog_input_tip\">補充描述替代文字</string>\n    <string name=\"shared_publish_media_alt_dialog_input_hint\">替代文字</string>\n    <string name=\"shared_feeds_not_login_title\">登入資訊已失效，請重新登入</string>\n    <string name=\"shared_feeds_go_to_login\">登入</string>\n    <string name=\"shared_publish_select_account_title\">請選擇帳號</string>\n    <string name=\"shared_select_language_title\">選擇語言</string>\n    <string name=\"shared_status_context_screen_title\">詳情</string>\n    <string name=\"shared_alt_label\">替代文字</string>\n    <string name=\"status_ui_embed_quote_unavailable\">當前引用無法使用</string>\n    <string name=\"status_ui_action_unforward\">取消轉發</string>\n    <string name=\"status_ui_quote_approval_public\">任何人</string>\n    <string name=\"status_ui_quote_approval_follower\">僅限追蹤者</string>\n    <string name=\"status_ui_quote_approval_nobody\">僅限自己</string>\n    <string name=\"status_ui_visibility\">可見性</string>\n</resources>\n"
  },
  {
    "path": "localization/src/commonMain/kotlin/com/zhangke/fread/localization/LanguageCode.kt",
    "content": "package com.zhangke.fread.localization\n\nenum class LanguageCode(val code: String) {\n    EN_US(\"en-US\"),\n    DE_DE(\"de-DE\"),\n    ES_ES(\"es-ES\"),\n    FR_FR(\"fr-FR\"),\n    JA_JP(\"ja-JP\"),\n    PT_PT(\"pt-PT\"),\n    RU_RU(\"ru-RU\"),\n    ZH_CN(\"zh-CN\"),\n    ZH_HK(\"zh-HK\"),\n    ZH_TW(\"zh-TW\");\n\n    companion object {\n\n        fun fromCode(code: String): LanguageCode? {\n            return LanguageCode.entries.find { it.code == code }\n        }\n    }\n}\n"
  },
  {
    "path": "localization/src/commonMain/kotlin/com/zhangke/fread/localization/LanguageCodeNames.kt",
    "content": "package com.zhangke.fread.localization\n\nimport androidx.compose.runtime.Composable\n\nval LanguageCode.displayName: String\n    @Composable\n    get() = when (this) {\n        LanguageCode.ZH_CN -> \"简体中文\"\n        LanguageCode.ZH_HK -> \"繁體中文（香港）\"\n        LanguageCode.ZH_TW -> \"繁體中文（台灣）\"\n        LanguageCode.EN_US -> \"English\"\n        LanguageCode.DE_DE -> \"Deutsch\"\n        LanguageCode.ES_ES -> \"Español\"\n        LanguageCode.FR_FR -> \"Français\"\n        LanguageCode.JA_JP -> \"日本語\"\n        LanguageCode.PT_PT -> \"Português\"\n        LanguageCode.RU_RU -> \"Русский\"\n    }\n"
  },
  {
    "path": "localization/src/commonMain/kotlin/com/zhangke/fread/localization/Localization.kt",
    "content": "package com.zhangke.fread.localization\n\nobject Localization {\n\n    val supportedLanguage: List<LanguageCode> = LanguageCode.entries\n}\n"
  },
  {
    "path": "localization/src/commonMain/kotlin/com/zhangke/fread/localization/LocalizedString.kt",
    "content": "package com.zhangke.fread.localization\n\nval localizedString = Res.string\n\n/**\n * generated by LLM\n */\nobject LocalizedString {\n\n    val skip = localizedString.skip\n    val alert = localizedString.alert\n    val durationMinute = localizedString.duration_minute\n    val durationHour = localizedString.duration_hour\n    val durationDay = localizedString.duration_day\n    val durationWeek = localizedString.duration_week\n    val cancel = localizedString.cancel\n    val ok = localizedString.ok\n    val empty = localizedString.empty\n    val durationSelectorTitle = localizedString.duration_selector_title\n    val retry = localizedString.retry\n    val loadMoreError = localizedString.load_more_error\n    val unknownError = localizedString.unknown_error\n    val networkError = localizedString.network_error\n    val resourceNotFound = localizedString.resource_not_found\n    val imageSaving = localizedString.image_saving\n    val imageSaveSuccess = localizedString.image_save_success\n    val imageSaveFailed = localizedString.image_save_failed\n    val permissionWriteExternalPermissionDenied =\n        localizedString.permission_write_external_permission_denied\n    val feedsLoadPreviousPageLabel = localizedString.feeds_load_previous_page_label\n    val feedsLoadPreviousPageFailedLabel = localizedString.feeds_load_previous_page_failed_label\n    val image = localizedString.image\n    val video = localizedString.video\n    val login = localizedString.login\n    val add = localizedString.add\n    val search = localizedString.search\n    val authSuccess = localizedString.auth_success\n    val authFailed = localizedString.auth_failed\n    val select = localizedString.select\n    val logout = localizedString.logout\n    val settings = localizedString.settings\n    val donate = localizedString.donate\n    val feeds = localizedString.feeds\n    val save = localizedString.save\n    val done = localizedString.done\n    val blueskyName = localizedString.bluesky_name\n    val blueskyDescription = localizedString.bluesky_description\n    val mastodonName = localizedString.mastodon_name\n    val mastodonDescription = localizedString.mastodon_description\n    val mixedContentName = localizedString.mixed_content_name\n    val mixedContentDescription = localizedString.mixed_content_description\n    val statusProviderTypeActivityPub = localizedString.status_provider_type_activity_pub\n    val addContentTitle = localizedString.add_content_title\n    val addContentSuccessWithAccount = localizedString.add_content_success_with_account\n    val addContentSuccessWithLoginReminder = localizedString.add_content_success_with_login_reminder\n    val contentExistTips = localizedString.content_exist_tips\n    val contentAddSuccess = localizedString.content_add_success\n    val addFeedsPageEmptyNameExist = localizedString.add_feeds_page_empty_name_exist\n    val editProfileNameEmpty = localizedString.edit_profile_name_empty\n    val editProfileLabelName = localizedString.edit_profile_label_name\n    val editProfileLabelNote = localizedString.edit_profile_label_note\n    val editProfileInputNameHint = localizedString.edit_profile_input_name_hint\n    val editProfileInputNoteHint = localizedString.edit_profile_input_note_hint\n    val editProfileEditConfirmMessage = localizedString.edit_profile_edit_confirm_message\n    val addContentSuccessSnackbar = localizedString.add_content_success_snackbar\n    val sharedSelectLanguageTitle = localizedString.shared_select_language_title\n    val sharedStatusContextScreenTitle = localizedString.shared_status_context_screen_title\n    val sharedAltLabel = localizedString.shared_alt_label\n    val loginDialogInputHint = localizedString.login_dialog_input_hint\n    val loginDialogTitle = localizedString.login_dialog_title\n    val loginDialogInputTip = localizedString.login_dialog_input_tip\n    val loginDialogInputTitle = localizedString.login_dialog_input_title\n    val loginDialogInputLabel = localizedString.login_dialog_input_label\n    val loginDialogTargetTitle = localizedString.login_dialog_target_title\n    val profileDescription = localizedString.profile_description\n    val listContentEmptyPlaceholder = localizedString.list_content_empty_placeholder\n    val sharedNotificationUnknownDesc = localizedString.shared_notification_unknown_desc\n    val sharedNotificationFavouritedDesc = localizedString.shared_notification_favourited_desc\n    val sharedNotificationReblogDesc = localizedString.shared_notification_reblog_desc\n    val sharedNotificationPollDesc = localizedString.shared_notification_poll_desc\n    val sharedNotificationPollCount = localizedString.shared_notification_poll_count\n    val sharedNotificationFollowDesc = localizedString.shared_notification_follow_desc\n    val sharedNotificationUpdateDesc = localizedString.shared_notification_update_desc\n    val sharedNotificationFollowRequest = localizedString.shared_notification_follow_request\n    val sharedNotificationNewStatusDesc = localizedString.shared_notification_new_status_desc\n    val sharedNotificationSeveredDesc = localizedString.shared_notification_severed_desc\n    val sharedNotificationQuoteDesc = localizedString.shared_notification_quote_desc\n    val sharedNotificationReplyDesc = localizedString.shared_notification_reply_desc\n    val sharedUserListTitleFollowing = localizedString.shared_user_list_title_following\n    val sharedUserListTitleFollowers = localizedString.shared_user_list_title_followers\n    val sharedUserListTitleMutes = localizedString.shared_user_list_title_mutes\n    val sharedUserListTitleBlocks = localizedString.shared_user_list_title_blocks\n    val sharedUserListTitleLikes = localizedString.shared_user_list_title_likes\n    val sharedUserListTitleReblog = localizedString.shared_user_list_title_reblog\n    val sharedUserListActionMute = localizedString.shared_user_list_action_mute\n    val sharedUserListActionBlock = localizedString.shared_user_list_action_block\n    val sharedUserListActionMuted = localizedString.shared_user_list_action_muted\n    val sharedUserListActionBlocked = localizedString.shared_user_list_action_blocked\n    val sharedUserListActionMuteDialogMessage =\n        localizedString.shared_user_list_action_mute_dialog_message\n    val sharedUserListActionBlockDialogMessage =\n        localizedString.shared_user_list_action_block_dialog_message\n    val sharedPublishBlogTitle = localizedString.shared_publish_blog_title\n    val sharedPublishBlogTextHint = localizedString.shared_publish_blog_text_hint\n    val sharedPublishReplyInputHint = localizedString.shared_publish_reply_input_hint\n    val sharedPublishInteractionNoLimit = localizedString.shared_publish_interaction_no_limit\n    val sharedPublishInteractionLimited = localizedString.shared_publish_interaction_limited\n    val sharedPublishInteractionDialogTitle =\n        localizedString.shared_publish_interaction_dialog_title\n    val sharedPublishInteractionDialogSubtitle =\n        localizedString.shared_publish_interaction_dialog_subtitle\n    val sharedPublishInteractionDialogQuoteTitle =\n        localizedString.shared_publish_interaction_dialog_quote_title\n    val sharedPublishInteractionDialogQuoteAllow =\n        localizedString.shared_publish_interaction_dialog_quote_allow\n    val sharedPublishInteractionDialogReplyTitle =\n        localizedString.shared_publish_interaction_dialog_reply_title\n    val sharedPublishInteractionDialogReplySubtitle =\n        localizedString.shared_publish_interaction_dialog_reply_subtitle\n    val sharedPublishInteractionDialogReplyAll =\n        localizedString.shared_publish_interaction_dialog_reply_all\n    val sharedPublishInteractionDialogReplyNobody =\n        localizedString.shared_publish_interaction_dialog_reply_nobody\n    val sharedPublishInteractionDialogReplyCombineTitle =\n        localizedString.shared_publish_interaction_dialog_reply_combine_title\n    val sharedPublishInteractionDialogMentioned =\n        localizedString.shared_publish_interaction_dialog_mentioned\n    val sharedPublishInteractionDialogFollowing =\n        localizedString.shared_publish_interaction_dialog_following\n    val sharedPublishInteractionDialogFollower =\n        localizedString.shared_publish_interaction_dialog_follower\n    val sharedPublishInteractionDialogInList =\n        localizedString.shared_publish_interaction_dialog_in_list\n    val sharedPublishMediaAltDialogTitle = localizedString.shared_publish_media_alt_dialog_title\n    val sharedPublishMediaAltDialogInputTip =\n        localizedString.shared_publish_media_alt_dialog_input_tip\n    val sharedPublishMediaAltDialogInputHint =\n        localizedString.shared_publish_media_alt_dialog_input_hint\n    val sharedFeedsNotLoginTitle = localizedString.shared_feeds_not_login_title\n    val sharedFeedsGoToLogin = localizedString.shared_feeds_go_to_login\n    val sharedPublishSelectAccountTitle = localizedString.shared_publish_select_account_title\n    val postStatusScopePublic = localizedString.post_status_scope_public\n    val postStatusScopeUnlisted = localizedString.post_status_scope_unlisted\n    val postStatusScopeFollowerOnly = localizedString.post_status_scope_follower_only\n    val postStatusScopeMentionedOnly = localizedString.post_status_scope_mentioned_only\n    val postStatusContentWarning = localizedString.post_status_content_warning\n    val postStatusSuccess = localizedString.post_status_success\n    val postStatusFailed = localizedString.post_status_failed\n    val postStatusExitDialogContent = localizedString.post_status_exit_dialog_content\n    val postStatusContentIsEmpty = localizedString.post_status_content_is_empty\n    val postStatusPartFailed = localizedString.post_status_part_failed\n    val selectAccountOpenStatusTitle = localizedString.select_account_open_status_title\n    val selectAccountOpenStatusEmpty = localizedString.select_account_open_status_empty\n    val selectAccountOpenStatusSearchIn = localizedString.select_account_open_status_search_in\n    val selectAccountOpenStatusSearchFailed =\n        localizedString.select_account_open_status_search_failed\n    val status_ui_repost = localizedString.status_ui_repost\n    val status_ui_reposted = localizedString.status_ui_reposted\n    val statusUiQuote = localizedString.status_ui_quote\n    val statusUiLike = localizedString.status_ui_like\n    val statusUiComment = localizedString.status_ui_comment\n    val statusUiDelete = localizedString.status_ui_delete\n    val statusUiShare = localizedString.status_ui_share\n    val statusUiBookmark = localizedString.status_ui_bookmark\n    val statusUiUnbookmark = localizedString.status_ui_unbookmark\n    val statusUiReply = localizedString.status_ui_reply\n    val statusUiFollow = localizedString.status_ui_follow\n    val statusUiUnfollow = localizedString.status_ui_unfollow\n    val statusUiPin = localizedString.status_ui_pin\n    val statusUiUnpin = localizedString.status_ui_unpin\n    val statusUiEdit = localizedString.status_ui_edit\n    val statusUiSensitiveByFilter = localizedString.status_ui_sensitive_by_filter\n    val statusUiNewStatus = localizedString.status_ui_new_status\n    val statusUiDeleteStatusConfirm = localizedString.status_ui_delete_status_confirm\n    val statusUiImageSensitiveLabel = localizedString.status_ui_image_sensitive_label\n    val statusUiImageContentShowHiddenLabel =\n        localizedString.status_ui_image_content_show_hidden_label\n    val statusUiImageContentHideHiddenLabel =\n        localizedString.status_ui_image_content_hide_hidden_label\n    val statusUiPollVote = localizedString.status_ui_poll_vote\n    val statusUiPollVoteFinishedTip = localizedString.status_ui_poll_vote_finished_tip\n    val statusUiPollVotesFinishedTip = localizedString.status_ui_poll_votes_finished_tip\n    val statusUiInteractionOpenInBrowser = localizedString.status_ui_interaction_open_in_browser\n    val statusUiInteractionOpenOriginalInstance =\n        localizedString.status_ui_interaction_open_original_instance\n    val statusUiInteractionCopyUrl = localizedString.status_ui_interaction_copy_url\n    val statusUiInteractionTranslate = localizedString.status_ui_interaction_translate\n    val statusUiInteractionOpenBlogByOtherAccount =\n        localizedString.status_ui_interaction_open_blog_by_other_account\n    val statusUiVisibilityMentionedOnly = localizedString.status_ui_visibility_mentioned_only\n    val statusUiLabelPinned = localizedString.status_ui_label_pinned\n    val statusUiInfoLabelEdited = localizedString.status_ui_info_label_edited\n    val statusUiInteractionLabelFavouritedCount =\n        localizedString.status_ui_interaction_label_favourited_count\n    val statusUiInteractionLabelBoostedCount =\n        localizedString.status_ui_interaction_label_boosted_count\n    val statusUiBottomLabelEditedAt = localizedString.status_ui_bottom_label_edited_at\n    val statusUiTranslating = localizedString.status_ui_translating\n    val statusUiTranslateShowOriginal = localizedString.status_ui_translate_show_original\n    val statusUiEditContentDeleteDialogContent =\n        localizedString.status_ui_edit_content_delete_dialog_content\n    val statusUiEditContentNameTitle = localizedString.status_ui_edit_content_name_title\n    val statusUiEditContentNameLabel = localizedString.status_ui_edit_content_name_label\n    val statusUiEditContentNameHint = localizedString.status_ui_edit_content_name_hint\n    val statusUiEditContentConfigShowingListTitle =\n        localizedString.status_ui_edit_content_config_showing_list_title\n    val statusUiEditContentConfigShowingListDescription =\n        localizedString.status_ui_edit_content_config_showing_list_description\n    val statusUiEditContentConfigHiddenListTitle =\n        localizedString.status_ui_edit_content_config_hidden_list_title\n    val statusUiUserDetailRelationshipMutuals =\n        localizedString.status_ui_user_detail_relationship_mutuals\n    val statusUiUserDetailRelationshipBlocking =\n        localizedString.status_ui_user_detail_relationship_blocking\n    val statusUiUserDetailRelationshipFollowing =\n        localizedString.status_ui_user_detail_relationship_following\n    val statusUiUserDetailRelationshipNotFollow =\n        localizedString.status_ui_user_detail_relationship_not_follow\n    val statusUiUserDetailRelationshipRequested =\n        localizedString.status_ui_user_detail_relationship_requested\n    val statusUiUserDetailRelationshipFollowBack =\n        localizedString.status_ui_user_detail_relationship_follow_back\n    val statusUiUserDetailRequestByTip = localizedString.status_ui_user_detail_request_by_tip\n    val statusUiRelationshipBtnDialogContentCancelFollow =\n        localizedString.status_ui_relationship_btn_dialog_content_cancel_follow\n    val statusUiRelationshipBtnDialogContentCancelFollowRequest =\n        localizedString.status_ui_relationship_btn_dialog_content_cancel_follow_request\n    val statusUiRelationshipBtnDialogContentCancelBlocking =\n        localizedString.status_ui_relationship_btn_dialog_content_cancel_blocking\n    val statusUiUserDetailFollowsYou = localizedString.status_ui_user_detail_follows_you\n    val statusUiUserDetailPosts = localizedString.status_ui_user_detail_posts\n    val statusUiUserDetailFollowerInfo = localizedString.status_ui_user_detail_follower_info\n    val statusUiUserDetailFollowingInfo = localizedString.status_ui_user_detail_following_info\n    val statusUiTopLabelContinuedThread = localizedString.status_ui_top_label_continued_thread\n    val statusUiUpdateDialogTitle = localizedString.status_ui_update_dialog_title\n    val statusUiUpdateDialogReleaseNote = localizedString.status_ui_update_dialog_release_note\n    val statusUiSwitchAccountDialogTitle = localizedString.status_ui_switch_account_dialog_title\n    val statusUiLikes = localizedString.status_ui_likes\n    val statusUiBookmarks = localizedString.status_ui_bookmarks\n    val statusUiEditProfile = localizedString.status_ui_edit_profile\n    val statusUiLogout = localizedString.status_ui_logout\n    val statusUiLogoutDialogContent = localizedString.status_ui_logout_dialog_content\n    val statusUiSearchAccountStatusHint = localizedString.status_ui_search_account_status_hint\n\n    val explorerSearchNoResults = localizedString.explorer_search_no_results\n    val explorerSearchBarHint = localizedString.explorer_search_bar_hint\n    val explorerSearchBarHintSpecializePlatform =\n        localizedString.explorer_search_bar_hint_specialize_platform\n    val explorerSearchTabTitleAuthor = localizedString.explorer_search_tab_title_author\n    val explorerSearchTabTitleStatus = localizedString.explorer_search_tab_title_status\n    val explorerSearchTabTitleHashtag = localizedString.explorer_search_tab_title_hashtag\n    val explorerSearchTabTitleServer = localizedString.explorer_search_tab_title_server\n    val explorerTabTitle = localizedString.explorer_tab_title\n    val explorerTabStatusTitle = localizedString.explorer_tab_status_title\n    val explorerTabUsersTitle = localizedString.explorer_tab_users_title\n    val explorerTabHashtagTitle = localizedString.explorer_tab_hashtag_title\n\n    val addProviderPageTitle = localizedString.add_provider_page_title\n    val addProviderPageConfirmButton = localizedString.add_provider_page_confirm_button\n    val searchPageTitle = localizedString.search_page_title\n    val searchResultNotFound = localizedString.search_result_not_found\n    val feedsImportPageTitle = localizedString.feeds_import_page_title\n    val feedsImportPageHint = localizedString.feeds_import_page_hint\n    val feedsImportButton = localizedString.feeds_import_button\n    val addFeedsPageTitle = localizedString.add_feeds_page_title\n    val addFeedsPageFeedsNameLabel = localizedString.add_feeds_page_feeds_name_label\n    val addFeedsPageFeedsNameHint = localizedString.add_feeds_page_feeds_name_hint\n    val addFeedsPageFeedsEmpty = localizedString.add_feeds_page_feeds_empty\n    val preAddFeedsNoResult = localizedString.pre_add_feeds_no_result\n    val preAddFeedsHint = localizedString.pre_add_feeds_hint\n    val preAddFeedsInputLabel1 = localizedString.pre_add_feeds_input_label_1\n    val preAddFeedsInputLabel2 = localizedString.pre_add_feeds_input_label_2\n    val emptyContentHintTitle = localizedString.empty_content_hint_title\n    val emptyContentHintDesc = localizedString.empty_content_hint_desc\n    val feedsAddContent = localizedString.feeds_add_content\n    val selectFeedsTypeScreenTitle = localizedString.select_feeds_type_screen_title\n    val feedsPreAddLoginDialogContent = localizedString.feeds_pre_add_login_dialog_content\n    val addFeedsPageEmptyNameTips = localizedString.add_feeds_page_empty_name_tips\n    val addFeedsPageEmptySourceTips = localizedString.add_feeds_page_empty_source_tips\n    val addFeedsChooseAuthDialogTitle = localizedString.add_feeds_choose_auth_dialog_title\n    val searchFeedsTitle = localizedString.search_feeds_title\n    val searchFeedsTitleHint = localizedString.search_feeds_title_hint\n    val feedsSelectAccountForPostStatus = localizedString.feeds_select_account_for_post_status\n    val feedsMixedConfigEditNewNameDialogTitle =\n        localizedString.feeds_mixed_config_edit_new_name_dialog_title\n    val feedsMixedConfigEditNewNameDialogLabel =\n        localizedString.feeds_mixed_config_edit_new_name_dialog_label\n    val feedsMixedConfigEditDeleteContentDialogMessage =\n        localizedString.feeds_mixed_config_edit_delete_content_dialog_message\n    val feedsMixedConfigNotFound = localizedString.feeds_mixed_config_not_found\n    val feedsImportBackDialogMessage = localizedString.feeds_import_back_dialog_message\n    val feedsDeleteConfirmContent = localizedString.feeds_delete_confirm_content\n    val feedsSelectTypeScreenTitle = localizedString.feeds_select_type_screen_title\n    val feedsSelectTypeScreenContent = localizedString.feeds_select_type_screen_content\n    val notificationTabTitle = localizedString.notification_tab_title\n    val notificationsAccountEmptyTip = localizedString.notifications_account_empty_tip\n    val notificationsTabAll = localizedString.notifications_tab_all\n    val notificationsTabMention = localizedString.notifications_tab_mention\n    val profilePageTitle = localizedString.profile_page_title\n    val profileSettingAboutTitle = localizedString.profile_setting_about_title\n    val profileSettingDonateDesc = localizedString.profile_setting_donate_desc\n    val profileSettingInlineVideoAutoPlay = localizedString.profile_setting_inline_video_auto_play\n    val profileSettingInlineVideoAutoPlaySubtitle =\n        localizedString.profile_setting_inline_video_auto_play_subtitle\n    val profileSettingAlwaysShowSensitiveContent =\n        localizedString.profile_setting_always_show_sensitive_content\n    val profileSettingAlwaysShowSensitiveContentSubtitle =\n        localizedString.profile_setting_always_show_sensitive_content_subtitle\n    val profileSettingDarkModeTitle = localizedString.profile_setting_dark_mode_title\n    val profileSettingDarkModeDark = localizedString.profile_setting_dark_mode_dark\n    val profileSettingDarkModeLight = localizedString.profile_setting_dark_mode_light\n    val profileSettingDarkModeFollowSystem = localizedString.profile_setting_dark_mode_follow_system\n    val profileSettingOpenSourceTitle = localizedString.profile_setting_open_source_title\n    val profileSettingOpenSourceDesc = localizedString.profile_setting_open_source_desc\n    val profileSettingOpenSourceFeedback = localizedString.profile_setting_open_source_feedback\n    val profileSettingOpenSourceFeedbackDesc =\n        localizedString.profile_setting_open_source_feedback_desc\n    val profileSettingOpenSourceFeedbackTelegram =\n        localizedString.profile_setting_open_source_feedback_telegram\n    val profileSettingOpenSourceFeedbackGithub =\n        localizedString.profile_setting_open_source_feedback_github\n    val profileSettingOpenSourceFeedbackEmail =\n        localizedString.profile_setting_open_source_feedback_email\n    val profileSettingLanguageTitle = localizedString.profile_setting_language_title\n    val profileSettingLanguageZh = localizedString.profile_setting_language_zh\n    val profileSettingLanguageEn = localizedString.profile_setting_language_en\n    val profileSettingLanguageSystem = localizedString.profile_setting_language_system\n    val profileSettingRatting = localizedString.profile_setting_ratting\n    val profileSettingRattingDesc = localizedString.profile_setting_ratting_desc\n    val profileSettingFontSize = localizedString.profile_setting_font_size\n    val profileSettingFontSizeDesc = localizedString.profile_setting_font_size_desc\n    val profileSettingFontSizeSmall = localizedString.profile_setting_font_size_small\n    val profileSettingFontSizeMedium = localizedString.profile_setting_font_size_medium\n    val profileSettingFontSizeLarge = localizedString.profile_setting_font_size_large\n    val profileSettingImmersiveNavBar = localizedString.profile_setting_immersive_nav_bar\n    val profileSettingImmersiveNavBarDesc = localizedString.profile_setting_immersive_nav_bar_desc\n    val profileSettingTimelinePosition = localizedString.profile_setting_timeline_position\n    val profileSettingTimelinePositionLastRead =\n        localizedString.profile_setting_timeline_position_last_read\n    val profileSettingTimelinePositionNewest =\n        localizedString.profile_setting_timeline_position_newest\n    val profileSettingThemeTitle = localizedString.profile_setting_theme_title\n    val profileSettingThemeDefault = localizedString.profile_setting_theme_default\n    val profileSettingThemeSystem = localizedString.profile_setting_theme_system\n    val profileAboutVersion = localizedString.profile_about_version\n    val profileAboutWebsite = localizedString.profile_about_website\n    val profileAboutDeveloper = localizedString.profile_about_developer\n    val profileAboutContractUs = localizedString.profile_about_contract_us\n    val profileAboutTelegram = localizedString.profile_about_telegram\n    val profileAboutPrivacyPolicy = localizedString.profile_about_privacy_policy\n    val profileSettingCheckForUpdate = localizedString.profile_setting_check_for_update\n    val profileSettingAlreadyLatestVersion = localizedString.profile_setting_already_latest_version\n    val profileSettingHaveNewVersion = localizedString.profile_setting_have_new_version\n    val profileDonatePageTitle = localizedString.profile_donate_page_title\n    val profileAccountNotLogin = localizedString.profile_account_not_login\n\n    val rss_source_detail_screen_title = localizedString.rss_source_detail_screen_title\n    val rss_source_detail_screen_custom_title =\n        localizedString.rss_source_detail_screen_custom_title\n    val rss_source_detail_screen_url = localizedString.rss_source_detail_screen_url\n    val rss_source_detail_screen_home_url = localizedString.rss_source_detail_screen_home_url\n    val rss_source_detail_screen_add_date = localizedString.rss_source_detail_screen_add_date\n    val rss_source_detail_screen_last_update_date =\n        localizedString.rss_source_detail_screen_last_update_date\n\n    val bluesky_protocol_name = localizedString.bluesky_protocol_name\n\n    val bsky_add_content_title = localizedString.bsky_add_content_title\n    val bsky_add_content_hosting_provider = localizedString.bsky_add_content_hosting_provider\n    val bsky_add_content_user_name = localizedString.bsky_add_content_user_name\n    val bsky_add_content_password = localizedString.bsky_add_content_password\n    val bsky_add_content_factor_token = localizedString.bsky_add_content_factor_token\n\n    val bsky_edit_content_title = localizedString.bsky_edit_content_title\n\n    val bsky_feeds_explorer_more = localizedString.bsky_feeds_explorer_more\n    val bsky_feeds_explorer_creator_label = localizedString.bsky_feeds_explorer_creator_label\n    val bsky_feeds_explorer_liked_by = localizedString.bsky_feeds_explorer_liked_by\n    val bsky_feeds_detail_creator_prefix = localizedString.bsky_feeds_detail_creator_prefix\n    val bsky_feeds_item_subtitle = localizedString.bsky_feeds_item_subtitle\n\n    val bsky_feeds_following_name = localizedString.bsky_feeds_following_name\n\n    val bsky_feeds_user_posts = localizedString.bsky_feeds_user_posts\n    val bsky_feeds_user_replies = localizedString.bsky_feeds_user_replies\n    val bsky_feeds_user_medias = localizedString.bsky_feeds_user_medias\n    val bsky_feeds_user_likes = localizedString.bsky_feeds_user_likes\n\n    val bsky_user_detail_action_blocked_list = localizedString.bsky_user_detail_action_blocked_list\n    val bsky_user_detail_action_muted_list = localizedString.bsky_user_detail_action_muted_list\n    val bsky_user_detail_action_mute_user = localizedString.bsky_user_detail_action_mute_user\n    val bsky_user_detail_action_unmute_user = localizedString.bsky_user_detail_action_unmute_user\n    val bsky_user_detail_action_block_user = localizedString.bsky_user_detail_action_block_user\n    val bsky_user_detail_action_unblock_user = localizedString.bsky_user_detail_action_unblock_user\n    val bsky_user_detail_action_block_user_dialog_message =\n        localizedString.bsky_user_detail_action_block_user_dialog_message\n    val bsky_user_detail_action_mute_user_dialog_message =\n        localizedString.bsky_user_detail_action_mute_user_dialog_message\n\n    val bsky_edit_profile_title = localizedString.bsky_edit_profile_title\n\n    val activity_pub_login_exception = localizedString.activity_pub_login_exception\n\n    val activity_pub_home_timeline = localizedString.activity_pub_home_timeline\n    val activity_pub_local_timeline = localizedString.activity_pub_local_timeline\n    val activity_pub_public_timeline = localizedString.activity_pub_public_timeline\n\n    val activity_pub_instance_detail_language_label =\n        localizedString.activity_pub_instance_detail_language_label\n    val activity_pub_instance_detail_active_month_label =\n        localizedString.activity_pub_instance_detail_active_month_label\n\n    val activity_pub_user_detail_join_date = localizedString.activity_pub_user_detail_join_date\n    val activity_pub_user_detail_tab_post = localizedString.activity_pub_user_detail_tab_post\n    val activity_pub_user_detail_tab_replies = localizedString.activity_pub_user_detail_tab_replies\n    val activity_pub_user_detail_tab_media = localizedString.activity_pub_user_detail_tab_media\n    val activity_pub_user_detail_tab_about = localizedString.activity_pub_user_detail_tab_about\n    val activity_pub_user_detail_tab_about_joined =\n        localizedString.activity_pub_user_detail_tab_about_joined\n    val activity_pub_user_detail_menu_block = localizedString.activity_pub_user_detail_menu_block\n    val activity_pub_user_detail_menu_block_domain =\n        localizedString.activity_pub_user_detail_menu_block_domain\n    val activity_pub_user_detail_menu_unblock_domain =\n        localizedString.activity_pub_user_detail_menu_unblock_domain\n    val activity_pub_user_detail_menu_edit_private_note =\n        localizedString.activity_pub_user_detail_menu_edit_private_note\n    val activity_pub_user_detail_menu_edit_private_note_dialog_hint =\n        localizedString.activity_pub_user_detail_menu_edit_private_note_dialog_hint\n    val activity_pub_user_detail_dialog_content_block =\n        localizedString.activity_pub_user_detail_dialog_content_block\n    val activity_pub_user_detail_dialog_content_block_domain =\n        localizedString.activity_pub_user_detail_dialog_content_block_domain\n    val activity_pub_user_menu_blocked_user_list =\n        localizedString.activity_pub_user_menu_blocked_user_list\n    val activity_pub_user_menu_muted_user_list =\n        localizedString.activity_pub_user_menu_muted_user_list\n    val activity_pub_user_detail_menu_mute_user =\n        localizedString.activity_pub_user_detail_menu_mute_user\n    val activity_pub_user_detail_menu_unmute_user =\n        localizedString.activity_pub_user_detail_menu_unmute_user\n    val activity_pub_mute_user_bottom_sheet_title =\n        localizedString.activity_pub_mute_user_bottom_sheet_title\n    val activity_pub_mute_user_bottom_sheet_role1 =\n        localizedString.activity_pub_mute_user_bottom_sheet_role1\n    val activity_pub_mute_user_bottom_sheet_role2 =\n        localizedString.activity_pub_mute_user_bottom_sheet_role2\n    val activity_pub_mute_user_bottom_sheet_role3 =\n        localizedString.activity_pub_mute_user_bottom_sheet_role3\n    val activity_pub_mute_user_bottom_sheet_role4 =\n        localizedString.activity_pub_mute_user_bottom_sheet_role4\n    val activity_pub_mute_user_bottom_sheet_btn_mute =\n        localizedString.activity_pub_mute_user_bottom_sheet_btn_mute\n\n    val activity_pub_about = localizedString.activity_pub_about\n    val activity_pub_about_rule_title = localizedString.activity_pub_about_rule_title\n    val activity_pub_trends_tag = localizedString.activity_pub_trends_tag\n    val activity_pub_trends_tag_description = localizedString.activity_pub_trends_tag_description\n    val activity_pub_trends_status = localizedString.activity_pub_trends_status\n\n    val input_media_desc_page_title = localizedString.input_media_desc_page_title\n    val input_media_desc_input_hint = localizedString.input_media_desc_input_hint\n\n    val post_status_page_title = localizedString.post_status_page_title\n    val post_screen_input_hint = localizedString.post_screen_input_hint\n    val post_screen_media_descriptor_placeholder =\n        localizedString.post_screen_media_descriptor_placeholder\n    val post_status_poll_item_hint = localizedString.post_status_poll_item_hint\n    val post_status_poll_duration = localizedString.post_status_poll_duration\n    val post_status_poll_function_title = localizedString.post_status_poll_function_title\n    val post_status_poll_single = localizedString.post_status_poll_single\n    val post_status_poll_multiple = localizedString.post_status_poll_multiple\n    val post_status_poll_style_select_dialog_title =\n        localizedString.post_status_poll_style_select_dialog_title\n    val post_status_poll_is_empty = localizedString.post_status_poll_is_empty\n    val post_status_media_is_not_upload = localizedString.post_status_media_is_not_upload\n\n    val add_instance_screen_title = localizedString.add_instance_screen_title\n    val add_instance_input_service_hint = localizedString.add_instance_input_service_hint\n    val add_instance_input_service_error = localizedString.add_instance_input_service_error\n\n    val activity_pub_content_tab_home = localizedString.activity_pub_content_tab_home\n    val activity_pub_content_tab_local_timeline =\n        localizedString.activity_pub_content_tab_local_timeline\n    val activity_pub_content_tab_public_timeline =\n        localizedString.activity_pub_content_tab_public_timeline\n    val activity_pub_content_tab_trending = localizedString.activity_pub_content_tab_trending\n\n    val activity_pub_hashtag_timeline_description =\n        localizedString.activity_pub_hashtag_timeline_description\n    val activity_pub_hashtag_unfollow_dialog_message =\n        localizedString.activity_pub_hashtag_unfollow_dialog_message\n\n    val activity_pub_edit_account_info_label_about =\n        localizedString.activity_pub_edit_account_info_label_about\n\n    val activity_pub_edit_content_screen_config_not_found =\n        localizedString.activity_pub_edit_content_screen_config_not_found\n\n    val activity_pub_user_list_empty = localizedString.activity_pub_user_list_empty\n\n    val activity_pub_muted_user_list_empty = localizedString.activity_pub_muted_user_list_empty\n    val activity_pub_muted_user_list_unmute = localizedString.activity_pub_muted_user_list_unmute\n\n    val activity_pub_blocked_user_list_empty = localizedString.activity_pub_blocked_user_list_empty\n    val activity_pub_blocked_user_list_unblock =\n        localizedString.activity_pub_blocked_user_list_unblock\n\n    val activity_pub_bookmarks_list_title = localizedString.activity_pub_bookmarks_list_title\n    val activity_pub_favourites_list_title = localizedString.activity_pub_favourites_list_title\n\n    val activity_pub_followed_tags_screen_title =\n        localizedString.activity_pub_followed_tags_screen_title\n\n    val activity_pub_filters_list_page_title = localizedString.activity_pub_filters_list_page_title\n    val activity_pub_filters_active = localizedString.activity_pub_filters_active\n    val activity_pub_filters_expired = localizedString.activity_pub_filters_expired\n\n    val activity_pub_select_platform_title = localizedString.activity_pub_select_platform_title\n    val activity_pub_select_platform_text_hint =\n        localizedString.activity_pub_select_platform_text_hint\n\n    val activity_pub_filter_edit_title = localizedString.activity_pub_filter_edit_title\n    val activity_pub_filter_edit_input_title_label =\n        localizedString.activity_pub_filter_edit_input_title_label\n    val activity_pub_filter_edit_input_title_hint =\n        localizedString.activity_pub_filter_edit_input_title_hint\n    val activity_pub_filter_edit_duration = localizedString.activity_pub_filter_edit_duration\n    val activity_pub_filter_edit_duration_permanent =\n        localizedString.activity_pub_filter_edit_duration_permanent\n    val activity_pub_filter_edit_duration_thirty_minutes =\n        localizedString.activity_pub_filter_edit_duration_thirty_minutes\n    val activity_pub_filter_edit_duration_one_hour =\n        localizedString.activity_pub_filter_edit_duration_one_hour\n    val activity_pub_filter_edit_duration_twelve_hours =\n        localizedString.activity_pub_filter_edit_duration_twelve_hours\n    val activity_pub_filter_edit_duration_one_day =\n        localizedString.activity_pub_filter_edit_duration_one_day\n    val activity_pub_filter_edit_duration_three_day =\n        localizedString.activity_pub_filter_edit_duration_three_day\n    val activity_pub_filter_edit_duration_one_week =\n        localizedString.activity_pub_filter_edit_duration_one_week\n    val activity_pub_filter_edit_duration_custom =\n        localizedString.activity_pub_filter_edit_duration_custom\n    val activity_pub_filter_edit_duration_finish_subtitle =\n        localizedString.activity_pub_filter_edit_duration_finish_subtitle\n\n    val activity_pub_filter_edit_keyword_title =\n        localizedString.activity_pub_filter_edit_keyword_title\n    val activity_pub_filter_edit_keyword_dialog_title =\n        localizedString.activity_pub_filter_edit_keyword_dialog_title\n    val activity_pub_filter_edit_keyword_dialog_hint =\n        localizedString.activity_pub_filter_edit_keyword_dialog_hint\n    val activity_pub_filter_edit_keyword_remove_keyword_dialog_content =\n        localizedString.activity_pub_filter_edit_keyword_remove_keyword_dialog_content\n    val activity_pub_filter_edit_keyword_list_title =\n        localizedString.activity_pub_filter_edit_keyword_list_title\n    val activity_pub_filter_edit_keyword_list_desc =\n        localizedString.activity_pub_filter_edit_keyword_list_desc\n    val activity_pub_filter_edit_context_title =\n        localizedString.activity_pub_filter_edit_context_title\n    val activity_pub_filter_edit_context_selector_title =\n        localizedString.activity_pub_filter_edit_context_selector_title\n    val activity_pub_filter_edit_context_home =\n        localizedString.activity_pub_filter_edit_context_home\n    val activity_pub_filter_edit_context_notification =\n        localizedString.activity_pub_filter_edit_context_notification\n    val activity_pub_filter_edit_context_timeline =\n        localizedString.activity_pub_filter_edit_context_timeline\n    val activity_pub_filter_edit_context_thread =\n        localizedString.activity_pub_filter_edit_context_thread\n    val activity_pub_filter_edit_context_account =\n        localizedString.activity_pub_filter_edit_context_account\n    val activity_pub_filter_edit_empty_context =\n        localizedString.activity_pub_filter_edit_empty_context\n    val activity_pub_filter_edit_warning_title =\n        localizedString.activity_pub_filter_edit_warning_title\n    val activity_pub_filter_edit_warning_desc =\n        localizedString.activity_pub_filter_edit_warning_desc\n    val activity_pub_filter_edit_delete_content =\n        localizedString.activity_pub_filter_edit_delete_content\n    val activity_pub_filter_edit_back_dialog = localizedString.activity_pub_filter_edit_back_dialog\n    val activity_pub_filter_edit_whole_word = localizedString.activity_pub_filter_edit_whole_word\n\n    val activity_pub_explorer_tab_status_title =\n        localizedString.activity_pub_explorer_tab_status_title\n    val activity_pub_explorer_tab_users_title =\n        localizedString.activity_pub_explorer_tab_users_title\n    val activity_pub_explorer_tab_hashtag_title =\n        localizedString.activity_pub_explorer_tab_hashtag_title\n\n    val activity_pub_created_list_title = localizedString.activity_pub_created_list_title\n    val activity_pub_add_list_title = localizedString.activity_pub_add_list_title\n    val activity_pub_add_list_name = localizedString.activity_pub_add_list_name\n    val activity_pub_add_list_replies = localizedString.activity_pub_add_list_replies\n    val activity_pub_add_list_replies_non = localizedString.activity_pub_add_list_replies_non\n    val activity_pub_add_list_replies_list = localizedString.activity_pub_add_list_replies_list\n    val activity_pub_add_list_replies_followers =\n        localizedString.activity_pub_add_list_replies_followers\n    val activity_pub_add_list_accounts = localizedString.activity_pub_add_list_accounts\n    val activity_pub_add_list_hide_in_timeline =\n        localizedString.activity_pub_add_list_hide_in_timeline\n    val activity_pub_add_list_hide_in_timeline_desc =\n        localizedString.activity_pub_add_list_hide_in_timeline_desc\n    val activity_pub_add_list_remove_user_message =\n        localizedString.activity_pub_add_list_remove_user_message\n    val activity_pub_add_list_name_is_empty = localizedString.activity_pub_add_list_name_is_empty\n    val activity_pub_add_list_back_reminder = localizedString.activity_pub_add_list_back_reminder\n    val activity_pub_search_user_placeholder = localizedString.activity_pub_search_user_placeholder\n    val activity_pub_list_delete_confirm = localizedString.activity_pub_list_delete_confirm\n\n    val mixed_content_subtitle_1 = localizedString.mixed_content_subtitle_1\n    val mixed_content_subtitle_2 = localizedString.mixed_content_subtitle_2\n    val date_time_ago = localizedString.date_time_ago\n    val date_time_ago_prefix = localizedString.date_time_ago_prefix\n    val date_time_ago_suffix = localizedString.date_time_ago_suffix\n    val date_time_day = localizedString.date_time_day\n    val date_time_hour = localizedString.date_time_hour\n    val date_time_minute = localizedString.date_time_minute\n    val date_time_second = localizedString.date_time_second\n\n    val authentication_page_normal_title = localizedString.authentication_page_normal_title\n    val authentication_page_failed_title = localizedString.authentication_page_failed_title\n    val main_drawer_title = localizedString.main_drawer_title\n    val main_drawer_mixed_item_subtitle_1 = localizedString.main_drawer_mixed_item_subtitle_1\n    val main_drawer_mixed_item_subtitle_2 = localizedString.main_drawer_mixed_item_subtitle_2\n    val main_drawer_settings = localizedString.main_drawer_settings\n    val main_drawer_donate = localizedString.main_drawer_donate\n    val main_request_notification_dialog_title =\n        localizedString.main_request_notification_dialog_title\n    val main_request_notification_dialog_reject_button =\n        localizedString.main_request_notification_dialog_reject_button\n    val main_request_notification_dialog_allow_button =\n        localizedString.main_request_notification_dialog_allow_button\n    val main_request_notification_dialog_content =\n        localizedString.main_request_notification_dialog_content\n    val status_ui_embed_quote_unavailable = localizedString.status_ui_embed_quote_unavailable\n    val status_ui_action_unforward = localizedString.status_ui_action_unforward\n    val status_ui_quote_approval_public = localizedString.status_ui_quote_approval_public\n    val status_ui_quote_approval_follower = localizedString.status_ui_quote_approval_follower\n    val status_ui_quote_approval_nobody = localizedString.status_ui_quote_approval_nobody\n    val status_ui_visibility = localizedString.status_ui_visibility\n    val setting_item_amoled_mode = localizedString.setting_item_amoled_mode\n    val setting_item_amoled_mode_description = localizedString.setting_item_amoled_mode_description\n    val setting_group_appearance = localizedString.setting_group_appearance\n    val setting_group_appearance_subtitle = localizedString.setting_group_appearance_subtitle\n    val setting_group_behavior = localizedString.setting_group_behavior\n    val setting_group_behavior_subtitle = localizedString.setting_group_behavior_subtitle\n    val setting_item_home_tab_next_title = localizedString.setting_item_home_tab_next_title\n    val setting_item_home_tab_next_subtitle = localizedString.setting_item_home_tab_next_subtitle\n    val setting_item_home_tab_refresh_title = localizedString.setting_item_home_tab_refresh_title\n    val setting_item_home_tab_refresh_subtitle =\n        localizedString.setting_item_home_tab_refresh_subtitle\n    val setting_item_open_url_by_system_title =\n        localizedString.setting_item_open_url_by_system_title\n    val setting_item_open_url_by_system_subtitle =\n        localizedString.setting_item_open_url_by_system_subtitle\n    val setting_item_solid_bar_background_title =\n        localizedString.setting_item_solid_bar_background_title\n    val setting_item_solid_bar_background_subtitle =\n        localizedString.setting_item_solid_bar_background_subtitle\n}\n"
  },
  {
    "path": "plugins/activitypub-app/.gitignore",
    "content": "/build"
  },
  {
    "path": "plugins/activitypub-app/build.gradle.kts",
    "content": "plugins {\n    id(\"fread.project.feature.kmp\")\n    id(\"com.google.devtools.ksp\")\n    id(\"kotlin-parcelize\")\n    alias(libs.plugins.room)\n}\n\nandroid {\n    namespace = \"com.zhangke.fread.activitypub.app\"\n    sourceSets {\n        getByName(\"main\") {\n            assets.srcDirs(\"src/androidMain/assets\")\n            res.srcDirs(\"src/androidMain/res\")\n            manifest.srcFile(\"src/androidMain/AndroidManifest.xml\")\n        }\n    }\n}\n\nkotlin {\n    sourceSets {\n        commonMain {\n            dependencies {\n                implementation(project(path = \":framework\"))\n                implementation(project(path = \":commonbiz:common\"))\n                implementation(project(path = \":bizframework:status-provider\"))\n                implementation(project(path = \":commonbiz:status-ui\"))\n                implementation(project(path = \":commonbiz:sharedscreen\"))\n                implementation(project(path = \":commonbiz:analytics\"))\n\n                implementation(compose.components.resources)\n\n                implementation(libs.arrow.core)\n                implementation(libs.activity.pub.client)\n\n                implementation(libs.kotlinx.serialization.core)\n                implementation(libs.kotlinx.serialization.json)\n\n                implementation(libs.haze)\n                implementation(libs.haze.materials)\n\n                implementation(libs.jetbrains.lifecycle.viewmodel)\n                implementation(libs.androidx.constraintlayout.compose.kmp)\n                implementation(libs.imageLoader)\n                implementation(libs.androidx.room)\n                implementation(libs.auto.service.annotations)\n                implementation(libs.androidx.paging.common)\n                implementation(libs.leftright)\n\n                implementation(libs.krouter.runtime)\n            }\n        }\n        commonTest {\n            dependencies {\n                implementation(kotlin(\"test\"))\n            }\n        }\n        androidMain {\n            dependencies {\n                implementation(libs.androidx.core.ktx)\n                implementation(libs.bundles.androidx.activity)\n                implementation(libs.androidx.browser)\n            }\n        }\n    }\n    configureCommonMainKsp()\n}\n\ndependencies {\n    kspAll(libs.androidx.room.compiler)\n    kspAll(libs.auto.service.ksp)\n    kspAll(libs.krouter.collecting.compiler)\n}\n\n\ncompose {\n    resources {\n        publicResClass = false\n        packageOfResClass = \"com.zhangke.fread.activitypub.app\"\n        generateResClass = always\n    }\n}\n\nroom {\n    schemaDirectory(\"$projectDir/schemas\")\n}\n"
  },
  {
    "path": "plugins/activitypub-app/consumer-rules.pro",
    "content": ""
  },
  {
    "path": "plugins/activitypub-app/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguardFiles setting in build.gradle.kts.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n\n# Uncomment this to preserve the line number information for\n# debugging stack traces.\n#-keepattributes SourceFile,LineNumberTable\n\n# If you keep the line number information, uncomment this to\n# hide the original source file name.\n#-renamesourcefileattribute SourceFile"
  },
  {
    "path": "plugins/activitypub-app/src/androidMain/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <uses-permission android:name=\"android.permission.POST_NOTIFICATIONS\"/>\n\n    <application>\n\n        <activity\n            android:name=\"com.zhangke.fread.activitypub.app.internal.auth.ActivityPubOAuthRedirectActivity\"\n            android:exported=\"true\">\n            <intent-filter>\n                <action android:name=\"android.intent.action.VIEW\" />\n\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <category android:name=\"android.intent.category.BROWSABLE\" />\n\n                <data\n                    android:host=\"fread.xyz\"\n                    android:scheme=\"freadapp\" />\n            </intent-filter>\n        </activity>\n    </application>\n</manifest>\n"
  },
  {
    "path": "plugins/activitypub-app/src/androidMain/kotlin/com/zhangke/fread/activitypub/app/ActivityPubAndroidModule.kt",
    "content": "package com.zhangke.fread.activitypub.app\n\nimport androidx.room.Room\nimport com.zhangke.fread.activitypub.app.internal.db.ActivityPubDatabases\nimport com.zhangke.fread.activitypub.app.internal.db.ActivityPubLoggedAccountDatabase\nimport com.zhangke.fread.activitypub.app.internal.db.status.ActivityPubStatusDatabases\nimport com.zhangke.fread.activitypub.app.internal.db.status.ActivityPubStatusReadStateDatabases\nimport com.zhangke.fread.activitypub.app.internal.push.ActivityPubPushManager\nimport com.zhangke.fread.activitypub.app.internal.push.PushInfoDatabase\nimport com.zhangke.fread.activitypub.app.internal.push.PushInfoRepo\nimport com.zhangke.fread.activitypub.app.internal.push.notification.PushNotificationManager\nimport org.koin.android.ext.koin.androidContext\nimport org.koin.core.module.Module\nimport org.koin.core.module.dsl.factoryOf\nimport org.koin.core.module.dsl.singleOf\n\nactual fun Module.createPlatformModule() {\n    single<ActivityPubDatabases> {\n        Room.databaseBuilder(\n            androidContext(),\n            ActivityPubDatabases::class.java,\n            ActivityPubDatabases.DB_NAME,\n        ).build()\n    }\n    single<ActivityPubLoggedAccountDatabase> {\n        Room.databaseBuilder(\n            androidContext(),\n            ActivityPubLoggedAccountDatabase::class.java,\n            ActivityPubLoggedAccountDatabase.DB_NAME,\n        ).build()\n    }\n    single<ActivityPubStatusDatabases> {\n        Room.databaseBuilder(\n            androidContext(),\n            ActivityPubStatusDatabases::class.java,\n            ActivityPubStatusDatabases.DB_NAME,\n        ).addMigrations(ActivityPubStatusDatabases.MIGRATION_1_2)\n            .build()\n    }\n    single<ActivityPubStatusReadStateDatabases> {\n        Room.databaseBuilder(\n            androidContext(),\n            ActivityPubStatusReadStateDatabases::class.java,\n            ActivityPubStatusReadStateDatabases.DB_NAME,\n        ).build()\n    }\n    single<PushInfoDatabase> {\n        Room.databaseBuilder(\n            androidContext(),\n            PushInfoDatabase::class.java,\n            PushInfoDatabase.DB_NAME,\n        ).build()\n    }\n    singleOf(::PushInfoRepo)\n    factoryOf(::PushNotificationManager)\n    factoryOf(::ActivityPubPushManager)\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/androidMain/kotlin/com/zhangke/fread/activitypub/app/internal/auth/ActivityPubOAuthRedirectActivity.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.auth\n\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport androidx.lifecycle.lifecycleScope\nimport com.zhangke.framework.toast.toast\nimport com.zhangke.fread.common.browser.OAuthHandler\nimport com.zhangke.fread.localization.LocalizedString\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.launch\nimport org.koin.android.ext.android.inject\n\n/**\n * Created by ZhangKe on 2022/12/4.\n */\nclass ActivityPubOAuthRedirectActivity : ComponentActivity() {\n\n    private val oauthHandler by inject<OAuthHandler>()\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n\n        val code = intent.data?.getQueryParameter(\"code\")\n        lifecycleScope.launch {\n            if (code.isNullOrEmpty()) {\n                toast(org.jetbrains.compose.resources.getString(LocalizedString.activity_pub_login_exception))\n                delay(2000)\n            } else {\n                oauthHandler.onOauthSuccess(code)\n            }\n            finish()\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/androidMain/kotlin/com/zhangke/fread/activitypub/app/internal/push/ActivityPubPushManager.android.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.push\n\nimport android.util.Log\nimport com.zhangke.activitypub.entities.SubscribePushRequestEntity\nimport com.zhangke.activitypub.entities.SubscriptionAlertsEntity\nimport com.zhangke.fread.activitypub.app.internal.auth.ActivityPubClientManager\nimport com.zhangke.fread.common.config.FreadConfigManager\nimport com.zhangke.fread.common.push.IPushManager\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.krouter.KRouter\nimport kotlin.io.encoding.Base64\nimport kotlin.io.encoding.ExperimentalEncodingApi\n\nactual class ActivityPubPushManager (\n    private val freadConfigManager: FreadConfigManager,\n    private val clientManager: ActivityPubClientManager,\n    private val pushInfoRepo: PushInfoRepo,\n) {\n\n    private val pushManager: IPushManager? by lazy {\n        KRouter.getServices<IPushManager>().firstOrNull()\n    }\n\n    @OptIn(ExperimentalEncodingApi::class)\n    actual suspend fun subscribe(locator: PlatformLocator, accountId: String) {\n        Log.d(\"PushManager\", \"subscribe for ${locator.accountUri}, $accountId\")\n        val pushManager = pushManager ?: return\n        val deviceId = freadConfigManager.getDeviceId()\n        val encodedAccountId = Base64.UrlSafe.encode(accountId.encodeToByteArray())\n        val endpointUrl =\n            pushManager.getEndpointUrl(encodedAccountId, deviceId)\n        val keys = CryptoUtil.generate()\n        val subscribeRequest = SubscribePushRequestEntity(\n            subscription = SubscribePushRequestEntity.Subscription(\n                endpoint = endpointUrl,\n                keys = SubscribePushRequestEntity.Subscription.Keys(\n                    p256dh = keys.encodedPublicKey,\n                    auth = keys.authKey,\n                ),\n            ),\n            data = SubscribePushRequestEntity.Data(\n                alerts = SubscriptionAlertsEntity(\n                    mention = true,\n                    status = true,\n                    follow = true,\n                    reblog = true,\n                    followRequest = true,\n                    favourite = true,\n                    poll = true,\n                    update = true,\n                ),\n                policy = \"all\",\n            ),\n        )\n        Log.d(\"PushManager\", \"subscribe end point: $endpointUrl\")\n        Log.d(\"PushManager\", \"subscribe keys : $keys\")\n        clientManager.getClient(locator)\n            .pushRepo\n            .subscribePush(subscribeRequest)\n            .onSuccess {\n                Log.d(\"PushManager\", \"subscribe success: $it\")\n                registerRelay(pushManager, deviceId, accountId, encodedAccountId, keys)\n            }.onFailure {\n                Log.d(\"PushManager\", \"subscribe failed: $it\")\n            }\n    }\n\n    actual suspend fun unsubscribe(locator: PlatformLocator, accountId: String) {\n        Log.d(\"PushManager\", \"unsubscribe for ${locator.accountUri}, $accountId\")\n        val pushManager = pushManager ?: return\n        clientManager.getClient(locator)\n            .pushRepo\n            .removeSubscription()\n            .onSuccess {\n                Log.d(\"PushManager\", \"unsubscribe success.\")\n            }.onFailure {\n                Log.d(\"PushManager\", \"unsubscribe failed: $it\")\n            }\n        unregisterRelay(pushManager, freadConfigManager.getDeviceId(), accountId)\n    }\n\n    private suspend fun registerRelay(\n        pushManager: IPushManager,\n        deviceId: String,\n        accountId: String,\n        encodedAccountId: String,\n        keys: CryptoKeys,\n    ) {\n        pushManager.registerToRelay(encodedAccountId, deviceId)\n            .onSuccess {\n                Log.d(\n                    \"PushManager\",\n                    \"registerRelay success, account id is $accountId, encoded: $encodedAccountId\"\n                )\n                val pushInfo = PushInfo(\n                    accountId = accountId,\n                    publicKey = keys.publicKey,\n                    privateKey = keys.privateKey,\n                    authKey = keys.authKey,\n                )\n                pushInfoRepo.insert(pushInfo)\n            }.onFailure {\n                Log.d(\"PushManager\", \"registerRelay failed: $it\")\n            }\n    }\n\n    @OptIn(ExperimentalEncodingApi::class)\n    private suspend fun unregisterRelay(\n        pushManager: IPushManager,\n        deviceId: String,\n        accountId: String,\n    ) {\n        Log.d(\"PushManager\", \"unregisterRelay: $accountId\")\n        val encodedAccountId = Base64.UrlSafe.encode(accountId.encodeToByteArray())\n        pushManager.unregisterToRelay(encodedAccountId, deviceId)\n            .onSuccess {\n                Log.d(\"PushManager\", \"unregisterRelay success\")\n            }.onFailure {\n                Log.d(\"PushManager\", \"unregisterRelay failed: $it\")\n            }\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/androidMain/kotlin/com/zhangke/fread/activitypub/app/internal/push/ActivityPubPushMessageReceiverHelper.android.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.push\n\nimport android.util.Log\nimport com.zhangke.framework.architect.json.fromJson\nimport com.zhangke.framework.architect.json.getLongOrNull\nimport com.zhangke.framework.architect.json.getStringOrNull\nimport com.zhangke.framework.architect.json.globalJson\nimport com.zhangke.framework.utils.appContext\nimport com.zhangke.fread.activitypub.app.internal.push.notification.ActivityPubPushMessage\nimport com.zhangke.fread.activitypub.app.internal.push.notification.PushNotificationManager\nimport com.zhangke.fread.activitypub.app.internal.repo.account.ActivityPubLoggedAccountRepo\nimport com.zhangke.fread.common.push.PushMessage\nimport com.zhangke.fread.status.account.LoggedAccount\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.SupervisorJob\nimport kotlinx.coroutines.launch\nimport kotlinx.serialization.json.JsonObject\nimport org.koin.core.component.KoinComponent\nimport org.koin.core.component.inject\nimport kotlin.io.encoding.Base64\nimport kotlin.io.encoding.ExperimentalEncodingApi\n\nactual class ActivityPubPushMessageReceiverHelper : KoinComponent {\n\n    private val coroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob())\n\n    private val pushNotificationManager: PushNotificationManager by inject()\n    private val accountRepo: ActivityPubLoggedAccountRepo by inject()\n    private val pushInfoRepo: PushInfoRepo by inject()\n\n    @OptIn(ExperimentalEncodingApi::class)\n    actual fun onReceiveNewMessage(message: PushMessage) {\n        val accountId = String(Base64.UrlSafe.decode(message.encodedAccountId))\n        coroutineScope.launch {\n            val info = pushInfoRepo.getPushInfo(accountId) ?: return@launch\n            val account =\n                accountRepo.queryAll().firstOrNull { it.userId == accountId } ?: return@launch\n            val pushMessage = try {\n                CryptoUtil.decryptData(\n                    keys = info,\n                    serverPublicKeyEncoded = message.cryptoKey,\n                    data = Base64.decode(message.messageData),\n                    encryptionSalt = message.encryption,\n                    contentEncoding = message.contentEncoding,\n                ).let { convertDataToNotification(it, account) }\n            } catch (e: Throwable) {\n                Log.d(\"PushManager\", \"decrypted data error: ${e.stackTraceToString()}\")\n                null\n            }\n            Log.d(\"PushManager\", \"pushMessage: $pushMessage\")\n            if (pushMessage != null) {\n                pushNotificationManager.onReceiveNewMessage(appContext, pushMessage)\n            }\n        }\n    }\n\n    private fun convertDataToNotification(\n        data: String,\n        account: LoggedAccount,\n    ): ActivityPubPushMessage? {\n        val jsonObject = globalJson.fromJson<JsonObject>(data)\n        return ActivityPubPushMessage(\n            accessToken = jsonObject.getStringOrNull(\"access_token\"),\n            preferredLocale = jsonObject.getStringOrNull(\"preferred_locale\"),\n            notificationId = jsonObject.getLongOrNull(\"notification_id\"),\n            notificationType = jsonObject.getStringOrNull(\"notification_type\")\n                ?.let { ActivityPubPushMessage.Type.fromName(it) }\n                ?: return null,\n            icon = jsonObject.getStringOrNull(\"icon\") ?: return null,\n            title = jsonObject.getStringOrNull(\"title\") ?: return null,\n            body = jsonObject.getStringOrNull(\"body\") ?: return null,\n            account = account,\n        )\n    }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/androidMain/kotlin/com/zhangke/fread/activitypub/app/internal/push/CryptoUtil.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.push\n\nimport android.util.Base64\nimport java.io.ByteArrayOutputStream\nimport java.io.IOException\nimport java.nio.charset.StandardCharsets\nimport java.security.KeyFactory\nimport java.security.KeyPairGenerator\nimport java.security.PublicKey\nimport java.security.SecureRandom\nimport java.security.interfaces.ECPublicKey\nimport java.security.spec.ECGenParameterSpec\nimport java.security.spec.PKCS8EncodedKeySpec\nimport java.security.spec.X509EncodedKeySpec\nimport java.util.Arrays\nimport javax.crypto.Cipher\nimport javax.crypto.KeyAgreement\nimport javax.crypto.Mac\nimport javax.crypto.spec.GCMParameterSpec\nimport javax.crypto.spec.SecretKeySpec\n\nobject CryptoUtil {\n\n    private const val EC_CURVE_NAME = \"prime256v1\"\n\n    private val P256_HEAD = byteArrayOf(\n        0x30.toByte(),\n        0x59.toByte(),\n        0x30.toByte(),\n        0x13.toByte(),\n        0x06.toByte(),\n        0x07.toByte(),\n        0x2a.toByte(),\n        0x86.toByte(),\n        0x48.toByte(),\n        0xce.toByte(),\n        0x3d.toByte(),\n        0x02.toByte(),\n        0x01.toByte(),\n        0x06.toByte(),\n        0x08.toByte(),\n        0x2a.toByte(),\n        0x86.toByte(),\n        0x48.toByte(),\n        0xce.toByte(),\n        0x3d.toByte(),\n        0x03.toByte(),\n        0x01.toByte(),\n        0x07.toByte(),\n        0x03.toByte(),\n        0x42.toByte(),\n        0x00.toByte()\n    )\n\n    private val reBase64UrlSafe = \"\"\"[_-]\"\"\".toRegex()\n\n    fun generate(): CryptoKeys {\n        val base64Flag = Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING\n        val generator = KeyPairGenerator.getInstance(\"EC\")\n        generator.initialize(ECGenParameterSpec(EC_CURVE_NAME))\n        val keyPair = generator.generateKeyPair()\n        val privateKey = Base64.encodeToString(keyPair.private.encoded, base64Flag)\n        val publicKey = Base64.encodeToString(keyPair.public.encoded, base64Flag)\n        val authKey = ByteArray(16)\n        val secureRandom = SecureRandom()\n        secureRandom.nextBytes(authKey)\n        val pushAuthKey = Base64.encodeToString(authKey, base64Flag)\n        return CryptoKeys(\n            privateKey = privateKey,\n            publicKey = publicKey,\n            encodedPublicKey = Base64.encodeToString(\n                serializeRawPublicKey(keyPair.public),\n                base64Flag,\n            ),\n            authKey = pushAuthKey,\n        )\n    }\n\n    private fun serializeRawPublicKey(key: PublicKey): ByteArray {\n        val point = (key as ECPublicKey).w\n        var x = point.affineX.toByteArray()\n        var y = point.affineY.toByteArray()\n        if (x.size > 32) x = Arrays.copyOfRange(x, x.size - 32, x.size)\n        if (y.size > 32) y = Arrays.copyOfRange(y, y.size - 32, y.size)\n        val result = ByteArray(65)\n        result[0] = 4\n        System.arraycopy(x, 0, result, 1 + (32 - x.size), x.size)\n        System.arraycopy(y, 0, result, result.size - y.size, y.size)\n        return result\n    }\n\n    fun decryptData(\n        keys: PushInfo,\n        serverPublicKeyEncoded: String,\n        encryptionSalt: String,\n        contentEncoding: String,\n        data: ByteArray,\n    ): String {\n        val base64Flag = Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING\n        val salt = encryptionSalt.decodeBase64()\n        val serverKey = deserializeRawPublicKey(serverPublicKeyEncoded.decodeBase64())!!\n\n        val keyFactory = KeyFactory.getInstance(\"EC\")\n        val formalPrivateKey = keyFactory.generatePrivate(\n            PKCS8EncodedKeySpec(Base64.decode(keys.privateKey, base64Flag))\n        )\n        val publicKey = keyFactory.generatePublic(\n            X509EncodedKeySpec(Base64.decode(keys.publicKey, base64Flag))\n        )\n\n        val keyAgreement = KeyAgreement.getInstance(\"ECDH\")\n        keyAgreement.init(formalPrivateKey)\n        keyAgreement.doPhase(serverKey, true)\n        val sharedSecret = keyAgreement.generateSecret()\n\n        val secondSaltInfo = \"Content-Encoding: auth\\u0000\".toByteArray(StandardCharsets.UTF_8)\n        val authKey = Base64.decode(keys.authKey, base64Flag)\n        val secondSalt: ByteArray = deriveKey(authKey, sharedSecret, secondSaltInfo, 32)\n        val keyInfo: ByteArray = info(contentEncoding, publicKey, serverKey)\n        val key = deriveKey(salt, secondSalt, keyInfo, 16)\n        val nonceInfo: ByteArray = info(\"nonce\", publicKey, serverKey)\n        val nonce = deriveKey(salt, secondSalt, nonceInfo, 12)\n\n        val cipher = Cipher.getInstance(\"AES/GCM/NoPadding\")\n        val aesKey = SecretKeySpec(key, \"AES\")\n        val iv = GCMParameterSpec(128, nonce)\n        cipher.init(Cipher.DECRYPT_MODE, aesKey, iv)\n        val decrypted = cipher.doFinal(data)\n        return String(decrypted, 2, decrypted.size - 2, StandardCharsets.UTF_8)\n    }\n\n    private fun String.decodeBase64(): ByteArray {\n        return Base64.decode(\n            this,\n            if (reBase64UrlSafe.containsMatchIn(this)) Base64.URL_SAFE else Base64.DEFAULT,\n        )\n    }\n\n    private fun deriveKey(\n        firstSalt: ByteArray,\n        secondSalt: ByteArray,\n        info: ByteArray,\n        length: Int\n    ): ByteArray {\n        val hmacContext = Mac.getInstance(\"HmacSHA256\")\n        hmacContext.init(SecretKeySpec(firstSalt, \"HmacSHA256\"))\n        val hmac = hmacContext.doFinal(secondSalt)\n        hmacContext.init(SecretKeySpec(hmac, \"HmacSHA256\"))\n        hmacContext.update(info)\n        val result = hmacContext.doFinal(byteArrayOf(1))\n        return if (result.size <= length) result else Arrays.copyOfRange(result, 0, length)\n    }\n\n    private fun deserializeRawPublicKey(rawBytes: ByteArray): PublicKey? {\n        if (rawBytes.size != 65 && rawBytes.size != 64) return null\n        val kf = KeyFactory.getInstance(\"EC\")\n        val os = ByteArrayOutputStream()\n        os.write(P256_HEAD)\n        if (rawBytes.size == 64) os.write(4)\n        os.write(rawBytes)\n        return kf.generatePublic(X509EncodedKeySpec(os.toByteArray()))\n    }\n\n    private fun info(\n        type: String,\n        clientPublicKey: PublicKey,\n        serverPublicKey: PublicKey\n    ): ByteArray {\n        val info = ByteArrayOutputStream()\n        try {\n            info.write(\"Content-Encoding: \".toByteArray(StandardCharsets.UTF_8))\n            info.write(type.toByteArray(StandardCharsets.UTF_8))\n            info.write(0)\n            info.write(\"P-256\".toByteArray(StandardCharsets.UTF_8))\n            info.write(0)\n            info.write(0)\n            info.write(65)\n            info.write(serializeRawPublicKey(clientPublicKey))\n            info.write(0)\n            info.write(65)\n            info.write(serializeRawPublicKey(serverPublicKey))\n        } catch (ignore: IOException) {\n        }\n        return info.toByteArray()\n    }\n}\n\ndata class CryptoKeys(\n    val privateKey: String,\n    val encodedPublicKey: String,\n    val publicKey: String,\n    val authKey: String,\n)\n"
  },
  {
    "path": "plugins/activitypub-app/src/androidMain/kotlin/com/zhangke/fread/activitypub/app/internal/push/PushInfoRepo.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.push\n\nimport androidx.room.Dao\nimport androidx.room.Database\nimport androidx.room.Entity\nimport androidx.room.Insert\nimport androidx.room.OnConflictStrategy\nimport androidx.room.PrimaryKey\nimport androidx.room.Query\nimport androidx.room.RoomDatabase\n\nprivate const val DB_VERSION = 1\nprivate const val TABLE_NAME = \"PushInfo\"\n\n@Entity(tableName = TABLE_NAME)\ndata class PushInfo(\n    @PrimaryKey val accountId: String,\n    val publicKey: String,\n    val privateKey: String,\n    val authKey: String,\n)\n\n@Dao\ninterface PushInfoDao {\n\n    @Query(\"SELECT * FROM $TABLE_NAME WHERE accountId = :accountId\")\n    suspend fun getPushInfo(accountId: String): PushInfo?\n\n    @Insert(onConflict = OnConflictStrategy.REPLACE)\n    suspend fun insert(info: PushInfo)\n\n    @Query(\"DELETE FROM $TABLE_NAME WHERE accountId = :accountId\")\n    suspend fun delete(accountId: String)\n}\n\n@Database(\n    entities = [PushInfo::class],\n    version = DB_VERSION,\n    exportSchema = false,\n)\nabstract class PushInfoDatabase : RoomDatabase() {\n\n    abstract fun pushInfoDao(): PushInfoDao\n\n    companion object {\n\n        const val DB_NAME = \"ActivityPubPushInfo.db\"\n    }\n}\n\nclass PushInfoRepo(database: PushInfoDatabase) {\n\n    private val pushDao = database.pushInfoDao()\n\n    suspend fun getPushInfo(accountId: String): PushInfo? {\n        return pushDao.getPushInfo(accountId)\n    }\n\n    suspend fun insert(info: PushInfo) {\n        pushDao.insert(info)\n    }\n\n    suspend fun delete(accountId: String) {\n        pushDao.delete(accountId)\n    }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/androidMain/kotlin/com/zhangke/fread/activitypub/app/internal/push/notification/ActivityPubPushMessage.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.push.notification\n\nimport com.zhangke.fread.status.account.LoggedAccount\n\ndata class ActivityPubPushMessage(\n    val accessToken: String?,\n    val preferredLocale: String?,\n    val notificationId: Long?,\n    val account: LoggedAccount?,\n    val notificationType: Type,\n    val icon: String,\n    val title: String,\n    val body: String,\n) {\n\n    enum class Type {\n\n        FAVORITE,\n        MENTION,\n        REBLOG,\n        FOLLOW,\n        POLL;\n\n        companion object {\n\n            fun fromName(name: String): Type? {\n                return when (name) {\n                    \"favourite\" -> FAVORITE\n                    \"mention\" -> MENTION\n                    \"reblog\" -> REBLOG\n                    \"follow\" -> FOLLOW\n                    \"poll\" -> POLL\n                    else -> null\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/androidMain/kotlin/com/zhangke/fread/activitypub/app/internal/push/notification/PushNotificationManager.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.push.notification\n\nimport android.Manifest\nimport android.app.Notification\nimport android.app.NotificationChannel\nimport android.app.NotificationManager\nimport android.app.PendingIntent\nimport android.content.Context\nimport android.content.Intent\nimport android.content.pm.PackageManager\nimport android.graphics.Bitmap\nimport android.graphics.BitmapFactory\nimport android.os.Build\nimport androidx.core.app.NotificationCompat\nimport androidx.core.app.NotificationManagerCompat\nimport androidx.core.net.toUri\nimport com.seiko.imageloader.imageLoader\nimport com.seiko.imageloader.model.ImageRequest\nimport com.seiko.imageloader.option.SizeResolver\nimport com.zhangke.framework.imageloader.executeSafety\nimport com.zhangke.framework.utils.asBitmapOrNull\nimport com.zhangke.framework.utils.maybe\nimport com.zhangke.fread.activitypub.app.internal.repo.account.ActivityPubLoggedAccountRepo\nimport com.zhangke.fread.common.action.OpenNotificationPageAction\nimport com.zhangke.fread.commonbiz.R\nimport com.zhangke.fread.commonbiz.ic_fread_logo\nimport kotlin.random.Random\n\nclass PushNotificationManager (\n    private val accountRepo: ActivityPubLoggedAccountRepo\n) {\n\n    companion object {\n\n        private const val NOTIFICATION_CHANNEL_ID = \"InteractionMessages\"\n        private const val NOTIFICATION_GROUP_KEY = \"fread.xyz.interaction\"\n    }\n\n    suspend fun onReceiveNewMessage(context: Context, message: ActivityPubPushMessage) {\n        if (!checkSelfPushPermission(context)) return\n        createNotificationChannel(context)\n        val bitmap = downloadIcon(context, message.icon)\n        val loggedAccountCount = accountRepo.queryAll().size\n        val notificationIconColor =\n            context.getColor(R.color.color_logo_background)\n        val builder = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID)\n            .setSmallIcon(R.drawable.ic_logo_skeleton)\n            .setLargeIcon(BitmapFactory.decodeResource(context.resources, R.drawable.ic_fread_logo))\n            .setLights(notificationIconColor, 500, 1000)\n            .setColor(notificationIconColor)\n            .maybe(bitmap != null) {\n                it.setLargeIcon(bitmap)\n            }\n            .setContentTitle(message.title)\n            .setContentText(message.body)\n            .setPriority(NotificationCompat.PRIORITY_DEFAULT)\n            .setGroup(NOTIFICATION_GROUP_KEY)\n            .setContentIntent(buildNotificationIntent(context, message))\n            .maybe(loggedAccountCount > 1) {\n                it.setSubText(message.account?.userName)\n            }\n            .setShowWhen(true)\n            .setAutoCancel(true)\n            .setCategory(Notification.CATEGORY_SOCIAL)\n        val notificationId = Random.nextInt(0, Int.MAX_VALUE)\n        (context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager)\n        NotificationManagerCompat.from(context).notify(notificationId, builder.build())\n    }\n\n    private suspend fun downloadIcon(context: Context, iconUrl: String): Bitmap? {\n        val request = ImageRequest(iconUrl) {\n            size(SizeResolver(100, 100))\n            options {\n                isBitmap = true\n            }\n        }\n        return context.imageLoader.executeSafety(request).asBitmapOrNull()\n    }\n\n    private fun buildNotificationIntent(\n        context: Context,\n        message: ActivityPubPushMessage\n    ): PendingIntent {\n        val openNavigationUri = OpenNotificationPageAction.buildOpenNotificationPageRoute()\n        val intent = context.packageManager.getLaunchIntentForPackage(context.packageName)!!\n        intent.data = openNavigationUri.toUri()\n        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)\n        return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)\n    }\n\n    private fun checkSelfPushPermission(context: Context): Boolean {\n        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {\n            context.checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED\n        } else {\n            true\n        }\n    }\n\n    private fun createNotificationChannel(context: Context) {\n        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return\n        val channel = NotificationChannel(\n            NOTIFICATION_CHANNEL_ID,\n            \"Interaction messages\",\n            NotificationManager.IMPORTANCE_DEFAULT,\n        ).apply {\n            description = \"Notification for interaction messages\"\n        }\n        context.pusManager.createNotificationChannel(channel)\n    }\n\n    private val Context.pusManager: NotificationManager\n        get() = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager\n}"
  },
  {
    "path": "plugins/activitypub-app/src/androidUnitTest/kotlin/com/zhangke/utopia/activitypubapp/ExampleUnitTest.kt",
    "content": "package com.zhangke.fread.activitypubapp\n\nimport org.junit.Test\n\nimport org.junit.Assert.*\n\n/**\n * Example local unit test, which will execute on the development machine (host).\n *\n * See [testing documentation](http://d.android.com/tools/testing).\n */\nclass ExampleUnitTest {\n\n    @Test\n    fun addition_isCorrect() {\n        for (i in 10 downTo 0){\n            println(i)\n        }\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/composeResources/drawable/detail_page_banner_background.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"412dp\"\n    android:height=\"171dp\"\n    android:viewportWidth=\"412\"\n    android:viewportHeight=\"171\">\n  <path\n      android:pathData=\"M6,24.41L18.24,24.41A6,6 0,0 1,24.24 30.41L24.24,42.65A6,6 0,0 1,18.24 48.65L6,48.65A6,6 0,0 1,0 42.65L0,30.41A6,6 0,0 1,6 24.41z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.12\"/>\n  <path\n      android:pathData=\"M6,72.88L18.24,72.88A6,6 0,0 1,24.24 78.88L24.24,91.12A6,6 0,0 1,18.24 97.12L6,97.12A6,6 0,0 1,0 91.12L0,78.88A6,6 0,0 1,6 72.88z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.12\"/>\n  <path\n      android:pathData=\"M6,121.35L18.24,121.35A6,6 0,0 1,24.24 127.35L24.24,139.59A6,6 0,0 1,18.24 145.59L6,145.59A6,6 0,0 1,0 139.59L0,127.35A6,6 0,0 1,6 121.35z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.06\"/>\n  <path\n      android:pathData=\"M6,48.65L18.24,48.65A6,6 0,0 1,24.24 54.65L24.24,66.88A6,6 0,0 1,18.24 72.88L6,72.88A6,6 0,0 1,0 66.88L0,54.65A6,6 0,0 1,6 48.65z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.18\"/>\n  <path\n      android:pathData=\"M6,97.12L18.24,97.12A6,6 0,0 1,24.24 103.12L24.24,115.35A6,6 0,0 1,18.24 121.35L6,121.35A6,6 0,0 1,0 115.35L0,103.12A6,6 0,0 1,6 97.12z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.072\"/>\n  <path\n      android:pathData=\"M6,0.18L18.24,0.18A6,6 0,0 1,24.24 6.18L24.24,18.41A6,6 0,0 1,18.24 24.41L6,24.41A6,6 0,0 1,0 18.41L0,6.18A6,6 0,0 1,6 0.18z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.24\"/>\n  <path\n      android:pathData=\"M6,145.59L18.24,145.59A6,6 0,0 1,24.24 151.59L24.24,163.82A6,6 0,0 1,18.24 169.82L6,169.82A6,6 0,0 1,0 163.82L0,151.59A6,6 0,0 1,6 145.59z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.012\"/>\n  <path\n      android:pathData=\"M30.24,24.41L42.47,24.41A6,6 0,0 1,48.47 30.41L48.47,42.65A6,6 0,0 1,42.47 48.65L30.24,48.65A6,6 0,0 1,24.24 42.65L24.24,30.41A6,6 0,0 1,30.24 24.41z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.18\"/>\n  <path\n      android:pathData=\"M30.24,72.88L42.47,72.88A6,6 0,0 1,48.47 78.88L48.47,91.12A6,6 0,0 1,42.47 97.12L30.24,97.12A6,6 0,0 1,24.24 91.12L24.24,78.88A6,6 0,0 1,30.24 72.88z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.132\"/>\n  <path\n      android:pathData=\"M30.24,121.35L42.47,121.35A6,6 0,0 1,48.47 127.35L48.47,139.59A6,6 0,0 1,42.47 145.59L30.24,145.59A6,6 0,0 1,24.24 139.59L24.24,127.35A6,6 0,0 1,30.24 121.35z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.03\"/>\n  <path\n      android:pathData=\"M30.24,48.65L42.47,48.65A6,6 0,0 1,48.47 54.65L48.47,66.88A6,6 0,0 1,42.47 72.88L30.24,72.88A6,6 0,0 1,24.24 66.88L24.24,54.65A6,6 0,0 1,30.24 48.65z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.084\"/>\n  <path\n      android:pathData=\"M30.24,97.12L42.47,97.12A6,6 0,0 1,48.47 103.12L48.47,115.35A6,6 0,0 1,42.47 121.35L30.24,121.35A6,6 0,0 1,24.24 115.35L24.24,103.12A6,6 0,0 1,30.24 97.12z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.096\"/>\n  <path\n      android:pathData=\"M30.24,0.18L42.47,0.18A6,6 0,0 1,48.47 6.18L48.47,18.41A6,6 0,0 1,42.47 24.41L30.24,24.41A6,6 0,0 1,24.24 18.41L24.24,6.18A6,6 0,0 1,30.24 0.18z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.24\"/>\n  <path\n      android:pathData=\"M30.24,145.59L42.47,145.59A6,6 0,0 1,48.47 151.59L48.47,163.82A6,6 0,0 1,42.47 169.82L30.24,169.82A6,6 0,0 1,24.24 163.82L24.24,151.59A6,6 0,0 1,30.24 145.59z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.06\"/>\n  <path\n      android:pathData=\"M54.47,24.41L66.71,24.41A6,6 0,0 1,72.71 30.41L72.71,42.65A6,6 0,0 1,66.71 48.65L54.47,48.65A6,6 0,0 1,48.47 42.65L48.47,30.41A6,6 0,0 1,54.47 24.41z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.12\"/>\n  <path\n      android:pathData=\"M54.47,72.88L66.71,72.88A6,6 0,0 1,72.71 78.88L72.71,91.12A6,6 0,0 1,66.71 97.12L54.47,97.12A6,6 0,0 1,48.47 91.12L48.47,78.88A6,6 0,0 1,54.47 72.88z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.048\"/>\n  <path\n      android:pathData=\"M54.47,121.35L66.71,121.35A6,6 0,0 1,72.71 127.35L72.71,139.59A6,6 0,0 1,66.71 145.59L54.47,145.59A6,6 0,0 1,48.47 139.59L48.47,127.35A6,6 0,0 1,54.47 121.35z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.06\"/>\n  <path\n      android:pathData=\"M54.47,48.65L66.71,48.65A6,6 0,0 1,72.71 54.65L72.71,66.88A6,6 0,0 1,66.71 72.88L54.47,72.88A6,6 0,0 1,48.47 66.88L48.47,54.65A6,6 0,0 1,54.47 48.65z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.18\"/>\n  <path\n      android:pathData=\"M54.47,97.12L66.71,97.12A6,6 0,0 1,72.71 103.12L72.71,115.35A6,6 0,0 1,66.71 121.35L54.47,121.35A6,6 0,0 1,48.47 115.35L48.47,103.12A6,6 0,0 1,54.47 97.12z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.084\"/>\n  <path\n      android:pathData=\"M54.47,0.18L66.71,0.18A6,6 0,0 1,72.71 6.18L72.71,18.41A6,6 0,0 1,66.71 24.41L54.47,24.41A6,6 0,0 1,48.47 18.41L48.47,6.18A6,6 0,0 1,54.47 0.18z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.204\"/>\n  <path\n      android:pathData=\"M54.47,145.59L66.71,145.59A6,6 0,0 1,72.71 151.59L72.71,163.82A6,6 0,0 1,66.71 169.82L54.47,169.82A6,6 0,0 1,48.47 163.82L48.47,151.59A6,6 0,0 1,54.47 145.59z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.012\"/>\n  <path\n      android:pathData=\"M78.71,24.41L90.94,24.41A6,6 0,0 1,96.94 30.41L96.94,42.65A6,6 0,0 1,90.94 48.65L78.71,48.65A6,6 0,0 1,72.71 42.65L72.71,30.41A6,6 0,0 1,78.71 24.41z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.264\"/>\n  <path\n      android:pathData=\"M78.71,72.88L90.94,72.88A6,6 0,0 1,96.94 78.88L96.94,91.12A6,6 0,0 1,90.94 97.12L78.71,97.12A6,6 0,0 1,72.71 91.12L72.71,78.88A6,6 0,0 1,78.71 72.88z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.096\"/>\n  <path\n      android:pathData=\"M78.71,121.35L90.94,121.35A6,6 0,0 1,96.94 127.35L96.94,139.59A6,6 0,0 1,90.94 145.59L78.71,145.59A6,6 0,0 1,72.71 139.59L72.71,127.35A6,6 0,0 1,78.71 121.35z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.06\"/>\n  <path\n      android:pathData=\"M78.71,48.65L90.94,48.65A6,6 0,0 1,96.94 54.65L96.94,66.88A6,6 0,0 1,90.94 72.88L78.71,72.88A6,6 0,0 1,72.71 66.88L72.71,54.65A6,6 0,0 1,78.71 48.65z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.24\"/>\n  <path\n      android:pathData=\"M78.71,97.12L90.94,97.12A6,6 0,0 1,96.94 103.12L96.94,115.35A6,6 0,0 1,90.94 121.35L78.71,121.35A6,6 0,0 1,72.71 115.35L72.71,103.12A6,6 0,0 1,78.71 97.12z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.06\"/>\n  <path\n      android:pathData=\"M78.71,0.18L90.94,0.18A6,6 0,0 1,96.94 6.18L96.94,18.41A6,6 0,0 1,90.94 24.41L78.71,24.41A6,6 0,0 1,72.71 18.41L72.71,6.18A6,6 0,0 1,78.71 0.18z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.12\"/>\n  <path\n      android:pathData=\"M78.71,145.59L90.94,145.59A6,6 0,0 1,96.94 151.59L96.94,163.82A6,6 0,0 1,90.94 169.82L78.71,169.82A6,6 0,0 1,72.71 163.82L72.71,151.59A6,6 0,0 1,78.71 145.59z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.012\"/>\n  <path\n      android:pathData=\"M102.94,24.41L115.18,24.41A6,6 0,0 1,121.18 30.41L121.18,42.65A6,6 0,0 1,115.18 48.65L102.94,48.65A6,6 0,0 1,96.94 42.65L96.94,30.41A6,6 0,0 1,102.94 24.41z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.12\"/>\n  <path\n      android:pathData=\"M102.94,72.88L115.18,72.88A6,6 0,0 1,121.18 78.88L121.18,91.12A6,6 0,0 1,115.18 97.12L102.94,97.12A6,6 0,0 1,96.94 91.12L96.94,78.88A6,6 0,0 1,102.94 72.88z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.048\"/>\n  <path\n      android:pathData=\"M102.94,121.35L115.18,121.35A6,6 0,0 1,121.18 127.35L121.18,139.59A6,6 0,0 1,115.18 145.59L102.94,145.59A6,6 0,0 1,96.94 139.59L96.94,127.35A6,6 0,0 1,102.94 121.35z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.084\"/>\n  <path\n      android:pathData=\"M102.94,48.65L115.18,48.65A6,6 0,0 1,121.18 54.65L121.18,66.88A6,6 0,0 1,115.18 72.88L102.94,72.88A6,6 0,0 1,96.94 66.88L96.94,54.65A6,6 0,0 1,102.94 48.65z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.18\"/>\n  <path\n      android:pathData=\"M102.94,97.12L115.18,97.12A6,6 0,0 1,121.18 103.12L121.18,115.35A6,6 0,0 1,115.18 121.35L102.94,121.35A6,6 0,0 1,96.94 115.35L96.94,103.12A6,6 0,0 1,102.94 97.12z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.12\"/>\n  <path\n      android:pathData=\"M102.94,0.18L115.18,0.18A6,6 0,0 1,121.18 6.18L121.18,18.41A6,6 0,0 1,115.18 24.41L102.94,24.41A6,6 0,0 1,96.94 18.41L96.94,6.18A6,6 0,0 1,102.94 0.18z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.24\"/>\n  <path\n      android:pathData=\"M102.94,145.59L115.18,145.59A6,6 0,0 1,121.18 151.59L121.18,163.82A6,6 0,0 1,115.18 169.82L102.94,169.82A6,6 0,0 1,96.94 163.82L96.94,151.59A6,6 0,0 1,102.94 145.59z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.06\"/>\n  <path\n      android:pathData=\"M127.18,24.41L139.41,24.41A6,6 0,0 1,145.41 30.41L145.41,42.65A6,6 0,0 1,139.41 48.65L127.18,48.65A6,6 0,0 1,121.18 42.65L121.18,30.41A6,6 0,0 1,127.18 24.41z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.12\"/>\n  <path\n      android:pathData=\"M127.18,72.88L139.41,72.88A6,6 0,0 1,145.41 78.88L145.41,91.12A6,6 0,0 1,139.41 97.12L127.18,97.12A6,6 0,0 1,121.18 91.12L121.18,78.88A6,6 0,0 1,127.18 72.88z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.18\"/>\n  <path\n      android:pathData=\"M127.18,121.35L139.41,121.35A6,6 0,0 1,145.41 127.35L145.41,139.59A6,6 0,0 1,139.41 145.59L127.18,145.59A6,6 0,0 1,121.18 139.59L121.18,127.35A6,6 0,0 1,127.18 121.35z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.048\"/>\n  <path\n      android:pathData=\"M127.18,48.65L139.41,48.65A6,6 0,0 1,145.41 54.65L145.41,66.88A6,6 0,0 1,139.41 72.88L127.18,72.88A6,6 0,0 1,121.18 66.88L121.18,54.65A6,6 0,0 1,127.18 48.65z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.24\"/>\n  <path\n      android:pathData=\"M127.18,97.12L139.41,97.12A6,6 0,0 1,145.41 103.12L145.41,115.35A6,6 0,0 1,139.41 121.35L127.18,121.35A6,6 0,0 1,121.18 115.35L121.18,103.12A6,6 0,0 1,127.18 97.12z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.072\"/>\n  <path\n      android:pathData=\"M127.18,0.18L139.41,0.18A6,6 0,0 1,145.41 6.18L145.41,18.41A6,6 0,0 1,139.41 24.41L127.18,24.41A6,6 0,0 1,121.18 18.41L121.18,6.18A6,6 0,0 1,127.18 0.18z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.24\"/>\n  <path\n      android:pathData=\"M127,146L139.24,146A6,6 0,0 1,145.24 152L145.24,164.24A6,6 0,0 1,139.24 170.24L127,170.24A6,6 0,0 1,121 164.24L121,152A6,6 0,0 1,127 146z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.024\"/>\n  <path\n      android:pathData=\"M151.41,24.41L163.65,24.41A6,6 0,0 1,169.65 30.41L169.65,42.65A6,6 0,0 1,163.65 48.65L151.41,48.65A6,6 0,0 1,145.41 42.65L145.41,30.41A6,6 0,0 1,151.41 24.41z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.18\"/>\n  <path\n      android:pathData=\"M151.41,72.88L163.65,72.88A6,6 0,0 1,169.65 78.88L169.65,91.12A6,6 0,0 1,163.65 97.12L151.41,97.12A6,6 0,0 1,145.41 91.12L145.41,78.88A6,6 0,0 1,151.41 72.88z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.132\"/>\n  <path\n      android:pathData=\"M151.41,121.35L163.65,121.35A6,6 0,0 1,169.65 127.35L169.65,139.59A6,6 0,0 1,163.65 145.59L151.41,145.59A6,6 0,0 1,145.41 139.59L145.41,127.35A6,6 0,0 1,151.41 121.35z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.06\"/>\n  <path\n      android:pathData=\"M151.41,48.65L163.65,48.65A6,6 0,0 1,169.65 54.65L169.65,66.88A6,6 0,0 1,163.65 72.88L151.41,72.88A6,6 0,0 1,145.41 66.88L145.41,54.65A6,6 0,0 1,151.41 48.65z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.18\"/>\n  <path\n      android:pathData=\"M151.41,97.12L163.65,97.12A6,6 0,0 1,169.65 103.12L169.65,115.35A6,6 0,0 1,163.65 121.35L151.41,121.35A6,6 0,0 1,145.41 115.35L145.41,103.12A6,6 0,0 1,151.41 97.12z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.12\"/>\n  <path\n      android:pathData=\"M151.41,0.18L163.65,0.18A6,6 0,0 1,169.65 6.18L169.65,18.41A6,6 0,0 1,163.65 24.41L151.41,24.41A6,6 0,0 1,145.41 18.41L145.41,6.18A6,6 0,0 1,151.41 0.18z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.18\"/>\n  <path\n      android:pathData=\"M151.41,145.59L163.65,145.59A6,6 0,0 1,169.65 151.59L169.65,163.82A6,6 0,0 1,163.65 169.82L151.41,169.82A6,6 0,0 1,145.41 163.82L145.41,151.59A6,6 0,0 1,151.41 145.59z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.012\"/>\n  <path\n      android:pathData=\"M175.65,24.41L187.88,24.41A6,6 0,0 1,193.88 30.41L193.88,42.65A6,6 0,0 1,187.88 48.65L175.65,48.65A6,6 0,0 1,169.65 42.65L169.65,30.41A6,6 0,0 1,175.65 24.41z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.12\"/>\n  <path\n      android:pathData=\"M175.65,72.88L187.88,72.88A6,6 0,0 1,193.88 78.88L193.88,91.12A6,6 0,0 1,187.88 97.12L175.65,97.12A6,6 0,0 1,169.65 91.12L169.65,78.88A6,6 0,0 1,175.65 72.88z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.084\"/>\n  <path\n      android:pathData=\"M175.65,121.35L187.88,121.35A6,6 0,0 1,193.88 127.35L193.88,139.59A6,6 0,0 1,187.88 145.59L175.65,145.59A6,6 0,0 1,169.65 139.59L169.65,127.35A6,6 0,0 1,175.65 121.35z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.024\"/>\n  <path\n      android:pathData=\"M175.65,48.65L187.88,48.65A6,6 0,0 1,193.88 54.65L193.88,66.88A6,6 0,0 1,187.88 72.88L175.65,72.88A6,6 0,0 1,169.65 66.88L169.65,54.65A6,6 0,0 1,175.65 48.65z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.12\"/>\n  <path\n      android:pathData=\"M175.65,97.12L187.88,97.12A6,6 0,0 1,193.88 103.12L193.88,115.35A6,6 0,0 1,187.88 121.35L175.65,121.35A6,6 0,0 1,169.65 115.35L169.65,103.12A6,6 0,0 1,175.65 97.12z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.12\"/>\n  <path\n      android:pathData=\"M175.65,0.18L187.88,0.18A6,6 0,0 1,193.88 6.18L193.88,18.41A6,6 0,0 1,187.88 24.41L175.65,24.41A6,6 0,0 1,169.65 18.41L169.65,6.18A6,6 0,0 1,175.65 0.18z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.276\"/>\n  <path\n      android:pathData=\"M175.65,145.59L187.88,145.59A6,6 0,0 1,193.88 151.59L193.88,163.82A6,6 0,0 1,187.88 169.82L175.65,169.82A6,6 0,0 1,169.65 163.82L169.65,151.59A6,6 0,0 1,175.65 145.59z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.012\"/>\n  <path\n      android:pathData=\"M199.88,24.41L212.12,24.41A6,6 0,0 1,218.12 30.41L218.12,42.65A6,6 0,0 1,212.12 48.65L199.88,48.65A6,6 0,0 1,193.88 42.65L193.88,30.41A6,6 0,0 1,199.88 24.41z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.264\"/>\n  <path\n      android:pathData=\"M199.88,72.88L212.12,72.88A6,6 0,0 1,218.12 78.88L218.12,91.12A6,6 0,0 1,212.12 97.12L199.88,97.12A6,6 0,0 1,193.88 91.12L193.88,78.88A6,6 0,0 1,199.88 72.88z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.18\"/>\n  <path\n      android:pathData=\"M199.88,121.35L212.12,121.35A6,6 0,0 1,218.12 127.35L218.12,139.59A6,6 0,0 1,212.12 145.59L199.88,145.59A6,6 0,0 1,193.88 139.59L193.88,127.35A6,6 0,0 1,199.88 121.35z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.036\"/>\n  <path\n      android:pathData=\"M199.88,48.65L212.12,48.65A6,6 0,0 1,218.12 54.65L218.12,66.88A6,6 0,0 1,212.12 72.88L199.88,72.88A6,6 0,0 1,193.88 66.88L193.88,54.65A6,6 0,0 1,199.88 48.65z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.132\"/>\n  <path\n      android:pathData=\"M199.88,97.12L212.12,97.12A6,6 0,0 1,218.12 103.12L218.12,115.35A6,6 0,0 1,212.12 121.35L199.88,121.35A6,6 0,0 1,193.88 115.35L193.88,103.12A6,6 0,0 1,199.88 97.12z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.048\"/>\n  <path\n      android:pathData=\"M199.88,0.18L212.12,0.18A6,6 0,0 1,218.12 6.18L218.12,18.41A6,6 0,0 1,212.12 24.41L199.88,24.41A6,6 0,0 1,193.88 18.41L193.88,6.18A6,6 0,0 1,199.88 0.18z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.18\"/>\n  <path\n      android:pathData=\"M199.88,145.59L212.12,145.59A6,6 0,0 1,218.12 151.59L218.12,163.82A6,6 0,0 1,212.12 169.82L199.88,169.82A6,6 0,0 1,193.88 163.82L193.88,151.59A6,6 0,0 1,199.88 145.59z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.012\"/>\n  <path\n      android:pathData=\"M224.12,24.41L236.35,24.41A6,6 0,0 1,242.35 30.41L242.35,42.65A6,6 0,0 1,236.35 48.65L224.12,48.65A6,6 0,0 1,218.12 42.65L218.12,30.41A6,6 0,0 1,224.12 24.41z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.12\"/>\n  <path\n      android:pathData=\"M224.12,72.88L236.35,72.88A6,6 0,0 1,242.35 78.88L242.35,91.12A6,6 0,0 1,236.35 97.12L224.12,97.12A6,6 0,0 1,218.12 91.12L218.12,78.88A6,6 0,0 1,224.12 72.88z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.108\"/>\n  <path\n      android:pathData=\"M224.12,121.35L236.35,121.35A6,6 0,0 1,242.35 127.35L242.35,139.59A6,6 0,0 1,236.35 145.59L224.12,145.59A6,6 0,0 1,218.12 139.59L218.12,127.35A6,6 0,0 1,224.12 121.35z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.06\"/>\n  <path\n      android:pathData=\"M224.12,48.65L236.35,48.65A6,6 0,0 1,242.35 54.65L242.35,66.88A6,6 0,0 1,236.35 72.88L224.12,72.88A6,6 0,0 1,218.12 66.88L218.12,54.65A6,6 0,0 1,224.12 48.65z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.18\"/>\n  <path\n      android:pathData=\"M224.12,97.12L236.35,97.12A6,6 0,0 1,242.35 103.12L242.35,115.35A6,6 0,0 1,236.35 121.35L224.12,121.35A6,6 0,0 1,218.12 115.35L218.12,103.12A6,6 0,0 1,224.12 97.12z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.06\"/>\n  <path\n      android:pathData=\"M224.12,0.18L236.35,0.18A6,6 0,0 1,242.35 6.18L242.35,18.41A6,6 0,0 1,236.35 24.41L224.12,24.41A6,6 0,0 1,218.12 18.41L218.12,6.18A6,6 0,0 1,224.12 0.18z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.24\"/>\n  <path\n      android:pathData=\"M224.12,145.59L236.35,145.59A6,6 0,0 1,242.35 151.59L242.35,163.82A6,6 0,0 1,236.35 169.82L224.12,169.82A6,6 0,0 1,218.12 163.82L218.12,151.59A6,6 0,0 1,224.12 145.59z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.012\"/>\n  <path\n      android:pathData=\"M248.35,24.41L260.59,24.41A6,6 0,0 1,266.59 30.41L266.59,42.65A6,6 0,0 1,260.59 48.65L248.35,48.65A6,6 0,0 1,242.35 42.65L242.35,30.41A6,6 0,0 1,248.35 24.41z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.24\"/>\n  <path\n      android:pathData=\"M248.35,72.88L260.59,72.88A6,6 0,0 1,266.59 78.88L266.59,91.12A6,6 0,0 1,260.59 97.12L248.35,97.12A6,6 0,0 1,242.35 91.12L242.35,78.88A6,6 0,0 1,248.35 72.88z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.048\"/>\n  <path\n      android:pathData=\"M248.35,48.65L260.59,48.65A6,6 0,0 1,266.59 54.65L266.59,66.88A6,6 0,0 1,260.59 72.88L248.35,72.88A6,6 0,0 1,242.35 66.88L242.35,54.65A6,6 0,0 1,248.35 48.65z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.18\"/>\n  <path\n      android:pathData=\"M248.35,97.12L260.59,97.12A6,6 0,0 1,266.59 103.12L266.59,115.35A6,6 0,0 1,260.59 121.35L248.35,121.35A6,6 0,0 1,242.35 115.35L242.35,103.12A6,6 0,0 1,248.35 97.12z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.12\"/>\n  <path\n      android:pathData=\"M248.35,0.18L260.59,0.18A6,6 0,0 1,266.59 6.18L266.59,18.41A6,6 0,0 1,260.59 24.41L248.35,24.41A6,6 0,0 1,242.35 18.41L242.35,6.18A6,6 0,0 1,248.35 0.18z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.24\"/>\n  <path\n      android:pathData=\"M248.35,145.59L260.59,145.59A6,6 0,0 1,266.59 151.59L266.59,163.82A6,6 0,0 1,260.59 169.82L248.35,169.82A6,6 0,0 1,242.35 163.82L242.35,151.59A6,6 0,0 1,248.35 145.59z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.048\"/>\n  <path\n      android:pathData=\"M272.59,24.41L284.82,24.41A6,6 0,0 1,290.82 30.41L290.82,42.65A6,6 0,0 1,284.82 48.65L272.59,48.65A6,6 0,0 1,266.59 42.65L266.59,30.41A6,6 0,0 1,272.59 24.41z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.24\"/>\n  <path\n      android:pathData=\"M272.59,72.88L284.82,72.88A6,6 0,0 1,290.82 78.88L290.82,91.12A6,6 0,0 1,284.82 97.12L272.59,97.12A6,6 0,0 1,266.59 91.12L266.59,78.88A6,6 0,0 1,272.59 72.88z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.09\"/>\n  <path\n      android:pathData=\"M272.59,121.35L284.82,121.35A6,6 0,0 1,290.82 127.35L290.82,139.59A6,6 0,0 1,284.82 145.59L272.59,145.59A6,6 0,0 1,266.59 139.59L266.59,127.35A6,6 0,0 1,272.59 121.35z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.036\"/>\n  <path\n      android:pathData=\"M272.59,48.65L284.82,48.65A6,6 0,0 1,290.82 54.65L290.82,66.88A6,6 0,0 1,284.82 72.88L272.59,72.88A6,6 0,0 1,266.59 66.88L266.59,54.65A6,6 0,0 1,272.59 48.65z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.12\"/>\n  <path\n      android:pathData=\"M272.59,97.12L284.82,97.12A6,6 0,0 1,290.82 103.12L290.82,115.35A6,6 0,0 1,284.82 121.35L272.59,121.35A6,6 0,0 1,266.59 115.35L266.59,103.12A6,6 0,0 1,272.59 97.12z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.06\"/>\n  <path\n      android:pathData=\"M272.59,0.18L284.82,0.18A6,6 0,0 1,290.82 6.18L290.82,18.41A6,6 0,0 1,284.82 24.41L272.59,24.41A6,6 0,0 1,266.59 18.41L266.59,6.18A6,6 0,0 1,272.59 0.18z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.3\"/>\n  <path\n      android:pathData=\"M272.59,145.59L284.82,145.59A6,6 0,0 1,290.82 151.59L290.82,163.82A6,6 0,0 1,284.82 169.82L272.59,169.82A6,6 0,0 1,266.59 163.82L266.59,151.59A6,6 0,0 1,272.59 145.59z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.012\"/>\n  <path\n      android:pathData=\"M296.82,24.41L309.06,24.41A6,6 0,0 1,315.06 30.41L315.06,42.65A6,6 0,0 1,309.06 48.65L296.82,48.65A6,6 0,0 1,290.82 42.65L290.82,30.41A6,6 0,0 1,296.82 24.41z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.24\"/>\n  <path\n      android:pathData=\"M296.82,72.88L309.06,72.88A6,6 0,0 1,315.06 78.88L315.06,91.12A6,6 0,0 1,309.06 97.12L296.82,97.12A6,6 0,0 1,290.82 91.12L290.82,78.88A6,6 0,0 1,296.82 72.88z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.072\"/>\n  <path\n      android:pathData=\"M296.82,121.35L309.06,121.35A6,6 0,0 1,315.06 127.35L315.06,139.59A6,6 0,0 1,309.06 145.59L296.82,145.59A6,6 0,0 1,290.82 139.59L290.82,127.35A6,6 0,0 1,296.82 121.35z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.06\"/>\n  <path\n      android:pathData=\"M296.82,48.65L309.06,48.65A6,6 0,0 1,315.06 54.65L315.06,66.88A6,6 0,0 1,309.06 72.88L296.82,72.88A6,6 0,0 1,290.82 66.88L290.82,54.65A6,6 0,0 1,296.82 48.65z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.18\"/>\n  <path\n      android:pathData=\"M296.82,97.12L309.06,97.12A6,6 0,0 1,315.06 103.12L315.06,115.35A6,6 0,0 1,309.06 121.35L296.82,121.35A6,6 0,0 1,290.82 115.35L290.82,103.12A6,6 0,0 1,296.82 97.12z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.12\"/>\n  <path\n      android:pathData=\"M296.82,0.18L309.06,0.18A6,6 0,0 1,315.06 6.18L315.06,18.41A6,6 0,0 1,309.06 24.41L296.82,24.41A6,6 0,0 1,290.82 18.41L290.82,6.18A6,6 0,0 1,296.82 0.18z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.24\"/>\n  <path\n      android:pathData=\"M296.82,145.59L309.06,145.59A6,6 0,0 1,315.06 151.59L315.06,163.82A6,6 0,0 1,309.06 169.82L296.82,169.82A6,6 0,0 1,290.82 163.82L290.82,151.59A6,6 0,0 1,296.82 145.59z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.012\"/>\n  <path\n      android:pathData=\"M321.06,24.41L333.3,24.41A6,6 0,0 1,339.3 30.41L339.3,42.65A6,6 0,0 1,333.3 48.65L321.06,48.65A6,6 0,0 1,315.06 42.65L315.06,30.41A6,6 0,0 1,321.06 24.41z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.12\"/>\n  <path\n      android:pathData=\"M321.06,72.88L333.3,72.88A6,6 0,0 1,339.3 78.88L339.3,91.12A6,6 0,0 1,333.3 97.12L321.06,97.12A6,6 0,0 1,315.06 91.12L315.06,78.88A6,6 0,0 1,321.06 72.88z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.156\"/>\n  <path\n      android:pathData=\"M321.06,121.35L333.3,121.35A6,6 0,0 1,339.3 127.35L339.3,139.59A6,6 0,0 1,333.3 145.59L321.06,145.59A6,6 0,0 1,315.06 139.59L315.06,127.35A6,6 0,0 1,321.06 121.35z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.024\"/>\n  <path\n      android:pathData=\"M321.06,48.65L333.3,48.65A6,6 0,0 1,339.3 54.65L339.3,66.88A6,6 0,0 1,333.3 72.88L321.06,72.88A6,6 0,0 1,315.06 66.88L315.06,54.65A6,6 0,0 1,321.06 48.65z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.06\"/>\n  <path\n      android:pathData=\"M321.06,97.12L333.3,97.12A6,6 0,0 1,339.3 103.12L339.3,115.35A6,6 0,0 1,333.3 121.35L321.06,121.35A6,6 0,0 1,315.06 115.35L315.06,103.12A6,6 0,0 1,321.06 97.12z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.06\"/>\n  <path\n      android:pathData=\"M321.06,0.18L333.3,0.18A6,6 0,0 1,339.3 6.18L339.3,18.41A6,6 0,0 1,333.3 24.41L321.06,24.41A6,6 0,0 1,315.06 18.41L315.06,6.18A6,6 0,0 1,321.06 0.18z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.24\"/>\n  <path\n      android:pathData=\"M321.06,145.59L333.3,145.59A6,6 0,0 1,339.3 151.59L339.3,163.82A6,6 0,0 1,333.3 169.82L321.06,169.82A6,6 0,0 1,315.06 163.82L315.06,151.59A6,6 0,0 1,321.06 145.59z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.012\"/>\n  <path\n      android:pathData=\"M345.3,24.41L357.53,24.41A6,6 0,0 1,363.53 30.41L363.53,42.65A6,6 0,0 1,357.53 48.65L345.3,48.65A6,6 0,0 1,339.3 42.65L339.3,30.41A6,6 0,0 1,345.3 24.41z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.24\"/>\n  <path\n      android:pathData=\"M345.3,72.88L357.53,72.88A6,6 0,0 1,363.53 78.88L363.53,91.12A6,6 0,0 1,357.53 97.12L345.3,97.12A6,6 0,0 1,339.3 91.12L339.3,78.88A6,6 0,0 1,345.3 72.88z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.072\"/>\n  <path\n      android:pathData=\"M345.3,121.35L357.53,121.35A6,6 0,0 1,363.53 127.35L363.53,139.59A6,6 0,0 1,357.53 145.59L345.3,145.59A6,6 0,0 1,339.3 139.59L339.3,127.35A6,6 0,0 1,345.3 121.35z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.06\"/>\n  <path\n      android:pathData=\"M345.3,48.65L357.53,48.65A6,6 0,0 1,363.53 54.65L363.53,66.88A6,6 0,0 1,357.53 72.88L345.3,72.88A6,6 0,0 1,339.3 66.88L339.3,54.65A6,6 0,0 1,345.3 48.65z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.12\"/>\n  <path\n      android:pathData=\"M345.3,97.12L357.53,97.12A6,6 0,0 1,363.53 103.12L363.53,115.35A6,6 0,0 1,357.53 121.35L345.3,121.35A6,6 0,0 1,339.3 115.35L339.3,103.12A6,6 0,0 1,345.3 97.12z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.12\"/>\n  <path\n      android:pathData=\"M345.3,0.18L357.53,0.18A6,6 0,0 1,363.53 6.18L363.53,18.41A6,6 0,0 1,357.53 24.41L345.3,24.41A6,6 0,0 1,339.3 18.41L339.3,6.18A6,6 0,0 1,345.3 0.18z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.24\"/>\n  <path\n      android:pathData=\"M345.3,145.59L357.53,145.59A6,6 0,0 1,363.53 151.59L363.53,163.82A6,6 0,0 1,357.53 169.82L345.3,169.82A6,6 0,0 1,339.3 163.82L339.3,151.59A6,6 0,0 1,345.3 145.59z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.048\"/>\n  <path\n      android:pathData=\"M369.53,24.41L381.77,24.41A6,6 0,0 1,387.77 30.41L387.77,42.65A6,6 0,0 1,381.77 48.65L369.53,48.65A6,6 0,0 1,363.53 42.65L363.53,30.41A6,6 0,0 1,369.53 24.41z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.12\"/>\n  <path\n      android:pathData=\"M369.53,72.88L381.77,72.88A6,6 0,0 1,387.77 78.88L387.77,91.12A6,6 0,0 1,381.77 97.12L369.53,97.12A6,6 0,0 1,363.53 91.12L363.53,78.88A6,6 0,0 1,369.53 72.88z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.108\"/>\n  <path\n      android:pathData=\"M369.53,121.35L381.77,121.35A6,6 0,0 1,387.77 127.35L387.77,139.59A6,6 0,0 1,381.77 145.59L369.53,145.59A6,6 0,0 1,363.53 139.59L363.53,127.35A6,6 0,0 1,369.53 121.35z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.12\"/>\n  <path\n      android:pathData=\"M369.53,48.65L381.77,48.65A6,6 0,0 1,387.77 54.65L387.77,66.88A6,6 0,0 1,381.77 72.88L369.53,72.88A6,6 0,0 1,363.53 66.88L363.53,54.65A6,6 0,0 1,369.53 48.65z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.18\"/>\n  <path\n      android:pathData=\"M369.53,97.12L381.77,97.12A6,6 0,0 1,387.77 103.12L387.77,115.35A6,6 0,0 1,381.77 121.35L369.53,121.35A6,6 0,0 1,363.53 115.35L363.53,103.12A6,6 0,0 1,369.53 97.12z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.084\"/>\n  <path\n      android:pathData=\"M369.53,0.18L381.77,0.18A6,6 0,0 1,387.77 6.18L387.77,18.41A6,6 0,0 1,381.77 24.41L369.53,24.41A6,6 0,0 1,363.53 18.41L363.53,6.18A6,6 0,0 1,369.53 0.18z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.18\"/>\n  <path\n      android:pathData=\"M369.53,145.59L381.77,145.59A6,6 0,0 1,387.77 151.59L387.77,163.82A6,6 0,0 1,381.77 169.82L369.53,169.82A6,6 0,0 1,363.53 163.82L363.53,151.59A6,6 0,0 1,369.53 145.59z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.012\"/>\n  <path\n      android:pathData=\"M393.77,24.41L406,24.41A6,6 0,0 1,412 30.41L412,42.65A6,6 0,0 1,406 48.65L393.77,48.65A6,6 0,0 1,387.77 42.65L387.77,30.41A6,6 0,0 1,393.77 24.41z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.12\"/>\n  <path\n      android:pathData=\"M393.77,72.88L406,72.88A6,6 0,0 1,412 78.88L412,91.12A6,6 0,0 1,406 97.12L393.77,97.12A6,6 0,0 1,387.77 91.12L387.77,78.88A6,6 0,0 1,393.77 72.88z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.204\"/>\n  <path\n      android:pathData=\"M393.77,121.35L406,121.35A6,6 0,0 1,412 127.35L412,139.59A6,6 0,0 1,406 145.59L393.77,145.59A6,6 0,0 1,387.77 139.59L387.77,127.35A6,6 0,0 1,393.77 121.35z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.06\"/>\n  <path\n      android:pathData=\"M393.77,48.65L406,48.65A6,6 0,0 1,412 54.65L412,66.88A6,6 0,0 1,406 72.88L393.77,72.88A6,6 0,0 1,387.77 66.88L387.77,54.65A6,6 0,0 1,393.77 48.65z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.12\"/>\n  <path\n      android:pathData=\"M393.77,97.12L406,97.12A6,6 0,0 1,412 103.12L412,115.35A6,6 0,0 1,406 121.35L393.77,121.35A6,6 0,0 1,387.77 115.35L387.77,103.12A6,6 0,0 1,393.77 97.12z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.096\"/>\n  <path\n      android:pathData=\"M393.77,0.18L406,0.18A6,6 0,0 1,412 6.18L412,18.41A6,6 0,0 1,406 24.41L393.77,24.41A6,6 0,0 1,387.77 18.41L387.77,6.18A6,6 0,0 1,393.77 0.18z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.24\"/>\n  <path\n      android:pathData=\"M393.77,145.59L406,145.59A6,6 0,0 1,412 151.59L412,163.82A6,6 0,0 1,406 169.82L393.77,169.82A6,6 0,0 1,387.77 163.82L387.77,151.59A6,6 0,0 1,393.77 145.59z\"\n      android:strokeAlpha=\"0.6\"\n      android:fillColor=\"#4086F6\"\n      android:fillAlpha=\"0.036\"/>\n</vector>\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/ActivityPubAccountManager.kt",
    "content": "package com.zhangke.fread.activitypub.app\n\nimport com.zhangke.framework.utils.exceptionOrThrow\nimport com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubAccountEntityAdapter\nimport com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubLoggedAccountAdapter\nimport com.zhangke.fread.activitypub.app.internal.auth.ActivityPubClientManager\nimport com.zhangke.fread.activitypub.app.internal.auth.ActivityPubOAuthor\nimport com.zhangke.fread.activitypub.app.internal.auth.LoggedAccountProvider\nimport com.zhangke.fread.activitypub.app.internal.content.ActivityPubContent\nimport com.zhangke.fread.activitypub.app.internal.model.ActivityPubLoggedAccount\nimport com.zhangke.fread.activitypub.app.internal.push.ActivityPubPushManager\nimport com.zhangke.fread.activitypub.app.internal.repo.account.ActivityPubLoggedAccountRepo\nimport com.zhangke.fread.activitypub.app.internal.uri.UserUriTransformer\nimport com.zhangke.fread.activitypub.app.internal.usecase.ActivityPubAccountLogoutUseCase\nimport com.zhangke.fread.common.di.ApplicationCoroutineScope\nimport com.zhangke.fread.status.account.AccountRefreshResult\nimport com.zhangke.fread.status.account.IAccountManager\nimport com.zhangke.fread.status.account.LoggedAccount\nimport com.zhangke.fread.status.author.BlogAuthor\nimport com.zhangke.fread.status.model.FreadContent\nimport com.zhangke.fread.status.model.LoggedAccountDetail\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.model.Relationships\nimport com.zhangke.fread.status.model.notActivityPub\nimport com.zhangke.fread.status.platform.BlogPlatform\nimport com.zhangke.fread.status.uri.FormalUri\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.map\nimport kotlinx.coroutines.launch\n\nclass ActivityPubAccountManager(\n    private val oAuthor: ActivityPubOAuthor,\n    private val clientManager: ActivityPubClientManager,\n    private val loggedAccountProvider: LoggedAccountProvider,\n    private val accountRepo: ActivityPubLoggedAccountRepo,\n    private val userUriTransformer: UserUriTransformer,\n    private val accountAdapter: ActivityPubLoggedAccountAdapter,\n    private val activityPubPushManager: ActivityPubPushManager,\n    private val applicationCoroutineScope: ApplicationCoroutineScope,\n    private val accountEntityAdapter: ActivityPubAccountEntityAdapter,\n    private val accountLogout: ActivityPubAccountLogoutUseCase,\n) : IAccountManager {\n\n    init {\n        applicationCoroutineScope.launch {\n            accountRepo.getAllAccountFlow()\n                .collect {\n                    clientManager.clearCache()\n                    loggedAccountProvider.updateAccounts(it)\n                }\n        }\n    }\n\n    override suspend fun getAllLoggedAccount(): List<ActivityPubLoggedAccount> {\n        return accountRepo.queryAll()\n    }\n\n    override fun getAllAccountFlow(): Flow<List<ActivityPubLoggedAccount>> {\n        return accountRepo.getAllAccountFlow()\n    }\n\n    override fun getAllAccountDetailFlow(): Flow<List<LoggedAccountDetail>>? {\n        return accountRepo.getAllAccountFlow()\n            .map { list ->\n                list.map { accountEntityAdapter.convertLoggedAccountDetail(it) }\n            }\n    }\n\n    fun observeAccount(accountUri: FormalUri): Flow<ActivityPubLoggedAccount?> {\n        return accountRepo.observeAccount(accountUri.toString())\n    }\n\n    override suspend fun refreshAllAccountInfo(): List<AccountRefreshResult> {\n        return accountRepo.queryAll().map { loggedAccount ->\n            val locator =\n                PlatformLocator(accountUri = loggedAccount.uri, baseUrl = loggedAccount.baseUrl)\n            val client = clientManager.getClient(locator)\n            val result = client.accountRepo\n                .getAccount(loggedAccount.userId)\n                .mapCatching {\n                    val newAccount = accountAdapter.createFromAccount(\n                        platform = loggedAccount.platform,\n                        account = it,\n                        token = loggedAccount.token,\n                    )\n                    accountRepo.update(newAccount)\n                    newAccount\n                }\n            if (result.isFailure) {\n                AccountRefreshResult.Failure(loggedAccount, result.exceptionOrThrow())\n            } else {\n                AccountRefreshResult.Success(result.getOrThrow())\n            }\n        }\n    }\n\n    override suspend fun triggerLaunchAuth(platform: BlogPlatform, account: LoggedAccount?) {\n        if (platform.protocol.notActivityPub) return\n        oAuthor.startOauth(platform.baseUrl)\n    }\n\n    override suspend fun logout(account: LoggedAccount): Boolean {\n        if (account !is ActivityPubLoggedAccount) {\n            return false\n        }\n        accountLogout(account)\n        return true\n    }\n\n    override fun subscribeNotification() {\n        applicationCoroutineScope.launch {\n            accountRepo.queryAll().forEach { account ->\n                subscribeNotificationForAccount(account)\n            }\n            accountRepo.onNewAccountFlow.collect {\n                subscribeNotificationForAccount(it)\n            }\n        }\n    }\n\n    override suspend fun cancelFollowRequest(\n        account: LoggedAccount,\n        user: BlogAuthor,\n    ): Result<Unit>? {\n        if (account.platform.protocol.notActivityPub) return null\n        if (user.userId.isNullOrEmpty()) {\n            return Result.failure(IllegalArgumentException(\"User ID cannot be null or empty\"))\n        }\n        val locator = PlatformLocator(baseUrl = account.platform.baseUrl, accountUri = account.uri)\n        return clientManager.getClient(locator)\n            .accountRepo\n            .unfollow(user.userId!!)\n            .map { }\n    }\n\n    private suspend fun subscribeNotificationForAccount(account: ActivityPubLoggedAccount) {\n        val role = PlatformLocator(baseUrl = account.platform.baseUrl, accountUri = account.uri)\n        activityPubPushManager.subscribe(role, account.userId)\n    }\n\n    override suspend fun getRelationships(\n        account: LoggedAccount,\n        accounts: List<BlogAuthor>,\n    ): Result<Map<FormalUri, Relationships>> {\n        val userIdList = accounts.filter { userUriTransformer.parse(it.uri) != null }\n            .mapNotNull { it.userId }\n        if (userIdList.isEmpty()) return Result.success(emptyMap())\n        val locator = PlatformLocator(\n            baseUrl = account.platform.baseUrl,\n            accountUri = account.uri,\n        )\n        return clientManager.getClient(locator)\n            .accountRepo\n            .getRelationships(idList = userIdList)\n            .map { list ->\n                val relationships = mutableMapOf<FormalUri, Relationships>()\n                for (entity in list) {\n                    val user = accounts.firstOrNull { it.userId == entity.id }\n                    if (user != null) {\n                        relationships[user.uri] = accountEntityAdapter.convertRelationship(entity)\n                    }\n                }\n                relationships\n            }\n    }\n\n    override suspend fun unblockAccount(\n        account: LoggedAccount,\n        user: BlogAuthor\n    ): Result<Unit>? {\n        if (account.platform.protocol.notActivityPub) return null\n        if (user.userId.isNullOrEmpty()) {\n            return Result.failure(IllegalArgumentException(\"User ID cannot be null or empty\"))\n        }\n        val locator = PlatformLocator(\n            baseUrl = account.platform.baseUrl,\n            accountUri = account.uri,\n        )\n        return clientManager.getClient(locator)\n            .accountRepo\n            .unblock(user.userId!!)\n            .map { }\n    }\n\n    override suspend fun selectContentWithAccount(\n        contentList: List<FreadContent>,\n        account: LoggedAccount,\n    ): List<FreadContent> {\n        return contentList.filterIsInstance<ActivityPubContent>()\n            .filter { it.baseUrl == account.platform.baseUrl }\n    }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/ActivityPubContentManager.kt",
    "content": "package com.zhangke.fread.activitypub.app\n\nimport com.zhangke.fread.activitypub.app.internal.content.ActivityPubContent\nimport com.zhangke.fread.activitypub.app.internal.screen.add.AddActivityPubContentScreenKey\nimport com.zhangke.fread.status.content.AddContentAction\nimport com.zhangke.fread.status.content.IContentManager\nimport com.zhangke.fread.status.model.ContentConfig\nimport com.zhangke.fread.status.model.FreadContent\nimport com.zhangke.fread.status.model.notActivityPub\nimport com.zhangke.fread.status.platform.BlogPlatform\n\nclass ActivityPubContentManager () : IContentManager {\n\n    override suspend fun addContent(\n        platform: BlogPlatform,\n        action: AddContentAction,\n    ) {\n        if (platform.protocol.notActivityPub) return\n        action.onFinishPage()\n        action.onOpenNewPage(AddActivityPubContentScreenKey(platform))\n    }\n\n    override fun restoreContent(config: ContentConfig): FreadContent? {\n        if (config !is ContentConfig.ActivityPubContent) return null\n        return ActivityPubContent(\n            order = config.order,\n            name = config.name,\n            baseUrl = config.baseUrl,\n            tabList = buildList {\n                addAll(config.showingTabList.map { it.toTab(false) })\n                addAll(config.hiddenTabList.map { it.toTab(true) })\n            },\n            accountUri = null,\n        )\n    }\n\n    private fun ContentConfig.ActivityPubContent.ContentTab.toTab(\n        hide: Boolean,\n    ): ActivityPubContent.ContentTab {\n        return when (this) {\n            is ContentConfig.ActivityPubContent.ContentTab.HomeTimeline -> ActivityPubContent.ContentTab.HomeTimeline(\n                order = this.order,\n                hide = hide,\n            )\n\n            is ContentConfig.ActivityPubContent.ContentTab.LocalTimeline -> ActivityPubContent.ContentTab.LocalTimeline(\n                order = this.order,\n                hide = hide,\n            )\n\n            is ContentConfig.ActivityPubContent.ContentTab.ListTimeline -> ActivityPubContent.ContentTab.ListTimeline(\n                order = this.order,\n                listId = this.listId,\n                name = this.name,\n                hide = hide,\n            )\n\n            is ContentConfig.ActivityPubContent.ContentTab.PublicTimeline -> ActivityPubContent.ContentTab.PublicTimeline(\n                order = this.order,\n                hide = hide,\n            )\n\n            is ContentConfig.ActivityPubContent.ContentTab.Trending -> ActivityPubContent.ContentTab.Trending(\n                order = this.order,\n                hide = hide,\n            )\n        }\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/ActivityPubJsonBuilder.kt",
    "content": "package com.zhangke.fread.activitypub.app\n\nimport com.zhangke.framework.architect.json.JsonModuleBuilder\nimport com.zhangke.fread.activitypub.app.internal.content.ActivityPubContent\nimport com.zhangke.fread.status.model.FreadContent\nimport com.zhangke.krouter.annotation.Service\nimport kotlinx.serialization.modules.SerializersModuleBuilder\nimport kotlinx.serialization.serializer\n\n@Service\nclass ActivityPubJsonBuilder : JsonModuleBuilder {\n\n    override fun SerializersModuleBuilder.buildSerializersModule() {\n        polymorphic(\n            baseClass = FreadContent::class,\n            actualClass = ActivityPubContent::class,\n            actualSerializer = serializer(),\n        )\n    }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/ActivityPubModule.kt",
    "content": "package com.zhangke.fread.activitypub.app\n\nimport com.zhangke.framework.nav.NavEntryProvider\nimport com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubAccountEntityAdapter\nimport com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubApplicationEntityAdapter\nimport com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubBlogMetaAdapter\nimport com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubContentAdapter\nimport com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubCustomEmojiEntityAdapter\nimport com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubInstanceAdapter\nimport com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubLoggedAccountAdapter\nimport com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubPlatformEntityAdapter\nimport com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubPollAdapter\nimport com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubSearchAdapter\nimport com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubStatusAdapter\nimport com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubTagAdapter\nimport com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubTranslationEntityAdapter\nimport com.zhangke.fread.activitypub.app.internal.adapter.PostStatusAttachmentAdapter\nimport com.zhangke.fread.activitypub.app.internal.adapter.RegisterApplicationEntryAdapter\nimport com.zhangke.fread.activitypub.app.internal.auth.ActivityPubClientManager\nimport com.zhangke.fread.activitypub.app.internal.auth.ActivityPubOAuthor\nimport com.zhangke.fread.activitypub.app.internal.auth.LoggedAccountProvider\nimport com.zhangke.fread.activitypub.app.internal.migrate.ActivityPubContentMigrator\nimport com.zhangke.fread.activitypub.app.internal.repo.WebFingerBaseUrlToUserIdRepo\nimport com.zhangke.fread.activitypub.app.internal.repo.account.ActivityPubLoggedAccountRepo\nimport com.zhangke.fread.activitypub.app.internal.repo.application.ActivityPubApplicationRepo\nimport com.zhangke.fread.activitypub.app.internal.repo.platform.ActivityPubPlatformRepo\nimport com.zhangke.fread.activitypub.app.internal.repo.platform.BlogPlatformResourceLoader\nimport com.zhangke.fread.activitypub.app.internal.repo.platform.MastodonInstanceRepo\nimport com.zhangke.fread.activitypub.app.internal.repo.status.ActivityPubStatusReadStateRepo\nimport com.zhangke.fread.activitypub.app.internal.repo.status.ActivityPubTimelineStatusRepo\nimport com.zhangke.fread.activitypub.app.internal.repo.user.UserRepo\nimport com.zhangke.fread.activitypub.app.internal.screen.account.EditAccountInfoViewModel\nimport com.zhangke.fread.activitypub.app.internal.screen.add.AddActivityPubContentViewModel\nimport com.zhangke.fread.activitypub.app.internal.screen.add.select.SelectPlatformViewModel\nimport com.zhangke.fread.activitypub.app.internal.screen.content.ActivityPubContentViewModel\nimport com.zhangke.fread.activitypub.app.internal.screen.content.edit.EditContentConfigViewModel\nimport com.zhangke.fread.activitypub.app.internal.screen.content.timeline.ActivityPubTimelineContainerViewModel\nimport com.zhangke.fread.activitypub.app.internal.screen.explorer.ExplorerContainerViewModel\nimport com.zhangke.fread.activitypub.app.internal.screen.filters.edit.EditFilterViewModel\nimport com.zhangke.fread.activitypub.app.internal.screen.filters.list.FiltersListViewModel\nimport com.zhangke.fread.activitypub.app.internal.screen.hashtag.HashtagTimelineContainerViewModel\nimport com.zhangke.fread.activitypub.app.internal.screen.instance.InstanceDetailViewModel\nimport com.zhangke.fread.activitypub.app.internal.screen.instance.about.ServerAboutViewModel\nimport com.zhangke.fread.activitypub.app.internal.screen.instance.tags.ServerTrendsTagsViewModel\nimport com.zhangke.fread.activitypub.app.internal.screen.list.CreatedListsViewModel\nimport com.zhangke.fread.activitypub.app.internal.screen.list.add.AddListViewModel\nimport com.zhangke.fread.activitypub.app.internal.screen.list.edit.EditListViewModel\nimport com.zhangke.fread.activitypub.app.internal.screen.search.SearchStatusViewModel\nimport com.zhangke.fread.activitypub.app.internal.screen.status.post.PostStatusViewModel\nimport com.zhangke.fread.activitypub.app.internal.screen.status.post.adapter.CustomEmojiAdapter\nimport com.zhangke.fread.activitypub.app.internal.screen.status.post.usecase.GenerateInitPostStatusUiStateUseCase\nimport com.zhangke.fread.activitypub.app.internal.screen.status.post.usecase.PublishPostUseCase\nimport com.zhangke.fread.activitypub.app.internal.screen.trending.TrendingStatusViewModel\nimport com.zhangke.fread.activitypub.app.internal.screen.user.UserDetailContainerViewModel\nimport com.zhangke.fread.activitypub.app.internal.screen.user.list.UserListType\nimport com.zhangke.fread.activitypub.app.internal.screen.user.list.UserListViewModel\nimport com.zhangke.fread.activitypub.app.internal.screen.user.search.SearchUserViewModel\nimport com.zhangke.fread.activitypub.app.internal.screen.user.status.StatusListContainerViewModel\nimport com.zhangke.fread.activitypub.app.internal.screen.user.tags.TagListViewModel\nimport com.zhangke.fread.activitypub.app.internal.screen.user.timeline.UserTimelineContainerViewModel\nimport com.zhangke.fread.activitypub.app.internal.source.UserSourceTransformer\nimport com.zhangke.fread.activitypub.app.internal.uri.PlatformUriTransformer\nimport com.zhangke.fread.activitypub.app.internal.uri.UserUriTransformer\nimport com.zhangke.fread.activitypub.app.internal.usecase.ActivityPubAccountLogoutUseCase\nimport com.zhangke.fread.activitypub.app.internal.usecase.GetDefaultBaseUrlUseCase\nimport com.zhangke.fread.activitypub.app.internal.usecase.GetInstanceAnnouncementUseCase\nimport com.zhangke.fread.activitypub.app.internal.usecase.GetServerTrendTagsUseCase\nimport com.zhangke.fread.activitypub.app.internal.usecase.UpdateActivityPubUserListUseCase\nimport com.zhangke.fread.activitypub.app.internal.usecase.content.GetUserCreatedListUseCase\nimport com.zhangke.fread.activitypub.app.internal.usecase.content.ReorderActivityPubTabUseCase\nimport com.zhangke.fread.activitypub.app.internal.usecase.emoji.GetCustomEmojiUseCase\nimport com.zhangke.fread.activitypub.app.internal.usecase.emoji.MapCustomEmojiUseCase\nimport com.zhangke.fread.activitypub.app.internal.usecase.media.UploadMediaAttachmentUseCase\nimport com.zhangke.fread.activitypub.app.internal.usecase.platform.GetInstancePostStatusRulesUseCase\nimport com.zhangke.fread.activitypub.app.internal.usecase.source.user.SearchUserSourceNoTokenUseCase\nimport com.zhangke.fread.activitypub.app.internal.usecase.status.GetStatusContextUseCase\nimport com.zhangke.fread.activitypub.app.internal.usecase.status.GetTimelineStatusUseCase\nimport com.zhangke.fread.activitypub.app.internal.usecase.status.GetUserStatusUseCase\nimport com.zhangke.fread.activitypub.app.internal.usecase.status.StatusInteractiveUseCase\nimport com.zhangke.fread.activitypub.app.internal.usecase.status.VotePollUseCase\nimport com.zhangke.fread.activitypub.app.internal.utils.MastodonHelper\nimport com.zhangke.fread.common.browser.BrowserInterceptor\nimport com.zhangke.fread.status.IStatusProvider\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.uri.FormalUri\nimport org.koin.core.module.Module\nimport org.koin.core.module.dsl.factoryOf\nimport org.koin.core.module.dsl.singleOf\nimport org.koin.core.module.dsl.viewModel\nimport org.koin.core.module.dsl.viewModelOf\nimport org.koin.dsl.bind\nimport org.koin.dsl.module\n\nval activityPubModule = module {\n\n    createPlatformModule()\n\n    factoryOf(::ActivityPubNavEntryProvider) bind NavEntryProvider::class\n\n    singleOf(::ActivityPubAccountManager)\n    singleOf(::ActivityPubClientManager)\n    singleOf(::ActivityPubOAuthor)\n    singleOf(::LoggedAccountProvider)\n    singleOf(::ActivityPubLoggedAccountRepo)\n    singleOf(::MastodonHelper)\n\n    factoryOf(::ActivityPubContentManager)\n    factoryOf(::ActivityPubScreenProvider)\n    factoryOf(::ActivityPubSearchEngine)\n    factoryOf(::ActivityPubSourceResolver)\n    factoryOf(::ActivityPubStatusResolver)\n    factoryOf(::ActivityPubNotificationResolver)\n    factoryOf(::ActivityPubPublishManager)\n    factoryOf(::ActivityPubStartup)\n    factoryOf(::ActivityPubUrlInterceptor) bind BrowserInterceptor::class\n    factoryOf(::ActivityPubContentMigrator)\n\n    factoryOf(::ActivityPubAccountEntityAdapter)\n    factoryOf(::ActivityPubApplicationEntityAdapter)\n    factoryOf(::ActivityPubBlogMetaAdapter)\n    factoryOf(::ActivityPubContentAdapter)\n    factoryOf(::ActivityPubCustomEmojiEntityAdapter)\n    factoryOf(::ActivityPubInstanceAdapter)\n    factoryOf(::ActivityPubLoggedAccountAdapter)\n    factoryOf(::ActivityPubPlatformEntityAdapter)\n    factoryOf(::ActivityPubPollAdapter)\n    factoryOf(::ActivityPubSearchAdapter)\n    factoryOf(::ActivityPubStatusAdapter)\n    factoryOf(::ActivityPubTagAdapter)\n    factoryOf(::ActivityPubTranslationEntityAdapter)\n    factoryOf(::PostStatusAttachmentAdapter)\n    factoryOf(::RegisterApplicationEntryAdapter)\n    factoryOf(::CustomEmojiAdapter)\n\n    factoryOf(::ActivityPubApplicationRepo)\n    factoryOf(::ActivityPubPlatformRepo)\n    factoryOf(::ActivityPubStatusReadStateRepo)\n    factoryOf(::ActivityPubTimelineStatusRepo)\n    factoryOf(::BlogPlatformResourceLoader)\n    factoryOf(::MastodonInstanceRepo)\n    factoryOf(::UserRepo)\n    factoryOf(::WebFingerBaseUrlToUserIdRepo)\n\n    factoryOf(::UserSourceTransformer)\n    factoryOf(::PlatformUriTransformer)\n    factoryOf(::UserUriTransformer)\n\n    factoryOf(::ActivityPubAccountLogoutUseCase)\n    factoryOf(::GetDefaultBaseUrlUseCase)\n    factoryOf(::GetInstanceAnnouncementUseCase)\n    factoryOf(::GetServerTrendTagsUseCase)\n    factoryOf(::UpdateActivityPubUserListUseCase)\n    factoryOf(::GetUserCreatedListUseCase)\n    factoryOf(::ReorderActivityPubTabUseCase)\n    factoryOf(::GetCustomEmojiUseCase)\n    factoryOf(::MapCustomEmojiUseCase)\n    factoryOf(::UploadMediaAttachmentUseCase)\n    factoryOf(::GetInstancePostStatusRulesUseCase)\n    factoryOf(::SearchUserSourceNoTokenUseCase)\n    factoryOf(::GetStatusContextUseCase)\n    factoryOf(::GetTimelineStatusUseCase)\n    factoryOf(::GetUserStatusUseCase)\n    factoryOf(::StatusInteractiveUseCase)\n    factoryOf(::VotePollUseCase)\n    factoryOf(::GenerateInitPostStatusUiStateUseCase)\n    factoryOf(::PublishPostUseCase)\n    viewModelOf(::EditAccountInfoViewModel)\n    viewModelOf(::AddActivityPubContentViewModel)\n    viewModelOf(::SelectPlatformViewModel)\n    viewModelOf(::ActivityPubContentViewModel)\n    viewModelOf(::EditContentConfigViewModel)\n    viewModelOf(::ActivityPubTimelineContainerViewModel)\n    viewModelOf(::ExplorerContainerViewModel)\n    viewModel {\n        EditFilterViewModel(\n            clientManager = get(),\n            locator = it.get(),\n            id = it.getOrNull<String>(),\n        )\n    }\n    viewModelOf(::FiltersListViewModel)\n    viewModelOf(::HashtagTimelineContainerViewModel)\n    viewModelOf(::InstanceDetailViewModel)\n    viewModelOf(::ServerAboutViewModel)\n    viewModelOf(::ServerTrendsTagsViewModel)\n    viewModelOf(::CreatedListsViewModel)\n    viewModelOf(::AddListViewModel)\n    viewModelOf(::EditListViewModel)\n    viewModelOf(::SearchStatusViewModel)\n    viewModelOf(::PostStatusViewModel)\n    viewModelOf(::TrendingStatusViewModel)\n    viewModelOf(::UserDetailContainerViewModel)\n    viewModel { params ->\n        UserListViewModel(\n            clientManager = get(),\n            userUriTransformer = get(),\n            webFingerBaseUrlToUserIdRepo = get(),\n            accountEntityAdapter = get(),\n            locator = params.values[0] as PlatformLocator,\n            type = params.values[1] as UserListType,\n            statusId = params.values.getOrNull(2) as String?,\n            userUri = params.values.getOrNull(3) as FormalUri?,\n            userId = params.values.getOrNull(4) as String?,\n        )\n    }\n    viewModelOf(::SearchUserViewModel)\n    viewModelOf(::StatusListContainerViewModel)\n    viewModelOf(::TagListViewModel)\n    viewModelOf(::UserTimelineContainerViewModel)\n\n    factoryOf(::ActivityPubProvider) bind IStatusProvider::class\n}\n\nexpect fun Module.createPlatformModule()\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/ActivityPubNavEntryProvider.kt",
    "content": "package com.zhangke.fread.activitypub.app\n\nimport androidx.navigation3.runtime.EntryProviderScope\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.nav.NavEntryProvider\nimport com.zhangke.framework.network.FormalBaseUrl\nimport com.zhangke.fread.activitypub.app.internal.screen.account.EditAccountInfoScreen\nimport com.zhangke.fread.activitypub.app.internal.screen.account.EditAccountInfoScreenNavKey\nimport com.zhangke.fread.activitypub.app.internal.screen.add.AddActivityPubContentScreen\nimport com.zhangke.fread.activitypub.app.internal.screen.add.AddActivityPubContentScreenKey\nimport com.zhangke.fread.activitypub.app.internal.screen.add.select.SelectPlatformScreen\nimport com.zhangke.fread.activitypub.app.internal.screen.add.select.SelectPlatformScreenKey\nimport com.zhangke.fread.activitypub.app.internal.screen.content.edit.EditContentConfigScreen\nimport com.zhangke.fread.activitypub.app.internal.screen.content.edit.EditContentConfigScreenKey\nimport com.zhangke.fread.activitypub.app.internal.screen.filters.edit.EditFilterScreen\nimport com.zhangke.fread.activitypub.app.internal.screen.filters.edit.EditFilterScreenKey\nimport com.zhangke.fread.activitypub.app.internal.screen.filters.edit.HiddenKeywordScreen\nimport com.zhangke.fread.activitypub.app.internal.screen.filters.edit.HiddenKeywordScreenNavKey\nimport com.zhangke.fread.activitypub.app.internal.screen.filters.list.FiltersListScreen\nimport com.zhangke.fread.activitypub.app.internal.screen.filters.list.FiltersListScreenKey\nimport com.zhangke.fread.activitypub.app.internal.screen.hashtag.HashtagTimelineScreen\nimport com.zhangke.fread.activitypub.app.internal.screen.hashtag.HashtagTimelineScreenKey\nimport com.zhangke.fread.activitypub.app.internal.screen.instance.InstanceDetailScreen\nimport com.zhangke.fread.activitypub.app.internal.screen.instance.InstanceDetailScreenKey\nimport com.zhangke.fread.activitypub.app.internal.screen.list.CreatedListsScreen\nimport com.zhangke.fread.activitypub.app.internal.screen.list.CreatedListsScreenKey\nimport com.zhangke.fread.activitypub.app.internal.screen.list.add.AddListScreen\nimport com.zhangke.fread.activitypub.app.internal.screen.list.add.AddListScreenNavKey\nimport com.zhangke.fread.activitypub.app.internal.screen.list.edit.EditListScreen\nimport com.zhangke.fread.activitypub.app.internal.screen.list.edit.EditListScreenNavKey\nimport com.zhangke.fread.activitypub.app.internal.screen.search.SearchStatusScreen\nimport com.zhangke.fread.activitypub.app.internal.screen.search.SearchStatusScreenNavKey\nimport com.zhangke.fread.activitypub.app.internal.screen.status.post.PostStatusScreen\nimport com.zhangke.fread.activitypub.app.internal.screen.status.post.PostStatusScreenKey\nimport com.zhangke.fread.activitypub.app.internal.screen.status.post.PostStatusScreenRoute\nimport com.zhangke.fread.activitypub.app.internal.screen.user.UserDetailScreen\nimport com.zhangke.fread.activitypub.app.internal.screen.user.UserDetailScreenKey\nimport com.zhangke.fread.activitypub.app.internal.screen.user.list.UserListScreen\nimport com.zhangke.fread.activitypub.app.internal.screen.user.list.UserListScreenKey\nimport com.zhangke.fread.activitypub.app.internal.screen.user.search.SearchUserScreen\nimport com.zhangke.fread.activitypub.app.internal.screen.user.search.SearchUserScreenNavKey\nimport com.zhangke.fread.activitypub.app.internal.screen.user.status.StatusListScreen\nimport com.zhangke.fread.activitypub.app.internal.screen.user.status.StatusListScreenKey\nimport com.zhangke.fread.activitypub.app.internal.screen.user.tags.TagListScreen\nimport com.zhangke.fread.activitypub.app.internal.screen.user.tags.TagListScreenKey\nimport com.zhangke.fread.status.uri.FormalUri\nimport kotlinx.serialization.modules.PolymorphicModuleBuilder\nimport kotlinx.serialization.modules.subclass\nimport org.koin.compose.viewmodel.koinViewModel\nimport org.koin.core.parameter.parametersOf\n\nclass ActivityPubNavEntryProvider : NavEntryProvider {\n\n    override fun EntryProviderScope<NavKey>.build() {\n        entry<SelectPlatformScreenKey> {\n            SelectPlatformScreen(koinViewModel())\n        }\n        entry<AddActivityPubContentScreenKey> { key ->\n            AddActivityPubContentScreen(\n                platform = key.platform,\n                viewModel = koinViewModel { parametersOf(key.platform) },\n            )\n        }\n        entry<PostStatusScreenKey> {\n            PostStatusScreen(\n                viewModel = koinViewModel {\n                    parametersOf(\n                        PostStatusScreenRoute.buildParams(\n                            accountUri = it.accountUri,\n                            defaultContent = it.defaultContent,\n                            editBlog = it.editBlogJsonString,\n                            replyToBlogJsonString = it.replyingBlogJsonString,\n                            quoteBlogJsonString = it.quoteBlogJsonString,\n                        )\n                    )\n                }\n            )\n        }\n        entry<EditContentConfigScreenKey> { key ->\n            EditContentConfigScreen(\n                contentId = key.contentId,\n                viewModel = koinViewModel { parametersOf(key.contentId) },\n            )\n        }\n        entry<InstanceDetailScreenKey> {\n            InstanceDetailScreen(it.locator, koinViewModel { parametersOf(it.baseUrl) })\n        }\n        entry<UserDetailScreenKey> {\n            UserDetailScreen(\n                viewModel = koinViewModel(),\n                locator = it.locator,\n                userUri = it.userUri,\n                webFinger = it.webFinger,\n                userId = it.userId,\n            )\n        }\n        entry<StatusListScreenKey> {\n            StatusListScreen(it.locator, it.type)\n        }\n        entry<HashtagTimelineScreenKey> {\n            HashtagTimelineScreen(\n                viewModel = koinViewModel(),\n                locator = it.locator,\n                hashtag = it.hashtag,\n            )\n        }\n        entry<UserListScreenKey> {\n            UserListScreen(\n                viewModel = koinViewModel {\n                    parametersOf(\n                        it.locator,\n                        it.type,\n                        it.statusId,\n                        it.userUri,\n                        it.userId,\n                    )\n                }\n            )\n        }\n        entry<TagListScreenKey> {\n            TagListScreen(\n                viewModel = koinViewModel { parametersOf(it.locator) }\n            )\n        }\n        entry<FiltersListScreenKey> {\n            FiltersListScreen(\n                viewModel = koinViewModel { parametersOf(it.locator) },\n                locator = it.locator,\n            )\n        }\n        entry<EditFilterScreenKey> {\n            EditFilterScreen(\n                viewModel = koinViewModel { parametersOf(it.locator, it.id) },\n                id = it.id,\n            )\n        }\n        entry<CreatedListsScreenKey> {\n            CreatedListsScreen(\n                viewModel = koinViewModel { parametersOf(it.locator) },\n                locator = it.locator,\n            )\n        }\n        entry<AddListScreenNavKey> {\n            AddListScreen(\n                viewModel = koinViewModel { parametersOf(it.locator) },\n                locator = it.locator,\n            )\n        }\n        entry<SearchUserScreenNavKey> {\n            SearchUserScreen(\n                viewModel = koinViewModel { parametersOf(it.locator, it.onlyFollowing) }\n            )\n        }\n        entry<EditListScreenNavKey> {\n            EditListScreen(\n                locator = it.locator,\n                viewModel = koinViewModel { parametersOf(it.locator, it.serializedList) },\n            )\n        }\n        entry<EditAccountInfoScreenNavKey> {\n            EditAccountInfoScreen(\n                viewModel = koinViewModel {\n                    parametersOf(\n                        FormalBaseUrl.parse(it.baseUrl)!!,\n                        FormalUri.from(it.accountUri)!!,\n                    )\n                },\n            )\n        }\n        entry<SearchStatusScreenNavKey> {\n            SearchStatusScreen(\n                viewModel = koinViewModel { parametersOf(it.locator, it.userId) }\n            )\n        }\n        entry<HiddenKeywordScreenNavKey> {\n            HiddenKeywordScreen(it.addedKeywords)\n        }\n    }\n\n    override fun PolymorphicModuleBuilder<NavKey>.polymorph() {\n        subclass(SelectPlatformScreenKey::class)\n        subclass(AddActivityPubContentScreenKey::class)\n        subclass(PostStatusScreenKey::class)\n        subclass(EditContentConfigScreenKey::class)\n        subclass(InstanceDetailScreenKey::class)\n        subclass(UserDetailScreenKey::class)\n        subclass(StatusListScreenKey::class)\n        subclass(HashtagTimelineScreenKey::class)\n        subclass(UserListScreenKey::class)\n        subclass(TagListScreenKey::class)\n        subclass(FiltersListScreenKey::class)\n        subclass(EditFilterScreenKey::class)\n        subclass(CreatedListsScreenKey::class)\n        subclass(AddListScreenNavKey::class)\n        subclass(SearchUserScreenNavKey::class)\n        subclass(EditListScreenNavKey::class)\n        subclass(EditAccountInfoScreenNavKey::class)\n        subclass(SearchStatusScreenNavKey::class)\n        subclass(HiddenKeywordScreenNavKey::class)\n    }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/ActivityPubNotificationResolver.kt",
    "content": "package com.zhangke.fread.activitypub.app\n\nimport com.zhangke.activitypub.entities.ActivityPubNotificationsEntity\nimport com.zhangke.framework.date.DateParser\nimport com.zhangke.framework.ktx.ifNullOrEmpty\nimport com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubAccountEntityAdapter\nimport com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubStatusAdapter\nimport com.zhangke.fread.activitypub.app.internal.auth.ActivityPubClientManager\nimport com.zhangke.fread.activitypub.app.internal.auth.LoggedAccountProvider\nimport com.zhangke.fread.activitypub.app.internal.model.ActivityPubLoggedAccount\nimport com.zhangke.fread.activitypub.app.internal.repo.platform.ActivityPubPlatformRepo\nimport com.zhangke.fread.status.account.LoggedAccount\nimport com.zhangke.fread.status.author.BlogAuthor\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.model.notActivityPub\nimport com.zhangke.fread.status.notification.INotificationResolver\nimport com.zhangke.fread.status.notification.PagedStatusNotification\nimport com.zhangke.fread.status.notification.StatusNotification\nimport com.zhangke.fread.status.platform.BlogPlatform\nimport kotlinx.coroutines.async\nimport kotlinx.coroutines.supervisorScope\n\nclass ActivityPubNotificationResolver (\n    private val clientManager: ActivityPubClientManager,\n    private val platformRepo: ActivityPubPlatformRepo,\n    private val loggedAccountProvider: LoggedAccountProvider,\n    private val accountAdapter: ActivityPubAccountEntityAdapter,\n    private val statusAdapter: ActivityPubStatusAdapter,\n) : INotificationResolver {\n\n    override suspend fun getNotifications(\n        account: LoggedAccount,\n        type: INotificationResolver.NotificationRequestType,\n        cursor: String?,\n    ): Result<PagedStatusNotification>? {\n        if (account.platform.protocol.notActivityPub) return null\n        val isFirstPage = cursor == null\n        val locator = PlatformLocator(baseUrl = account.platform.baseUrl, accountUri = account.uri)\n        val platform = platformRepo.getPlatform(locator).let {\n            if (it.isFailure) return Result.failure(it.exceptionOrNull()!!)\n            it.getOrThrow()\n        }\n        val loggedAccount = loggedAccountProvider.getAccount(locator)\n        val notificationsRepo = clientManager.getClient(locator).notificationsRepo\n        val types = mutableListOf<String>()\n        if (type == INotificationResolver.NotificationRequestType.MENTION) {\n            types += ActivityPubNotificationsEntity.Type.MENTION\n        }\n\n        val (unreadCountResult, notificationsResult) = supervisorScope {\n            val notificationDeferred = async {\n                notificationsRepo.getNotifications(\n                    limit = 50,\n                    types = types,\n                    maxId = cursor,\n                )\n            }\n            val unreadCountDeferred = async {\n                if (isFirstPage) {\n                    notificationsRepo.getUnreadNotificationCount(limit = 1000).getOrNull()?.count\n                        ?: 0\n                } else {\n                    0\n                }\n            }\n            val notifications = notificationDeferred.await()\n            val unreadCount = unreadCountDeferred.await()\n            unreadCount to notifications\n        }\n        return notificationsResult.map { notifications ->\n            val cursor = notifications.lastOrNull()?.id\n            PagedStatusNotification(\n                cursor = cursor,\n                reachEnd = cursor == null,\n                notifications = notifications.mapIndexed { index, entity ->\n                    val unread = if (isFirstPage) {\n                        index < unreadCountResult\n                    } else {\n                        false\n                    }\n                    convertNotification(\n                        locator = locator,\n                        entity = entity,\n                        unread = unread,\n                        loggedAccount = loggedAccount,\n                        platform = platform,\n                    )\n                }\n            )\n        }\n    }\n\n    override suspend fun getNotificationUserDetail(\n        account: LoggedAccount,\n        users: List<BlogAuthor>,\n    ): Result<List<BlogAuthor>>? {\n        if (account !is ActivityPubLoggedAccount) return null\n        val userIdList = users.mapNotNull {\n            if (it.relationships == null) it.userId else null\n        }\n        if (userIdList.isEmpty()) return Result.success(emptyList())\n        return clientManager.getClient(account.locator)\n            .accountRepo\n            .getRelationships(idList = userIdList)\n            .map { list ->\n                users.mapNotNull { user ->\n                    val relationship = list.firstOrNull { it.id == user.userId }\n                        ?.let { accountAdapter.convertRelationship(it) }\n                    if (relationship != null) {\n                        user.copy(relationships = relationship)\n                    } else {\n                        null\n                    }\n                }\n            }\n    }\n\n    private fun convertNotification(\n        locator: PlatformLocator,\n        entity: ActivityPubNotificationsEntity,\n        loggedAccount: ActivityPubLoggedAccount?,\n        platform: BlogPlatform,\n        unread: Boolean,\n    ): StatusNotification {\n        val createAt = DateParser.parseOrCurrent(entity.createdAt)\n        val author = accountAdapter.toAuthor(entity.account)\n        val status = entity.status?.let {\n            statusAdapter.toStatusUiState(\n                entity = it,\n                platform = platform,\n                locator = locator,\n                loggedAccount = loggedAccount,\n            )\n        }\n        return when (entity.type) {\n            ActivityPubNotificationsEntity.FAVORITE -> {\n                if (status == null) {\n                    StatusNotification.Unknown(\n                        id = entity.id,\n                        createAt = createAt,\n                        unread = unread,\n                        locator = locator,\n                        message = \"Unknown notification type: ${entity.type}\",\n                    )\n                } else {\n                    StatusNotification.Like(\n                        id = entity.id,\n                        author = author,\n                        locator = locator,\n                        blog = status.status.intrinsicBlog,\n                        createAt = createAt,\n                        unread = unread,\n                    )\n                }\n            }\n\n            ActivityPubNotificationsEntity.MENTION -> {\n                StatusNotification.Mention(\n                    id = entity.id,\n                    author = author,\n                    status = status!!,\n                    unread = unread,\n                )\n            }\n\n            ActivityPubNotificationsEntity.FOLLOW -> {\n                StatusNotification.Follow(\n                    id = entity.id,\n                    author = author,\n                    locator = locator,\n                    createAt = createAt,\n                    unread = unread,\n                )\n            }\n\n            ActivityPubNotificationsEntity.REBLOG -> {\n                StatusNotification.Repost(\n                    id = entity.id,\n                    author = author,\n                    locator = locator,\n                    createAt = createAt,\n                    blog = status!!.status.intrinsicBlog,\n                    unread = unread,\n                )\n            }\n\n            ActivityPubNotificationsEntity.STATUS -> {\n                if (status == null) {\n                    StatusNotification.Unknown(\n                        id = entity.id,\n                        createAt = createAt,\n                        unread = unread,\n                        locator = locator,\n                        message = \"Unknown notification type: ${entity.type}\",\n                    )\n                } else {\n                    StatusNotification.NewStatus(\n                        status = status,\n                        unread = unread,\n                    )\n                }\n            }\n\n            ActivityPubNotificationsEntity.FOLLOW_REQUEST -> {\n                StatusNotification.FollowRequest(\n                    id = entity.id,\n                    createAt = createAt,\n                    locator = locator,\n                    author = author,\n                    unread = unread,\n                )\n            }\n\n            ActivityPubNotificationsEntity.POLL -> {\n                if (status == null) {\n                    StatusNotification.Unknown(\n                        id = entity.id,\n                        createAt = createAt,\n                        unread = unread,\n                        locator = locator,\n                        message = \"Unknown notification type: ${entity.type}\",\n                    )\n                } else {\n                    StatusNotification.Poll(\n                        id = entity.id,\n                        createAt = createAt,\n                        locator = locator,\n                        unread = unread,\n                        blog = status.status.intrinsicBlog,\n                    )\n                }\n            }\n\n            ActivityPubNotificationsEntity.UPDATE -> {\n                if (status == null) {\n                    StatusNotification.Unknown(\n                        id = entity.id,\n                        createAt = createAt,\n                        unread = unread,\n                        locator = locator,\n                        message = \"Unknown notification type: ${entity.type}\",\n                    )\n                } else {\n                    StatusNotification.Update(\n                        id = entity.id,\n                        createAt = createAt,\n                        status = status,\n                        unread = unread,\n                    )\n                }\n            }\n\n            ActivityPubNotificationsEntity.SEVERED_RELATIONSHIPS -> {\n                StatusNotification.SeveredRelationships(\n                    id = entity.id,\n                    createAt = createAt,\n                    locator = locator,\n                    author = author,\n                    unread = unread,\n                    reason = entity.relationshipSeveranceEvent?.targetName.ifNullOrEmpty { \"Unknown\" },\n                )\n            }\n\n            ActivityPubNotificationsEntity.QUOTE -> {\n                if (status == null) {\n                    StatusNotification.Unknown(\n                        id = entity.id,\n                        createAt = createAt,\n                        unread = unread,\n                        locator = locator,\n                        message = \"Unknown notification type: ${entity.type}\",\n                    )\n                } else {\n                    StatusNotification.Quote(\n                        id = entity.id,\n                        author = author,\n                        quote = status,\n                        unread = unread,\n                    )\n                }\n            }\n\n            ActivityPubNotificationsEntity.QUOTED_UPDATE -> {\n                if (status == null) {\n                    StatusNotification.Unknown(\n                        id = entity.id,\n                        createAt = createAt,\n                        unread = unread,\n                        locator = locator,\n                        message = \"Unknown notification type: ${entity.type}\",\n                    )\n                } else {\n                    StatusNotification.QuoteUpdate(\n                        id = entity.id,\n                        author = author,\n                        quote = status,\n                        unread = unread,\n                        createAt = createAt,\n                    )\n                }\n            }\n\n            else -> StatusNotification.Unknown(\n                id = entity.id,\n                createAt = createAt,\n                unread = unread,\n                locator = locator,\n                message = \"Unknown notification type: ${entity.type}\",\n            )\n        }\n    }\n\n    override suspend fun rejectFollowRequest(\n        account: LoggedAccount,\n        requestAuthor: BlogAuthor\n    ): Result<Unit>? {\n        if (account.platform.protocol.notActivityPub) return null\n        if (account !is ActivityPubLoggedAccount) return null\n        val userId = requestAuthor.userId\n        if (userId.isNullOrEmpty()) {\n            return Result.failure(IllegalArgumentException(\"Request author userId is empty\"))\n        }\n        val role = PlatformLocator(baseUrl = account.platform.baseUrl, accountUri = account.uri)\n        return clientManager.getClient(role)\n            .accountRepo\n            .rejectFollowRequest(userId)\n            .map { }\n    }\n\n    override suspend fun acceptFollowRequest(\n        account: LoggedAccount,\n        requestAuthor: BlogAuthor\n    ): Result<Unit>? {\n        if (account.platform.protocol.notActivityPub) return null\n        if (account !is ActivityPubLoggedAccount) return null\n        val userId = requestAuthor.userId\n        if (userId.isNullOrEmpty()) {\n            return Result.failure(IllegalArgumentException(\"Request author userId is empty\"))\n        }\n        val role = PlatformLocator(baseUrl = account.platform.baseUrl, accountUri = account.uri)\n        return clientManager.getClient(role)\n            .accountRepo\n            .authorizeFollowRequest(userId)\n            .map { }\n    }\n\n    override suspend fun updateUnreadNotification(\n        account: LoggedAccount,\n        notificationLastReadId: String\n    ): Result<Unit>? {\n        if (account.platform.protocol.notActivityPub) return null\n        val role = PlatformLocator(baseUrl = account.platform.baseUrl, accountUri = account.uri)\n        return clientManager.getClient(role)\n            .markerRepo\n            .saveMarkers(notificationLastReadId = notificationLastReadId)\n            .map { }\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/ActivityPubProvider.kt",
    "content": "package com.zhangke.fread.activitypub.app\n\nimport com.zhangke.fread.status.IStatusProvider\nimport com.zhangke.fread.status.account.IAccountManager\nimport com.zhangke.fread.status.content.IContentManager\nimport com.zhangke.fread.status.notification.INotificationResolver\nimport com.zhangke.fread.status.screen.IStatusScreenProvider\nimport com.zhangke.fread.status.status.IStatusResolver\n\nclass ActivityPubProvider (\n    internalContentManager: ActivityPubContentManager,\n    internalScreenProvider: ActivityPubScreenProvider,\n    internalSearchEngine: ActivityPubSearchEngine,\n    internalStatusResolver: ActivityPubStatusResolver,\n    internalSourceResolver: ActivityPubSourceResolver,\n    internalAccountManager: ActivityPubAccountManager,\n    notificationResolver: ActivityPubNotificationResolver,\n    activityPubPublishManager: ActivityPubPublishManager,\n) : IStatusProvider {\n\n    override val contentManager: IContentManager = internalContentManager\n\n    override val screenProvider: IStatusScreenProvider = internalScreenProvider\n\n    override val searchEngine = internalSearchEngine\n\n    override val statusResolver: IStatusResolver = internalStatusResolver\n\n    override val statusSourceResolver = internalSourceResolver\n\n    override val accountManager: IAccountManager = internalAccountManager\n\n    override val notificationResolver: INotificationResolver = notificationResolver\n\n    override val publishManager = activityPubPublishManager\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/ActivityPubPublishManager.kt",
    "content": "package com.zhangke.fread.activitypub.app\n\nimport com.zhangke.activitypub.entities.ActivityPubInstanceConfigurationEntity\nimport com.zhangke.activitypub.entities.ActivityPubInstanceEntity\nimport com.zhangke.framework.utils.initLocale\nimport com.zhangke.fread.activitypub.app.internal.auth.ActivityPubClientManager\nimport com.zhangke.fread.activitypub.app.internal.model.ActivityPubLoggedAccount\nimport com.zhangke.fread.activitypub.app.internal.screen.status.post.PostStatusAttachment\nimport com.zhangke.fread.activitypub.app.internal.screen.status.post.PostStatusMediaAttachmentFile\nimport com.zhangke.fread.activitypub.app.internal.screen.status.post.usecase.PublishPostUseCase\nimport com.zhangke.fread.status.account.LoggedAccount\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.model.PublishBlogRules\nimport com.zhangke.fread.status.model.notActivityPub\nimport com.zhangke.fread.status.publish.IPublishBlogManager\nimport com.zhangke.fread.status.publish.PublishingMedia\nimport com.zhangke.fread.status.publish.PublishingPost\n\nclass ActivityPubPublishManager(\n    private val clientManager: ActivityPubClientManager,\n    private val publishPost: PublishPostUseCase,\n) : IPublishBlogManager {\n\n    private val baseUrlToInstanceInfo = mutableMapOf<String, ActivityPubInstanceEntity>()\n\n    override suspend fun getPublishBlogRules(account: LoggedAccount): Result<PublishBlogRules>? {\n        if (account.platform.protocol.notActivityPub) return null\n        baseUrlToInstanceInfo[account.platform.baseUrl.toString()]?.let { instance ->\n            return Result.success(instance.configuration.toRules())\n        }\n        val locator = PlatformLocator(accountUri = account.uri, baseUrl = account.platform.baseUrl)\n        return clientManager.getClient(locator)\n            .instanceRepo\n            .getInstanceInformation()\n            .map {\n                baseUrlToInstanceInfo[account.platform.baseUrl.toString()] = it\n                it.configuration.toRules()\n            }\n    }\n\n    private fun ActivityPubInstanceConfigurationEntity.toRules(): PublishBlogRules {\n        return PublishBlogRules(\n            maxCharacters = this.statuses?.maxCharacters ?: 200,\n            maxMediaCount = this.statuses?.maxMediaAttachments ?: 4,\n            maxPollOptions = this.polls?.maxOptions ?: 4,\n            supportSpoiler = true,\n            supportPoll = true,\n            maxLanguageCount = 1,\n            mediaAltMaxCharacters = 1500,\n        )\n    }\n\n    override suspend fun publish(\n        account: LoggedAccount,\n        post: PublishingPost\n    ): Result<Unit>? {\n        if (account.platform.protocol.notActivityPub) return null\n        val apAccount = account as ActivityPubLoggedAccount\n        return publishPost(\n            account = apAccount,\n            content = post.content,\n            attachment = post.medias.convert(),\n            sensitive = post.sensitive,\n            warningContent = post.warningText,\n            visibility = post.visibility,\n            language = initLocale(post.languageCode),\n        )\n    }\n\n    private fun List<PublishingMedia>.convert(): PostStatusAttachment? {\n        if (this.isEmpty()) return null\n        if (this.first().isVideo) {\n            return PostStatusAttachment.Video(this.first().convert())\n        }\n        return PostStatusAttachment.Image(this.map { it.convert() })\n    }\n\n    private fun PublishingMedia.convert(): PostStatusMediaAttachmentFile {\n        return PostStatusMediaAttachmentFile.LocalFile(\n            file = this.file,\n            alt = this.alt,\n        )\n    }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/ActivityPubScreenProvider.kt",
    "content": "package com.zhangke.fread.activitypub.app\n\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.nav.Tab\nimport com.zhangke.framework.network.FormalBaseUrl\nimport com.zhangke.framework.utils.WebFinger\nimport com.zhangke.fread.activitypub.app.internal.auth.LoggedAccountProvider\nimport com.zhangke.fread.activitypub.app.internal.content.ActivityPubContent\nimport com.zhangke.fread.activitypub.app.internal.model.ActivityPubLoggedAccount\nimport com.zhangke.fread.activitypub.app.internal.screen.add.select.SelectPlatformScreenKey\nimport com.zhangke.fread.activitypub.app.internal.screen.content.ActivityPubContentTab\nimport com.zhangke.fread.activitypub.app.internal.screen.content.edit.EditContentConfigScreenKey\nimport com.zhangke.fread.activitypub.app.internal.screen.explorer.ExplorerContainerTab\nimport com.zhangke.fread.activitypub.app.internal.screen.hashtag.HashtagTimelineScreenKey\nimport com.zhangke.fread.activitypub.app.internal.screen.instance.InstanceDetailScreenKey\nimport com.zhangke.fread.activitypub.app.internal.screen.status.post.PostStatusScreenKey\nimport com.zhangke.fread.activitypub.app.internal.screen.status.post.PostStatusScreenRoute\nimport com.zhangke.fread.activitypub.app.internal.screen.user.UserDetailScreenKey\nimport com.zhangke.fread.activitypub.app.internal.screen.user.list.UserListScreenKey\nimport com.zhangke.fread.activitypub.app.internal.screen.user.list.UserListType\nimport com.zhangke.fread.activitypub.app.internal.uri.UserUriTransformer\nimport com.zhangke.fread.status.account.LoggedAccount\nimport com.zhangke.fread.status.blog.Blog\nimport com.zhangke.fread.status.model.FreadContent\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.model.StatusProviderProtocol\nimport com.zhangke.fread.status.model.notActivityPub\nimport com.zhangke.fread.status.platform.BlogPlatform\nimport com.zhangke.fread.status.screen.IStatusScreenProvider\nimport com.zhangke.fread.status.uri.FormalUri\n\nclass ActivityPubScreenProvider(\n    private val userUriTransformer: UserUriTransformer,\n    private val loggedAccountProvider: LoggedAccountProvider,\n) : IStatusScreenProvider {\n\n    override fun getReplyBlogScreen(locator: PlatformLocator, blog: Blog): NavKey? {\n        return openPublishPostScreen(\n            locator = locator,\n            blog = blog,\n        ) { accountUri, blog ->\n            PostStatusScreenRoute.buildReplyScreen(\n                accountUri = accountUri,\n                blog = blog,\n            )\n        }\n    }\n\n    override fun getEditBlogScreen(locator: PlatformLocator, blog: Blog): NavKey? {\n        return openPublishPostScreen(\n            locator = locator,\n            blog = blog,\n        ) { accountUri, blog ->\n            PostStatusScreenRoute.buildEditBlogRoute(\n                accountUri = accountUri,\n                blog = blog,\n            )\n        }\n    }\n\n    override fun getQuoteBlogScreen(locator: PlatformLocator, blog: Blog): NavKey? {\n        return openPublishPostScreen(\n            locator = locator,\n            blog = blog,\n        ) { accountUri, blog ->\n            PostStatusScreenRoute.buildQuoteBlogScreen(\n                accountUri = accountUri,\n                quoteBlog = blog,\n            )\n        }\n    }\n\n    private fun openPublishPostScreen(\n        locator: PlatformLocator,\n        blog: Blog,\n        builder: (FormalUri, Blog) -> NavKey,\n    ): NavKey? {\n        if (blog.platform.protocol.notActivityPub) return null\n        var accountUri = locator.accountUri\n        if (accountUri == null) {\n            accountUri = loggedAccountProvider.getAccount(locator.baseUrl)?.uri\n        }\n        accountUri ?: return null\n        return builder(accountUri, blog)\n    }\n\n    override fun getContentScreen(content: FreadContent, isLatestTab: Boolean): Tab? {\n        if (content !is ActivityPubContent) return null\n        return ActivityPubContentTab(content.id, isLatestTab)\n    }\n\n    override fun getEditContentConfigScreenScreen(content: FreadContent): NavKey? {\n        if (content !is ActivityPubContent) return null\n        return EditContentConfigScreenKey(content.id)\n    }\n\n    override fun getUserDetailScreen(\n        locator: PlatformLocator,\n        uri: FormalUri,\n        userId: String?,\n    ): NavKey? {\n        userUriTransformer.parse(uri) ?: return null\n        return UserDetailScreenKey(locator = locator, userUri = uri, userId = userId)\n    }\n\n    override fun getUserDetailScreen(\n        locator: PlatformLocator,\n        webFinger: WebFinger,\n        protocol: StatusProviderProtocol,\n    ): NavKey? {\n        if (protocol.notActivityPub) return null\n        return UserDetailScreenKey(locator = locator, webFinger = webFinger)\n    }\n\n    override fun getUserDetailScreen(\n        locator: PlatformLocator,\n        did: String,\n        protocol: StatusProviderProtocol\n    ): NavKey? {\n        return null\n    }\n\n    override fun getTagTimelineScreen(\n        locator: PlatformLocator,\n        tag: String,\n        protocol: StatusProviderProtocol,\n    ): NavKey? {\n        if (protocol.notActivityPub) return null\n        return HashtagTimelineScreenKey(\n            locator = locator,\n            hashtag = tag.removePrefix(\"#\"),\n        )\n    }\n\n    override fun getBlogFavouritedScreen(\n        locator: PlatformLocator,\n        blog: Blog,\n        protocol: StatusProviderProtocol\n    ): NavKey? {\n        if (protocol.notActivityPub) return null\n        return UserListScreenKey(\n            locator = locator,\n            type = UserListType.FAVOURITES,\n            statusId = blog.id,\n        )\n    }\n\n    override fun getBlogBoostedScreen(\n        locator: PlatformLocator,\n        blog: Blog,\n        protocol: StatusProviderProtocol\n    ): NavKey? {\n        if (protocol.notActivityPub) return null\n        return UserListScreenKey(\n            locator = locator,\n            type = UserListType.REBLOGS,\n            statusId = blog.id,\n        )\n    }\n\n    override fun getInstanceDetailScreen(\n        locator: PlatformLocator,\n        protocol: StatusProviderProtocol,\n        baseUrl: FormalBaseUrl,\n    ): NavKey? {\n        if (protocol.notActivityPub) return null\n        return InstanceDetailScreenKey(locator = locator, baseUrl = baseUrl)\n    }\n\n    override fun getExplorerTab(locator: PlatformLocator, platform: BlogPlatform): Tab? {\n        if (platform.protocol.notActivityPub) return null\n        return ExplorerContainerTab(locator = locator, platform = platform)\n    }\n\n    override fun getAddContentScreen(protocol: StatusProviderProtocol): NavKey? {\n        if (protocol.notActivityPub) return null\n        return SelectPlatformScreenKey\n    }\n\n    override fun getPublishScreen(account: LoggedAccount, text: String): NavKey? {\n        if (account !is ActivityPubLoggedAccount) return null\n        return PostStatusScreenKey(accountUri = account.uri, defaultContent = text)\n    }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/ActivityPubSearchEngine.kt",
    "content": "package com.zhangke.fread.activitypub.app\n\nimport com.zhangke.activitypub.api.SearchRepo\nimport com.zhangke.framework.network.FormalBaseUrl\nimport com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubAccountEntityAdapter\nimport com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubSearchAdapter\nimport com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubStatusAdapter\nimport com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubTagAdapter\nimport com.zhangke.fread.activitypub.app.internal.auth.ActivityPubClientManager\nimport com.zhangke.fread.activitypub.app.internal.auth.LoggedAccountProvider\nimport com.zhangke.fread.activitypub.app.internal.repo.platform.ActivityPubPlatformRepo\nimport com.zhangke.fread.activitypub.app.internal.usecase.source.user.SearchUserSourceNoTokenUseCase\nimport com.zhangke.fread.status.author.BlogAuthor\nimport com.zhangke.fread.status.model.Hashtag\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.model.StatusProviderProtocol\nimport com.zhangke.fread.status.model.StatusUiState\nimport com.zhangke.fread.status.model.notActivityPub\nimport com.zhangke.fread.status.platform.BlogPlatform\nimport com.zhangke.fread.status.search.ISearchEngine\nimport com.zhangke.fread.status.search.SearchResult\nimport com.zhangke.fread.status.search.SearchedPlatform\nimport com.zhangke.fread.status.source.StatusSource\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.flow\n\nclass ActivityPubSearchEngine (\n    private val searchUserSource: SearchUserSourceNoTokenUseCase,\n    private val clientManager: ActivityPubClientManager,\n    private val platformRepo: ActivityPubPlatformRepo,\n    private val searchAdapter: ActivityPubSearchAdapter,\n    private val statusAdapter: ActivityPubStatusAdapter,\n    private val hashtagAdapter: ActivityPubTagAdapter,\n    private val accountAdapter: ActivityPubAccountEntityAdapter,\n    private val loggedAccountProvider: LoggedAccountProvider,\n) : ISearchEngine {\n\n    override suspend fun search(\n        locator: PlatformLocator,\n        query: String\n    ): Result<List<SearchResult>> {\n        val account = locator.accountUri?.let { loggedAccountProvider.getAccount(it) }\n        return doSearch(locator) { searchRepo, platform ->\n            searchRepo.query(query = query, resolve = true).map {\n                searchAdapter.toSearchResult(it, platform, locator, account)\n            }\n        }\n    }\n\n    override suspend fun searchStatus(\n        locator: PlatformLocator,\n        query: String,\n        maxId: String?,\n    ): Result<List<StatusUiState>> {\n        val account = locator.accountUri?.let { loggedAccountProvider.getAccount(it) }\n        return doSearch(locator) { searchRepo, blogPlatform ->\n            searchRepo.queryStatus(\n                query = query,\n                maxId = maxId,\n            ).map { list ->\n                list.map {\n                    statusAdapter.toStatusUiState(\n                        entity = it,\n                        platform = blogPlatform,\n                        locator = locator,\n                        loggedAccount = account,\n                    )\n                }\n            }\n        }\n    }\n\n    override suspend fun searchHashtag(\n        locator: PlatformLocator,\n        query: String,\n        offset: Int?,\n    ): Result<List<Hashtag>> {\n        return doSearch(locator) { searchRepo, _ ->\n            searchRepo.queryHashtags(\n                query = query,\n                offset = offset,\n            ).map { list ->\n                list.map { hashtagAdapter.adapt(it) }\n            }\n        }\n    }\n\n    override suspend fun searchAuthor(\n        locator: PlatformLocator,\n        query: String,\n        offset: Int?,\n    ): Result<List<BlogAuthor>> {\n        return doSearch(locator) { searchRepo, _ ->\n            searchRepo.queryAccount(\n                query = query,\n                offset = offset,\n            ).map { list ->\n                list.map { accountAdapter.toAuthor(it) }\n            }\n        }\n    }\n\n    private suspend fun <T> doSearch(\n        locator: PlatformLocator,\n        onSearch: suspend (SearchRepo, BlogPlatform) -> Result<List<T>>,\n    ): Result<List<T>> {\n        val platformResult = platformRepo.getPlatform(locator)\n        if (platformResult.isFailure) {\n            return Result.failure(platformResult.exceptionOrNull()!!)\n        }\n        val platform = platformResult.getOrThrow()\n        val searchRepo = clientManager.getClient(locator).searchRepo\n        return onSearch(searchRepo, platform)\n    }\n\n    override suspend fun searchSourceNoToken(query: String): Result<List<StatusSource>> {\n        return searchUserSource(query)\n    }\n\n    override suspend fun searchPlatform(\n        locator: PlatformLocator,\n        query: String\n    ): Flow<List<SearchedPlatform>>? {\n        return flow {\n            platformRepo.searchPlatformSnapshotFromLocal(query)\n                .map { SearchedPlatform.Snapshot(it) }\n                .let { emit(it) }\n            FormalBaseUrl.parse(query)\n                ?.let { platformRepo.getPlatform(it) }\n                ?.onSuccess {\n                    emit(listOf(SearchedPlatform.Platform(it)))\n                }\n            platformRepo.searchPlatformFromServer(query)\n                .map { list -> list.map { SearchedPlatform.Snapshot(it) } }\n                .onSuccess { emit(it) }\n        }\n    }\n\n    override suspend fun searchStatusByUrl(\n        protocol: StatusProviderProtocol,\n        locator: PlatformLocator,\n        url: String\n    ): Result<StatusUiState?>? {\n        if (protocol.notActivityPub) return null\n        val account = locator.accountUri?.let { loggedAccountProvider.getAccount(it) }\n        return doSearch(locator) { searchRepo, blogPlatform ->\n            searchRepo.queryStatus(query = url, resolve = true)\n                .map { statusEntities ->\n                    statusEntities.firstOrNull()?.let {\n                        statusAdapter.toStatusUiState(\n                            entity = it,\n                            platform = blogPlatform,\n                            locator = locator,\n                            loggedAccount = account,\n                        )\n                    }\n                }.map { listOf(it) }\n        }.map { it.firstOrNull() }\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/ActivityPubSourceResolver.kt",
    "content": "package com.zhangke.fread.activitypub.app\n\nimport com.zhangke.fread.activitypub.app.internal.repo.user.UserRepo\nimport com.zhangke.fread.activitypub.app.internal.uri.UserUriTransformer\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.source.IStatusSourceResolver\nimport com.zhangke.fread.status.source.StatusSource\nimport com.zhangke.fread.status.uri.FormalUri\n\nclass ActivityPubSourceResolver(\n    private val userRepo: UserRepo,\n    private val userUriTransformer: UserUriTransformer,\n) : IStatusSourceResolver {\n\n    override suspend fun resolveSourceByUri(uri: FormalUri): Result<StatusSource?> {\n        val userUriInsights = userUriTransformer.parse(uri) ?: return Result.success(null)\n        val locator = PlatformLocator(baseUrl = userUriInsights.baseUrl)\n        return userRepo.getUserSource(\n            locator = locator,\n            userUriInsights = userUriInsights,\n        )\n    }\n\n    override suspend fun resolveRssSource(rssUrl: String): Result<StatusSource>? {\n        return null\n    }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/ActivityPubStartup.kt",
    "content": "package com.zhangke.fread.activitypub.app\n\nimport com.zhangke.framework.architect.coroutines.ApplicationScope\nimport com.zhangke.framework.collections.container\nimport com.zhangke.framework.module.ModuleStartup\nimport com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubContentAdapter\nimport com.zhangke.fread.activitypub.app.internal.content.ActivityPubContent\nimport com.zhangke.fread.activitypub.app.internal.migrate.ActivityPubContentMigrator\nimport com.zhangke.fread.activitypub.app.internal.repo.account.ActivityPubLoggedAccountRepo\nimport com.zhangke.fread.common.content.FreadContentRepo\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.launch\n\nclass ActivityPubStartup(\n    private val contentRepo: FreadContentRepo,\n    private val accountRepo: ActivityPubLoggedAccountRepo,\n    private val contentAdapter: ActivityPubContentAdapter,\n    private val contentMigrator: ActivityPubContentMigrator,\n    private val loggedAccountRepo: ActivityPubLoggedAccountRepo,\n) : ModuleStartup {\n\n    override fun onAppCreate() {\n        ApplicationScope.launch {\n            contentMigrator.migrate()\n            loggedAccountRepo.initialize()\n            accountRepo.onNewAccountFlow.collect { account ->\n                delay(500)\n                val contentExist = contentRepo.getAllContent()\n                    .filterIsInstance<ActivityPubContent>()\n                    .container { it.baseUrl == account.baseUrl }\n                if (!contentExist) {\n                    contentAdapter.createContent(\n                        platform = account.platform,\n                        maxOrder = contentRepo.getMaxOrder(),\n                    ).let { contentRepo.insertContent(it) }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/ActivityPubStatusResolver.kt",
    "content": "package com.zhangke.fread.activitypub.app\n\nimport com.zhangke.activitypub.api.AccountsRepo\nimport com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubStatusAdapter\nimport com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubTranslationEntityAdapter\nimport com.zhangke.fread.activitypub.app.internal.auth.ActivityPubClientManager\nimport com.zhangke.fread.activitypub.app.internal.auth.LoggedAccountProvider\nimport com.zhangke.fread.activitypub.app.internal.repo.WebFingerBaseUrlToUserIdRepo\nimport com.zhangke.fread.activitypub.app.internal.uri.UserUriTransformer\nimport com.zhangke.fread.activitypub.app.internal.usecase.status.GetStatusContextUseCase\nimport com.zhangke.fread.activitypub.app.internal.usecase.status.GetUserStatusUseCase\nimport com.zhangke.fread.activitypub.app.internal.usecase.status.StatusInteractiveUseCase\nimport com.zhangke.fread.activitypub.app.internal.usecase.status.VotePollUseCase\nimport com.zhangke.fread.status.author.BlogAuthor\nimport com.zhangke.fread.status.blog.Blog\nimport com.zhangke.fread.status.blog.BlogPoll\nimport com.zhangke.fread.status.blog.BlogTranslation\nimport com.zhangke.fread.status.model.PagedData\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.model.StatusActionType\nimport com.zhangke.fread.status.model.StatusUiState\nimport com.zhangke.fread.status.model.notActivityPub\nimport com.zhangke.fread.status.platform.BlogPlatform\nimport com.zhangke.fread.status.status.IStatusResolver\nimport com.zhangke.fread.status.status.model.Status\nimport com.zhangke.fread.status.status.model.StatusContext\nimport com.zhangke.fread.status.uri.FormalUri\n\nclass ActivityPubStatusResolver (\n    private val clientManager: ActivityPubClientManager,\n    private val getUserStatus: GetUserStatusUseCase,\n    private val userUriTransformer: UserUriTransformer,\n    private val statusInteractive: StatusInteractiveUseCase,\n    private val activityPubStatusAdapter: ActivityPubStatusAdapter,\n    private val getStatusContextUseCase: GetStatusContextUseCase,\n    private val votePollUseCase: VotePollUseCase,\n    private val webFingerBaseUrlToUserIdRepo: WebFingerBaseUrlToUserIdRepo,\n    private val loggedAccountProvider: LoggedAccountProvider,\n    private val translationAdapter: ActivityPubTranslationEntityAdapter,\n) : IStatusResolver {\n\n    override suspend fun getStatus(\n        locator: PlatformLocator,\n        blogId: String?,\n        blogUri: String?,\n        platform: BlogPlatform\n    ): Result<StatusUiState>? {\n        if (platform.protocol.notActivityPub) return null\n        val statusRepo = clientManager.getClient(locator).statusRepo\n        val loggedAccount = loggedAccountProvider.getAccount(locator)\n        if (blogId.isNullOrEmpty()) return Result.failure(IllegalArgumentException(\"blogId is null or empty!\"))\n        return statusRepo.getStatuses(blogId)\n            .mapCatching { entity ->\n                if (entity == null) throw IllegalArgumentException(\"Can't find status(${blogId})\")\n                activityPubStatusAdapter.toStatusUiState(\n                    entity = entity,\n                    platform = platform,\n                    locator = locator,\n                    loggedAccount = loggedAccount,\n                )\n            }\n    }\n\n    override suspend fun getStatusList(\n        uri: FormalUri,\n        limit: Int,\n        maxId: String?,\n    ): Result<PagedData<StatusUiState>>? {\n        val userInsights = userUriTransformer.parse(uri) ?: return null\n        val locator = PlatformLocator(baseUrl = userInsights.baseUrl)\n        return getUserStatus(\n            locator = locator,\n            userInsights = userInsights,\n            limit = limit,\n            maxId = maxId,\n        ).map {\n            PagedData(it, it.lastOrNull()?.status?.id)\n        }\n    }\n\n    override suspend fun interactive(\n        locator: PlatformLocator,\n        status: Status,\n        type: StatusActionType,\n    ): Result<Status?>? {\n        if (status.notThisPlatform()) return null\n        return statusInteractive(locator, status, type)\n    }\n\n    override suspend fun votePoll(\n        locator: PlatformLocator,\n        blog: Blog,\n        votedOption: List<BlogPoll.Option>\n    ): Result<Status>? {\n        if (blog.platform.protocol.notActivityPub) return null\n        return votePollUseCase(locator, blog, votedOption)\n    }\n\n    override suspend fun getStatusContext(\n        locator: PlatformLocator,\n        status: Status\n    ): Result<StatusContext>? {\n        if (status.notThisPlatform()) return null\n        return getStatusContextUseCase(locator, status)\n    }\n\n    private fun Status.notThisPlatform(): Boolean {\n        return this.platform.protocol.notActivityPub\n    }\n\n    override suspend fun follow(locator: PlatformLocator, target: BlogAuthor): Result<Unit>? {\n        return updateRelationship(\n            locator = locator,\n            target = target,\n            updater = {\n                this.follow(it)\n            }\n        )\n    }\n\n    override suspend fun unfollow(locator: PlatformLocator, target: BlogAuthor): Result<Unit>? {\n        return updateRelationship(\n            locator = locator,\n            target = target,\n            updater = {\n                this.unfollow(it)\n            }\n        )\n    }\n\n    private suspend fun updateRelationship(\n        locator: PlatformLocator,\n        target: BlogAuthor,\n        updater: suspend AccountsRepo.(userId: String) -> Result<*>,\n    ): Result<Unit>? {\n        userUriTransformer.parse(target.uri) ?: return null\n        val userId = if (target.userId.isNullOrEmpty()) {\n            val userIdResult = webFingerBaseUrlToUserIdRepo.getUserId(target.webFinger, locator)\n            if (userIdResult.isFailure) return Result.failure(userIdResult.exceptionOrNull()!!)\n            userIdResult.getOrThrow()\n        } else {\n            target.userId!!\n        }\n        return clientManager.getClient(locator)\n            .accountRepo\n            .updater(userId)\n            .map {}\n    }\n\n    override suspend fun isFollowing(\n        locator: PlatformLocator,\n        target: BlogAuthor\n    ): Result<Boolean>? {\n        userUriTransformer.parse(target.uri) ?: return null\n        val userId = if (target.userId.isNullOrEmpty()) {\n            val userIdResult = webFingerBaseUrlToUserIdRepo.getUserId(target.webFinger, locator)\n            if (userIdResult.isFailure) return Result.failure(userIdResult.exceptionOrNull()!!)\n            userIdResult.getOrThrow()\n        } else {\n            target.userId!!\n        }\n        return clientManager.getClient(locator)\n            .accountRepo\n            .getRelationships(listOf(userId))\n            .map { it.firstOrNull()?.following ?: false }\n    }\n\n    override suspend fun translate(\n        locator: PlatformLocator,\n        status: Status,\n        lan: String,\n    ): Result<BlogTranslation>? {\n        if (status.notThisPlatform()) return null\n        return clientManager.getClient(locator)\n            .statusRepo\n            .translate(status.intrinsicBlog.id, lan)\n            .map {\n                translationAdapter.toTranslation(it)\n            }\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/ActivityPubUrlInterceptor.kt",
    "content": "package com.zhangke.fread.activitypub.app\n\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.network.FormalBaseUrl\nimport com.zhangke.framework.network.HttpScheme\nimport com.zhangke.framework.network.SimpleUri\nimport com.zhangke.framework.network.addProtocolSuffixIfNecessary\nimport com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubAccountEntityAdapter\nimport com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubStatusAdapter\nimport com.zhangke.fread.activitypub.app.internal.auth.ActivityPubClientManager\nimport com.zhangke.fread.activitypub.app.internal.auth.LoggedAccountProvider\nimport com.zhangke.fread.activitypub.app.internal.content.ActivityPubContent\nimport com.zhangke.fread.activitypub.app.internal.model.ActivityPubLoggedAccount\nimport com.zhangke.fread.activitypub.app.internal.repo.platform.ActivityPubPlatformRepo\nimport com.zhangke.fread.activitypub.app.internal.screen.user.UserDetailScreenKey\nimport com.zhangke.fread.common.browser.BrowserInterceptor\nimport com.zhangke.fread.common.browser.InterceptorResult\nimport com.zhangke.fread.common.content.FreadContentRepo\nimport com.zhangke.fread.commonbiz.shared.screen.status.context.StatusContextScreenNavKey\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.model.createActivityPubProtocol\nimport com.zhangke.fread.status.platform.BlogPlatform\n\nclass ActivityPubUrlInterceptor(\n    private val platformRepo: ActivityPubPlatformRepo,\n    private val clientManager: ActivityPubClientManager,\n    private val loggedAccountProvider: LoggedAccountProvider,\n    private val accountEntityAdapter: ActivityPubAccountEntityAdapter,\n    private val activityPubStatusAdapter: ActivityPubStatusAdapter,\n    private val contentRepo: FreadContentRepo,\n) : BrowserInterceptor {\n\n    override suspend fun intercept(\n        locator: PlatformLocator?,\n        url: String,\n        isFromExternal: Boolean,\n    ): InterceptorResult {\n        val uri = SimpleUri.parse(url) ?: return InterceptorResult.CanNotIntercept\n        if (!HttpScheme.validate(uri.scheme.orEmpty().addProtocolSuffixIfNecessary())) {\n            return InterceptorResult.CanNotIntercept\n        }\n        val isProfileUrl = isProfileUrl(uri)\n        val isStatusUrl = isMastodonStatusUrl(uri)\n        if (!isProfileUrl && !isStatusUrl) {\n            if (isFromExternal) {\n                val platform = parsePlatform(uri)\n                if (platform != null) {\n                    val content = contentRepo.getAllContent()\n                        .mapNotNull { it as? ActivityPubContent }\n                        .firstOrNull { it.baseUrl == platform.baseUrl }\n                    if (content != null) {\n                        return InterceptorResult.SwitchHomeContent(content)\n                    }\n                }\n            }\n            return InterceptorResult.CanNotIntercept\n        }\n        var account: ActivityPubLoggedAccount? = null\n        val fixedLocator = if (locator?.accountUri != null) {\n            locator\n        } else {\n            val accountList = loggedAccountProvider.getAllAccounts()\n            if (accountList.isEmpty()) {\n                val baseUrl = locator?.baseUrl ?: FormalBaseUrl.parse(uri.host!!)\n                ?: return InterceptorResult.CanNotIntercept\n                locator ?: PlatformLocator(baseUrl)\n            } else if (accountList.size == 1) {\n                account = accountList.first()\n                PlatformLocator(account.platform.baseUrl, account.uri)\n            } else {\n                return InterceptorResult.RequireSelectAccount(createActivityPubProtocol())\n            }\n        }\n        parseStatus(fixedLocator, uri, account)?.let {\n            return InterceptorResult.SuccessWithOpenNewScreen(it)\n        }\n        parseMastodonProfile(fixedLocator, uri)?.let {\n            return InterceptorResult.SuccessWithOpenNewScreen(it)\n        }\n        return InterceptorResult.CanNotIntercept\n    }\n\n    private fun isProfileUrl(uri: SimpleUri): Boolean {\n        val path = uri.path?.removePrefix(\"/\") ?: return false\n        if (path.isEmpty()) return false\n        if (uri.queries.isNotEmpty()) return false\n        if (path.contains(\"?\") || path.contains(\"/\")) return false\n        if (!path.startsWith(\"@\")) return false\n        return true\n    }\n\n    private suspend fun parseMastodonProfile(locator: PlatformLocator, uri: SimpleUri): NavKey? {\n        if (!isProfileUrl(uri)) return null\n        val path = uri.path?.removePrefix(\"/\") ?: return null\n        val baseUrl = FormalBaseUrl.parse(uri.toString()) ?: return null\n        val acct = \"$path@${baseUrl.host}\"\n        val accountRepo = clientManager.getClient(locator).accountRepo\n        val account = accountRepo.lookup(acct).getOrNull() ?: return null\n        val webFinger = accountEntityAdapter.toWebFinger(account)\n        return UserDetailScreenKey(locator = locator, webFinger = webFinger)\n    }\n\n    private fun isMastodonStatusUrl(uri: SimpleUri): Boolean {\n        return parseMastodonStatusParams(uri) != null\n    }\n\n    private fun parseMastodonStatusParams(uri: SimpleUri): String? {\n        FormalBaseUrl.parse(uri.toString()) ?: return null\n        if (uri.queries.isNotEmpty()) return null\n        val path = uri.path?.removePrefix(\"/\") ?: return null\n        val array = path.split(\"/\")\n        if (array.size != 2) return null\n        val acct = array[0]\n        val statusId = array[1]\n        if (!acct.startsWith(\"@\")) return null\n        if (statusId.isEmpty()) return null\n        return statusId\n    }\n\n    private suspend fun parseStatus(\n        locator: PlatformLocator,\n        uri: SimpleUri,\n        account: ActivityPubLoggedAccount?,\n    ): NavKey? {\n        val statusId = parseMastodonStatusParams(uri) ?: return null\n        val baseUrl = FormalBaseUrl.parse(uri.toString()) ?: return null\n        val client = clientManager.getClient(locator)\n        val statusRepo = client.statusRepo\n        val platform = platformRepo.getPlatform(baseUrl).getOrNull() ?: return null\n        val status = if (locator.baseUrl != baseUrl) {\n            // other platform, by search\n            val searchRepo = client.searchRepo\n            val searchedStatusResult =\n                searchRepo.queryStatus(uri.toString(), resolve = true).getOrNull() ?: return null\n            if (searchedStatusResult.size != 1) return null\n            searchedStatusResult.first().let {\n                activityPubStatusAdapter.toStatusUiState(\n                    entity = it,\n                    platform = platform,\n                    locator = locator,\n                    loggedAccount = account,\n                )\n            }\n        } else {\n            // same platform\n            statusRepo.getStatuses(statusId).getOrNull()?.let {\n                activityPubStatusAdapter.toStatusUiState(\n                    entity = it,\n                    platform = platform,\n                    locator = locator,\n                    loggedAccount = account,\n                )\n            }\n        }\n        return status?.let { StatusContextScreenNavKey.create(it) }\n    }\n\n    private suspend fun parsePlatform(uri: SimpleUri): BlogPlatform? {\n        val baseUrl = FormalBaseUrl.parse(uri.toString()) ?: return null\n        if (uri.queries.isNotEmpty()) return null\n        return platformRepo.getPlatform(baseUrl).getOrNull()\n    }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/DataTrackingElements.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal\n\nobject ApNotificationElements {\n\n    const val ALL = \"apNotificationAll\"\n    const val MENTION = \"apNotificationMention\"\n}\n\nobject ActivityPubDataElements {\n\n    const val INSTANCE_DETAIL_OPEN_IN_BROWSER = \"instanceDetailOpenInBrowser\"\n    const val INSTANCE_DETAIL_COPY_LINK = \"instanceDetailCopyLink\"\n\n    const val USER_DETAIL_OPEN_IN_BROWSER = \"userDetailOpenInBrowser\"\n    const val USER_DETAIL_COPY_LINK = \"userDetailCopyLink\"\n    const val USER_DETAIL_OPEN_ORIGINAL_INSTANCE = \"userDetailOpenOriginalInstance\"\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/adapter/ActivityPubAccountEntityAdapter.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.adapter\n\nimport com.zhangke.activitypub.entities.ActivityPubAccountEntity\nimport com.zhangke.activitypub.entities.ActivityPubRelationshipEntity\nimport com.zhangke.framework.network.FormalBaseUrl\nimport com.zhangke.framework.utils.WebFinger\nimport com.zhangke.fread.activitypub.app.internal.model.ActivityPubLoggedAccount\nimport com.zhangke.fread.activitypub.app.internal.uri.UserUriTransformer\nimport com.zhangke.fread.analytics.reportToLogger\nimport com.zhangke.fread.status.author.BlogAuthor\nimport com.zhangke.fread.status.model.LoggedAccountDetail\nimport com.zhangke.fread.status.model.Relationships\nimport com.zhangke.fread.status.uri.FormalUri\n\nclass ActivityPubAccountEntityAdapter (\n    private val userUriTransformer: UserUriTransformer,\n    private val emojiEntityAdapter: ActivityPubCustomEmojiEntityAdapter,\n) {\n\n    fun toAuthor(\n        entity: ActivityPubAccountEntity,\n    ): BlogAuthor {\n        val webFinger = toWebFinger(entity)\n        return BlogAuthor(\n            uri = toUri(entity),\n            webFinger = webFinger,\n            name = entity.displayName,\n            handle = webFinger.toString(),\n            description = entity.note,\n            avatar = entity.avatar,\n            emojis = entity.emojis.map(emojiEntityAdapter::toEmoji),\n            userId = entity.id,\n            bot = entity.bot,\n            banner = entity.header,\n            followersCount = entity.followersCount.toLong(),\n            followingCount = entity.followingCount.toLong(),\n            statusesCount = entity.statusesCount.toLong(),\n            relationships = null,\n        )\n    }\n\n    fun convertLoggedAccountDetail(\n        account: ActivityPubLoggedAccount,\n    ): LoggedAccountDetail {\n        val author = BlogAuthor(\n            uri = account.uri,\n            webFinger = account.webFinger,\n            name = account.userName,\n            handle = account.webFinger.toString(),\n            description = account.description.orEmpty(),\n            avatar = account.avatar,\n            emojis = account.emojis,\n            userId = account.id,\n            bot = account.bot,\n            banner = account.banner,\n            followersCount = account.followersCount,\n            followingCount = account.followingCount,\n            statusesCount = account.statusesCount,\n            relationships = null,\n        )\n        return LoggedAccountDetail(\n            account = account,\n            author = author,\n        )\n    }\n\n    fun toUri(entity: ActivityPubAccountEntity): FormalUri {\n        val webFinger = toWebFinger(entity)\n        return userUriTransformer.build(webFinger, FormalBaseUrl.parse(entity.url)!!)\n    }\n\n    fun toWebFinger(account: ActivityPubAccountEntity): WebFinger {\n        try {\n            WebFinger.create(account.acct)?.let { return it }\n            WebFinger.create(account.url)!!.let { return it }\n        } catch (e: Throwable) {\n            reportToLogger(\"ToWebFingerException\") {\n                put(\"acct\", account.acct)\n                put(\"url\", account.url)\n                put(\"displayName\", account.displayName)\n            }\n            throw e\n        }\n    }\n\n    fun convertRelationship(entity: ActivityPubRelationshipEntity): Relationships {\n        return Relationships(\n            following = entity.following,\n            followedBy = entity.followedBy,\n            blocking = entity.blocking,\n            blockedBy = entity.blockedBy,\n            muting = entity.muting,\n            requested = entity.requested,\n            requestedBy = entity.requestedBy,\n        )\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/adapter/ActivityPubApplicationEntityAdapter.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.adapter\n\nimport com.zhangke.fread.activitypub.app.internal.model.ActivityPubApplication\nimport com.zhangke.fread.activitypub.app.internal.db.ActivityPubApplicationEntity\n\nclass ActivityPubApplicationEntityAdapter () {\n\n    fun toApplication(entity: ActivityPubApplicationEntity) = ActivityPubApplication(\n        baseUrl = entity.baseUrl,\n        id = entity.id,\n        name = entity.name,\n        website = entity.website,\n        redirectUri = entity.redirectUri,\n        clientId = entity.clientId,\n        clientSecret = entity.clientSecret,\n        vapidKey = entity.vapidKey,\n    )\n\n    fun toEntity(application: ActivityPubApplication) = ActivityPubApplicationEntity(\n        baseUrl = application.baseUrl,\n        id = application.id,\n        name = application.name,\n        website = application.website,\n        redirectUri = application.redirectUri,\n        clientId = application.clientId,\n        clientSecret = application.clientSecret,\n        vapidKey = application.vapidKey,\n    )\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/adapter/ActivityPubBlogMetaAdapter.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.adapter\n\nimport com.zhangke.activitypub.entities.ActivityPubMediaMetaEntity\nimport com.zhangke.fread.status.blog.BlogMediaMeta\nimport com.zhangke.fread.status.blog.BlogMediaType\n\nclass ActivityPubBlogMetaAdapter () {\n\n    fun adapt(\n        type: BlogMediaType,\n        entity: ActivityPubMediaMetaEntity,\n    ): BlogMediaMeta? {\n        return when (type) {\n            BlogMediaType.IMAGE -> entity.toImageMeta()\n            BlogMediaType.GIFV -> entity.toGifvMeta()\n            BlogMediaType.VIDEO -> entity.toVideoMeta()\n            BlogMediaType.AUDIO -> entity.toAudioMeta()\n            else -> null\n        }\n    }\n\n    private fun ActivityPubMediaMetaEntity.toImageMeta() = BlogMediaMeta.ImageMeta(\n        original = original?.toImageLayoutMeta(),\n        small = original?.toImageLayoutMeta(),\n        focus = focus?.toImageFocusMeta(),\n    )\n\n    private fun ActivityPubMediaMetaEntity.LayoutMeta.toImageLayoutMeta() =\n        BlogMediaMeta.ImageMeta.LayoutMeta(\n            width = width?.toLong(),\n            height = height?.toLong(),\n            size = size,\n            aspect = aspect,\n        )\n\n    private fun ActivityPubMediaMetaEntity.FocusMeta.toImageFocusMeta() =\n        BlogMediaMeta.ImageMeta.FocusMeta(\n            x = x,\n            y = y,\n        )\n\n    private fun ActivityPubMediaMetaEntity.toVideoMeta() = BlogMediaMeta.VideoMeta(\n        length = length,\n        duration = duration,\n        fps = fps,\n        size = size,\n        width = width?.toLong(),\n        height = height?.toLong(),\n        aspect = aspect,\n        audioEncode = audioEncode,\n        audioBitrate = audioBitrate,\n        audioChannels = audioChannels,\n        original = original?.toVideoLayoutMeta(),\n        small = small?.toVideoLayoutMeta(),\n    )\n\n    private fun ActivityPubMediaMetaEntity.LayoutMeta.toVideoLayoutMeta() =\n        BlogMediaMeta.VideoMeta.LayoutMeta(\n            width = width?.toLong(),\n            height = height?.toLong(),\n            size = size,\n            frameRate = frameRate,\n            duration = duration,\n            aspect = aspect,\n            bitrate = bitrate,\n        )\n\n    private fun ActivityPubMediaMetaEntity.toGifvMeta() = BlogMediaMeta.GifvMeta(\n        length = length,\n        duration = duration,\n        fps = fps,\n        size = size,\n        width = width,\n        height = height,\n        aspect = aspect,\n        original = original?.toGifvLayoutMeta(),\n        small = small?.toGifvLayoutMeta(),\n    )\n\n    private fun ActivityPubMediaMetaEntity.LayoutMeta.toGifvLayoutMeta() =\n        BlogMediaMeta.GifvMeta.LayoutMeta(\n            width = width?.toLong(),\n            height = height?.toLong(),\n            size = size,\n            frameRate = frameRate,\n            duration = duration,\n            aspect = aspect,\n            bitrate = bitrate,\n        )\n\n    private fun ActivityPubMediaMetaEntity.toAudioMeta() = BlogMediaMeta.AudioMeta(\n        length = length,\n        duration = duration,\n        audioEncode = audioEncode,\n        audioBitrate = audioBitrate,\n        audioChannels = audioChannels,\n        original = original?.toAudioLayoutMeta(),\n    )\n\n    private fun ActivityPubMediaMetaEntity.LayoutMeta.toAudioLayoutMeta() =\n        BlogMediaMeta.AudioMeta.FrameMeta(\n            duration = duration,\n            bitrate = bitrate,\n        )\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/adapter/ActivityPubContentAdapter.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.adapter\n\nimport com.zhangke.fread.activitypub.app.internal.content.ActivityPubContent\nimport com.zhangke.fread.status.platform.BlogPlatform\n\nclass ActivityPubContentAdapter () {\n\n    fun createContent(\n        platform: BlogPlatform,\n        maxOrder: Int,\n    ): ActivityPubContent {\n        return ActivityPubContent(\n            name = platform.name,\n            baseUrl = platform.baseUrl,\n            order = maxOrder + 1,\n            tabList = buildInitialTabConfigList(),\n            accountUri = null,\n        )\n    }\n\n    private fun buildInitialTabConfigList(): List<ActivityPubContent.ContentTab> {\n        val tabList = mutableListOf<ActivityPubContent.ContentTab>()\n        tabList += ActivityPubContent.ContentTab.HomeTimeline(0)\n        tabList += ActivityPubContent.ContentTab.LocalTimeline(1)\n        tabList += ActivityPubContent.ContentTab.PublicTimeline(2)\n        tabList += ActivityPubContent.ContentTab.Trending(3)\n        return tabList\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/adapter/ActivityPubCustomEmojiEntityAdapter.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.adapter\n\nimport com.zhangke.activitypub.entities.ActivityPubCustomEmojiEntity\nimport com.zhangke.fread.activitypub.app.internal.model.CustomEmoji\nimport com.zhangke.fread.status.model.Emoji\n\nclass ActivityPubCustomEmojiEntityAdapter () {\n\n    fun toCustomEmoji(entity: ActivityPubCustomEmojiEntity) = CustomEmoji(\n        shortcode = entity.shortcode,\n        url = entity.url,\n        staticUrl = entity.staticUrl,\n        visibleInPicker = entity.visibleInPicker,\n        category = entity.category ?: \"Default\",\n    )\n\n    fun toEmoji(entity: ActivityPubCustomEmojiEntity): Emoji {\n        return Emoji(\n            shortcode = entity.shortcode,\n            url = entity.url,\n            staticUrl = entity.staticUrl,\n        )\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/adapter/ActivityPubInstanceAdapter.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.adapter\n\nimport com.zhangke.activitypub.entities.ActivityPubInstanceEntity\nimport com.zhangke.framework.network.FormalBaseUrl\nimport com.zhangke.fread.status.model.createActivityPubProtocol\nimport com.zhangke.fread.activitypub.app.internal.uri.PlatformUriTransformer\nimport com.zhangke.fread.status.platform.BlogPlatform\n\nclass ActivityPubInstanceAdapter (\n    private val platformUriTransformer: PlatformUriTransformer,\n) {\n\n    fun toPlatform(\n        baseUrl: FormalBaseUrl,\n        instance: ActivityPubInstanceEntity\n    ): BlogPlatform {\n        // 此处需要注意的是，ActivityPubInstanceEntity#domain 并不一定准确。\n        // 例如 https://mastodon.jakewharton.com/api/v2/instance\n        // 他的 domain 是 jakewharton.com，而不是 mastodon.jakewharton.com\n        val uri = platformUriTransformer.build(baseUrl)\n        return BlogPlatform(\n            uri = uri.toString(),\n            baseUrl = baseUrl,\n            name = instance.title,\n            description = instance.description,\n            protocol = createActivityPubProtocol(),\n            thumbnail = instance.thumbnail.url,\n            supportsQuotePost = instance.supportsQuotePost,\n        )\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/adapter/ActivityPubLoggedAccountAdapter.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.adapter\n\nimport com.zhangke.activitypub.entities.ActivityPubAccountEntity\nimport com.zhangke.activitypub.entities.ActivityPubInstanceEntity\nimport com.zhangke.activitypub.entities.ActivityPubTokenEntity\nimport com.zhangke.framework.network.FormalBaseUrl\nimport com.zhangke.framework.utils.WebFinger\nimport com.zhangke.fread.activitypub.app.internal.model.ActivityPubLoggedAccount\nimport com.zhangke.fread.activitypub.app.internal.uri.UserUriTransformer\nimport com.zhangke.fread.analytics.reportToLogger\nimport com.zhangke.fread.status.platform.BlogPlatform\n\nclass ActivityPubLoggedAccountAdapter (\n    private val instanceAdapter: ActivityPubInstanceAdapter,\n    private val userUriTransformer: UserUriTransformer,\n    private val emojiEntityAdapter: ActivityPubCustomEmojiEntityAdapter,\n) {\n\n    suspend fun createFromAccount(\n        baseUrl: FormalBaseUrl,\n        instance: ActivityPubInstanceEntity,\n        account: ActivityPubAccountEntity,\n        token: ActivityPubTokenEntity,\n    ): ActivityPubLoggedAccount {\n        return createFromAccount(\n            platform = instanceAdapter.toPlatform(baseUrl, instance),\n            account = account,\n            token = token,\n        )\n    }\n\n    fun createFromAccount(\n        platform: BlogPlatform,\n        account: ActivityPubAccountEntity,\n        token: ActivityPubTokenEntity,\n    ): ActivityPubLoggedAccount {\n        val webFinger = accountToWebFinger(account, platform.baseUrl)\n        return ActivityPubLoggedAccount(\n            userId = account.id,\n            uri = userUriTransformer.build(webFinger, platform.baseUrl),\n            webFinger = webFinger,\n            platform = platform,\n            baseUrl = platform.baseUrl,\n            userName = account.displayName,\n            description = account.note,\n            avatar = account.avatar,\n            url = account.url,\n            token = token,\n            banner = account.header,\n            note = account.note,\n            bot = account.bot,\n            followersCount = account.followersCount.toLong(),\n            followingCount = account.followingCount.toLong(),\n            statusesCount = account.statusesCount.toLong(),\n            emojis = account.emojis.map(emojiEntityAdapter::toEmoji),\n        )\n    }\n\n    private fun accountToWebFinger(\n        account: ActivityPubAccountEntity,\n        baseUrl: FormalBaseUrl,\n    ): WebFinger {\n        try {\n            WebFinger.create(account.acct, baseUrl)?.let { return it }\n            WebFinger.create(account.url)!!.let { return it }\n        } catch (e: Throwable) {\n            e.printStackTrace()\n            reportToLogger(\"WebFingerCreateError\") {\n                put(\"acct\", account.acct)\n                put(\"url\", account.url)\n            }\n            throw e\n        }\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/adapter/ActivityPubPlatformEntityAdapter.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.adapter\n\nimport com.zhangke.activitypub.entities.ActivityPubInstanceEntity\nimport com.zhangke.framework.network.FormalBaseUrl\nimport com.zhangke.fread.activitypub.app.internal.db.ActivityPubInstanceInfoEntity\nimport com.zhangke.fread.activitypub.app.internal.uri.PlatformUriTransformer\n\nclass ActivityPubPlatformEntityAdapter (\n    private val uriTransformer: PlatformUriTransformer,\n) {\n\n    fun toEntity(\n        baseUrl: FormalBaseUrl,\n        entity: ActivityPubInstanceEntity\n    ): ActivityPubInstanceInfoEntity {\n        return ActivityPubInstanceInfoEntity(\n            uri = uriTransformer.build(baseUrl).toString(),\n            baseUrl = baseUrl,\n            instanceEntity = entity,\n        )\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/adapter/ActivityPubPollAdapter.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.adapter\n\nimport com.zhangke.activitypub.entities.ActivityPubPollEntity\nimport com.zhangke.fread.status.blog.BlogPoll\n\nclass ActivityPubPollAdapter () {\n\n    fun adapt(entity: ActivityPubPollEntity): BlogPoll {\n        return BlogPoll(\n            id = entity.id,\n            expiresAt = entity.expiresAt,\n            expired = entity.expired,\n            multiple = entity.multiple,\n            votesCount = entity.votesCount,\n            votersCount = entity.votersCount,\n            voted = entity.voted,\n            options = entity.options.mapIndexed { index, item ->\n                item.convertToPoll(index)\n            },\n            ownVotes = entity.ownVotes ?: emptyList(),\n        )\n    }\n\n    private fun ActivityPubPollEntity.Option.convertToPoll(index: Int): BlogPoll.Option {\n        return BlogPoll.Option(\n            index = index,\n            title = title,\n            votesCount = votesCount,\n        )\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/adapter/ActivityPubSearchAdapter.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.adapter\n\nimport com.zhangke.activitypub.entities.ActivityPubSearchEntity\nimport com.zhangke.fread.activitypub.app.internal.model.ActivityPubLoggedAccount\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.platform.BlogPlatform\nimport com.zhangke.fread.status.search.SearchResult\n\nclass ActivityPubSearchAdapter (\n    private val accountEntityAdapter: ActivityPubAccountEntityAdapter,\n    private val hashtagAdapter: ActivityPubTagAdapter,\n    private val statusAdapter: ActivityPubStatusAdapter,\n) {\n\n    suspend fun toSearchResult(\n        entity: ActivityPubSearchEntity,\n        platform: BlogPlatform,\n        locator: PlatformLocator,\n        account: ActivityPubLoggedAccount?,\n    ): List<SearchResult> {\n        val authorList = entity.accounts.map {\n            SearchResult.Author(accountEntityAdapter.toAuthor(it))\n        }\n        val hashtagList = entity.hashtags.map {\n            SearchResult.SearchedHashtag(hashtagAdapter.adapt(it))\n        }\n        val statusList = entity.statuses.map {\n            SearchResult.SearchedStatus(statusAdapter.toStatusUiState(it, platform, locator, account))\n        }\n        return authorList + hashtagList + statusList\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/adapter/ActivityPubStatusAdapter.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.adapter\n\nimport com.zhangke.activitypub.entities.ActivityPubFilterEntity\nimport com.zhangke.activitypub.entities.ActivityPubFilterResultEntity\nimport com.zhangke.activitypub.entities.ActivityPubMediaAttachmentEntity\nimport com.zhangke.activitypub.entities.ActivityPubQuoteApprovalEntity\nimport com.zhangke.activitypub.entities.ActivityPubStatusEntity\nimport com.zhangke.framework.date.DateParser\nimport com.zhangke.framework.ktx.ifNullOrEmpty\nimport com.zhangke.framework.utils.WebFinger\nimport com.zhangke.fread.activitypub.app.internal.model.ActivityPubLoggedAccount\nimport com.zhangke.fread.common.utils.formatDefault\nimport com.zhangke.fread.status.author.BlogAuthor\nimport com.zhangke.fread.status.blog.Blog\nimport com.zhangke.fread.status.blog.BlogEmbed\nimport com.zhangke.fread.status.blog.BlogMedia\nimport com.zhangke.fread.status.blog.BlogMediaType\nimport com.zhangke.fread.status.blog.CurrentUserQuoteApproval\nimport com.zhangke.fread.status.blog.PostingApplication\nimport com.zhangke.fread.status.model.BlogFiltered\nimport com.zhangke.fread.status.model.BlogTranslationUiState\nimport com.zhangke.fread.status.model.HashtagInStatus\nimport com.zhangke.fread.status.model.Mention\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.model.StatusUiState\nimport com.zhangke.fread.status.model.StatusVisibility\nimport com.zhangke.fread.status.model.createActivityPubProtocol\nimport com.zhangke.fread.status.platform.BlogPlatform\nimport com.zhangke.fread.status.status.model.Status\n\nclass ActivityPubStatusAdapter (\n    private val activityPubAccountEntityAdapter: ActivityPubAccountEntityAdapter,\n    private val metaAdapter: ActivityPubBlogMetaAdapter,\n    private val pollAdapter: ActivityPubPollAdapter,\n    private val emojiEntityAdapter: ActivityPubCustomEmojiEntityAdapter,\n) {\n\n    fun toStatusUiState(\n        status: Status,\n        locator: PlatformLocator,\n        logged: Boolean,\n        isOwner: Boolean,\n    ): StatusUiState {\n        return StatusUiState(\n            status = status,\n            locator = locator,\n            logged = logged,\n            isOwner = isOwner,\n            blogTranslationState = BlogTranslationUiState(\n                support = status.intrinsicBlog.supportTranslate,\n                translating = false,\n                showingTranslation = false,\n                blogTranslation = null,\n            ),\n        )\n    }\n\n    fun toStatusUiState(\n        status: Status,\n        locator: PlatformLocator,\n        loggedAccount: ActivityPubLoggedAccount?,\n    ): StatusUiState {\n        return toStatusUiState(\n            status = status,\n            locator = locator,\n            logged = loggedAccount != null,\n            isOwner = loggedAccount?.webFinger?.equalsDomain(status.intrinsicBlog.author.webFinger) == true,\n        )\n    }\n\n    fun toStatusUiState(\n        entity: ActivityPubStatusEntity,\n        platform: BlogPlatform,\n        locator: PlatformLocator,\n        loggedAccount: ActivityPubLoggedAccount?,\n    ): StatusUiState {\n        val status = toStatus(entity, platform)\n        return toStatusUiState(status, locator, loggedAccount)\n    }\n\n    fun toStatusUiState(\n        entity: ActivityPubStatusEntity,\n        platform: BlogPlatform,\n        locator: PlatformLocator,\n        isOwner: Boolean,\n        logged: Boolean,\n    ): StatusUiState {\n        val status = toStatus(entity, platform)\n        return toStatusUiState(status, locator, logged = logged, isOwner = isOwner)\n    }\n\n    fun toStatus(\n        entity: ActivityPubStatusEntity,\n        platform: BlogPlatform,\n    ): Status {\n        return if (entity.reblog != null) {\n            transformReblog(entity, platform)\n        } else {\n            transformNewBlog(entity, platform)\n        }\n    }\n\n    private fun transformNewBlog(\n        entity: ActivityPubStatusEntity,\n        platform: BlogPlatform,\n    ): Status.NewBlog {\n        val blog = getBlogAndInteractions(entity, platform)\n        return Status.NewBlog(blog)\n    }\n\n    private fun transformReblog(\n        entity: ActivityPubStatusEntity,\n        platform: BlogPlatform,\n    ): Status.Reblog {\n        val blog = getBlogAndInteractions(entity.reblog!!, platform)\n        return Status.Reblog(\n            author = activityPubAccountEntityAdapter.toAuthor(entity.account),\n            id = entity.id,\n            createAt = DateParser.parseOrCurrent(entity.createdAt),\n            reblog = blog,\n        )\n    }\n\n    private fun getBlogAndInteractions(\n        entity: ActivityPubStatusEntity,\n        platform: BlogPlatform\n    ): Blog {\n        val statusAuthor = activityPubAccountEntityAdapter.toAuthor(entity.account)\n        return transformBlog(entity, platform, statusAuthor)\n    }\n\n    private fun transformBlog(\n        entity: ActivityPubStatusEntity,\n        platform: BlogPlatform,\n        author: BlogAuthor,\n    ): Blog {\n        val emojis = entity.emojis.map(emojiEntityAdapter::toEmoji)\n        val createAt = DateParser.parseOrCurrent(entity.createdAt)\n        return Blog(\n            id = entity.id,\n            author = author,\n            title = null,\n            description = null,\n            content = entity.content.orEmpty(),\n            sensitive = entity.sensitive,\n            spoilerText = entity.spoilerText,\n            createAt = createAt,\n            formattedCreateAt = createAt.formatDefault(),\n            url = entity.url.ifNullOrEmpty { entity.uri },\n            link = entity.url.ifNullOrEmpty { entity.uri },\n            language = entity.language,\n            like = Blog.Like(\n                support = true,\n                liked = entity.favourited,\n                likedCount = entity.favouritesCount.toLong()\n            ),\n            forward = Blog.Forward(\n                support = true,\n                forward = entity.reblogged,\n                forwardCount = entity.reblogsCount.toLong(),\n            ),\n            bookmark = Blog.Bookmark(\n                support = true,\n                bookmarked = entity.bookmarked,\n            ),\n            reply = Blog.Reply(\n                support = true,\n                repliesCount = entity.repliesCount.toLong(),\n            ),\n            quote = buildQuote(entity, platform),\n            supportEdit = true,\n            isReply = !entity.inReplyToId.isNullOrEmpty(),\n            platform = platform,\n            mediaList = entity.mediaAttachments?.map { it.toBlogMedia() } ?: emptyList(),\n            poll = entity.poll?.let(pollAdapter::adapt),\n            emojis = emojis,\n            pinned = entity.pinned == true,\n            facets = emptyList(),\n            supportTranslate = true,\n            mentions = entity.mentions.mapNotNull { it.toMention() },\n            tags = entity.tags.map { it.toTag() },\n            visibility = entity.visibility.convertActivityPubVisibility(),\n            embeds = buildEmbed(entity, platform),\n            editedAt = entity.editedAt?.let { DateParser.parseOrCurrent(it) },\n            application = entity.application?.toApplication(),\n            filtered = entity.filtered?.map { it.toFiltered() },\n        )\n    }\n\n    private fun String.convertActivityPubVisibility(): StatusVisibility {\n        return when (this) {\n            ActivityPubStatusEntity.VISIBILITY_PUBLIC -> StatusVisibility.PUBLIC\n            ActivityPubStatusEntity.VISIBILITY_UNLISTED -> StatusVisibility.UNLISTED\n            ActivityPubStatusEntity.VISIBILITY_PRIVATE -> StatusVisibility.PRIVATE\n            ActivityPubStatusEntity.VISIBILITY_DIRECT -> StatusVisibility.DIRECT\n            else -> StatusVisibility.PUBLIC\n        }\n    }\n\n    private fun ActivityPubMediaAttachmentEntity.toBlogMedia(): BlogMedia {\n        val mediaType = convertMediaType(type)\n        return BlogMedia(\n            id = id,\n            url = url.orEmpty(),\n            type = mediaType,\n            previewUrl = previewUrl,\n            remoteUrl = remoteUrl,\n            description = description,\n            meta = this.meta?.let { metaAdapter.adapt(mediaType, it) },\n            blurhash = blurhash,\n        )\n    }\n\n    private fun ActivityPubStatusEntity.Tag.toTag(): HashtagInStatus {\n        return HashtagInStatus(\n            name = name,\n            url = url,\n            protocol = createActivityPubProtocol(),\n        )\n    }\n\n    private fun convertMediaType(type: String): BlogMediaType {\n        return when (type) {\n            ActivityPubMediaAttachmentEntity.TYPE_IMAGE -> BlogMediaType.IMAGE\n            ActivityPubMediaAttachmentEntity.TYPE_AUDIO -> BlogMediaType.AUDIO\n            ActivityPubMediaAttachmentEntity.TYPE_VIDEO -> BlogMediaType.VIDEO\n            ActivityPubMediaAttachmentEntity.TYPE_GIFV -> BlogMediaType.GIFV\n            ActivityPubMediaAttachmentEntity.TYPE_UNKNOWN -> BlogMediaType.UNKNOWN\n            else -> throw IllegalArgumentException(\"Unsupported media type(${type})!\")\n        }\n    }\n\n    private fun ActivityPubStatusEntity.Mention.toMention(): Mention? {\n        val webFinger = WebFinger.create(acct) ?: WebFinger.create(this.url) ?: return null\n        return Mention(\n            id = id,\n            username = username,\n            url = url,\n            webFinger = webFinger,\n            protocol = createActivityPubProtocol(),\n        )\n    }\n\n    private fun buildEmbed(\n        entity: ActivityPubStatusEntity,\n        platform: BlogPlatform,\n    ): List<BlogEmbed> {\n        val list = mutableListOf<BlogEmbed>()\n        entity.card?.toEmbed()?.let { list += it }\n        if (entity.quote != null) {\n            val quotedStatus = entity.quote?.quotedStatus\n            if (quotedStatus != null) {\n                val statusAuthor = activityPubAccountEntityAdapter.toAuthor(quotedStatus.account)\n                val quotedBlog = transformBlog(quotedStatus, platform, statusAuthor)\n                list += BlogEmbed.Blog(quotedBlog)\n            } else {\n                list += BlogEmbed.UnavailableQuote(\n                    reason = entity.quote?.state.orEmpty(),\n                    blogId = entity.quote?.quotedStatusId,\n                )\n            }\n        }\n        return list\n    }\n\n    private fun ActivityPubStatusEntity.PreviewCard.toEmbed(): BlogEmbed {\n        return BlogEmbed.Link(\n            url = url,\n            title = title,\n            description = description,\n            video = type == ActivityPubStatusEntity.PreviewCard.TYPE_VIDEO,\n            authorName = authorName,\n            authorUrl = authorUrl,\n            providerName = providerName,\n            providerUrl = providerUrl,\n            html = html,\n            width = width,\n            height = height,\n            image = image,\n            embedUrl = embedUrl,\n            blurhash = blurhash,\n        )\n    }\n\n    private fun buildQuote(\n        entity: ActivityPubStatusEntity,\n        blogPlatform: BlogPlatform,\n    ): Blog.Quote {\n        val currentUserApproval = entity.quoteApproval?.currentUser?.toApproval()\n        return Blog.Quote(\n            support = blogPlatform.supportsQuotePost == true,\n            enabled = currentUserApproval?.quotable ?: false,\n            currentUserApproval = currentUserApproval,\n        )\n    }\n\n    private fun String.toApproval(): CurrentUserQuoteApproval {\n        return when (this) {\n            ActivityPubQuoteApprovalEntity.CURRENT_USER_AUTOMATIC -> CurrentUserQuoteApproval.AUTOMATIC\n            ActivityPubQuoteApprovalEntity.CURRENT_USER_MANUAL -> CurrentUserQuoteApproval.MANUAL\n            ActivityPubQuoteApprovalEntity.CURRENT_USER_DENIED -> CurrentUserQuoteApproval.DENIED\n            else -> CurrentUserQuoteApproval.UNKNOWN\n        }\n    }\n\n    private fun ActivityPubStatusEntity.Application.toApplication(): PostingApplication {\n        return PostingApplication(\n            name = name,\n            website = website,\n        )\n    }\n\n    private fun ActivityPubFilterResultEntity.toFiltered(): BlogFiltered {\n        //warn, hide, blur\n        return BlogFiltered(\n            id = this.filter.id,\n            title = this.filter.title,\n            action = when (this.filter.filterAction) {\n                ActivityPubFilterEntity.FILTER_ACTION_WARN -> BlogFiltered.FilterAction.WARN\n                ActivityPubFilterEntity.FILTER_ACTION_KEYWORDS -> BlogFiltered.FilterAction.HIDE\n                else -> BlogFiltered.FilterAction.BLUR\n            },\n            keywordMatches = keywordMatches ?: emptyList(),\n        )\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/adapter/ActivityPubTagAdapter.kt",
    "content": "@file:OptIn(ExperimentalTime::class)\n\npackage com.zhangke.fread.activitypub.app.internal.adapter\n\nimport com.zhangke.activitypub.entities.ActivityPubTagEntity\nimport com.zhangke.framework.composable.textOf\nimport com.zhangke.fread.status.model.createActivityPubProtocol\nimport com.zhangke.fread.common.utils.getCurrentInstant\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.model.Hashtag\nimport kotlinx.datetime.LocalDateTime\nimport kotlinx.datetime.TimeZone\nimport kotlinx.datetime.toInstant\nimport kotlinx.datetime.toLocalDateTime\nimport kotlin.time.ExperimentalTime\n\nclass ActivityPubTagAdapter () {\n\n    suspend fun adapt(entity: ActivityPubTagEntity): Hashtag {\n        val yesterdayTimeInMillis = getYesterdayTimeInMillis()\n        val pass2DayUses = entity.history\n            .filter { it.day * 1000 >= yesterdayTimeInMillis }\n            .map { it.accounts }\n            .takeIf { it.isNotEmpty() }\n            ?.reduce { acc, i -> acc + i }\n            .toString()\n\n        return Hashtag(\n            name = \"#${entity.name}\",\n            url = entity.url,\n            description = textOf(LocalizedString.activity_pub_trends_tag_description, pass2DayUses),\n            following = entity.following,\n            history = convertHistoryList(entity.history),\n            protocol = createActivityPubProtocol(),\n        )\n    }\n\n    private fun getYesterdayTimeInMillis(): Long {\n        val timeZone = TimeZone.currentSystemDefault()\n        val today = getCurrentInstant().toLocalDateTime(timeZone)\n        // 获取昨天的日期\n        return LocalDateTime(today.year, today.month, today.dayOfMonth, 0, 0, 0)\n            .toInstant(timeZone)\n            .toEpochMilliseconds()\n    }\n\n    private fun convertHistoryList(\n        list: List<ActivityPubTagEntity.History>\n    ): Hashtag.History {\n        if (list.isEmpty()) {\n            return Hashtag.History(\n                history = emptyList(),\n                max = 0F,\n                min = 0F,\n            )\n        }\n        val history = list.map { it.uses.toFloat() }\n        val min = history.min()\n        val max = history.max()\n        val height = max - min\n        val padding = height * 0.1F\n        val bottom = 0F.coerceAtLeast(min - padding)\n        val top = max + height\n        return Hashtag.History(\n            history = history,\n            max = top,\n            min = bottom,\n        )\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/adapter/ActivityPubTranslationEntityAdapter.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.adapter\n\nimport com.zhangke.activitypub.entities.ActivityPubTranslationEntity\nimport com.zhangke.fread.status.blog.BlogTranslation\n\nclass ActivityPubTranslationEntityAdapter () {\n\n    fun toTranslation(entity: ActivityPubTranslationEntity): BlogTranslation {\n        return BlogTranslation(\n            content = entity.content,\n            spoilerText = entity.spoilerText,\n            poll = entity.poll?.toPol(),\n            attachments = entity.mediaAttachments?.map { it.toAttachment() },\n            detectedSourceLanguage = entity.detectedSourceLanguage,\n            provider = entity.provider,\n        )\n    }\n\n    private fun ActivityPubTranslationEntity.Poll.toPol(): BlogTranslation.Poll {\n        return BlogTranslation.Poll(\n            id = this.id,\n            options = this.options.map {\n                BlogTranslation.Poll.Option(it.title)\n            },\n        )\n    }\n\n    private fun ActivityPubTranslationEntity.Attachment.toAttachment(): BlogTranslation.Attachment {\n        return BlogTranslation.Attachment(\n            id = this.id,\n            description = this.description,\n        )\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/adapter/PostStatusAttachmentAdapter.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.adapter\n\nimport com.zhangke.activitypub.entities.ActivityPubPollRequestEntity\nimport com.zhangke.fread.activitypub.app.internal.screen.status.post.PostStatusAttachment\n\nclass PostStatusAttachmentAdapter () {\n\n    fun toPollRequest(poll: PostStatusAttachment.Poll): ActivityPubPollRequestEntity {\n        return ActivityPubPollRequestEntity(\n            options = poll.optionList,\n            multiple = poll.multiple,\n            expiresIn = poll.duration.inWholeSeconds,\n            hideTotals = false,\n        )\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/adapter/QuoteApprovalPolicyExt.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.adapter\n\nimport com.zhangke.activitypub.entities.ActivityPubPostStatusRequestEntity\nimport com.zhangke.activitypub.entities.ActivityPubQuoteApprovalEntity\nimport com.zhangke.fread.status.model.QuoteApprovalPolicy\n\nval QuoteApprovalPolicy.apCode: String\n    get() = when (this) {\n        QuoteApprovalPolicy.PUBLIC -> ActivityPubPostStatusRequestEntity.QUOTE_APPROVAL_POLICY_PUBLIC\n        QuoteApprovalPolicy.FOLLOWERS -> ActivityPubPostStatusRequestEntity.QUOTE_APPROVAL_POLICY_FOLLOWERS\n        QuoteApprovalPolicy.FOLLOWING -> ActivityPubPostStatusRequestEntity.QUOTE_APPROVAL_POLICY_PUBLIC\n        QuoteApprovalPolicy.NOBODY -> ActivityPubPostStatusRequestEntity.QUOTE_APPROVAL_POLICY_NOBODY\n    }\n\nfun String.toQuoteApprovalPolicy(): QuoteApprovalPolicy = when (this) {\n    ActivityPubPostStatusRequestEntity.QUOTE_APPROVAL_POLICY_PUBLIC -> QuoteApprovalPolicy.PUBLIC\n    ActivityPubPostStatusRequestEntity.QUOTE_APPROVAL_POLICY_FOLLOWERS -> QuoteApprovalPolicy.FOLLOWERS\n    ActivityPubPostStatusRequestEntity.QUOTE_APPROVAL_POLICY_NOBODY -> QuoteApprovalPolicy.NOBODY\n    else -> QuoteApprovalPolicy.PUBLIC\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/adapter/RegisterApplicationEntryAdapter.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.adapter\n\nimport com.zhangke.activitypub.entities.RegisterApplicationEntry\nimport com.zhangke.framework.network.FormalBaseUrl\nimport com.zhangke.fread.activitypub.app.internal.model.ActivityPubApplication\n\nclass RegisterApplicationEntryAdapter () {\n\n    fun toApplication(\n        entity: RegisterApplicationEntry,\n        baseUrl: FormalBaseUrl,\n    ) = ActivityPubApplication(\n        baseUrl = baseUrl,\n        id = entity.id,\n        name = entity.name,\n        clientId = entity.clientId.orEmpty(),\n        clientSecret = entity.clientSecret.orEmpty(),\n        redirectUri = entity.redirectUri,\n        vapidKey = entity.vapidKey.orEmpty(),\n        website = entity.website.orEmpty(),\n    )\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/adapter/StatusVisibilityExt.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.adapter\n\nimport com.zhangke.activitypub.entities.ActivityPubStatusVisibilityEntity\nimport com.zhangke.fread.status.model.StatusVisibility\n\nfun StatusVisibility.toEntityVisibility(): ActivityPubStatusVisibilityEntity {\n    return when (this) {\n        StatusVisibility.PUBLIC -> ActivityPubStatusVisibilityEntity.PUBLIC\n        StatusVisibility.UNLISTED -> ActivityPubStatusVisibilityEntity.UNLISTED\n        StatusVisibility.PRIVATE -> ActivityPubStatusVisibilityEntity.PRIVATE\n        StatusVisibility.DIRECT -> ActivityPubStatusVisibilityEntity.DIRECT\n    }\n}\n\nfun String.toStatusVisibility(): StatusVisibility {\n    return when (this) {\n        ActivityPubStatusVisibilityEntity.PUBLIC.code -> StatusVisibility.PUBLIC\n        ActivityPubStatusVisibilityEntity.UNLISTED.code -> StatusVisibility.UNLISTED\n        ActivityPubStatusVisibilityEntity.PRIVATE.code -> StatusVisibility.PRIVATE\n        ActivityPubStatusVisibilityEntity.DIRECT.code -> StatusVisibility.DIRECT\n        else -> throw IllegalArgumentException(\"Unknown visibility code: $this\")\n    }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/auth/ActivityPubClientManager.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.auth\n\nimport com.zhangke.activitypub.ActivityPubClient\nimport com.zhangke.activitypub.entities.ActivityPubTokenEntity\nimport com.zhangke.framework.architect.http.createHttpClientEngine\nimport com.zhangke.framework.architect.json.globalJson\nimport com.zhangke.framework.network.FormalBaseUrl\nimport com.zhangke.fread.status.model.PlatformLocator\n\nclass ActivityPubClientManager (\n    private val loggedAccountProvider: LoggedAccountProvider,\n) {\n\n    private val cachedClient = mutableMapOf<PlatformLocator, ActivityPubClient>()\n\n    private val httpClientEngine by lazy {\n        createHttpClientEngine()\n    }\n\n    fun clearCache() {\n        cachedClient.clear()\n    }\n\n    fun getClient(locator: PlatformLocator): ActivityPubClient {\n        cachedClient[locator]?.let { return it }\n        return createClient(\n            baseUrl = locator.baseUrl,\n            tokenProvider = {\n                if (locator.accountUri != null) {\n                    loggedAccountProvider.getAccount(locator.accountUri!!)?.token\n                } else {\n                    loggedAccountProvider.getAccount(locator.baseUrl)?.token\n                }\n            },\n        ).also {\n            cachedClient[locator] = it\n        }\n    }\n\n    fun getClientNoAccount(baseUrl: FormalBaseUrl): ActivityPubClient {\n        return createClient(\n            baseUrl = baseUrl,\n            tokenProvider = { null },\n        )\n    }\n\n    private fun createClient(\n        baseUrl: FormalBaseUrl,\n        tokenProvider: () -> ActivityPubTokenEntity?,\n    ): ActivityPubClient {\n        return ActivityPubClient(\n            baseUrl = \"${baseUrl}/\",\n            engine = httpClientEngine,\n            json = globalJson,\n            tokenProvider = tokenProvider,\n            onAuthorizeFailed = {\n\n            },\n        )\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/auth/ActivityPubOAuthor.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.auth\n\nimport com.zhangke.activitypub.api.ActivityPubScope\nimport com.zhangke.framework.network.FormalBaseUrl\nimport com.zhangke.framework.toast.toast\nimport com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubLoggedAccountAdapter\nimport com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubPlatformEntityAdapter\nimport com.zhangke.fread.activitypub.app.internal.content.ActivityPubContent\nimport com.zhangke.fread.activitypub.app.internal.db.ActivityPubDatabases\nimport com.zhangke.fread.activitypub.app.internal.model.ActivityPubLoggedAccount\nimport com.zhangke.fread.activitypub.app.internal.repo.account.ActivityPubLoggedAccountRepo\nimport com.zhangke.fread.activitypub.app.internal.repo.application.ActivityPubApplicationRepo\nimport com.zhangke.fread.common.browser.OAuthHandler\nimport com.zhangke.fread.common.content.FreadContentRepo\nimport com.zhangke.fread.common.di.ApplicationCoroutineScope\nimport com.zhangke.fread.common.utils.getCurrentTimeMillis\nimport kotlinx.coroutines.launch\n\n/**\n * Created by ZhangKe on 2022/12/4.\n */\nclass ActivityPubOAuthor(\n    private val repo: ActivityPubLoggedAccountRepo,\n    private val applicationRepo: ActivityPubApplicationRepo,\n    private val clientManager: ActivityPubClientManager,\n    private val accountAdapter: ActivityPubLoggedAccountAdapter,\n    private val platformEntityAdapter: ActivityPubPlatformEntityAdapter,\n    private val activityPubDatabases: ActivityPubDatabases,\n    private val applicationScope: ApplicationCoroutineScope,\n    private val oAuthHandler: OAuthHandler,\n    private val freadContentRepo: FreadContentRepo,\n) {\n\n    internal fun startOauth(\n        baseUrl: FormalBaseUrl,\n    ) = applicationScope.launch {\n        val app = applicationRepo.getApplicationByBaseUrl(baseUrl)\n        if (app == null) {\n            toast(\"Application not registered\")\n            return@launch\n        }\n        val client = clientManager.getClientNoAccount(baseUrl)\n        val oauthUrl = client.oauthRepo.buildOAuthUrl(\n            baseUrl = baseUrl.toString(),\n            clientId = app.clientId,\n            redirectUri = app.redirectUri,\n        )\n        val code = try {\n            oAuthHandler.startOAuth(oauthUrl)\n        } catch (e: Exception) {\n            toast(e.message)\n            return@launch\n        }\n        val account = try {\n            val instance = client.instanceRepo.getInstanceInformation().getOrThrow()\n            activityPubDatabases.getPlatformDao()\n                .insert(platformEntityAdapter.toEntity(baseUrl, instance))\n            val token = client.oauthRepo.getToken(\n                code = code,\n                clientId = app.clientId,\n                clientSecret = app.clientSecret,\n                redirectUri = app.redirectUri,\n                scopeList = ActivityPubScope.ALL,\n            ).getOrThrow()\n            val accountEntity =\n                client.accountRepo.verifyCredentials(token.accessToken).getOrThrow()\n            accountAdapter.createFromAccount(baseUrl, instance, accountEntity, token)\n        } catch (e: Exception) {\n            toast(e.message)\n            return@launch\n        }\n        insertAccount(account)\n    }\n\n    private suspend fun insertAccount(account: ActivityPubLoggedAccount) {\n        repo.insert(account, getCurrentTimeMillis())\n        val contentList = freadContentRepo.getAllContent().filterIsInstance<ActivityPubContent>()\n        val addedContent = contentList.firstOrNull {\n            account.baseUrl == it.baseUrl && it.accountUri == null\n        }\n        if (addedContent != null) {\n            freadContentRepo.delete(addedContent.id)\n            freadContentRepo.insertContent(addedContent.copy(accountUri = account.uri))\n        }\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/auth/LoggedAccountProvider.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.auth\n\nimport com.zhangke.framework.network.FormalBaseUrl\nimport com.zhangke.fread.activitypub.app.internal.model.ActivityPubLoggedAccount\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.uri.FormalUri\nimport io.github.charlietap.leftright.LeftRight\n\n/**\n * 用于解除 [ActivityPubClientManager] 以及\n * [com.zhangke.fread.activitypub.app.ActivityPubAccountManager] 之间的依赖关系。\n */ class LoggedAccountProvider () {\n\n    // About LeftRight: https://github.com/CharlieTap/cachemap\n    private val accountSet = LeftRight<MutableSet<ActivityPubLoggedAccount>>(::mutableSetOf)\n\n    fun updateAccounts(accountList: List<ActivityPubLoggedAccount>) {\n        accountSet.mutate { set ->\n            set.clear()\n            set.addAll(accountList)\n        }\n    }\n\n    fun getAccount(userUri: FormalUri): ActivityPubLoggedAccount? {\n        return accountSet.read { set ->\n            set.find { it.uri == userUri }\n        }\n    }\n\n    fun getAccount(baseUrl: FormalBaseUrl): ActivityPubLoggedAccount? {\n        val accountList = accountSet.read { set ->\n            set.filter { it.baseUrl.equalsDomain(baseUrl) }\n        }\n        return accountList.firstOrNull()\n    }\n\n    fun getAccount(locator: PlatformLocator): ActivityPubLoggedAccount? {\n        if (locator.accountUri != null) {\n            return getAccount(locator.accountUri!!)\n        }\n        return getAccount(locator.baseUrl)\n    }\n\n    fun getAllAccounts(): List<ActivityPubLoggedAccount> {\n        return accountSet.read { set ->\n            set.toList()\n        }\n    }\n\n    fun removeAccount(uri: FormalUri) {\n        accountSet.mutate { set ->\n            set.find { it.uri == uri }?.let { set.remove(it) }\n        }\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/composable/ActivityPubTabNames.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.composable\n\nimport androidx.compose.runtime.Composable\nimport com.zhangke.fread.activitypub.app.internal.content.ActivityPubContent\nimport com.zhangke.fread.localization.LocalizedString\nimport org.jetbrains.compose.resources.stringResource\n\ninternal object ActivityPubTabNames {\n\n    val homeTimeline: String\n        @Composable get() = stringResource(LocalizedString.activity_pub_content_tab_home)\n\n    val publicTimeline: String\n        @Composable get() = stringResource(LocalizedString.activity_pub_content_tab_public_timeline)\n\n    val localTimeline: String\n        @Composable get() = stringResource(LocalizedString.activity_pub_content_tab_local_timeline)\n\n    val trending: String\n        @Composable get() = stringResource(LocalizedString.activity_pub_content_tab_trending)\n}\n\n@Composable\ninternal fun ActivityPubContent.ContentTab.tabName(): String {\n    return when (this) {\n        is ActivityPubContent.ContentTab.HomeTimeline -> ActivityPubTabNames.homeTimeline\n        is ActivityPubContent.ContentTab.PublicTimeline -> ActivityPubTabNames.publicTimeline\n        is ActivityPubContent.ContentTab.LocalTimeline -> ActivityPubTabNames.localTimeline\n        is ActivityPubContent.ContentTab.Trending -> ActivityPubTabNames.trending\n        is ActivityPubContent.ContentTab.ListTimeline -> this.name\n    }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/content/ActivityPubContent.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.content\n\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport com.zhangke.framework.network.FormalBaseUrl\nimport com.zhangke.framework.security.Md5\nimport com.zhangke.fread.commonbiz.mastodon_logo\nimport com.zhangke.fread.status.account.LoggedAccount\nimport com.zhangke.fread.status.model.FreadContent\nimport com.zhangke.fread.status.uri.FormalUri\nimport kotlinx.serialization.Serializable\nimport org.jetbrains.compose.resources.painterResource\n\n@Serializable\ndata class ActivityPubContent(\n    override val name: String,\n    override val order: Int,\n    val baseUrl: FormalBaseUrl,\n    val tabList: List<ContentTab>,\n    override val accountUri: FormalUri? = null,\n) : FreadContent {\n\n    private val _id: String by lazy {\n        if (accountUri == null) {\n            Md5.md5(baseUrl.toString())\n        } else {\n            Md5.md5(baseUrl.toString() + accountUri.toString())\n        }\n    }\n\n    override val id: String get() = _id\n\n    override fun newOrder(newOrder: Int): FreadContent {\n        return copy(order = newOrder)\n    }\n\n    @Composable\n    override fun Subtitle(account: LoggedAccount?) {\n        Row(verticalAlignment = Alignment.CenterVertically) {\n            Image(\n                modifier = Modifier.size(14.dp),\n                painter = painterResource(com.zhangke.fread.commonbiz.Res.drawable.mastodon_logo),\n                contentDescription = null,\n            )\n            Text(\n                modifier = Modifier\n                    .padding(start = 4.dp)\n                    .align(Alignment.Bottom),\n                text = account?.prettyHandle ?: baseUrl.host,\n                style = MaterialTheme.typography.labelMedium,\n                maxLines = 1,\n                overflow = TextOverflow.Ellipsis,\n                color = MaterialTheme.colorScheme.onSurfaceVariant,\n            )\n        }\n    }\n\n    @Serializable\n    sealed interface ContentTab {\n\n        val order: Int\n\n        val hide: Boolean\n\n        fun newOrder(order: Int): ContentTab\n\n        fun updateHide(hide: Boolean): ContentTab\n\n        @Serializable\n        data class HomeTimeline(\n            override val order: Int,\n            override val hide: Boolean = false,\n        ) : ContentTab {\n\n            override fun updateHide(hide: Boolean): ContentTab {\n                return copy(hide = hide)\n            }\n\n            override fun newOrder(order: Int): ContentTab {\n                return copy(order = order)\n            }\n        }\n\n        @Serializable\n        data class LocalTimeline(\n            override val order: Int,\n            override val hide: Boolean = false,\n        ) : ContentTab {\n\n            override fun updateHide(hide: Boolean): ContentTab {\n                return copy(hide = hide)\n            }\n\n            override fun newOrder(order: Int): ContentTab {\n                return copy(order = order)\n            }\n        }\n\n        @Serializable\n        data class PublicTimeline(\n            override val order: Int,\n            override val hide: Boolean = false,\n        ) : ContentTab {\n\n            override fun updateHide(hide: Boolean): ContentTab {\n                return copy(hide = hide)\n            }\n\n            override fun newOrder(order: Int): ContentTab {\n                return copy(order = order)\n            }\n        }\n\n        @Serializable\n        data class Trending(\n            override val order: Int,\n            override val hide: Boolean = false,\n        ) : ContentTab {\n\n            override fun updateHide(hide: Boolean): ContentTab {\n                return copy(hide = hide)\n            }\n\n            override fun newOrder(order: Int): ContentTab {\n                return copy(order = order)\n            }\n        }\n\n        @Serializable\n        data class ListTimeline(\n            val listId: String,\n            val name: String,\n            override val order: Int,\n            override val hide: Boolean = false,\n        ) : ContentTab {\n\n            override fun updateHide(hide: Boolean): ContentTab {\n                return copy(hide = hide)\n            }\n\n            override fun newOrder(order: Int): ContentTab {\n                return copy(order = order)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/db/ActivityPubApplicationTable.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.db\n\nimport androidx.room.Dao\nimport androidx.room.Delete\nimport androidx.room.Entity\nimport androidx.room.Insert\nimport androidx.room.OnConflictStrategy\nimport androidx.room.PrimaryKey\nimport androidx.room.Query\nimport com.zhangke.framework.network.FormalBaseUrl\n\nprivate const val TABLE_NAME = \"application_list\"\n\n@Entity(tableName = TABLE_NAME)\ndata class ActivityPubApplicationEntity(\n    @PrimaryKey val baseUrl: FormalBaseUrl,\n    val id: String,\n    val name: String,\n    val website: String,\n    val redirectUri: String,\n    val clientId: String,\n    val clientSecret: String,\n    val vapidKey: String,\n)\n\n@Dao\ninterface ActivityPubApplicationsDao {\n\n    @Query(\"SELECT * FROM $TABLE_NAME WHERE baseUrl=:baseUrl\")\n    suspend fun queryByBaseUrl(baseUrl: FormalBaseUrl): ActivityPubApplicationEntity?\n\n    @Insert(onConflict = OnConflictStrategy.REPLACE)\n    suspend fun insert(applicationEntity: ActivityPubApplicationEntity)\n\n    @Delete\n    suspend fun delete(applicationEntity: ActivityPubApplicationEntity)\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/db/ActivityPubDatabases.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.db\n\nimport androidx.room.ConstructedBy\nimport androidx.room.Database\nimport androidx.room.RoomDatabase\nimport androidx.room.RoomDatabaseConstructor\nimport androidx.room.TypeConverters\nimport com.zhangke.fread.activitypub.app.internal.db.converter.ActivityPubInstanceEntityConverter\nimport com.zhangke.fread.activitypub.app.internal.db.converter.ActivityPubUserTokenConverter\nimport com.zhangke.fread.activitypub.app.internal.db.converter.EmojiListConverter\nimport com.zhangke.fread.activitypub.app.internal.db.converter.FormalBaseUrlConverter\nimport com.zhangke.fread.activitypub.app.internal.db.converter.PlatformEntityTypeConverter\nimport com.zhangke.fread.activitypub.app.internal.db.old.OldActivityPubLoggedAccountEntity\nimport com.zhangke.fread.activitypub.app.internal.db.old.OldActivityPubLoggerAccountDao\nimport com.zhangke.fread.common.utils.WebFingerConverter\n\nprivate const val DB_VERSION = 1\n\n@TypeConverters(\n    WebFingerConverter::class,\n    PlatformEntityTypeConverter::class,\n    ActivityPubUserTokenConverter::class,\n    ActivityPubInstanceEntityConverter::class,\n    FormalBaseUrlConverter::class,\n    EmojiListConverter::class,\n)\n@Database(\n    entities = [\n        OldActivityPubLoggedAccountEntity::class,\n        ActivityPubApplicationEntity::class,\n        ActivityPubInstanceInfoEntity::class,\n        WebFingerBaseurlToIdEntity::class,\n    ],\n    version = DB_VERSION,\n    exportSchema = false\n)\n@ConstructedBy(ActivityPubDatabasesConstructor::class)\nabstract class ActivityPubDatabases : RoomDatabase() {\n\n    abstract fun getLoggedAccountDao(): OldActivityPubLoggerAccountDao\n\n    abstract fun getApplicationDao(): ActivityPubApplicationsDao\n\n    abstract fun getPlatformDao(): ActivityPubPlatformDao\n\n    abstract fun getUserIdDao(): WebFingerBaseurlToIdDao\n\n    companion object {\n        internal const val DB_NAME = \"ActivityPubStatusProvider\"\n    }\n}\n\n// The Room compiler generates the `actual` implementations.\n@Suppress(\"NO_ACTUAL_FOR_EXPECT\")\nexpect object ActivityPubDatabasesConstructor : RoomDatabaseConstructor<ActivityPubDatabases> {\n    override fun initialize(): ActivityPubDatabases\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/db/ActivityPubLoggedAccountDatabase.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.db\n\nimport androidx.room.ConstructedBy\nimport androidx.room.Dao\nimport androidx.room.Database\nimport androidx.room.Entity\nimport androidx.room.Insert\nimport androidx.room.OnConflictStrategy\nimport androidx.room.PrimaryKey\nimport androidx.room.Query\nimport androidx.room.RoomDatabase\nimport androidx.room.RoomDatabaseConstructor\nimport androidx.room.TypeConverters\nimport com.zhangke.fread.activitypub.app.internal.db.converter.ActivityPubLoggedAccountConverter\nimport com.zhangke.fread.activitypub.app.internal.model.ActivityPubLoggedAccount\nimport kotlinx.coroutines.flow.Flow\n\nprivate const val DB_VERSION = 1\nprivate const val TABLE_NAME = \"logged_accounts\"\n\n@Entity(tableName = TABLE_NAME)\ndata class ActivityPubLoggedAccountEntity(\n    @PrimaryKey val uri: String,\n    val account: ActivityPubLoggedAccount,\n    val addedTimestamp: Long,\n)\n\n@Dao\ninterface ActivityPubLoggedAccountDao {\n\n    @Query(\"SELECT * FROM $TABLE_NAME ORDER BY addedTimestamp\")\n    fun queryAllFlow(): Flow<List<ActivityPubLoggedAccountEntity>>\n\n    @Query(\"SELECT * FROM $TABLE_NAME WHERE uri=:uri\")\n    fun observeAccount(uri: String): Flow<ActivityPubLoggedAccountEntity?>\n\n    @Query(\"SELECT * FROM $TABLE_NAME ORDER BY addedTimestamp\")\n    suspend fun queryAll(): List<ActivityPubLoggedAccountEntity>\n\n    @Query(\"SELECT * FROM $TABLE_NAME WHERE uri=:uri\")\n    suspend fun queryByUri(uri: String): ActivityPubLoggedAccountEntity?\n\n    @Insert(onConflict = OnConflictStrategy.REPLACE)\n    suspend fun insert(entry: ActivityPubLoggedAccountEntity)\n\n    @Insert(onConflict = OnConflictStrategy.REPLACE)\n    suspend fun insert(entries: List<ActivityPubLoggedAccountEntity>)\n\n    @Query(\"DELETE FROM $TABLE_NAME WHERE uri=:uri\")\n    suspend fun deleteByUri(uri: String)\n\n    @Query(\"DELETE FROM $TABLE_NAME\")\n    suspend fun nukeTable()\n}\n\n@TypeConverters(ActivityPubLoggedAccountConverter::class)\n@Database(\n    entities = [\n        ActivityPubLoggedAccountEntity::class,\n    ],\n    version = DB_VERSION,\n    exportSchema = false,\n)\n@ConstructedBy(ActivityPubLoggedAccountDatabaseConstructor::class)\nabstract class ActivityPubLoggedAccountDatabase : RoomDatabase() {\n\n    abstract fun getDao(): ActivityPubLoggedAccountDao\n\n    companion object {\n        internal const val DB_NAME = \"ActivityPubLoggedAccount.db\"\n    }\n}\n\n// The Room compiler generates the `actual` implementations.\n@Suppress(\"NO_ACTUAL_FOR_EXPECT\")\nexpect object ActivityPubLoggedAccountDatabaseConstructor :\n    RoomDatabaseConstructor<ActivityPubLoggedAccountDatabase> {\n    override fun initialize(): ActivityPubLoggedAccountDatabase\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/db/ActivityPubPlatformTable.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.db\n\nimport androidx.room.Dao\nimport androidx.room.Delete\nimport androidx.room.Entity\nimport androidx.room.Insert\nimport androidx.room.OnConflictStrategy\nimport androidx.room.PrimaryKey\nimport androidx.room.Query\nimport com.zhangke.activitypub.entities.ActivityPubInstanceEntity\nimport com.zhangke.framework.network.FormalBaseUrl\n\nprivate const val TABLE_NAME = \"platform_info\"\n\n@Entity(tableName = TABLE_NAME)\ndata class ActivityPubInstanceInfoEntity(\n    @PrimaryKey val uri: String,\n    val baseUrl: FormalBaseUrl,\n    val instanceEntity: ActivityPubInstanceEntity,\n)\n\n@Dao\ninterface ActivityPubPlatformDao {\n\n    @Query(\"SELECT * FROM $TABLE_NAME\")\n    suspend fun queryAll(): List<ActivityPubInstanceInfoEntity>\n\n    @Query(\"SELECT * FROM $TABLE_NAME WHERE uri=:uri\")\n    suspend fun queryByUri(uri: String): ActivityPubInstanceInfoEntity?\n\n    @Query(\"SELECT * FROM $TABLE_NAME WHERE baseUrl=:baseUrl\")\n    suspend fun queryByBaseUrl(baseUrl: FormalBaseUrl): ActivityPubInstanceInfoEntity?\n\n    @Insert(onConflict = OnConflictStrategy.REPLACE)\n    suspend fun insert(entity: ActivityPubInstanceInfoEntity)\n\n    @Delete\n    suspend fun deleteByUri(entity: ActivityPubInstanceInfoEntity)\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/db/UserIdTable.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.db\n\nimport androidx.room.Dao\nimport androidx.room.Entity\nimport androidx.room.Insert\nimport androidx.room.OnConflictStrategy\nimport androidx.room.Query\nimport com.zhangke.framework.network.FormalBaseUrl\nimport com.zhangke.framework.utils.WebFinger\n\nprivate const val TABLE_NAME = \"web_finger_baseurl_to_id\"\n\n@Entity(tableName = TABLE_NAME, primaryKeys = [\"webFinger\", \"baseUrl\"])\ndata class WebFingerBaseurlToIdEntity(\n    val webFinger: WebFinger,\n    val baseUrl: FormalBaseUrl,\n    val userId: String,\n)\n\n@Dao\ninterface WebFingerBaseurlToIdDao {\n\n    @Query(\"SELECT userId FROM $TABLE_NAME WHERE webFinger = :webFinger AND baseUrl = :baseUrl\")\n    suspend fun queryUserId(webFinger: WebFinger, baseUrl: FormalBaseUrl): String?\n\n    @Insert(onConflict = OnConflictStrategy.REPLACE)\n    suspend fun insert(entity: WebFingerBaseurlToIdEntity)\n\n    @Query(\"DELETE FROM $TABLE_NAME WHERE webFinger = :webFinger AND baseUrl = :baseUrl\")\n    suspend fun delete(webFinger: WebFinger, baseUrl: String)\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/db/converter/ActivityPubAccountEntityConverter.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.db.converter\n\nimport androidx.room.TypeConverter\nimport com.zhangke.activitypub.entities.ActivityPubAccountEntity\nimport com.zhangke.framework.architect.json.globalJson\nimport kotlinx.serialization.encodeToString\n\nclass ActivityPubAccountEntityConverter {\n\n    @TypeConverter\n    fun toBlogAuthorString(blogAuthor: ActivityPubAccountEntity): String {\n        return globalJson.encodeToString(blogAuthor)\n    }\n\n    @TypeConverter\n    fun toBlogAuthor(jsonString: String): ActivityPubAccountEntity {\n        return globalJson.decodeFromString(jsonString)\n    }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/db/converter/ActivityPubInstanceEntityConverter.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.db.converter\n\nimport androidx.room.TypeConverter\nimport com.zhangke.activitypub.entities.ActivityPubInstanceEntity\nimport com.zhangke.framework.architect.json.globalJson\nimport kotlinx.serialization.encodeToString\n\nclass ActivityPubInstanceEntityConverter {\n\n    @TypeConverter\n    fun fromEntity(entity: ActivityPubInstanceEntity): String {\n        return globalJson.encodeToString(entity)\n    }\n\n    @TypeConverter\n    fun toEntity(text: String): ActivityPubInstanceEntity {\n        return globalJson.decodeFromString(text)\n    }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/db/converter/ActivityPubLoggedAccountConverter.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.db.converter\n\nimport androidx.room.TypeConverter\nimport com.zhangke.framework.architect.json.globalJson\nimport com.zhangke.fread.activitypub.app.internal.model.ActivityPubLoggedAccount\n\nclass ActivityPubLoggedAccountConverter {\n\n    @TypeConverter\n    fun toJsonString(blogAuthor: ActivityPubLoggedAccount): String {\n        return globalJson.encodeToString(blogAuthor)\n    }\n\n    @TypeConverter\n    fun toEntity(jsonString: String): ActivityPubLoggedAccount {\n        return globalJson.decodeFromString(jsonString)\n    }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/db/converter/ActivityPubStatusEntityConverter.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.db.converter\n\nimport androidx.room.TypeConverter\nimport com.zhangke.activitypub.entities.ActivityPubStatusEntity\nimport com.zhangke.framework.architect.json.globalJson\nimport kotlinx.serialization.encodeToString\n\nclass ActivityPubStatusEntityConverter {\n\n    @TypeConverter\n    fun fromEntity(entity: ActivityPubStatusEntity): String {\n        return globalJson.encodeToString(entity)\n    }\n\n    @TypeConverter\n    fun toEntity(text: String): ActivityPubStatusEntity {\n        return globalJson.decodeFromString(text)\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/db/converter/ActivityPubStatusSourceTypeConverter.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.db.converter\n\nimport androidx.room.TypeConverter\nimport com.zhangke.fread.activitypub.app.internal.model.ActivityPubStatusSourceType\n\nclass ActivityPubStatusSourceTypeConverter {\n\n    @TypeConverter\n    fun fromType(type: ActivityPubStatusSourceType): String {\n        return type.name\n    }\n\n    @TypeConverter\n    fun toType(text: String): ActivityPubStatusSourceType {\n        return ActivityPubStatusSourceType.valueOf(text)\n    }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/db/converter/ActivityPubUserTokenConverter.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.db.converter\n\nimport androidx.room.TypeConverter\nimport com.zhangke.activitypub.entities.ActivityPubTokenEntity\nimport com.zhangke.framework.architect.json.globalJson\nimport kotlinx.serialization.encodeToString\n\nclass ActivityPubUserTokenConverter {\n\n    @TypeConverter\n    fun fromType(token: ActivityPubTokenEntity): String {\n        return globalJson.encodeToString(token)\n    }\n\n    @TypeConverter\n    fun toType(text: String): ActivityPubTokenEntity {\n        return globalJson.decodeFromString(text)\n    }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/db/converter/BlogAuthorConverter.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.db.converter\n\nimport androidx.room.TypeConverter\nimport com.zhangke.framework.architect.json.globalJson\nimport com.zhangke.fread.status.author.BlogAuthor\nimport kotlinx.serialization.encodeToString\n\nclass BlogAuthorConverter {\n\n    @TypeConverter\n    fun toBlogAuthorString(blogAuthor: BlogAuthor): String {\n        return globalJson.encodeToString(blogAuthor)\n    }\n\n    @TypeConverter\n    fun toBlogAuthor(jsonString: String): BlogAuthor {\n        return globalJson.decodeFromString(jsonString)\n    }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/db/converter/EmojiListConverter.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.db.converter\n\nimport androidx.room.TypeConverter\nimport com.zhangke.framework.architect.json.globalJson\nimport com.zhangke.fread.status.model.Emoji\nimport kotlinx.serialization.encodeToString\n\nclass EmojiListConverter {\n\n    @TypeConverter\n    fun toJsonString(blogAuthor: List<Emoji>): String {\n        return globalJson.encodeToString(blogAuthor)\n    }\n\n    @TypeConverter\n    fun toEmoji(jsonString: String): List<Emoji> {\n        return globalJson.decodeFromString(jsonString)\n    }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/db/converter/FormalBaseUrlConverter.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.db.converter\n\nimport androidx.room.TypeConverter\nimport com.zhangke.framework.network.FormalBaseUrl\n\nclass FormalBaseUrlConverter {\n\n    @TypeConverter\n    fun fromBaseUrl(baseUrl: FormalBaseUrl): String {\n        return baseUrl.toString()\n    }\n\n    @TypeConverter\n    fun toBaseUrl(text: String): FormalBaseUrl {\n        return FormalBaseUrl.parse(text)!!\n    }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/db/converter/PlatformEntityTypeConverter.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.db.converter\n\nimport androidx.room.TypeConverter\nimport com.zhangke.framework.architect.json.globalJson\nimport com.zhangke.framework.network.FormalBaseUrl\nimport com.zhangke.fread.activitypub.app.internal.db.old.OldActivityPubLoggedAccountEntity\nimport kotlinx.serialization.json.JsonObject\nimport kotlinx.serialization.json.JsonPrimitive\n\nclass PlatformEntityTypeConverter {\n\n    @TypeConverter\n    fun fromType(platform: OldActivityPubLoggedAccountEntity.BlogPlatformEntity): String {\n        return globalJson.encodeToString(platform)\n    }\n\n    @TypeConverter\n    fun toType(text: String): OldActivityPubLoggedAccountEntity.BlogPlatformEntity {\n        return runCatching {\n            globalJson.decodeFromString<OldActivityPubLoggedAccountEntity.BlogPlatformEntity>(text)\n        }.getOrNull() ?: compatibleObfuscation(text)\n    }\n\n    private fun compatibleObfuscation(text: String): OldActivityPubLoggedAccountEntity.BlogPlatformEntity {\n        val jsonObject = runCatching { globalJson.decodeFromString<JsonObject>(text) }.getOrNull()\n            ?: JsonObject(emptyMap())\n        val baseUrlJson = jsonObject[\"d\"]?.let { it as? JsonObject }\n        val baseUrl = FormalBaseUrl.build(\n            host = baseUrlJson.getValueAsString(\"host\", \"mastodon.social\"),\n            scheme = baseUrlJson.getValueAsString(\"scheme\", \"https\"),\n        )\n        val defaultUri =\n            \"freadapp://activitypub.com/platform?serverBaseUrl=https%3A%2F%2Fmastodon.social\"\n        return OldActivityPubLoggedAccountEntity.BlogPlatformEntity(\n            uri = jsonObject.getValueAsString(\"a\", defaultUri),\n            name = jsonObject.getValueAsString(\"b\", \"mastodon.social\"),\n            description = jsonObject.getValueAsString(\"c\", \"\"),\n            baseUrl = baseUrl,\n            thumbnail = jsonObject.getValueAsString(\"e\", \"\"),\n        )\n    }\n\n    private fun JsonObject?.getValueAsString(key: String, default: String): String {\n        this ?: return default\n        val v = this[key] ?: return default\n        if (v is JsonPrimitive) {\n            return v.content\n        }\n        return v.toString()\n    }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/db/converter/RelationshipSeveranceEventConverter.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.db.converter\n\nimport androidx.room.TypeConverter\nimport com.zhangke.framework.architect.json.globalJson\nimport com.zhangke.fread.activitypub.app.internal.model.RelationshipSeveranceEvent\nimport kotlinx.serialization.encodeToString\n\nclass RelationshipSeveranceEventConverter {\n\n    @TypeConverter\n    fun fromType(platform: RelationshipSeveranceEvent?): String? {\n        return platform?.let { globalJson.encodeToString(it) }\n    }\n\n    @TypeConverter\n    fun toType(text: String?): RelationshipSeveranceEvent? {\n        if (text.isNullOrEmpty()) return null\n        return globalJson.decodeFromString(text)\n    }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/db/converter/StatusNotificationTypeConverter.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.db.converter\n\nimport androidx.room.TypeConverter\nimport com.zhangke.fread.activitypub.app.internal.model.StatusNotificationType\n\nclass StatusNotificationTypeConverter {\n\n    @TypeConverter\n    fun convertToString(type: StatusNotificationType): String {\n        return type.name\n    }\n\n    @TypeConverter\n    fun convertToType(name: String): StatusNotificationType {\n        return StatusNotificationType.valueOf(name)\n    }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/db/old/ActivityPubLoggedAccountTable.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.db.old\n\nimport androidx.room.Dao\nimport androidx.room.Entity\nimport androidx.room.Insert\nimport androidx.room.OnConflictStrategy\nimport androidx.room.PrimaryKey\nimport androidx.room.Query\nimport com.zhangke.activitypub.entities.ActivityPubTokenEntity\nimport com.zhangke.framework.network.FormalBaseUrl\nimport com.zhangke.framework.utils.WebFinger\nimport com.zhangke.fread.status.model.Emoji\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.serialization.Serializable\n\nprivate const val TABLE_NAME = \"logged_accounts\"\n\n@Entity(tableName = TABLE_NAME)\ndata class OldActivityPubLoggedAccountEntity(\n    @PrimaryKey val uri: String,\n    val userId: String,\n    val webFinger: WebFinger,\n    val platform: BlogPlatformEntity,\n    val baseUrl: FormalBaseUrl,\n    val name: String,\n    val description: String?,\n    val avatar: String?,\n    val url: String,\n    val token: ActivityPubTokenEntity,\n    val emojis: List<Emoji>,\n    val addedTimestamp: Long,\n) {\n\n    @Serializable\n    data class BlogPlatformEntity(\n        val uri: String,\n        val name: String,\n        val description: String,\n        val baseUrl: FormalBaseUrl,\n        val thumbnail: String?,\n    )\n}\n\n@Dao\ninterface OldActivityPubLoggerAccountDao {\n\n    @Query(\"SELECT * FROM $TABLE_NAME ORDER BY addedTimestamp\")\n    fun queryAllFlow(): Flow<List<OldActivityPubLoggedAccountEntity>>\n\n    @Query(\"SELECT * FROM $TABLE_NAME WHERE uri=:uri\")\n    fun observeAccount(uri: String): Flow<OldActivityPubLoggedAccountEntity?>\n\n    @Query(\"SELECT * FROM $TABLE_NAME ORDER BY addedTimestamp\")\n    suspend fun queryAll(): List<OldActivityPubLoggedAccountEntity>\n\n    @Query(\"SELECT * FROM $TABLE_NAME WHERE userId=:userId\")\n    suspend fun queryById(userId: String): OldActivityPubLoggedAccountEntity?\n\n    @Query(\"SELECT * FROM $TABLE_NAME WHERE uri=:uri\")\n    suspend fun queryByUri(uri: String): OldActivityPubLoggedAccountEntity?\n\n    @Query(\"SELECT * FROM $TABLE_NAME WHERE baseUrl=:baseUrl ORDER BY addedTimestamp\")\n    suspend fun queryByBaseUrl(baseUrl: FormalBaseUrl): List<OldActivityPubLoggedAccountEntity>\n\n    @Insert(onConflict = OnConflictStrategy.REPLACE)\n    suspend fun insert(entry: OldActivityPubLoggedAccountEntity)\n\n    @Insert(onConflict = OnConflictStrategy.REPLACE)\n    suspend fun insert(entries: List<OldActivityPubLoggedAccountEntity>)\n\n    @Query(\"DELETE FROM $TABLE_NAME WHERE uri=:uri\")\n    suspend fun deleteByUri(uri: String)\n\n    @Query(\"DELETE FROM $TABLE_NAME\")\n    suspend fun nukeTable()\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/db/status/ActivityPubStatusDatabases.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.db.status\n\nimport androidx.room.ConstructedBy\nimport androidx.room.Dao\nimport androidx.room.Database\nimport androidx.room.Entity\nimport androidx.room.Insert\nimport androidx.room.OnConflictStrategy\nimport androidx.room.Query\nimport androidx.room.RoomDatabase\nimport androidx.room.RoomDatabaseConstructor\nimport androidx.room.TypeConverters\nimport androidx.room.migration.Migration\nimport androidx.sqlite.SQLiteConnection\nimport androidx.sqlite.execSQL\nimport com.zhangke.fread.activitypub.app.internal.db.converter.ActivityPubStatusEntityConverter\nimport com.zhangke.fread.activitypub.app.internal.db.converter.ActivityPubStatusSourceTypeConverter\nimport com.zhangke.fread.activitypub.app.internal.db.converter.FormalBaseUrlConverter\nimport com.zhangke.fread.activitypub.app.internal.model.ActivityPubStatusSourceType\nimport com.zhangke.fread.common.db.converts.PlatformLocatorConverter\nimport com.zhangke.fread.common.db.converts.StatusConverter\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.status.model.Status\n\nprivate const val DB_VERSION = 2\nprivate const val TABLE_NAME = \"activity_pub_status\"\n\n@Entity(tableName = TABLE_NAME, primaryKeys = [\"id\", \"locator\", \"type\", \"listId\"])\ndata class ActivityPubStatusTableEntity(\n    // Status id\n    val id: String,\n    val locator: PlatformLocator,\n    val type: ActivityPubStatusSourceType,\n    val listId: String,\n    val status: Status,\n    val createTimestamp: Long,\n)\n\n@Dao\ninterface ActivityPubStatusDao {\n\n    @Query(\"SELECT * FROM $TABLE_NAME WHERE id = :id AND locator = :locator AND type = :type\")\n    suspend fun query(\n        locator: PlatformLocator,\n        type: ActivityPubStatusSourceType,\n        id: String\n    ): ActivityPubStatusTableEntity?\n\n    @Query(\"SELECT * FROM $TABLE_NAME WHERE id = :id AND locator = :locator AND type = :type AND listId = :listId ORDER BY createTimestamp DESC\")\n    suspend fun queryStatusInList(\n        locator: PlatformLocator,\n        type: ActivityPubStatusSourceType,\n        listId: String,\n        id: String\n    ): ActivityPubStatusTableEntity?\n\n    @Query(\"SELECT * FROM $TABLE_NAME WHERE locator = :locator AND type = :type ORDER BY createTimestamp DESC LIMIT :limit\")\n    suspend fun queryTimelineStatus(\n        locator: PlatformLocator,\n        type: ActivityPubStatusSourceType,\n        limit: Int,\n    ): List<ActivityPubStatusTableEntity>\n\n    @Query(\"SELECT * FROM $TABLE_NAME WHERE locator = :locator AND type = :type AND listId = :listId ORDER BY createTimestamp DESC LIMIT :limit\")\n    suspend fun queryListStatus(\n        locator: PlatformLocator,\n        type: ActivityPubStatusSourceType,\n        listId: String,\n        limit: Int\n    ): List<ActivityPubStatusTableEntity>\n\n    @Insert(onConflict = OnConflictStrategy.REPLACE)\n    suspend fun insert(entity: ActivityPubStatusTableEntity)\n\n    @Insert(onConflict = OnConflictStrategy.REPLACE)\n    suspend fun insert(entities: List<ActivityPubStatusTableEntity>)\n\n    @Query(\"DELETE FROM $TABLE_NAME WHERE locator=:locator AND type=:type\")\n    suspend fun delete(locator: PlatformLocator, type: ActivityPubStatusSourceType)\n\n    @Query(\"DELETE FROM $TABLE_NAME WHERE locator=:locator AND type=:type AND listId=:listId\")\n    suspend fun deleteListStatus(\n        locator: PlatformLocator,\n        type: ActivityPubStatusSourceType,\n        listId: String\n    )\n\n    @Query(\"DELETE FROM $TABLE_NAME WHERE id=:id\")\n    suspend fun delete(id: String)\n}\n\n@TypeConverters(\n    FormalBaseUrlConverter::class,\n    ActivityPubStatusSourceTypeConverter::class,\n    ActivityPubStatusEntityConverter::class,\n    StatusConverter::class,\n    PlatformLocatorConverter::class,\n)\n@Database(\n    entities = [ActivityPubStatusTableEntity::class],\n    version = DB_VERSION,\n    exportSchema = false,\n)\n@ConstructedBy(ActivityPubStatusDatabasesConstructor::class)\nabstract class ActivityPubStatusDatabases : RoomDatabase() {\n\n    abstract fun getDao(): ActivityPubStatusDao\n\n    companion object {\n        const val DB_NAME = \"activity_pub_status_1.db\"\n\n        val MIGRATION_1_2 = object : Migration(1, 2) {\n\n            override fun migrate(connection: SQLiteConnection) {\n                connection.execSQL(\"DELETE FROM $TABLE_NAME\")\n            }\n        }\n    }\n}\n\n// The Room compiler generates the `actual` implementations.\n@Suppress(\"NO_ACTUAL_FOR_EXPECT\")\nexpect object ActivityPubStatusDatabasesConstructor :\n    RoomDatabaseConstructor<ActivityPubStatusDatabases> {\n    override fun initialize(): ActivityPubStatusDatabases\n}\n\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/db/status/ActivityPubStatusReadStateDatabases.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.db.status\n\nimport androidx.room.ConstructedBy\nimport androidx.room.Dao\nimport androidx.room.Database\nimport androidx.room.Delete\nimport androidx.room.Entity\nimport androidx.room.Insert\nimport androidx.room.OnConflictStrategy\nimport androidx.room.Query\nimport androidx.room.RoomDatabase\nimport androidx.room.RoomDatabaseConstructor\nimport androidx.room.TypeConverters\nimport com.zhangke.fread.activitypub.app.internal.db.converter.ActivityPubStatusSourceTypeConverter\nimport com.zhangke.fread.activitypub.app.internal.model.ActivityPubStatusSourceType\nimport com.zhangke.fread.common.db.converts.PlatformLocatorConverter\nimport com.zhangke.fread.status.model.PlatformLocator\n\nprivate const val DB_VERSION = 1\nprivate const val TABLE_NAME = \"activity_pub_status_read_state\"\n\n@Entity(tableName = TABLE_NAME, primaryKeys = [\"locator\", \"type\", \"listId\"])\ndata class ActivityPubStatusReadStateEntity(\n    val locator: PlatformLocator,\n    val type: ActivityPubStatusSourceType,\n    val listId: String,\n    val latestReadId: String?,\n)\n\n@Dao\ninterface ActivityPubStatusReadStateDao {\n\n    @Query(\"SELECT * FROM $TABLE_NAME WHERE locator = :locator AND type = :type\")\n    suspend fun query(\n        locator: PlatformLocator,\n        type: ActivityPubStatusSourceType,\n    ): ActivityPubStatusReadStateEntity?\n\n    @Query(\"SELECT * FROM $TABLE_NAME WHERE locator = :locator AND type = :type AND listId = :listId\")\n    suspend fun queryList(\n        locator: PlatformLocator,\n        type: ActivityPubStatusSourceType,\n        listId: String,\n    ): ActivityPubStatusReadStateEntity?\n\n    @Insert(onConflict = OnConflictStrategy.REPLACE)\n    suspend fun update(entity: ActivityPubStatusReadStateEntity)\n\n    @Delete\n    suspend fun delete(entity: ActivityPubStatusReadStateEntity)\n}\n\n@TypeConverters(\n    ActivityPubStatusSourceTypeConverter::class,\n    PlatformLocatorConverter::class,\n)\n@Database(\n    entities = [ActivityPubStatusReadStateEntity::class],\n    version = DB_VERSION,\n    exportSchema = false,\n)\n@ConstructedBy(ActivityPubStatusReadStateDatabasesConstructor::class)\nabstract class ActivityPubStatusReadStateDatabases : RoomDatabase() {\n\n    abstract fun getDao(): ActivityPubStatusReadStateDao\n\n    companion object {\n        internal const val DB_NAME = \"activity_pub_status_read_state_1.db\"\n    }\n}\n\n// The Room compiler generates the `actual` implementations.\n@Suppress(\"NO_ACTUAL_FOR_EXPECT\")\nexpect object ActivityPubStatusReadStateDatabasesConstructor :\n    RoomDatabaseConstructor<ActivityPubStatusReadStateDatabases> {\n    override fun initialize(): ActivityPubStatusReadStateDatabases\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/migrate/ActivityPubContentMigrator.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.migrate\n\nimport com.zhangke.fread.activitypub.app.internal.content.ActivityPubContent\nimport com.zhangke.fread.activitypub.app.internal.repo.account.ActivityPubLoggedAccountRepo\nimport com.zhangke.fread.common.content.FreadContentRepo\n\nclass ActivityPubContentMigrator (\n    private val freadContentRepo: FreadContentRepo,\n    private val accountRepo: ActivityPubLoggedAccountRepo,\n) {\n\n    suspend fun migrate() {\n        val allContent =\n            freadContentRepo.getAllOldContents().filter { it.second is ActivityPubContent }\n        if (allContent.isEmpty()) return\n        val allAccounts = accountRepo.queryAll()\n        allContent.map { it.second as ActivityPubContent }\n            .map { content ->\n                if (content.accountUri != null) {\n                    content\n                } else {\n                    val account = allAccounts.firstOrNull { it.baseUrl == content.baseUrl }\n                    content.copy(accountUri = account?.uri)\n                }\n            }.let { freadContentRepo.insertAll(it) }\n        for (content in allContent) {\n            freadContentRepo.deleteOldContents(content.first)\n        }\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/model/ActivityPubApplication.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.model\n\nimport com.zhangke.framework.network.FormalBaseUrl\n\ndata class ActivityPubApplication(\n    val baseUrl: FormalBaseUrl,\n    val id: String,\n    val name: String,\n    val website: String,\n    val redirectUri: String,\n    val clientId: String,\n    val clientSecret: String,\n    val vapidKey: String,\n)\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/model/ActivityPubInstanceRule.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.model\n\ndata class ActivityPubInstanceRule(\n    val id: String,\n    val text: String,\n)\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/model/ActivityPubLoggedAccount.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.model\n\nimport com.zhangke.activitypub.entities.ActivityPubTokenEntity\nimport com.zhangke.framework.network.FormalBaseUrl\nimport com.zhangke.framework.utils.WebFinger\nimport com.zhangke.fread.status.account.LoggedAccount\nimport com.zhangke.fread.status.model.Emoji\nimport com.zhangke.fread.status.platform.BlogPlatform\nimport com.zhangke.fread.status.uri.FormalUri\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class ActivityPubLoggedAccount(\n    val userId: String,\n    val baseUrl: FormalBaseUrl,\n    val url: String,\n    val token: ActivityPubTokenEntity,\n    val banner: String,\n    val followersCount: Long,\n    val followingCount: Long,\n    val statusesCount: Long,\n    val note: String,\n    val bot: Boolean,\n    override val uri: FormalUri,\n    override val webFinger: WebFinger,\n    override val platform: BlogPlatform,\n    override val userName: String,\n    override val description: String?,\n    override val avatar: String?,\n    override val emojis: List<Emoji>,\n) : LoggedAccount {\n\n    override val id: String? get() = userId\n\n    override val prettyHandle: String\n        get() {\n            val handle = webFinger.toString()\n            return if (handle.startsWith('@')) handle else \"@$handle\"\n        }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/model/ActivityPubStatusSourceType.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.model\n\nenum class ActivityPubStatusSourceType {\n\n    TIMELINE_HOME,\n    TIMELINE_LOCAL,\n    TIMELINE_PUBLIC,\n    LIST,\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/model/ActivityPubTimelineType.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.model\n\nimport com.zhangke.fread.localization.LocalizedString\nimport org.jetbrains.compose.resources.getString\n\nenum class ActivityPubTimelineType {\n\n    PUBLIC,\n    LOCAL,\n    HOME;\n\n    suspend fun nickName(): String {\n        return when (this) {\n            HOME -> getString(LocalizedString.activity_pub_home_timeline)\n            LOCAL -> getString(LocalizedString.activity_pub_local_timeline)\n            PUBLIC -> getString(LocalizedString.activity_pub_public_timeline)\n        }\n    }\n\n    companion object {\n\n        fun valurOfOrNull(name: String): ActivityPubTimelineType? {\n            return entries.firstOrNull { it.name == name }\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/model/CustomEmoji.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.model\n\ndata class CustomEmoji(\n    val shortcode: String,\n    val url: String,\n    val staticUrl: String,\n    val visibleInPicker: Boolean,\n    val category: String,\n)\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/model/PlatformUriInsights.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.model\n\nimport com.zhangke.framework.network.FormalBaseUrl\nimport com.zhangke.fread.status.uri.FormalUri\n\ndata class PlatformUriInsights(\n    val uri: FormalUri,\n    val serverBaseUrl: FormalBaseUrl,\n)\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/model/RelationshipSeveranceEvent.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.model\n\nimport com.zhangke.framework.datetime.Instant\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class RelationshipSeveranceEvent(\n    val id: String,\n    val type: Type,\n    val purged: Boolean,\n    val targetName: String,\n    val relationshipsCount: Int?,\n    val createdAt: Instant,\n) {\n\n    enum class Type {\n\n        DOMAIN_BLOCK,\n        USER_DOMAIN_BLOCK,\n        ACCOUNT_SUSPENSION,\n    }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/model/ServerDetailContract.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.model\n\nimport com.zhangke.fread.status.author.BlogAuthor\n\ndata class ServerDetailContract(\n    val email: String,\n    val account: BlogAuthor,\n)\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/model/StatusNotification.kt",
    "content": "@file:OptIn(ExperimentalTime::class)\n\npackage com.zhangke.fread.activitypub.app.internal.model\n\nimport com.zhangke.activitypub.entities.ActivityPubAccountEntity\nimport com.zhangke.fread.status.status.model.Status\nimport kotlinx.datetime.Instant\nimport kotlin.time.ExperimentalTime\n\ndata class StatusNotification(\n    val id: String,\n    val type: StatusNotificationType,\n    val createdAt: Instant,\n    /**\n     * The account that performed the action that generated the notification.\n     */\n    val account: ActivityPubAccountEntity,\n    val status: Status?,\n    val relationshipSeveranceEvent: RelationshipSeveranceEvent?,\n)\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/model/StatusNotificationType.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.model\n\nenum class StatusNotificationType {\n\n    MENTION,\n    STATUS,\n    REBLOG,\n    FOLLOW,\n    FOLLOW_REQUEST,\n    FAVOURITE,\n    /**\n     * A poll you have voted in or created has ended\n     */\n    POLL,\n\n    /**\n     * A status you boosted with has been edited\n     */\n    UPDATE,\n    SEVERED_RELATIONSHIPS,\n    UNKNOWN;\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/model/UserSource.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.model\n\n//data class UserSource(\n//    val uri: StatusProviderUri,\n//    val userId: String,\n//    val webFinger: WebFinger,\n//    val name: String,\n//    val description: String,\n//    val thumbnail: String?,\n//)\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/model/UserUriInsights.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.model\n\nimport com.zhangke.framework.network.FormalBaseUrl\nimport com.zhangke.framework.utils.Parcelize\nimport com.zhangke.framework.utils.PlatformParcelable\nimport com.zhangke.framework.utils.WebFinger\nimport com.zhangke.fread.status.uri.FormalUri\nimport kotlinx.serialization.Serializable\n\n@Parcelize\n@Serializable\ndata class UserUriInsights(\n    val uri: FormalUri,\n    val webFinger: WebFinger,\n    val baseUrl: FormalBaseUrl,\n) : PlatformParcelable\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/platform/FreadApplicationRegisterInfo.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.platform\n\nimport com.zhangke.activitypub.api.AppsRepo\nimport com.zhangke.fread.common.config.AppCommonConfig\n\nobject FreadApplicationRegisterInfo {\n\n    const val CLIENT_NAME = \"Fread\"\n    val redirectUris = listOf(\"freadapp://fread.xyz\")\n    val scopes = AppsRepo.AppScopes.ALL\n    const val WEBSITE = AppCommonConfig.WEBSITE\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/push/ActivityPubPushManager.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.push\n\nimport com.zhangke.fread.status.model.PlatformLocator\n\nexpect class ActivityPubPushManager {\n\n    suspend fun subscribe(locator: PlatformLocator, accountId: String)\n\n    suspend fun unsubscribe(locator: PlatformLocator, accountId: String)\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/push/ActivityPubPushMessageReceiver.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.push\n\nimport com.zhangke.fread.common.push.PushMessage\nimport com.zhangke.fread.common.push.PushMessageReceiver\nimport com.zhangke.krouter.annotation.Service\n\nexpect class ActivityPubPushMessageReceiverHelper() {\n\n    fun onReceiveNewMessage(message: PushMessage)\n}\n\n@Service\nclass ActivityPubPushMessageReceiver : PushMessageReceiver {\n\n    private val pushMessageReceiverHelper = ActivityPubPushMessageReceiverHelper()\n\n    override fun onReceiveNewMessage(message: PushMessage) {\n        pushMessageReceiverHelper.onReceiveNewMessage(message)\n    }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/repo/WebFingerBaseUrlToUserIdRepo.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.repo\n\nimport com.zhangke.activitypub.entities.ActivityPubAccountEntity\nimport com.zhangke.framework.utils.WebFinger\nimport com.zhangke.fread.activitypub.app.internal.auth.ActivityPubClientManager\nimport com.zhangke.fread.activitypub.app.internal.db.ActivityPubDatabases\nimport com.zhangke.fread.activitypub.app.internal.db.WebFingerBaseurlToIdEntity\nimport com.zhangke.fread.status.model.PlatformLocator\n\nclass WebFingerBaseUrlToUserIdRepo (\n    activityPubDatabases: ActivityPubDatabases,\n    private val clientManager: ActivityPubClientManager,\n) {\n\n    private val userIdDao = activityPubDatabases.getUserIdDao()\n\n    suspend fun getUserId(webFinger: WebFinger, locator: PlatformLocator): Result<String> {\n        val baseUrl = locator.baseUrl\n        val userIdFromLocal = userIdDao.queryUserId(webFinger, baseUrl)\n        if (userIdFromLocal.isNullOrEmpty().not()) return Result.success(userIdFromLocal!!)\n        val result = lookupAccount(webFinger, locator)\n        if (result.isFailure) return Result.failure(result.exceptionOrNull()!!)\n        val account = result.getOrNull()\n            ?: return Result.failure(IllegalArgumentException(\"Can't find $webFinger in $baseUrl\"))\n        insert(webFinger, locator, account.id)\n        return Result.success(account.id)\n    }\n\n    private suspend fun lookupAccount(\n        webFinger: WebFinger,\n        locator: PlatformLocator,\n    ): Result<ActivityPubAccountEntity?> {\n        // 先通过 WebFinger 查找，找不到则通过 name 查找。\n        // 例如这个用户：https://m.cmx.im/@b0rk@jvns.ca\n        // 目前我们的客户端创建的 WebFinger 为：@b0rk@social.jvns.ca\n        // 因为我们的客户端的 WebFinger 中的 host 是实际上真正的 host。\n        // 但这个用户在自己服务器上的 WebFinger 是 @b0rk@jvns.ca，\n        // 直接使用我们的 WebFinger 是查找不到用户的，但是通过用户名可以查找到。\n        // 类似的，JW 大佬的账号也是这样，这是个普遍情况。\n        val accountRepo = clientManager.getClient(locator).accountRepo\n        val result = accountRepo.lookup(webFinger.toString())\n        if (result.isSuccess) return result\n        return accountRepo.lookup(webFinger.name)\n    }\n\n    suspend fun insert(webFinger: WebFinger, locator: PlatformLocator, userId: String) {\n        userIdDao.insert(WebFingerBaseurlToIdEntity(webFinger, locator.baseUrl, userId))\n    }\n\n    suspend fun delete(webFinger: WebFinger, locator: PlatformLocator) {\n        userIdDao.delete(webFinger, locator.baseUrl.toString())\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/repo/account/ActivityPubLoggedAccountRepo.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.repo.account\n\nimport com.zhangke.fread.activitypub.app.internal.db.ActivityPubDatabases\nimport com.zhangke.fread.activitypub.app.internal.db.ActivityPubLoggedAccountDao\nimport com.zhangke.fread.activitypub.app.internal.db.ActivityPubLoggedAccountDatabase\nimport com.zhangke.fread.activitypub.app.internal.db.ActivityPubLoggedAccountEntity\nimport com.zhangke.fread.activitypub.app.internal.db.old.OldActivityPubLoggerAccountDao\nimport com.zhangke.fread.activitypub.app.internal.model.ActivityPubLoggedAccount\nimport com.zhangke.fread.common.utils.getCurrentTimeMillis\nimport com.zhangke.fread.status.uri.FormalUri\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.map\n\nclass ActivityPubLoggedAccountRepo (\n    private val oldDatabases: ActivityPubDatabases,\n    private val accountDatabase: ActivityPubLoggedAccountDatabase,\n) {\n\n    private val _onNewAccountFlow = MutableSharedFlow<ActivityPubLoggedAccount>()\n    val onNewAccountFlow: Flow<ActivityPubLoggedAccount> = _onNewAccountFlow\n\n    private val oldAccountDao: OldActivityPubLoggerAccountDao\n        get() = oldDatabases.getLoggedAccountDao()\n\n    private val accountDao: ActivityPubLoggedAccountDao\n        get() = accountDatabase.getDao()\n\n    suspend fun initialize() {\n        LoggedAccountMigrateUtil.migrate(\n            oldDao = oldAccountDao,\n            accountDao = accountDao,\n        )\n    }\n\n    fun getAllAccountFlow(): Flow<List<ActivityPubLoggedAccount>> {\n        return accountDao.queryAllFlow().map { list ->\n            list.map { it.account }\n        }\n    }\n\n    fun observeAccount(uri: String): Flow<ActivityPubLoggedAccount?> {\n        return accountDao.observeAccount(uri)\n            .map { it?.account }\n    }\n\n    suspend fun queryAll(): List<ActivityPubLoggedAccount> =\n        accountDao.queryAll().map { it.account }\n\n    suspend fun queryByUri(uri: String): ActivityPubLoggedAccount? =\n        accountDao.queryByUri(uri)?.account\n\n    suspend fun insert(\n        account: ActivityPubLoggedAccount,\n        addedTimestamp: Long,\n    ) {\n        val entity = buildAccountEntity(account, addedTimestamp)\n        accountDao.insert(entity)\n        _onNewAccountFlow.emit(account)\n    }\n\n    suspend fun update(account: ActivityPubLoggedAccount) {\n        val addedTimestamp =\n            accountDao.queryByUri(account.uri.toString())?.addedTimestamp ?: getCurrentTimeMillis()\n        val entity = buildAccountEntity(account, addedTimestamp)\n        accountDao.insert(entity)\n    }\n\n    private fun buildAccountEntity(\n        account: ActivityPubLoggedAccount,\n        addedTimestamp: Long,\n    ): ActivityPubLoggedAccountEntity {\n        return ActivityPubLoggedAccountEntity(\n            uri = account.uri.toString(),\n            account = account,\n            addedTimestamp = addedTimestamp,\n        )\n    }\n\n    suspend fun deleteByUri(uri: FormalUri) {\n        accountDao.deleteByUri(uri.toString())\n        oldAccountDao.deleteByUri(uri.toString())\n    }\n\n    suspend fun clear() = accountDao.nukeTable()\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/repo/account/LoggedAccountMigrateUtil.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.repo.account\n\nimport com.zhangke.fread.status.model.createActivityPubProtocol\nimport com.zhangke.fread.activitypub.app.internal.db.ActivityPubLoggedAccountDao\nimport com.zhangke.fread.activitypub.app.internal.db.ActivityPubLoggedAccountEntity\nimport com.zhangke.fread.activitypub.app.internal.db.old.OldActivityPubLoggedAccountEntity\nimport com.zhangke.fread.activitypub.app.internal.db.old.OldActivityPubLoggerAccountDao\nimport com.zhangke.fread.activitypub.app.internal.model.ActivityPubLoggedAccount\nimport com.zhangke.fread.status.platform.BlogPlatform\nimport com.zhangke.fread.status.uri.FormalUri\n\nobject LoggedAccountMigrateUtil {\n\n    suspend fun migrate(\n        oldDao: OldActivityPubLoggerAccountDao,\n        accountDao: ActivityPubLoggedAccountDao,\n    ) {\n        val oldAccountList = oldDao.queryAll()\n            .map { convertToLoggedAccount(it) }\n        if (oldAccountList.isEmpty()) return\n        accountDao.insert(oldAccountList)\n        oldDao.nukeTable()\n    }\n\n    private suspend fun convertToLoggedAccount(\n        entity: OldActivityPubLoggedAccountEntity,\n    ): ActivityPubLoggedAccountEntity {\n        val account = ActivityPubLoggedAccount(\n            userId = entity.userId,\n            uri = FormalUri.from(entity.uri)!!,\n            webFinger = entity.webFinger,\n            platform = entity.platform.toPlatform(),\n            baseUrl = entity.baseUrl,\n            userName = entity.name,\n            description = entity.description,\n            avatar = entity.avatar,\n            url = entity.url,\n            token = entity.token,\n            emojis = entity.emojis,\n            followersCount = 0L,\n            followingCount = 0L,\n            statusesCount = 0L,\n            banner = \"\",\n            note = \"\",\n            bot = false,\n        )\n        return ActivityPubLoggedAccountEntity(\n            uri = account.uri.toString(),\n            account = account,\n            addedTimestamp = entity.addedTimestamp,\n        )\n    }\n\n    private fun OldActivityPubLoggedAccountEntity.BlogPlatformEntity.toPlatform(): BlogPlatform =\n        BlogPlatform(\n            uri = uri,\n            name = name,\n            description = description,\n            baseUrl = baseUrl,\n            thumbnail = thumbnail,\n            protocol = createActivityPubProtocol(),\n        )\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/repo/application/ActivityPubApplicationRepo.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.repo.application\n\nimport com.zhangke.framework.network.FormalBaseUrl\nimport com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubApplicationEntityAdapter\nimport com.zhangke.fread.activitypub.app.internal.adapter.RegisterApplicationEntryAdapter\nimport com.zhangke.fread.activitypub.app.internal.auth.ActivityPubClientManager\nimport com.zhangke.fread.activitypub.app.internal.db.ActivityPubApplicationsDao\nimport com.zhangke.fread.activitypub.app.internal.db.ActivityPubDatabases\nimport com.zhangke.fread.activitypub.app.internal.model.ActivityPubApplication\nimport com.zhangke.fread.activitypub.app.internal.platform.FreadApplicationRegisterInfo\nimport com.zhangke.fread.status.model.PlatformLocator\n\nclass ActivityPubApplicationRepo (\n    private val databases: ActivityPubDatabases,\n    private val clientManager: ActivityPubClientManager,\n    private val registerApplicationEntryAdapter: RegisterApplicationEntryAdapter,\n    private val applicationEntityAdapter: ActivityPubApplicationEntityAdapter,\n) {\n\n    private val applicationsDao: ActivityPubApplicationsDao get() = databases.getApplicationDao()\n\n    suspend fun getApplicationByBaseUrl(baseUrl: FormalBaseUrl): ActivityPubApplication? {\n        applicationsDao.queryByBaseUrl(baseUrl)\n            ?.let(applicationEntityAdapter::toApplication)\n            ?.let { return it }\n        val locator = PlatformLocator(accountUri = null, baseUrl = baseUrl)\n        val application = clientManager.getClient(locator)\n            .appsRepo\n            .registerApplication(\n                clientName = FreadApplicationRegisterInfo.CLIENT_NAME,\n                redirectUris = FreadApplicationRegisterInfo.redirectUris,\n                scopes = FreadApplicationRegisterInfo.scopes,\n                website = FreadApplicationRegisterInfo.WEBSITE,\n            ).map { registerApplicationEntryAdapter.toApplication(it, baseUrl) }\n            .getOrNull() ?: return null\n        insert(application)\n        return application\n    }\n\n    suspend fun insert(application: ActivityPubApplication) {\n        applicationsDao.insert(applicationEntityAdapter.toEntity(application))\n    }\n\n    suspend fun delete(application: ActivityPubApplication) {\n        applicationsDao.delete(applicationEntityAdapter.toEntity(application))\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/repo/platform/ActivityPubPlatformRepo.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.repo.platform\n\nimport com.zhangke.activitypub.entities.ActivityPubInstanceEntity\nimport com.zhangke.framework.architect.coroutines.ApplicationScope\nimport com.zhangke.framework.network.FormalBaseUrl\nimport com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubInstanceAdapter\nimport com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubPlatformEntityAdapter\nimport com.zhangke.fread.activitypub.app.internal.auth.ActivityPubClientManager\nimport com.zhangke.fread.activitypub.app.internal.auth.LoggedAccountProvider\nimport com.zhangke.fread.activitypub.app.internal.db.ActivityPubDatabases\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.platform.BlogPlatform\nimport com.zhangke.fread.status.platform.PlatformSnapshot\nimport kotlinx.coroutines.launch\n\nclass ActivityPubPlatformRepo (\n    databases: ActivityPubDatabases,\n    private val clientManager: ActivityPubClientManager,\n    private val activityPubPlatformEntityAdapter: ActivityPubPlatformEntityAdapter,\n    private val activityPubInstanceAdapter: ActivityPubInstanceAdapter,\n    private val platformResourceLoader: BlogPlatformResourceLoader,\n    private val mastodonInstanceRepo: MastodonInstanceRepo,\n    private val loggedAccountProvider: LoggedAccountProvider,\n) {\n\n    private val localPlatformSnapshotList = mutableListOf<PlatformSnapshot>()\n\n    private val platformDao = databases.getPlatformDao()\n\n    suspend fun getPlatform(locator: PlatformLocator): Result<BlogPlatform> {\n        return getPlatform(locator.baseUrl)\n    }\n\n    suspend fun getPlatform(baseUrl: FormalBaseUrl): Result<BlogPlatform> {\n        return getInstanceInfo(baseUrl).map {\n            activityPubInstanceAdapter.toPlatform(baseUrl, it)\n        }\n    }\n\n    suspend fun getInstanceEntity(baseUrl: FormalBaseUrl): Result<ActivityPubInstanceEntity> {\n        return getInstanceInfo(baseUrl)\n    }\n\n    suspend fun getSuggestedPlatformSnapshotList(): List<PlatformSnapshot> {\n        return getAllLocalPlatformSnapshot()\n    }\n\n    suspend fun searchPlatformSnapshotFromLocal(query: String): List<PlatformSnapshot> {\n        val localPlatforms = getAllLocalPlatformSnapshot()\n        return localPlatforms.filter {\n            it.domain.contains(query, true) || it.description.contains(query, true)\n        }.distinctBy { it.domain }\n    }\n\n    suspend fun searchPlatformFromServer(query: String): Result<List<PlatformSnapshot>> {\n        return mastodonInstanceRepo.searchWithName(query)\n    }\n\n    private suspend fun getAllLocalPlatformSnapshot(): List<PlatformSnapshot> {\n        if (localPlatformSnapshotList.isEmpty()) {\n            localPlatformSnapshotList += platformResourceLoader.loadLocalPlatforms()\n        }\n        return localPlatformSnapshotList\n    }\n\n    private suspend fun getInstanceInfo(baseUrl: FormalBaseUrl): Result<ActivityPubInstanceEntity> {\n        val instanceFromLocal = platformDao.queryByBaseUrl(baseUrl)\n        val locator = PlatformLocator(accountUri = null, baseUrl = baseUrl)\n        if (instanceFromLocal != null) {\n            if (instanceFromLocal.instanceEntity.apiVersions == null) {\n                // refresh local data\n                val accountUri = loggedAccountProvider.getAllAccounts()\n                    .firstOrNull { it.baseUrl == baseUrl }\n                    ?.uri\n                val fixedLocator = PlatformLocator(accountUri = accountUri, baseUrl = baseUrl)\n                ApplicationScope.launch {\n                    clientManager.getClient(fixedLocator)\n                        .instanceRepo\n                        .getInstanceInformation()\n                        .map { activityPubPlatformEntityAdapter.toEntity(baseUrl, it) }\n                        .onSuccess { platformDao.insert(it) }\n                }\n            }\n            return Result.success(instanceFromLocal.instanceEntity)\n        }\n        val instanceResult = clientManager.getClient(locator).instanceRepo.getInstanceInformation()\n        if (instanceResult.isFailure) {\n            return Result.failure(instanceResult.exceptionOrNull()!!)\n        }\n        val instanceEntity = instanceResult.getOrThrow()\n        platformDao.insert(activityPubPlatformEntityAdapter.toEntity(baseUrl, instanceEntity))\n        return Result.success(instanceEntity)\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/repo/platform/BlogPlatformResourceLoader.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.repo.platform\n\nimport com.zhangke.framework.architect.json.globalJson\nimport com.zhangke.fread.status.model.createActivityPubProtocol\nimport com.zhangke.fread.activitypub.app.internal.utils.MastodonHelper\nimport com.zhangke.fread.status.platform.PlatformSnapshot\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.IO\nimport kotlinx.coroutines.withContext\nimport kotlinx.serialization.json.JsonArray\nimport kotlinx.serialization.json.JsonObject\nimport kotlinx.serialization.json.JsonPrimitive\nimport kotlinx.serialization.json.contentOrNull\nimport kotlinx.serialization.json.intOrNull\n\nclass BlogPlatformResourceLoader (\n    private val mastodonHelper: MastodonHelper,\n) {\n\n    suspend fun loadLocalPlatforms(): List<PlatformSnapshot> = withContext(Dispatchers.IO) {\n        val json = mastodonHelper.getLocalMastodonJson()\n        if (json.isNullOrEmpty()) return@withContext emptyList()\n        return@withContext globalJson.decodeFromString<JsonArray?>(json)\n            ?.mapNotNull { it as? JsonObject }\n            ?.mapNotNull { it.toPlatformSnapshot() }\n            ?.distinctBy { it.domain }\n            ?: emptyList()\n    }\n\n    private fun JsonObject.toPlatformSnapshot(): PlatformSnapshot? {\n        val domain = getAsString(\"domain\") ?: return null\n        return PlatformSnapshot(\n            domain = domain.lowercase(),\n            description = getAsString(\"description\").orEmpty(),\n            thumbnail = getAsString(\"proxied_thumbnail\").orEmpty(),\n            protocol = createActivityPubProtocol(),\n        )\n    }\n\n    private fun JsonObject.getAsString(key: String): String? {\n        val element = get(key)\n        if (element is JsonPrimitive) {\n            return element.contentOrNull\n        }\n        return null\n    }\n\n    private fun JsonObject.getAsInt(key: String): Int? {\n        val element = get(key)\n        if (element is JsonPrimitive) {\n            return element.intOrNull\n        }\n        return null\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/repo/platform/MastodonInstanceRepo.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.repo.platform\n\nimport com.zhangke.framework.architect.http.sharedHttpClient\nimport com.zhangke.fread.status.model.createActivityPubProtocol\nimport com.zhangke.fread.status.platform.PlatformSnapshot\nimport io.ktor.client.call.body\nimport io.ktor.client.request.HttpRequestBuilder\nimport io.ktor.client.request.get\nimport io.ktor.client.request.header\nimport io.ktor.client.request.parameter\nimport io.ktor.client.request.url\nimport io.ktor.http.takeFrom\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\nclass MastodonInstanceRepo () {\n\n    private companion object {\n\n        private const val BASE_URL = \"https://instances.social/api/1.0\"\n        private const val PATH_SEARCH = \"$BASE_URL/instances/search\"\n        private const val TOKEN =\n            \"kw1opiSJ24Gwddb49rRCbOZlFrI6cCxSxprWhsGMDzKQA36obwpcFr9DldvIwUDAXvbprvwCEUCUZ4eKQYr1vxZ8QP5PkD3dGUKlaETF4MAMsKJXEFiY5gKfzYL8MoUE\"\n    }\n\n    suspend fun searchAll(query: String): Result<List<PlatformSnapshot>> {\n        return runCatching {\n            sharedHttpClient.get {\n                url(PATH_SEARCH) {\n                    parameter(\"q\", query)\n                    parameter(\"name\", false)\n                    parameter(\"count\", 120)\n                }\n                applyCommonHeaders()\n            }.body<QueryResult>()\n                .instances\n                .map { it.toPlatformSnapshot() }\n        }\n    }\n\n    suspend fun searchWithName(name: String): Result<List<PlatformSnapshot>> {\n        return runCatching {\n            sharedHttpClient.get {\n                url {\n                    takeFrom(PATH_SEARCH)\n                    parameter(\"q\", name)\n                    parameter(\"name\", true)\n                    parameter(\"count\", 20)\n                }\n                applyCommonHeaders()\n            }.body<QueryResult>()\n                .instances\n                .map { it.toPlatformSnapshot() }\n        }\n    }\n\n    private fun HttpRequestBuilder.applyCommonHeaders() {\n        header(\"Authorization\", \"Bearer $TOKEN\")\n    }\n\n    private suspend fun MastodonInstance.toPlatformSnapshot(): PlatformSnapshot {\n        return PlatformSnapshot(\n            domain = name,\n            description = info?.shortDescription.orEmpty(),\n            thumbnail = thumbnail.orEmpty(),\n            protocol = createActivityPubProtocol(),\n        )\n    }\n\n    @Serializable\n    data class QueryResult(\n        val instances: List<MastodonInstance>,\n    )\n\n    @Serializable\n    data class MastodonInstance(\n        val name: String,\n        val info: Info?,\n        val thumbnail: String?,\n        val version: String?,\n        val users: Int?,\n        @SerialName(\"active_users\")\n        val activeUsers: Int?,\n    ) {\n\n        @Serializable\n        data class Info(\n            @SerialName(\"short_description\")\n            val shortDescription: String?,\n            val languages: List<String>?,\n            val categories: List<String>?,\n        )\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/repo/status/ActivityPubStatusReadStateRepo.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.repo.status\n\nimport com.zhangke.fread.activitypub.app.internal.db.status.ActivityPubStatusReadStateDatabases\nimport com.zhangke.fread.activitypub.app.internal.db.status.ActivityPubStatusReadStateEntity\nimport com.zhangke.fread.activitypub.app.internal.model.ActivityPubStatusSourceType\nimport com.zhangke.fread.status.model.PlatformLocator\n\nclass ActivityPubStatusReadStateRepo (\n    activityPubStatusReadStateDatabases: ActivityPubStatusReadStateDatabases,\n) {\n\n    private val readStateDao = activityPubStatusReadStateDatabases.getDao()\n\n    suspend fun getLatestReadId(\n        locator: PlatformLocator,\n        type: ActivityPubStatusSourceType,\n        listId: String? = null\n    ): String? {\n        return if (listId == null) {\n            readStateDao.query(\n                locator = locator,\n                type = type,\n            )?.latestReadId\n        } else {\n            readStateDao.queryList(\n                locator = locator,\n                type = type,\n                listId = listId,\n            )?.latestReadId\n        }\n    }\n\n    suspend fun updateLatestReadId(\n        locator: PlatformLocator,\n        type: ActivityPubStatusSourceType,\n        listId: String? = null,\n        latestReadId: String\n    ) {\n        val entity = ActivityPubStatusReadStateEntity(\n            locator = locator,\n            type = type,\n            listId = listId.orEmpty(),\n            latestReadId = latestReadId,\n        )\n        readStateDao.update(entity)\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/repo/status/ActivityPubTimelineStatusRepo.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.repo.status\n\nimport com.zhangke.fread.activitypub.app.internal.db.status.ActivityPubStatusDatabases\nimport com.zhangke.fread.activitypub.app.internal.db.status.ActivityPubStatusTableEntity\nimport com.zhangke.fread.activitypub.app.internal.model.ActivityPubStatusSourceType\nimport com.zhangke.fread.activitypub.app.internal.usecase.status.GetTimelineStatusUseCase\nimport com.zhangke.fread.common.status.StatusConfigurationDefault\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.status.model.Status\n\nclass ActivityPubTimelineStatusRepo (\n    activityPubStatusDatabases: ActivityPubStatusDatabases,\n    private val getTimeline: GetTimelineStatusUseCase,\n) {\n\n    private val statusDao = activityPubStatusDatabases.getDao()\n    private val statusConfig = StatusConfigurationDefault.config\n\n    /**\n     * 获取最新的帖子列表\n     */\n    suspend fun getFresherStatus(\n        locator: PlatformLocator,\n        type: ActivityPubStatusSourceType,\n        listId: String? = null,\n        limit: Int = statusConfig.loadFromServerLimit,\n    ): Result<List<Status>> {\n        return getTimeline(\n            locator = locator,\n            type = type,\n            limit = limit,\n            listId = listId,\n            maxId = null,\n            minId = null,\n        ).onSuccess {\n            saveFresherStatus(\n                locator = locator,\n                type = type,\n                listId = listId,\n                statusList = it,\n            )\n        }\n    }\n\n    private suspend fun saveFresherStatus(\n        locator: PlatformLocator,\n        type: ActivityPubStatusSourceType,\n        listId: String?,\n        statusList: List<Status>,\n    ) {\n        if (statusList.isEmpty()) return\n        val earlierStatus = statusList.minBy { it.createAt.epochMillis }\n        val localEarlierStatus = queryLocalStatus(\n            locator = locator,\n            type = type,\n            listId = listId,\n            statusId = earlierStatus.id,\n        )\n        if (localEarlierStatus == null) {\n            // local data are expired\n            deleteStatus(locator, type, listId)\n        }\n        statusList.insertToLocal(locator, type, listId)\n    }\n\n    suspend fun loadPreviousPageStatus(\n        locator: PlatformLocator,\n        type: ActivityPubStatusSourceType,\n        minId: String,\n        listId: String? = null,\n        limit: Int = statusConfig.loadFromServerLimit,\n    ): Result<List<Status>> {\n        return getTimeline(\n            locator = locator,\n            type = type,\n            limit = limit,\n            maxId = null,\n            minId = minId,\n            listId = listId,\n        ).onSuccess {\n            it.insertToLocal(locator, type, listId)\n        }\n    }\n\n    suspend fun getStatusFromLocal(\n        locator: PlatformLocator,\n        type: ActivityPubStatusSourceType,\n        limit: Int = statusConfig.loadFromLocalLimit,\n        listId: String? = null,\n    ): List<Status> {\n        return queryLocalStatusList(\n            locator = locator,\n            type = type,\n            limit = limit,\n            listId = listId,\n        ).map { it.status }\n    }\n\n    suspend fun loadMore(\n        locator: PlatformLocator,\n        type: ActivityPubStatusSourceType,\n        maxId: String,\n        listId: String? = null,\n        limit: Int = statusConfig.loadFromLocalLimit,\n    ): Result<List<Status>> {\n        return getTimeline(\n            locator = locator,\n            type = type,\n            limit = limit,\n            maxId = maxId,\n            minId = null,\n            listId = listId,\n        ).onSuccess {\n            it.insertToLocal(locator, type, listId)\n        }\n    }\n\n    suspend fun updateStatus(\n        locator: PlatformLocator,\n        type: ActivityPubStatusSourceType,\n        status: Status,\n        listId: String? = null,\n    ) {\n        val localStatus = queryLocalStatus(\n            locator = locator,\n            type = type,\n            listId = listId,\n            statusId = status.id,\n        )\n        if (localStatus != null) {\n            statusDao.insert(status.toDBEntity(locator, type, listId))\n        }\n        val leftTypes = ActivityPubStatusSourceType.entries.filter { it != type }\n        leftTypes.mapNotNull {\n            queryLocalStatus(\n                locator = locator,\n                type = it,\n                listId = listId,\n                statusId = status.id,\n            )\n        }.forEach {\n            statusDao.insert(status.toDBEntity(locator, it.type, it.listId))\n        }\n    }\n\n    suspend fun deleteStatus(statusId: String) {\n        statusDao.delete(statusId)\n    }\n\n    private suspend fun List<Status>.insertToLocal(\n        locator: PlatformLocator,\n        type: ActivityPubStatusSourceType,\n        listId: String?,\n    ) {\n        statusDao.insert(this.toDBEntities(locator, type, listId))\n    }\n\n    private suspend fun queryLocalStatus(\n        locator: PlatformLocator,\n        type: ActivityPubStatusSourceType,\n        listId: String?,\n        statusId: String,\n    ): ActivityPubStatusTableEntity? {\n        return if (type == ActivityPubStatusSourceType.LIST && listId != null) {\n            statusDao.queryStatusInList(\n                locator = locator,\n                type = type,\n                listId = listId,\n                id = statusId,\n            )\n        } else {\n            statusDao.query(locator, type, statusId)\n        }\n    }\n\n    private suspend fun queryLocalStatusList(\n        locator: PlatformLocator,\n        type: ActivityPubStatusSourceType,\n        limit: Int,\n        listId: String?,\n    ): List<ActivityPubStatusTableEntity> {\n        return if (type == ActivityPubStatusSourceType.LIST) {\n            statusDao.queryListStatus(\n                locator = locator,\n                type = type,\n                limit = limit,\n                listId = listId!!,\n            )\n        } else {\n            statusDao.queryTimelineStatus(locator, type, limit)\n        }\n    }\n\n    private suspend fun deleteStatus(\n        locator: PlatformLocator,\n        type: ActivityPubStatusSourceType,\n        listId: String?,\n    ) {\n        if (listId.isNullOrEmpty()) {\n            statusDao.delete(locator, type)\n        } else {\n            statusDao.deleteListStatus(locator, type, listId)\n        }\n    }\n\n    private fun List<Status>.toDBEntities(\n        locator: PlatformLocator,\n        type: ActivityPubStatusSourceType,\n        listId: String?,\n    ): List<ActivityPubStatusTableEntity> {\n        return this.map {\n            it.toDBEntity(\n                locator = locator,\n                type = type,\n                listId = listId,\n            )\n        }\n    }\n\n    private fun Status.toDBEntity(\n        locator: PlatformLocator,\n        type: ActivityPubStatusSourceType,\n        listId: String?,\n    ): ActivityPubStatusTableEntity {\n        return ActivityPubStatusTableEntity(\n            id = this.id,\n            locator = locator,\n            type = type,\n            createTimestamp = this.createAt.epochMillis,\n            listId = listId.orEmpty(),\n            status = this,\n        )\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/repo/user/UserRepo.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.repo.user\n\nimport com.zhangke.framework.utils.WebFinger\nimport com.zhangke.fread.activitypub.app.internal.auth.ActivityPubClientManager\nimport com.zhangke.fread.activitypub.app.internal.model.UserUriInsights\nimport com.zhangke.fread.activitypub.app.internal.repo.WebFingerBaseUrlToUserIdRepo\nimport com.zhangke.fread.activitypub.app.internal.source.UserSourceTransformer\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.source.StatusSource\n\nclass UserRepo (\n    private val clientManager: ActivityPubClientManager,\n    private val webFingerBaseUrlToUserIdRepo: WebFingerBaseUrlToUserIdRepo,\n    private val userSourceTransformer: UserSourceTransformer,\n) {\n\n    suspend fun getUserSource(\n        locator: PlatformLocator,\n        userUriInsights: UserUriInsights,\n    ): Result<StatusSource> {\n        val userIdResult =\n            webFingerBaseUrlToUserIdRepo.getUserId(userUriInsights.webFinger, locator)\n        if (userIdResult.isFailure) return Result.failure(userIdResult.exceptionOrNull()!!)\n        val userId = userIdResult.getOrThrow()\n        return clientManager.getClient(locator)\n            .accountRepo\n            .getAccount(userId)\n            .map {\n                userSourceTransformer.createByUserEntity(it)\n            }\n    }\n\n    suspend fun lookupUserSource(\n        locator: PlatformLocator,\n        acct: String,\n    ): Result<StatusSource?> {\n        return clientManager.getClient(locator).accountRepo\n            .lookup(acct)\n            .onSuccess {\n                if (it != null) {\n                    val webFinger = WebFinger.create(it.acct)\n                    if (webFinger != null) {\n                        webFingerBaseUrlToUserIdRepo.insert(webFinger, locator, it.id)\n                    }\n                }\n            }\n            .map { it?.let { userSourceTransformer.createByUserEntity(it) } }\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/route/ActivityPubRoutes.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.route\n\nimport com.zhangke.framework.network.GlobalRoutes\n\nobject ActivityPubRoutes {\n\n    const val ROOT = \"${GlobalRoutes.ROOT_PREFIX}activitypub\"\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/account/EditAccountInfoScreen.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.account\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.BoxScope\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.imePadding\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Add\nimport androidx.compose.material.icons.filled.Check\nimport androidx.compose.material.icons.filled.Close\nimport androidx.compose.material.icons.filled.Delete\nimport androidx.compose.material.icons.filled.Edit\nimport androidx.compose.material3.CircularProgressIndicator\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.OutlinedTextField\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.SnackbarHost\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.alpha\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport androidx.navigation3.runtime.NavKey\nimport com.seiko.imageloader.ui.AutoSizeImage\nimport com.zhangke.framework.composable.ConsumeFlow\nimport com.zhangke.framework.composable.ConsumeSnackbarFlow\nimport com.zhangke.framework.composable.IconButtonStyle\nimport com.zhangke.framework.composable.SimpleIconButton\nimport com.zhangke.framework.composable.StyledIconButton\nimport com.zhangke.framework.composable.TextString\nimport com.zhangke.framework.composable.currentOrThrow\nimport com.zhangke.framework.composable.freadPlaceholder\nimport com.zhangke.framework.composable.pick.PickVisualMediaLauncherContainer\nimport com.zhangke.framework.composable.rememberSnackbarHostState\nimport com.zhangke.framework.nav.LocalNavBackStack\nimport com.zhangke.framework.utils.PlatformUri\nimport com.zhangke.fread.localization.LocalizedString\nimport kotlinx.coroutines.flow.SharedFlow\nimport kotlinx.serialization.Serializable\nimport org.jetbrains.compose.resources.stringResource\n\n@Serializable\ndata class EditAccountInfoScreenNavKey(\n    val baseUrl: String,\n    val accountUri: String,\n) : NavKey\n\n@Composable\nfun EditAccountInfoScreen(viewModel: EditAccountInfoViewModel) {\n    val backStack = LocalNavBackStack.currentOrThrow\n    val uiState by viewModel.uiState.collectAsState()\n    EditAccountInfoContent(\n        uiState = uiState,\n        snackBarMessageFlow = viewModel.snackBarMessageFlow,\n        onBackClick = backStack::removeLastOrNull,\n        onEditClick = viewModel::onEditClick,\n        onUserNameChanged = viewModel::onUserNameInput,\n        onBioChanged = viewModel::onUserDescriptionInput,\n        onFieldChanged = viewModel::onFieldInput,\n        onFieldDeleteClick = viewModel::onFieldDelete,\n        onFieldAddClick = viewModel::onFieldAddClick,\n        onAvatarSelected = viewModel::onAvatarSelected,\n        onHeaderSelected = viewModel::onHeaderSelected,\n    )\n    ConsumeFlow(viewModel.finishPageFlow) {\n        backStack.removeLastOrNull()\n    }\n}\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nprivate fun EditAccountInfoContent(\n    uiState: EditAccountUiState,\n    snackBarMessageFlow: SharedFlow<TextString>,\n    onBackClick: () -> Unit,\n    onEditClick: () -> Unit,\n    onUserNameChanged: (String) -> Unit,\n    onBioChanged: (String) -> Unit,\n    onFieldChanged: (Int, String, String) -> Unit,\n    onFieldDeleteClick: (Int) -> Unit,\n    onFieldAddClick: () -> Unit,\n    onAvatarSelected: (PlatformUri) -> Unit,\n    onHeaderSelected: (PlatformUri) -> Unit,\n) {\n    val snackBarHost = rememberSnackbarHostState()\n    ConsumeSnackbarFlow(snackBarHost, snackBarMessageFlow)\n    Scaffold(\n        snackbarHost = {\n            SnackbarHost(snackBarHost)\n        },\n        topBar = {\n            TopAppBar(\n                navigationIcon = {\n                    SimpleIconButton(\n                        onClick = onBackClick,\n                        imageVector = Icons.Default.Close,\n                        contentDescription = \"Close\",\n                    )\n                },\n                title = {\n                    Text(text = uiState.name)\n                },\n                actions = {\n                    if (uiState.requesting) {\n                        CircularProgressIndicator(\n                            modifier = Modifier\n                                .padding(end = 8.dp)\n                                .size(24.dp)\n                        )\n                    } else {\n                        SimpleIconButton(\n                            onClick = onEditClick,\n                            imageVector = Icons.Default.Check,\n                            contentDescription = \"Edit\",\n                        )\n                    }\n                }\n            )\n        }\n    ) { paddingValues ->\n        Column(\n            modifier = Modifier\n                .padding(paddingValues)\n                .padding(bottom = 30.dp)\n                .verticalScroll(rememberScrollState())\n                .imePadding(),\n        ) {\n            val headerHeight = 150.dp\n            val avatarSize = 80.dp\n            Box(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .height(headerHeight + avatarSize / 2)\n            ) {\n                HeaderInEdit(\n                    uiState = uiState,\n                    headerHeight = headerHeight,\n                    onHeaderSelected = onHeaderSelected,\n                )\n                AvatarInEdit(\n                    uiState = uiState,\n                    avatarSize = avatarSize,\n                    onAvatarSelected = onAvatarSelected,\n                )\n            }\n            OutlinedTextField(\n                modifier = Modifier\n                    .padding(start = 16.dp, top = 16.dp, end = 16.dp)\n                    .fillMaxWidth(),\n                value = uiState.name,\n                onValueChange = onUserNameChanged,\n                placeholder = {\n                    Text(\n                        modifier = Modifier.alpha(0.7F),\n                        text = stringResource(LocalizedString.editProfileInputNameHint)\n                    )\n                },\n                label = {\n                    Text(text = stringResource(LocalizedString.editProfileLabelName))\n                },\n            )\n            OutlinedTextField(\n                modifier = Modifier\n                    .padding(start = 16.dp, top = 16.dp, end = 16.dp)\n                    .fillMaxWidth(),\n                value = uiState.description,\n                onValueChange = onBioChanged,\n                placeholder = {\n                    Text(\n                        modifier = Modifier.alpha(0.7F),\n                        text = stringResource(LocalizedString.editProfileInputNoteHint),\n                    )\n                },\n                label = {\n                    Text(text = stringResource(LocalizedString.editProfileLabelNote))\n                },\n            )\n            AccountFieldListUi(\n                uiState = uiState,\n                onFieldChanged = onFieldChanged,\n                onFieldDeleteClick = onFieldDeleteClick,\n                onFieldAddClick = onFieldAddClick,\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun AccountFieldListUi(\n    uiState: EditAccountUiState,\n    onFieldChanged: (Int, String, String) -> Unit,\n    onFieldDeleteClick: (Int) -> Unit,\n    onFieldAddClick: () -> Unit,\n) {\n    Row(\n        modifier = Modifier\n            .fillMaxWidth()\n            .padding(start = 16.dp, top = 16.dp, end = 8.dp),\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        Text(\n            text = stringResource(LocalizedString.activity_pub_edit_account_info_label_about),\n            style = MaterialTheme.typography.headlineSmall,\n        )\n        Box(modifier = Modifier.weight(1F))\n        if (uiState.fieldAddable) {\n            StyledIconButton(\n                onClick = onFieldAddClick,\n                imageVector = Icons.Default.Add,\n                style = IconButtonStyle.STANDARD,\n                contentDescription = \"Add\",\n            )\n        }\n    }\n    Box(modifier = Modifier.height(6.dp))\n    uiState.fieldList.forEach { fieldUiState ->\n        Row(\n            modifier = Modifier\n                .fillMaxWidth()\n                .padding(start = 16.dp, top = 11.dp, end = 8.dp, bottom = 11.dp),\n        ) {\n            Column(\n                modifier = Modifier\n                    .weight(1F)\n                    .padding(end = 8.dp)\n            ) {\n\n                OutlinedTextField(\n                    modifier = Modifier.fillMaxWidth(),\n                    value = fieldUiState.name,\n                    onValueChange = {\n                        onFieldChanged(\n                            fieldUiState.idForUi,\n                            it,\n                            fieldUiState.value\n                        )\n                    },\n                )\n\n                OutlinedTextField(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .padding(top = 6.dp),\n                    value = fieldUiState.value,\n                    onValueChange = {\n                        onFieldChanged(\n                            fieldUiState.idForUi,\n                            fieldUiState.name,\n                            it\n                        )\n                    },\n                )\n            }\n\n            SimpleIconButton(\n                onClick = { onFieldDeleteClick(fieldUiState.idForUi) },\n                imageVector = Icons.Default.Delete,\n                contentDescription = \"Delete\",\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun HeaderInEdit(\n    uiState: EditAccountUiState,\n    headerHeight: Dp,\n    onHeaderSelected: (PlatformUri) -> Unit,\n) {\n    PickVisualMediaLauncherContainer(\n        onResult = { it.firstOrNull()?.let(onHeaderSelected) },\n    ) {\n        AutoSizeImage(\n            uiState.header,\n            modifier = Modifier\n                .height(headerHeight)\n                .fillMaxWidth()\n                .freadPlaceholder(uiState.header.isEmpty())\n                .clickable {\n                    launchImage()\n                },\n            contentScale = ContentScale.Crop,\n            contentDescription = null,\n        )\n    }\n}\n\n@Composable\nprivate fun BoxScope.AvatarInEdit(\n    uiState: EditAccountUiState,\n    avatarSize: Dp,\n    onAvatarSelected: (PlatformUri) -> Unit,\n) {\n    Box(\n        modifier = Modifier\n            .align(Alignment.BottomStart)\n            .padding(start = 16.dp)\n            .size(avatarSize)\n            .clip(CircleShape)\n            .border(2.dp, Color.White, CircleShape)\n            .freadPlaceholder(uiState.avatar.isEmpty())\n    ) {\n        AutoSizeImage(\n            uiState.avatar,\n            modifier = Modifier.fillMaxSize(),\n            contentScale = ContentScale.Crop,\n            contentDescription = \"avatar\",\n        )\n        PickVisualMediaLauncherContainer(\n            onResult = {\n                it.firstOrNull()?.let(onAvatarSelected)\n            },\n        ) {\n            SimpleIconButton(\n                modifier = Modifier\n                    .fillMaxSize()\n                    .background(Color.Black.copy(alpha = 0.5F))\n                    .padding(10.dp),\n                onClick = {\n                    launchImage()\n                },\n                imageVector = Icons.Default.Edit,\n                tint = Color.White,\n                contentDescription = \"Edit Avatar\",\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/account/EditAccountInfoViewModel.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.account\n\nimport androidx.lifecycle.ViewModel\nimport com.zhangke.activitypub.entities.ActivityPubAccountEntity\nimport com.zhangke.activitypub.entities.UpdateFieldRequestEntity\nimport com.zhangke.framework.composable.TextString\nimport com.zhangke.framework.composable.textOf\nimport com.zhangke.framework.ktx.launchInViewModel\nimport com.zhangke.framework.network.FormalBaseUrl\nimport com.zhangke.framework.utils.PlatformUri\nimport com.zhangke.framework.utils.toPlatformUri\nimport com.zhangke.fread.activitypub.app.internal.auth.ActivityPubClientManager\nimport com.zhangke.fread.common.utils.PlatformUriHelper\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.uri.FormalUri\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.SharedFlow\nimport kotlinx.coroutines.flow.asSharedFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.update\nclass EditAccountInfoViewModel(\n    private val clientManager: ActivityPubClientManager,\n    private val platformUriHelper: PlatformUriHelper,\n    private val baseUrl: FormalBaseUrl,\n    private val accountUri: FormalUri,\n) : ViewModel() {\n\n    companion object {\n\n        const val FIELD_MAX_COUNT = 4\n    }\n\n    private val _uiState = MutableStateFlow(\n        EditAccountUiState(\n            name = \"\",\n            header = \"\",\n            avatar = \"\",\n            description = \"\",\n            fieldList = emptyList(),\n            fieldAddable = false,\n            requesting = false,\n        )\n    )\n    val uiState = _uiState.asStateFlow()\n\n    private val _snackBarMessageFlow = MutableSharedFlow<TextString>()\n    val snackBarMessageFlow: SharedFlow<TextString> = _snackBarMessageFlow.asSharedFlow()\n\n    private val _finishPageFlow = MutableSharedFlow<Unit>()\n    val finishPageFlow = _finishPageFlow.asSharedFlow()\n\n    private var originalAccountInfo: ActivityPubAccountEntity? = null\n\n    private val locator = PlatformLocator(accountUri = accountUri, baseUrl = baseUrl)\n\n    init {\n        launchInViewModel {\n            clientManager.getClient(locator)\n                .accountRepo\n                .getCredentialAccount()\n                .onSuccess {\n                    updateUiStateByEntity(it)\n                    originalAccountInfo = it\n                }.onFailure { e ->\n                    e.message?.let { textOf(it) }?.let { _snackBarMessageFlow.emit(it) }\n                }\n        }\n    }\n\n    fun onUserNameInput(name: String) {\n        _uiState.update { it.copy(name = name) }\n    }\n\n    fun onUserDescriptionInput(description: String) {\n        _uiState.update { it.copy(description = description) }\n    }\n\n    fun onFieldInput(idForUi: Int, name: String, value: String) {\n        _uiState.update { currentState ->\n            val fieldList = currentState.fieldList\n            currentState.copy(\n                fieldList = fieldList.map {\n                    if (it.idForUi == idForUi) {\n                        it.copy(name = name, value = value)\n                    } else {\n                        it\n                    }\n                }\n            )\n        }\n    }\n\n    fun onFieldDelete(idForUi: Int) {\n        _uiState.update { currentState ->\n            val newFieldList = currentState.fieldList\n                .filter { it.idForUi != idForUi }\n            currentState.copy(\n                fieldList = newFieldList,\n                fieldAddable = newFieldList.size < FIELD_MAX_COUNT,\n            )\n        }\n    }\n\n    fun onFieldAddClick() {\n        _uiState.update { currentState ->\n            val newFieldList = currentState.fieldList.toMutableList()\n            newFieldList.add(\n                EditAccountFieldUiState(\n                    idForUi = newFieldList.size,\n                    name = \"\",\n                    value = \"\",\n                )\n            )\n            currentState.copy(\n                fieldList = newFieldList,\n                fieldAddable = newFieldList.size < FIELD_MAX_COUNT,\n            )\n        }\n    }\n\n    fun onAvatarSelected(uri: PlatformUri) {\n        _uiState.update { it.copy(avatar = uri.toString()) }\n    }\n\n    fun onHeaderSelected(uri: PlatformUri) {\n        _uiState.update { it.copy(header = uri.toString()) }\n    }\n\n    fun onEditClick() = launchInViewModel {\n        val originalAccountInfo = originalAccountInfo ?: return@launchInViewModel\n        val currentUiState = _uiState.value\n        if (currentUiState.name.isBlank()) {\n            _snackBarMessageFlow.emit(textOf(LocalizedString.editProfileNameEmpty))\n            return@launchInViewModel\n        }\n        val newName = currentUiState.name.takeIf { it != originalAccountInfo.nameOfUiState }\n        val newNote = currentUiState.description.takeIf { it != originalAccountInfo.noteOfUiState }\n        val newAvatar = currentUiState.avatar\n            .takeIf { it != originalAccountInfo.avatar }\n            ?.toPlatformUri()\n            ?.let { platformUriHelper.read(it) }\n        val newHeader = currentUiState.header\n            .takeIf { it != originalAccountInfo.header }\n            ?.toPlatformUri()\n            ?.let { platformUriHelper.read(it) }\n        val newFieldList =\n            currentUiState.fieldList.takeIf { !it.compare(originalAccountInfo.fieldOfUiState) }\n        if (newName == null &&\n            newNote == null &&\n            newAvatar == null &&\n            newHeader == null &&\n            newFieldList == null\n        ) {\n            return@launchInViewModel\n        }\n        _uiState.update { it.copy(requesting = true) }\n        clientManager.getClient(locator)\n            .accountRepo\n            .updateCredentials(\n                name = newName,\n                note = newNote,\n                avatarFileName = newAvatar?.fileName,\n                avatarByteArray = newAvatar?.readBytes(),\n                headerFileName = newHeader?.fileName,\n                headerByteArray = newHeader?.readBytes(),\n                fieldList = newFieldList?.map { it.toUpdateFieldRequestEntity() },\n            ).onSuccess {\n                updateUiStateByEntity(it)\n                _finishPageFlow.emit(Unit)\n            }.onFailure { e ->\n                e.message?.let { textOf(it) }?.let { _snackBarMessageFlow.emit(it) }\n                _uiState.update { it.copy(requesting = false) }\n            }\n    }\n\n    private fun updateUiStateByEntity(entity: ActivityPubAccountEntity) {\n        originalAccountInfo = entity\n        _uiState.update { currentState ->\n            val fieldList = entity.fieldOfUiState\n            currentState.copy(\n                name = entity.nameOfUiState,\n                header = entity.headerOfUiState,\n                avatar = entity.avatarOfUiState,\n                description = entity.noteOfUiState,\n                fieldList = fieldList,\n                fieldAddable = fieldList.size < FIELD_MAX_COUNT,\n                requesting = false,\n            )\n        }\n    }\n\n    private val ActivityPubAccountEntity.nameOfUiState: String get() = displayName\n\n    private val ActivityPubAccountEntity.headerOfUiState: String get() = header\n\n    private val ActivityPubAccountEntity.avatarOfUiState: String get() = avatar\n\n    private val ActivityPubAccountEntity.noteOfUiState: String get() = source?.note.orEmpty()\n\n    private val ActivityPubAccountEntity.fieldOfUiState: List<EditAccountFieldUiState>\n        get() = source?.fields?.mapIndexed { index, field ->\n            EditAccountFieldUiState(\n                idForUi = index,\n                name = field.name,\n                value = field.value\n            )\n        } ?: emptyList()\n\n    /**\n     * Compare theos two list is same or not\n     */\n    private fun List<EditAccountFieldUiState>.compare(\n        original: List<EditAccountFieldUiState>\n    ): Boolean {\n        if (size != original.size) {\n            return false\n        }\n        this.forEach { item ->\n            val originalState = original.firstOrNull { it.idForUi == item.idForUi }\n            if (originalState == null || originalState.name != item.name || originalState.value != item.value) {\n                return false\n            }\n        }\n        return true\n    }\n\n    private fun EditAccountFieldUiState.toUpdateFieldRequestEntity() = UpdateFieldRequestEntity(\n        name = name,\n        value = value\n    )\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/account/EditAccountUiState.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.account\n\ndata class EditAccountUiState(\n    val name: String,\n    val header: String,\n    val avatar: String,\n    val description: String,\n    val fieldList: List<EditAccountFieldUiState>,\n    val fieldAddable: Boolean,\n    val requesting: Boolean,\n)\n\ndata class EditAccountFieldUiState(\n    val idForUi: Int,\n    val name: String,\n    val value: String,\n)\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/add/AddActivityPubContentScreen.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.add\n\nimport androidx.compose.animation.core.Animatable\nimport androidx.compose.animation.core.EaseOutBack\nimport androidx.compose.animation.core.tween\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.scale\nimport androidx.compose.ui.unit.dp\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.composable.Toolbar\nimport com.zhangke.framework.composable.currentOrThrow\nimport com.zhangke.framework.nav.LocalNavBackStack\nimport com.zhangke.fread.commonbiz.Res\nimport com.zhangke.fread.commonbiz.emoji_celebrate\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.platform.BlogPlatform\nimport com.zhangke.fread.status.ui.source.BlogPlatformCard\nimport kotlinx.coroutines.delay\nimport kotlinx.serialization.Serializable\nimport org.jetbrains.compose.resources.painterResource\nimport org.jetbrains.compose.resources.stringResource\n\n@Serializable\ndata class AddActivityPubContentScreenKey(val platform: BlogPlatform) : NavKey\n\n@Composable\nfun AddActivityPubContentScreen(\n    platform: BlogPlatform,\n    viewModel: AddActivityPubContentViewModel,\n) {\n    val backStack = LocalNavBackStack.currentOrThrow\n    AddActivityPubContentContent(\n        platform = platform,\n        onBackClick = { backStack.removeLastOrNull() },\n        onLoginClick = {\n            viewModel.onLoginClick()\n            backStack.removeLastOrNull()\n        },\n    )\n}\n\n@Composable\nprivate fun AddActivityPubContentContent(\n    platform: BlogPlatform,\n    onBackClick: () -> Unit,\n    onLoginClick: () -> Unit,\n) {\n    Scaffold(\n        topBar = {\n            Toolbar(\n                title = stringResource(LocalizedString.addContentTitle),\n                onBackClick = onBackClick,\n            )\n        },\n    ) { innerPadding ->\n        Column(\n            modifier = Modifier.fillMaxSize()\n                .padding(innerPadding)\n                .padding(top = 32.dp)\n                .padding(horizontal = 16.dp),\n        ) {\n            ContentAddingState(Modifier.align(Alignment.CenterHorizontally).fillMaxWidth())\n            Spacer(modifier = Modifier.height(16.dp))\n            PlatformPreview(\n                modifier = Modifier\n                    .align(Alignment.CenterHorizontally)\n                    .fillMaxWidth(),\n                platform = platform,\n                onLoginClick = onLoginClick,\n            )\n            Spacer(modifier = Modifier.padding(top = 16.dp))\n        }\n    }\n}\n\n@Composable\nprivate fun ContentAddingState(modifier: Modifier) {\n    Column(\n        modifier = modifier,\n        horizontalAlignment = Alignment.CenterHorizontally,\n    ) {\n        val scale = remember { Animatable(0.1F) }\n        LaunchedEffect(Unit) {\n            delay(100)\n            scale.animateTo(\n                targetValue = 1f,\n                animationSpec = tween(\n                    durationMillis = 600,\n                    easing = EaseOutBack,\n                )\n            )\n        }\n        Image(\n            modifier = Modifier.size(68.dp)\n                .scale(scale.value),\n            painter = painterResource(Res.drawable.emoji_celebrate),\n            contentDescription = null,\n        )\n        Text(\n            text = stringResource(LocalizedString.contentAddSuccess),\n            modifier = Modifier.padding(top = 8.dp),\n        )\n    }\n}\n\n@Composable\nprivate fun PlatformPreview(\n    modifier: Modifier,\n    platform: BlogPlatform,\n    onLoginClick: () -> Unit,\n) {\n    BlogPlatformCard(\n        modifier = modifier,\n        platform = platform,\n        onLoginClick = onLoginClick,\n    )\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/add/AddActivityPubContentViewModel.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.add\n\nimport androidx.lifecycle.ViewModel\nimport com.zhangke.framework.collections.container\nimport com.zhangke.framework.ktx.launchInViewModel\nimport com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubContentAdapter\nimport com.zhangke.fread.activitypub.app.internal.auth.ActivityPubOAuthor\nimport com.zhangke.fread.activitypub.app.internal.content.ActivityPubContent\nimport com.zhangke.fread.common.content.FreadContentRepo\nimport com.zhangke.fread.common.onboarding.OnboardingComponent\nimport com.zhangke.fread.status.platform.BlogPlatform\nclass AddActivityPubContentViewModel (\n    private val contentRepo: FreadContentRepo,\n    private val oAuthor: ActivityPubOAuthor,\n    private val contentAdapter: ActivityPubContentAdapter,\n    private val onboardingComponent: OnboardingComponent,\n    private val platform: BlogPlatform,\n) : ViewModel() {\n\n    init {\n        launchInViewModel {\n            val content = contentAdapter.createContent(\n                platform = platform,\n                maxOrder = contentRepo.getMaxOrder(),\n            )\n            val allContent = contentRepo.getAllContent().filterIsInstance<ActivityPubContent>()\n            if (!allContent.container { it.id == content.id }) {\n                contentRepo.insertContent(content)\n            }\n        }\n    }\n\n    fun onLoginClick() {\n        launchInViewModel {\n            onboardingComponent.onboardingSuccess()\n        }\n        oAuthor.startOauth(platform.baseUrl)\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/add/select/SelectPlatformScreen.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.add.select\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.foundation.text.KeyboardActions\nimport androidx.compose.foundation.text.KeyboardOptions\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Search\nimport androidx.compose.material3.CircularProgressIndicator\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.OutlinedTextField\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.SnackbarHost\nimport androidx.compose.material3.SnackbarHostState\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.text.input.ImeAction\nimport androidx.compose.ui.unit.dp\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.composable.ConsumeFlow\nimport com.zhangke.framework.composable.ConsumeOpenScreenFlow\nimport com.zhangke.framework.composable.ConsumeSnackbarFlow\nimport com.zhangke.framework.composable.LoadingDialog\nimport com.zhangke.framework.composable.SimpleIconButton\nimport com.zhangke.framework.composable.Toolbar\nimport com.zhangke.framework.composable.currentOrThrow\nimport com.zhangke.framework.composable.rememberSnackbarHostState\nimport com.zhangke.framework.nav.LocalNavBackStack\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.ui.source.BlogPlatformSnapshotUi\nimport com.zhangke.fread.status.ui.source.BlogPlatformUi\nimport kotlinx.serialization.Serializable\nimport org.jetbrains.compose.resources.stringResource\n\n@Serializable\nobject SelectPlatformScreenKey : NavKey\n\n@Composable\nfun SelectPlatformScreen(viewModel: SelectPlatformViewModel) {\n    val backStack = LocalNavBackStack.currentOrThrow\n    val uiState by viewModel.uiState.collectAsState()\n    val snackbarHostState = rememberSnackbarHostState()\n    SelectPlatformContent(\n        uiState = uiState,\n        snackbarHostState = snackbarHostState,\n        onBackClick = { backStack.removeLastOrNull() },\n        onQueryChanged = viewModel::onQueryChanged,\n        onSearchClick = viewModel::onSearchClick,\n        onPlatformClick = viewModel::onResultClick,\n    )\n    ConsumeOpenScreenFlow(viewModel.openNewPageFlow)\n    ConsumeSnackbarFlow(snackbarHostState, viewModel.snackBarMessage)\n    ConsumeFlow(viewModel.finishPageFlow) { backStack.removeLastOrNull() }\n    LoadingDialog(\n        loading = uiState.loadingPlatformForAdd,\n        onDismissRequest = viewModel::onLoadingPlatformForAddCancel,\n    )\n    LaunchedEffect(Unit) { viewModel.onPageResumed(this) }\n}\n\n@Composable\nprivate fun SelectPlatformContent(\n    uiState: SelectPlatformUiState,\n    snackbarHostState: SnackbarHostState,\n    onBackClick: () -> Unit,\n    onQueryChanged: (String) -> Unit,\n    onSearchClick: () -> Unit,\n    onPlatformClick: (SearchPlatformResult) -> Unit,\n) {\n    Scaffold(\n        modifier = Modifier.fillMaxSize(),\n        topBar = {\n            Toolbar(\n                title = stringResource(LocalizedString.activity_pub_select_platform_title),\n                onBackClick = onBackClick,\n            )\n        },\n        snackbarHost = {\n            SnackbarHost(snackbarHostState)\n        },\n    ) { innerPadding ->\n        Column(\n            modifier = Modifier.fillMaxSize()\n                .padding(innerPadding),\n        ) {\n            OutlinedTextField(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .padding(start = 16.dp, top = 18.dp, end = 16.dp),\n                value = uiState.query,\n                onValueChange = onQueryChanged,\n                maxLines = 1,\n                keyboardOptions = KeyboardOptions.Default.copy(\n                    imeAction = ImeAction.Search\n                ),\n                placeholder = {\n                    Text(\n                        text = stringResource(LocalizedString.activity_pub_select_platform_text_hint),\n                        style = MaterialTheme.typography.bodyMedium,\n                    )\n                },\n                keyboardActions = KeyboardActions(\n                    onSearch = { onSearchClick() }\n                ),\n                trailingIcon = {\n                    if (uiState.querying) {\n                        CircularProgressIndicator(\n                            modifier = Modifier.size(18.dp),\n                            strokeWidth = 2.dp,\n                            color = MaterialTheme.colorScheme.primary,\n                        )\n                    } else {\n                        SimpleIconButton(\n                            onClick = onSearchClick,\n                            imageVector = Icons.Default.Search,\n                            contentDescription = \"Search\",\n                        )\n                    }\n                },\n            )\n\n            Spacer(modifier = Modifier.height(16.dp))\n\n            if (uiState.searchedResult.isNotEmpty()) {\n                LazyColumn(\n                    modifier = Modifier.fillMaxSize(),\n                ) {\n                    items(uiState.searchedResult) {\n                        SearchPlatformResultUi(\n                            result = it,\n                            onClick = onPlatformClick,\n                        )\n                    }\n                }\n            } else {\n                LazyColumn(\n                    modifier = Modifier.fillMaxSize(),\n                ) {\n                    items(uiState.platformSnapshotList) {\n                        SearchPlatformResultUi(\n                            result = it,\n                            onClick = onPlatformClick,\n                        )\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun SearchPlatformResultUi(\n    result: SearchPlatformResult,\n    onClick: (SearchPlatformResult) -> Unit,\n) {\n    when (result) {\n        is SearchPlatformResult.SearchedPlatform -> BlogPlatformUi(\n            modifier = Modifier\n                .fillMaxWidth()\n                .clickable { onClick(result) },\n            platform = result.platform,\n        )\n\n        is SearchPlatformResult.SearchedSnapshot -> BlogPlatformSnapshotUi(\n            modifier = Modifier\n                .fillMaxWidth()\n                .clickable { onClick(result) },\n            platform = result.snapshot,\n        )\n    }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/add/select/SelectPlatformUiState.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.add.select\n\nimport com.zhangke.fread.status.platform.BlogPlatform\nimport com.zhangke.fread.status.platform.PlatformSnapshot\n\ndata class SelectPlatformUiState(\n    val query: String,\n    val platformSnapshotList: List<SearchPlatformResult>,\n    val querying: Boolean,\n    val searchedResult: List<SearchPlatformResult>,\n    val loadingPlatformForAdd: Boolean,\n) {\n\n    companion object {\n\n        fun default(): SelectPlatformUiState {\n            return SelectPlatformUiState(\n                query = \"\",\n                platformSnapshotList = emptyList(),\n                querying = false,\n                searchedResult = emptyList(),\n                loadingPlatformForAdd = false,\n            )\n        }\n    }\n}\n\nsealed interface SearchPlatformResult {\n\n    data class SearchedSnapshot(val snapshot: PlatformSnapshot) : SearchPlatformResult\n\n    data class SearchedPlatform(val platform: BlogPlatform) : SearchPlatformResult\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/add/select/SelectPlatformViewModel.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.add.select\n\nimport androidx.lifecycle.ViewModel\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.composable.TextString\nimport com.zhangke.framework.composable.emitTextMessageFromThrowable\nimport com.zhangke.framework.composable.textOf\nimport com.zhangke.framework.coroutines.invokeOnCancel\nimport com.zhangke.framework.ktx.launchInViewModel\nimport com.zhangke.framework.network.FormalBaseUrl\nimport com.zhangke.fread.activitypub.app.internal.repo.platform.ActivityPubPlatformRepo\nimport com.zhangke.fread.activitypub.app.internal.screen.add.AddActivityPubContentScreenKey\nimport com.zhangke.fread.common.onboarding.OnboardingComponent\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asSharedFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.update\nimport kotlinx.coroutines.launch\n\nclass SelectPlatformViewModel (\n    private val platformRepo: ActivityPubPlatformRepo,\n    private val onboardingComponent: OnboardingComponent,\n) : ViewModel() {\n\n    private val _uiState = MutableStateFlow(SelectPlatformUiState.default())\n    val uiState = _uiState.asStateFlow()\n\n    private val _openNewPageFlow = MutableSharedFlow<NavKey>()\n    val openNewPageFlow = _openNewPageFlow.asSharedFlow()\n\n    private val _finishPageFlow = MutableSharedFlow<Unit>()\n    val finishPageFlow = _finishPageFlow.asSharedFlow()\n\n    private val _snackBarMessage = MutableSharedFlow<TextString>()\n    val snackBarMessage = _snackBarMessage.asSharedFlow()\n\n    private var queryJob: Job? = null\n    private var loadingPlatformForAddJob: Job? = null\n\n    init {\n        onboardingComponent.clearState()\n        launchInViewModel {\n            platformRepo.getSuggestedPlatformSnapshotList()\n                .map { SearchPlatformResult.SearchedSnapshot(it) }\n                .let { snapshots ->\n                    _uiState.update { it.copy(platformSnapshotList = snapshots) }\n                }\n        }\n    }\n\n    fun onPageResumed(uiScope: CoroutineScope) {\n        uiScope.launch {\n            onboardingComponent.onboardingFinishedFlow.collect {\n                _finishPageFlow.emit(Unit)\n            }\n        }\n    }\n\n    fun onSearchClick() {\n        doSearch(_uiState.value.query)\n    }\n\n    fun onQueryChanged(query: String) {\n        if (query == _uiState.value.query) return\n        _uiState.update { it.copy(query = query) }\n        if (query.isEmpty()) {\n            if (queryJob?.isActive == true) queryJob?.cancel()\n            _uiState.update { it.copy(searchedResult = emptyList(), querying = false) }\n            return\n        }\n        doSearch(query)\n    }\n\n    private fun doSearch(query: String) {\n        if (queryJob?.isActive == true) queryJob?.cancel()\n        queryJob = launchInViewModel {\n            _uiState.update { it.copy(querying = true) }\n            val localResult = platformRepo.searchPlatformSnapshotFromLocal(query)\n                .map { SearchPlatformResult.SearchedSnapshot(it) }\n            _uiState.update { it.copy(searchedResult = localResult) }\n            val platformAsUrl = FormalBaseUrl.parse(query)\n                ?.let { platformRepo.getPlatform(it).getOrNull() }\n                ?.let { SearchPlatformResult.SearchedPlatform(it) }\n            if (platformAsUrl != null) {\n                _uiState.update {\n                    val newList = (it.searchedResult + platformAsUrl).distinctByDomain()\n                    it.copy(searchedResult = newList)\n                }\n            }\n            platformRepo.searchPlatformFromServer(query)\n                .map { list -> list.map { SearchPlatformResult.SearchedSnapshot(it) } }\n                .onSuccess { result ->\n                    _uiState.update {\n                        it.copy(searchedResult = (it.searchedResult + result).distinctByDomain())\n                    }\n                }\n            _uiState.update { it.copy(querying = false) }\n        }\n        queryJob?.invokeOnCancel {\n            _uiState.update { it.copy(querying = false) }\n        }\n    }\n\n    private fun List<SearchPlatformResult>.distinctByDomain(): List<SearchPlatformResult> {\n        return distinctBy { result ->\n            when (result) {\n                is SearchPlatformResult.SearchedPlatform -> result.platform.baseUrl.host\n                is SearchPlatformResult.SearchedSnapshot -> result.snapshot.domain\n            }\n        }\n    }\n\n    fun onResultClick(result: SearchPlatformResult) {\n        when (result) {\n            is SearchPlatformResult.SearchedPlatform -> {\n                launchInViewModel {\n                    _openNewPageFlow.emit(AddActivityPubContentScreenKey(result.platform))\n                }\n            }\n\n            is SearchPlatformResult.SearchedSnapshot -> {\n                if (loadingPlatformForAddJob?.isActive == true) {\n                    loadingPlatformForAddJob?.cancel()\n                }\n                loadingPlatformForAddJob = launchInViewModel {\n                    val baseUrl = FormalBaseUrl.parse(result.snapshot.domain)\n                    if (baseUrl == null) {\n                        _snackBarMessage.emit(textOf(\"Invalid platform domain: ${result.snapshot.domain}\"))\n                        return@launchInViewModel\n                    }\n                    _uiState.update { it.copy(loadingPlatformForAdd = true) }\n                    platformRepo.getPlatform(baseUrl)\n                        .onSuccess { platform ->\n                            _uiState.update { it.copy(loadingPlatformForAdd = false) }\n                            _openNewPageFlow.emit(AddActivityPubContentScreenKey(platform))\n                        }.onFailure { t ->\n                            _uiState.update { it.copy(loadingPlatformForAdd = false) }\n                            _snackBarMessage.emitTextMessageFromThrowable(t)\n                        }\n                }\n            }\n        }\n    }\n\n    fun onLoadingPlatformForAddCancel() {\n        loadingPlatformForAddJob?.cancel()\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/content/ActivityPubContentSubViewModel.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.content\n\nimport com.zhangke.framework.ktx.launchInViewModel\nimport com.zhangke.framework.lifecycle.SubViewModel\nimport com.zhangke.fread.activitypub.app.ActivityPubAccountManager\nimport com.zhangke.fread.activitypub.app.internal.content.ActivityPubContent\nimport com.zhangke.fread.activitypub.app.internal.usecase.UpdateActivityPubUserListUseCase\nimport com.zhangke.fread.activitypub.app.internal.usecase.content.GetUserCreatedListUseCase\nimport com.zhangke.fread.activitypub.app.internal.utils.createPlatformLocator\nimport com.zhangke.fread.common.config.FreadConfigManager\nimport com.zhangke.fread.common.content.FreadContentRepo\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.distinctUntilChanged\nimport kotlinx.coroutines.flow.map\nimport kotlinx.coroutines.flow.update\n\nclass ActivityPubContentSubViewModel(\n    private val contentRepo: FreadContentRepo,\n    private val getUserCreatedList: GetUserCreatedListUseCase,\n    private val accountManager: ActivityPubAccountManager,\n    private val freadConfigManager: FreadConfigManager,\n    private val updateActivityPubUserList: UpdateActivityPubUserListUseCase,\n    val contentId: String,\n) : SubViewModel() {\n\n    private val _uiState = MutableStateFlow(ActivityPubContentUiState.default())\n    val uiState = _uiState.asStateFlow()\n\n    private var updateUserListJob: Job? = null\n    private var userCreatedListUpdated = false\n    private var observeAccountJob: Job? = null\n\n    init {\n        launchInViewModel {\n            contentRepo.getContentFlow(contentId)\n                .distinctUntilChanged()\n                .map { it as? ActivityPubContent }\n                .collect { contentConfig ->\n                    if (contentConfig != null) {\n                        val locator = createPlatformLocator(contentConfig)\n                        _uiState.update {\n                            it.copy(locator = locator, config = contentConfig)\n                        }\n                        startObserveAccount(contentConfig)\n                        updateUserCreateList()\n                    } else {\n                        _uiState.update {\n                            it.copy(\n                                errorMessage = \"Cant find validate config by id: $contentId\"\n                            )\n                        }\n                    }\n                }\n        }\n        launchInViewModel {\n            freadConfigManager.homeTabRefreshButtonVisibleFlow\n                .collect { visible ->\n                    _uiState.update { it.copy(showRefreshButton = visible) }\n                }\n        }\n        launchInViewModel {\n            freadConfigManager.homeTabNextButtonVisibleFlow\n                .collect { visible ->\n                    _uiState.update { it.copy(showNextButton = visible) }\n                }\n        }\n    }\n\n    private fun startObserveAccount(content: ActivityPubContent) {\n        observeAccountJob?.cancel()\n        val accountUri = content.accountUri\n        if (accountUri == null) {\n            _uiState.update { it.copy(account = null) }\n            return\n        }\n        observeAccountJob = launchInViewModel {\n            accountManager.observeAccount(accountUri)\n                .distinctUntilChanged()\n                .collect { account ->\n                    val accountCountInSamePlatform = if (account == null) {\n                        0\n                    } else {\n                        accountManager.getAllLoggedAccount().count { it.baseUrl == account.baseUrl }\n                    }\n                    _uiState.update {\n                        it.copy(\n                            account = account,\n                            showAccountInTopBar = accountCountInSamePlatform > 1\n                        )\n                    }\n                    userCreatedListUpdated = false\n                    updateUserCreateList()\n                }\n        }\n    }\n\n    private fun updateUserCreateList() {\n        if (userCreatedListUpdated) return\n        userCreatedListUpdated = true\n        updateUserListJob?.cancel()\n        val locator = _uiState.value.locator ?: return\n        updateUserListJob = launchInViewModel {\n            getUserCreatedList(locator)\n                .map { list ->\n                    list.map {\n                        // 此处的 order 并不会使用，repo 内部会重新计算，因此次数放一个较大的值填充即可。\n                        ActivityPubContent.ContentTab.ListTimeline(\n                            listId = it.id,\n                            name = it.title,\n                            order = 1000,\n                        )\n                    }\n                }\n                .onSuccess { list ->\n                    _uiState.value.config?.let { updateActivityPubUserList(it, list) }\n                }\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/content/ActivityPubContentTab.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.content\n\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.navigationBarsPadding\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.pager.HorizontalPager\nimport androidx.compose.foundation.pager.rememberPagerState\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.SnackbarHost\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBarDefaults\nimport androidx.compose.material3.rememberTopAppBarState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.input.nestedscroll.nestedScroll\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.unit.dp\nimport com.zhangke.framework.composable.LocalContentPadding\nimport com.zhangke.framework.composable.LocalSnackbarHostState\nimport com.zhangke.framework.composable.contentBottomPadding\nimport com.zhangke.framework.composable.currentOrThrow\nimport com.zhangke.framework.composable.plusContentPadding\nimport com.zhangke.framework.composable.rememberSnackbarHostState\nimport com.zhangke.framework.nav.BaseTab\nimport com.zhangke.framework.nav.LocalNavBackStack\nimport com.zhangke.framework.nav.Tab\nimport com.zhangke.framework.nav.TabOptions\nimport com.zhangke.fread.activitypub.app.internal.content.ActivityPubContent\nimport com.zhangke.fread.activitypub.app.internal.model.ActivityPubLoggedAccount\nimport com.zhangke.fread.activitypub.app.internal.model.ActivityPubStatusSourceType\nimport com.zhangke.fread.activitypub.app.internal.screen.content.timeline.ActivityPubTimelineTab\nimport com.zhangke.fread.activitypub.app.internal.screen.instance.InstanceDetailScreenKey\nimport com.zhangke.fread.activitypub.app.internal.screen.status.post.PostStatusScreenKey\nimport com.zhangke.fread.activitypub.app.internal.screen.trending.TrendingStatusTab\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.ui.common.HomeContentTabsTopBar\nimport com.zhangke.fread.status.ui.common.LocalNestedTabConnection\nimport com.zhangke.fread.status.ui.common.PublishingFab\nimport com.zhangke.fread.status.ui.style.LocalStatusUiConfig\nimport kotlinx.coroutines.launch\nimport org.koin.compose.viewmodel.koinViewModel\n\ninternal class ActivityPubContentTab(\n    private val configId: String,\n    private val isLatestContent: Boolean,\n) : BaseTab() {\n\n    override val options: TabOptions?\n        @Composable get() = null\n\n    @Composable\n    override fun Content() {\n        super.Content()\n        val navBackStack = LocalNavBackStack.currentOrThrow\n        val viewModel = koinViewModel<ActivityPubContentViewModel>().getSubViewModel(configId)\n        val uiState by viewModel.uiState.collectAsState()\n        ActivityPubContentUi(\n            uiState = uiState,\n            onTitleClick = { content ->\n                uiState.locator?.let {\n                    navBackStack.add(InstanceDetailScreenKey(it, content.baseUrl))\n                }\n            },\n            onPostBlogClick = {\n                navBackStack.add(PostStatusScreenKey(accountUri = it.uri))\n            },\n        )\n    }\n\n    @OptIn(ExperimentalMaterial3Api::class)\n    @Composable\n    private fun ActivityPubContentUi(\n        uiState: ActivityPubContentUiState,\n        onTitleClick: (ActivityPubContent) -> Unit,\n        onPostBlogClick: (ActivityPubLoggedAccount) -> Unit,\n    ) {\n        val coroutineScope = rememberCoroutineScope()\n        val mainTabConnection = LocalNestedTabConnection.current\n        val snackBarHostState = rememberSnackbarHostState()\n        val showFb = uiState.account != null\n        val tabList = remember(uiState.locator, uiState.config) {\n            if (uiState.locator != null && uiState.config != null) {\n                createTabs(uiState.locator, uiState.config)\n            } else {\n                emptyList()\n            }\n        }\n        val tabTitles = tabList.map { it.options?.title.orEmpty() }\n        val pagerState = rememberPagerState(0) { tabList.size }\n        val topBarState = rememberTopAppBarState()\n        val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(topBarState)\n        LaunchedEffect(mainTabConnection, topBarState) {\n            mainTabConnection.scrollToTopFlow.collect {\n                topBarState.contentOffset = 0F\n                topBarState.heightOffset = 0F\n            }\n        }\n        Scaffold(\n            modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),\n            topBar = {\n                if (uiState.config != null && tabList.isNotEmpty()) {\n                    HomeContentTabsTopBar(\n                        title = uiState.config.name,\n                        account = uiState.account,\n                        showAccountInfo = uiState.showAccountInTopBar,\n                        selectedTabIndex = pagerState.currentPage,\n                        tabTitles = tabTitles,\n                        scrollBehavior = scrollBehavior,\n                        showNextIcon = !isLatestContent && uiState.showNextButton,\n                        showRefreshButton = uiState.showRefreshButton,\n                        onMenuClick = {\n                            coroutineScope.launch {\n                                mainTabConnection.openDrawer()\n                            }\n                        },\n                        onRefreshClick = {\n                            coroutineScope.launch {\n                                mainTabConnection.scrollToTop()\n                                mainTabConnection.refresh()\n                            }\n                        },\n                        onNextClick = {\n                            coroutineScope.launch {\n                                mainTabConnection.switchToNextTab()\n                            }\n                        },\n                        onTitleClick = {\n                            onTitleClick(uiState.config)\n                        },\n                        onDoubleClick = {\n                            coroutineScope.launch {\n                                mainTabConnection.scrollToTop()\n                            }\n                        },\n                        onTabClick = {\n                            coroutineScope.launch {\n                                pagerState.scrollToPage(it)\n                            }\n                        },\n                    )\n                }\n            },\n            snackbarHost = {\n                val modifier = if (showFb) {\n                    Modifier.navigationBarsPadding()\n                } else {\n                    Modifier.navigationBarsPadding().contentBottomPadding()\n                }\n                SnackbarHost(\n                    modifier = modifier,\n                    hostState = snackBarHostState,\n                )\n            },\n            contentWindowInsets = WindowInsets(0, 0, 0, 0),\n            floatingActionButton = {\n                if (showFb) {\n                    val inImmersiveMode by mainTabConnection.inImmersiveFlow.collectAsState()\n                    val immersiveNavBar = LocalStatusUiConfig.current.immersiveNavBar\n                    PublishingFab(\n                        visible = !inImmersiveMode || !immersiveNavBar,\n                        onPublishClick = { onPostBlogClick(uiState.account) },\n                    )\n                }\n            },\n        ) { paddings ->\n            CompositionLocalProvider(\n                LocalSnackbarHostState provides snackBarHostState,\n                LocalContentPadding provides plusContentPadding(paddings),\n            ) {\n                if (uiState.locator != null && uiState.config != null) {\n                    if (tabList.isNotEmpty()) {\n                        val contentScrollInProgress by mainTabConnection.contentScrollInpProgress.collectAsState()\n                        HorizontalPager(\n                            modifier = Modifier.fillMaxSize(),\n                            state = pagerState,\n                            userScrollEnabled = !contentScrollInProgress,\n                        ) { pageIndex ->\n                            tabList[pageIndex].Content()\n                        }\n                    }\n                } else if (!uiState.errorMessage.isNullOrBlank()) {\n                    Box(\n                        modifier = Modifier.fillMaxSize()\n                            .padding(LocalContentPadding.current),\n                    ) {\n                        Text(\n                            modifier = Modifier\n                                .padding(start = 16.dp, top = 64.dp, end = 16.dp)\n                                .fillMaxWidth()\n                                .align(Alignment.TopCenter),\n                            text = uiState.errorMessage,\n                            textAlign = TextAlign.Center,\n                        )\n                    }\n                }\n            }\n        }\n    }\n\n    private fun createTabs(\n        locator: PlatformLocator,\n        config: ActivityPubContent,\n    ): List<Tab> {\n        return config\n            .tabList\n            .filter { !it.hide }\n            .sortedBy { it.order }\n            .map { it.toPagerTab(locator) }\n    }\n\n    private fun ActivityPubContent.ContentTab.toPagerTab(locator: PlatformLocator): Tab {\n        return when (this) {\n            is ActivityPubContent.ContentTab.HomeTimeline -> {\n                ActivityPubTimelineTab(\n                    locator = locator,\n                    type = ActivityPubStatusSourceType.TIMELINE_HOME,\n                )\n            }\n\n            is ActivityPubContent.ContentTab.LocalTimeline -> {\n                ActivityPubTimelineTab(\n                    locator = locator,\n                    type = ActivityPubStatusSourceType.TIMELINE_LOCAL,\n                )\n            }\n\n            is ActivityPubContent.ContentTab.PublicTimeline -> {\n                ActivityPubTimelineTab(\n                    locator = locator,\n                    type = ActivityPubStatusSourceType.TIMELINE_PUBLIC,\n                )\n            }\n\n            is ActivityPubContent.ContentTab.Trending -> {\n                TrendingStatusTab(locator = locator)\n            }\n\n            is ActivityPubContent.ContentTab.ListTimeline -> {\n                ActivityPubTimelineTab(\n                    locator = locator,\n                    type = ActivityPubStatusSourceType.LIST,\n                    listId = listId,\n                    listTitle = name,\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/content/ActivityPubContentUiState.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.content\n\nimport com.zhangke.fread.activitypub.app.internal.content.ActivityPubContent\nimport com.zhangke.fread.activitypub.app.internal.model.ActivityPubLoggedAccount\nimport com.zhangke.fread.status.model.PlatformLocator\n\ndata class ActivityPubContentUiState(\n    val locator: PlatformLocator?,\n    val config: ActivityPubContent?,\n    val account: ActivityPubLoggedAccount?,\n    val showAccountInTopBar: Boolean,\n    val errorMessage: String?,\n    val showRefreshButton: Boolean,\n    val showNextButton: Boolean,\n) {\n\n    companion object {\n\n        fun default(): ActivityPubContentUiState {\n            return ActivityPubContentUiState(\n                locator = null,\n                config = null,\n                account = null,\n                showAccountInTopBar = false,\n                errorMessage = null,\n                showRefreshButton = false,\n                showNextButton = false,\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/content/ActivityPubContentViewModel.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.content\n\nimport com.zhangke.framework.lifecycle.ContainerViewModel\nimport com.zhangke.fread.activitypub.app.ActivityPubAccountManager\nimport com.zhangke.fread.activitypub.app.internal.usecase.UpdateActivityPubUserListUseCase\nimport com.zhangke.fread.activitypub.app.internal.usecase.content.GetUserCreatedListUseCase\nimport com.zhangke.fread.common.config.FreadConfigManager\nimport com.zhangke.fread.common.content.FreadContentRepo\n\nclass ActivityPubContentViewModel (\n    private val contentRepo: FreadContentRepo,\n    private val freadConfigManager: FreadConfigManager,\n    private val accountManager: ActivityPubAccountManager,\n    private val getUserCreatedList: GetUserCreatedListUseCase,\n    private val updateActivityPubUserList: UpdateActivityPubUserListUseCase,\n) : ContainerViewModel<ActivityPubContentSubViewModel, ActivityPubContentViewModel.Params>() {\n\n    override fun createSubViewModel(params: Params): ActivityPubContentSubViewModel {\n        return ActivityPubContentSubViewModel(\n            contentRepo = contentRepo,\n            getUserCreatedList = getUserCreatedList,\n            freadConfigManager = freadConfigManager,\n            accountManager = accountManager,\n            contentId = params.contentId,\n            updateActivityPubUserList = updateActivityPubUserList,\n        )\n    }\n\n    fun getSubViewModel(contentId: String): ActivityPubContentSubViewModel {\n        val params = Params(contentId)\n        return obtainSubViewModel(params)\n    } class Params(val contentId: String) : SubViewModelParams() {\n\n        override val key: String\n            get() = contentId.toString()\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/content/edit/EditContentConfigScreen.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.content.edit\n\nimport androidx.compose.animation.core.animateDpAsState\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Menu\nimport androidx.compose.material.icons.filled.Visibility\nimport androidx.compose.material.icons.filled.VisibilityOff\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.SnackbarHost\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.key\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.composable.ConsumeFlow\nimport com.zhangke.framework.composable.ConsumeSnackbarFlow\nimport com.zhangke.framework.composable.SimpleIconButton\nimport com.zhangke.framework.composable.TextString\nimport com.zhangke.framework.composable.currentOrThrow\nimport com.zhangke.framework.composable.freadPlaceholder\nimport com.zhangke.framework.composable.rememberSnackbarHostState\nimport com.zhangke.framework.nav.LocalNavBackStack\nimport com.zhangke.fread.activitypub.app.internal.composable.tabName\nimport com.zhangke.fread.activitypub.app.internal.content.ActivityPubContent\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.ui.bar.EditContentTopBar\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.serialization.Serializable\nimport org.burnoutcrew.reorderable.ReorderableItem\nimport org.burnoutcrew.reorderable.detectReorderAfterLongPress\nimport org.burnoutcrew.reorderable.rememberReorderableLazyListState\nimport org.burnoutcrew.reorderable.reorderable\nimport org.jetbrains.compose.resources.stringResource\nimport org.koin.compose.viewmodel.koinViewModel\nimport org.koin.core.parameter.parametersOf\n\n@Serializable\ndata class EditContentConfigScreenKey(val contentId: String) : NavKey\n\nprivate val TAB_ITEM_HEIGHT = 62.dp\n\n@Composable\nfun EditContentConfigScreen(\n    contentId: String,\n    viewModel: EditContentConfigViewModel = koinViewModel { parametersOf(contentId) },\n) {\n    val backStack = LocalNavBackStack.currentOrThrow\n    val uiState by viewModel.uiState.collectAsState()\n    EditContentConfigScreenContent(\n        uiState = uiState,\n        snackbarMessageFlow = viewModel.snackbarMessageFlow,\n        onBackClick = { backStack.removeLastOrNull() },\n        onShowingTabMove = viewModel::onShowingTabMove,\n        onShowingTabMoveDown = viewModel::onShowingTabMoveDown,\n        onHiddenTabMoveUp = viewModel::onHiddenTabMoveUp,\n        onDeleteClick = viewModel::onDeleteClick,\n        onEditNameClick = viewModel::onEditNameClick,\n    )\n    ConsumeFlow(viewModel.finishScreenFlow) {\n        backStack.removeLastOrNull()\n    }\n}\n\n@Composable\nprivate fun EditContentConfigScreenContent(\n    uiState: EditContentConfigUiState?,\n    snackbarMessageFlow: Flow<TextString>,\n    onBackClick: () -> Unit,\n    onShowingTabMove: (from: Int, to: Int) -> Unit,\n    onShowingTabMoveDown: (ActivityPubContent.ContentTab) -> Unit,\n    onHiddenTabMoveUp: (ActivityPubContent.ContentTab) -> Unit,\n    onEditNameClick: (String) -> Unit,\n    onDeleteClick: () -> Unit,\n) {\n    val snackbarHostState = rememberSnackbarHostState()\n    ConsumeSnackbarFlow(snackbarHostState, snackbarMessageFlow)\n    Scaffold(\n        snackbarHost = {\n            SnackbarHost(snackbarHostState)\n        },\n        topBar = {\n            EditContentTopBar(\n                contentName = uiState?.content?.name.orEmpty(),\n                onBackClick = onBackClick,\n                onNameEdit = onEditNameClick,\n                onDeleteClick = onDeleteClick,\n            )\n        }\n    ) { innerPaddings ->\n        Column(\n            modifier = Modifier\n                .padding(innerPaddings)\n                .fillMaxSize()\n                .freadPlaceholder(uiState == null)\n                .verticalScroll(rememberScrollState())\n        ) {\n            if (uiState != null) {\n                ShowingUserList(\n                    uiState = uiState,\n                    onShowingTabMove = onShowingTabMove,\n                    onMoveDown = onShowingTabMoveDown,\n                )\n                Spacer(modifier = Modifier.height(16.dp))\n                HiddenUserList(\n                    uiState = uiState,\n                    onMoveUp = onHiddenTabMoveUp,\n                )\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun ShowingUserList(\n    uiState: EditContentConfigUiState,\n    onShowingTabMove: (from: Int, to: Int) -> Unit,\n    onMoveDown: (ActivityPubContent.ContentTab) -> Unit,\n) {\n    Text(\n        modifier = Modifier.padding(start = 16.dp, top = 16.dp),\n        text = stringResource(LocalizedString.statusUiEditContentConfigShowingListTitle),\n        style = MaterialTheme.typography.titleMedium,\n    )\n    Text(\n        modifier = Modifier.padding(start = 16.dp, top = 2.dp),\n        text = stringResource(LocalizedString.statusUiEditContentConfigShowingListDescription),\n        style = MaterialTheme.typography.bodySmall,\n        color = MaterialTheme.colorScheme.onSurfaceVariant,\n    )\n    var tabsInUi by remember(uiState.content.showingTabList) {\n        mutableStateOf(uiState.content.showingTabList)\n    }\n    key(uiState.content.showingTabList) {\n        val state = rememberReorderableLazyListState(\n            onMove = { from, to ->\n                if (tabsInUi.isEmpty()) return@rememberReorderableLazyListState\n                tabsInUi = tabsInUi.toMutableList().apply {\n                    add(to.index, removeAt(from.index))\n                }\n            },\n            onDragEnd = { startIndex, endIndex ->\n                onShowingTabMove(startIndex, endIndex)\n            },\n        )\n        LazyColumn(\n            state = state.listState,\n            modifier = Modifier\n                .padding(top = 16.dp)\n                .fillMaxWidth()\n                .height(TAB_ITEM_HEIGHT * tabsInUi.size + 4.dp)\n                .reorderable(state)\n                .detectReorderAfterLongPress(state),\n        ) {\n            itemsIndexed(\n                items = tabsInUi,\n                key = { _, item -> item.uiKey }\n            ) { _, tabItem ->\n                ReorderableItem(state, tabItem.uiKey) { dragging ->\n                    val shadowElevation by animateDpAsState(\n                        if (dragging) 8.dp else 2.dp,\n                        label = \"\"\n                    )\n                    val tonalElevation by animateDpAsState(\n                        if (dragging) 16.dp else 2.dp,\n                        label = \"\"\n                    )\n                    Surface(\n                        modifier = Modifier\n                            .padding(horizontal = 16.dp)\n                            .fillMaxWidth()\n                            .height(TAB_ITEM_HEIGHT),\n                        tonalElevation = tonalElevation,\n                        shadowElevation = shadowElevation,\n                    ) {\n                        Row(\n                            modifier = Modifier.fillMaxSize(),\n                            verticalAlignment = Alignment.CenterVertically,\n                        ) {\n                            Text(\n                                modifier = Modifier.padding(start = 16.dp),\n                                text = tabItem.tabName(),\n                                style = MaterialTheme.typography.bodyMedium,\n                            )\n                            Spacer(modifier = Modifier.weight(1F))\n                            SimpleIconButton(\n                                onClick = { onMoveDown(tabItem) },\n                                imageVector = Icons.Default.VisibilityOff,\n                                contentDescription = \"Move Down\",\n                            )\n                            Spacer(modifier = Modifier.width(16.dp))\n                            Icon(\n                                imageVector = Icons.Default.Menu,\n                                contentDescription = null,\n                            )\n                            Spacer(modifier = Modifier.width(16.dp))\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun HiddenUserList(\n    uiState: EditContentConfigUiState,\n    onMoveUp: (ActivityPubContent.ContentTab) -> Unit,\n) {\n    Text(\n        modifier = Modifier.padding(start = 16.dp, top = 24.dp),\n        text = stringResource(LocalizedString.statusUiEditContentConfigHiddenListTitle),\n        style = MaterialTheme.typography.titleMedium,\n    )\n    Spacer(modifier = Modifier.height(16.dp))\n    if (uiState.content.hidingTabList.isEmpty()) {\n        Box(\n            modifier = Modifier.fillMaxWidth(),\n            contentAlignment = Alignment.Center,\n        ) {\n            Text(\n                modifier = Modifier,\n                text = stringResource(LocalizedString.empty),\n                style = MaterialTheme.typography.bodyMedium,\n            )\n        }\n    } else {\n        uiState.content.hidingTabList.forEach { tabItem ->\n            Surface(\n                modifier = Modifier\n                    .padding(horizontal = 16.dp)\n                    .fillMaxWidth()\n                    .height(TAB_ITEM_HEIGHT),\n                tonalElevation = 2.dp,\n                shadowElevation = 2.dp,\n            ) {\n                Row(\n                    modifier = Modifier.fillMaxSize(),\n                    verticalAlignment = Alignment.CenterVertically,\n                ) {\n                    Text(\n                        modifier = Modifier.padding(start = 16.dp),\n                        text = tabItem.tabName(),\n                        style = MaterialTheme.typography.bodyMedium,\n                    )\n                    Spacer(modifier = Modifier.weight(1F))\n                    SimpleIconButton(\n                        onClick = { onMoveUp(tabItem) },\n                        imageVector = Icons.Default.Visibility,\n                        contentDescription = \"Move Up\",\n                    )\n                    Spacer(modifier = Modifier.width(8.dp))\n                }\n            }\n        }\n    }\n}\n\nprivate val ActivityPubContent.showingTabList: List<ActivityPubContent.ContentTab>\n    get() = tabList.filter { !it.hide }\n\nprivate val ActivityPubContent.hidingTabList: List<ActivityPubContent.ContentTab>\n    get() = tabList.filter { it.hide }\n\nprivate val ActivityPubContent.ContentTab.uiKey: String\n    get() = \"${this::class.simpleName}@${hashCode()}\"\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/content/edit/EditContentConfigUiState.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.content.edit\n\nimport com.zhangke.fread.activitypub.app.internal.content.ActivityPubContent\n\ndata class EditContentConfigUiState (\n    val content: ActivityPubContent,\n)\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/content/edit/EditContentConfigViewModel.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.content.edit\n\nimport androidx.lifecycle.ViewModel\nimport com.zhangke.framework.composable.TextString\nimport com.zhangke.framework.composable.textOf\nimport com.zhangke.framework.ktx.launchInViewModel\nimport com.zhangke.fread.activitypub.app.internal.content.ActivityPubContent\nimport com.zhangke.fread.activitypub.app.internal.usecase.content.ReorderActivityPubTabUseCase\nimport com.zhangke.fread.common.content.FreadContentRepo\nimport com.zhangke.fread.localization.LocalizedString\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asSharedFlow\nimport kotlinx.coroutines.flow.asStateFlow\nclass EditContentConfigViewModel (\n    private val contentRepo: FreadContentRepo,\n    private val reorderTab: ReorderActivityPubTabUseCase,\n    private val contentId: String\n) : ViewModel() {\n\n    private val _uiState = MutableStateFlow<EditContentConfigUiState?>(null)\n    val uiState = _uiState.asStateFlow()\n    private val _snackbarMessageFlow = MutableSharedFlow<TextString>()\n    val snackbarMessageFlow = _snackbarMessageFlow.asSharedFlow()\n    private val _finishScreenFlow = MutableSharedFlow<Unit>()\n    val finishScreenFlow = _finishScreenFlow.asSharedFlow()\n\n    init {\n        launchInViewModel {\n            contentRepo.getContentFlow(contentId)\n                .collect { content ->\n                    if (content !is ActivityPubContent) {\n                        _snackbarMessageFlow.emit(textOf(LocalizedString.activity_pub_edit_content_screen_config_not_found))\n                        return@collect\n                    }\n                    _uiState.value = EditContentConfigUiState(content)\n                }\n        }\n    }\n\n    fun onShowingTabMove(from: Int, to: Int) {\n        val uiState = _uiState.value ?: return\n        val content = uiState.content\n        launchInViewModel {\n            reorderTab(\n                content = content,\n                fromTab = content.tabList[from],\n                toTab = content.tabList[to],\n            )\n        }\n    }\n\n    fun onShowingTabMoveDown(tab: ActivityPubContent.ContentTab) {\n        launchInViewModel {\n            updateTabHideState(tab, true)\n        }\n    }\n\n    fun onHiddenTabMoveUp(tab: ActivityPubContent.ContentTab) {\n        launchInViewModel {\n            updateTabHideState(tab, false)\n        }\n    }\n\n    private suspend fun updateTabHideState(\n        tab: ActivityPubContent.ContentTab,\n        hide: Boolean,\n    ) {\n        val content = _uiState.value?.content ?: return\n        val newContent = content.copy(\n            tabList = content.tabList.map {\n                if (it == tab) {\n                    it.updateHide(hide)\n                } else {\n                    it\n                }\n            }\n        )\n        contentRepo.insertContent(newContent)\n    }\n\n    fun onDeleteClick() {\n        launchInViewModel {\n            contentRepo.delete(contentId)\n            _finishScreenFlow.emit(Unit)\n        }\n    }\n\n    fun onEditNameClick(contentName: String) {\n        launchInViewModel {\n            if (contentRepo.checkNameExist(contentName)) {\n                _snackbarMessageFlow.emit(textOf(LocalizedString.addFeedsPageEmptyNameExist))\n                return@launchInViewModel\n            }\n            val newContent = contentRepo.getContent(contentId)\n                ?.let { it as? ActivityPubContent }\n                ?.copy(name = contentName) ?: return@launchInViewModel\n            contentRepo.insertContent(newContent)\n        }\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/content/timeline/ActivityPubTimelineContainerViewModel.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.content.timeline\n\nimport com.zhangke.framework.lifecycle.ContainerViewModel\nimport com.zhangke.fread.activitypub.app.ActivityPubAccountManager\nimport com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubStatusAdapter\nimport com.zhangke.fread.activitypub.app.internal.auth.LoggedAccountProvider\nimport com.zhangke.fread.activitypub.app.internal.model.ActivityPubStatusSourceType\nimport com.zhangke.fread.activitypub.app.internal.repo.status.ActivityPubStatusReadStateRepo\nimport com.zhangke.fread.activitypub.app.internal.repo.status.ActivityPubTimelineStatusRepo\nimport com.zhangke.fread.common.adapter.StatusUiStateAdapter\nimport com.zhangke.fread.common.config.FreadConfigManager\nimport com.zhangke.fread.common.status.StatusUpdater\nimport com.zhangke.fread.commonbiz.shared.usecase.RefactorToNewStatusUseCase\nimport com.zhangke.fread.status.StatusProvider\nimport com.zhangke.fread.status.model.PlatformLocator\n\nclass ActivityPubTimelineContainerViewModel (\n    private val statusProvider: StatusProvider,\n    private val statusUpdater: StatusUpdater,\n    private val statusAdapter: ActivityPubStatusAdapter,\n    private val statusUiStateAdapter: StatusUiStateAdapter,\n    private val refactorToNewStatus: RefactorToNewStatusUseCase,\n    private val loggedAccountProvider: LoggedAccountProvider,\n    private val timelineRepo: ActivityPubTimelineStatusRepo,\n    private val accountManager: ActivityPubAccountManager,\n    private val statusReadStateRepo: ActivityPubStatusReadStateRepo,\n    private val freadConfigManager: FreadConfigManager,\n) : ContainerViewModel<ActivityPubTimelineViewModel, ActivityPubTimelineContainerViewModel.Params>() {\n\n    override fun createSubViewModel(params: Params): ActivityPubTimelineViewModel {\n        return ActivityPubTimelineViewModel(\n            statusProvider = statusProvider,\n            statusUpdater = statusUpdater,\n            statusUiStateAdapter = statusUiStateAdapter,\n            statusAdapter = statusAdapter,\n            loggedAccountProvider = loggedAccountProvider,\n            freadConfigManager = freadConfigManager,\n            refactorToNewStatus = refactorToNewStatus,\n            statusReadStateRepo = statusReadStateRepo,\n            accountManager = accountManager,\n            timelineRepo = timelineRepo,\n            locator = params.locator,\n            type = params.type,\n            listId = params.listId,\n        )\n    }\n\n    fun getSubViewModel(\n        locator: PlatformLocator,\n        type: ActivityPubStatusSourceType,\n        listId: String?\n    ): ActivityPubTimelineViewModel {\n        return obtainSubViewModel(\n            Params(\n                locator = locator,\n                type = type,\n                listId = listId,\n            )\n        )\n    } class Params(\n        val locator: PlatformLocator,\n        val type: ActivityPubStatusSourceType,\n        val listId: String?,\n    ) : SubViewModelParams() {\n\n        override val key: String\n            get() = locator.toString() + type + listId\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/content/timeline/ActivityPubTimelineTab.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.content.timeline\n\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.ui.Modifier\nimport com.zhangke.framework.composable.ConsumeOpenScreenFlow\nimport com.zhangke.framework.composable.ConsumeSnackbarFlow\nimport com.zhangke.framework.composable.LocalSnackbarHostState\nimport com.zhangke.framework.composable.textOf\nimport com.zhangke.framework.loadable.lazycolumn.LoadableInlineVideoLazyColumn\nimport com.zhangke.framework.loadable.lazycolumn.rememberLoadableInlineVideoLazyColumnState\nimport com.zhangke.framework.nav.BaseTab\nimport com.zhangke.framework.nav.TabOptions\nimport com.zhangke.fread.activitypub.app.internal.composable.ActivityPubTabNames\nimport com.zhangke.fread.activitypub.app.internal.model.ActivityPubStatusSourceType\nimport com.zhangke.fread.commonbiz.shared.composable.FeedsStatusNode\nimport com.zhangke.fread.commonbiz.shared.composable.InitErrorContent\nimport com.zhangke.fread.commonbiz.shared.composable.ObserveForFeedsConnection\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.ui.ComposedStatusInteraction\nimport com.zhangke.fread.status.ui.StatusListPlaceholder\nimport com.zhangke.fread.status.ui.common.ObserveScrollStopedPosition\nimport org.koin.compose.viewmodel.koinViewModel\n\ninternal class ActivityPubTimelineTab(\n    private val locator: PlatformLocator,\n    private val type: ActivityPubStatusSourceType,\n    private val listId: String? = null,\n    private val listTitle: String? = null,\n) : BaseTab() {\n\n    override val options: TabOptions\n        @Composable get() = TabOptions(\n            title = when (type) {\n                ActivityPubStatusSourceType.TIMELINE_HOME -> ActivityPubTabNames.homeTimeline\n                ActivityPubStatusSourceType.TIMELINE_LOCAL -> ActivityPubTabNames.localTimeline\n                ActivityPubStatusSourceType.TIMELINE_PUBLIC -> ActivityPubTabNames.publicTimeline\n                ActivityPubStatusSourceType.LIST -> listTitle!!\n            }\n        )\n\n    @Composable\n    override fun Content() {\n        super.Content()\n        val viewModel = koinViewModel<ActivityPubTimelineContainerViewModel>()\n            .getSubViewModel(locator, type, listId)\n        val uiState by viewModel.uiState.collectAsState()\n        val snackbarHostState = LocalSnackbarHostState.current\n        ActivityPubTimelineContent(\n            uiState = uiState,\n            composedStatusInteraction = viewModel.composedStatusInteraction,\n            onJumpedToStatus = viewModel::onJumpedToStatus,\n            onLoadPrevious = viewModel::onLoadPreviousPage,\n            onRefresh = viewModel::onRefresh,\n            onLoadMore = viewModel::onLoadMore,\n            onReadPositionIndexChanged = viewModel::updateMaxReadStatus,\n        )\n        ConsumeSnackbarFlow(snackbarHostState, viewModel.errorMessageFlow)\n        ConsumeOpenScreenFlow(viewModel.openScreenFlow)\n    }\n\n    @Composable\n    private fun ActivityPubTimelineContent(\n        uiState: ActivityPubTimelineUiState,\n        composedStatusInteraction: ComposedStatusInteraction,\n        onJumpedToStatus: () -> Unit,\n        onLoadPrevious: () -> Unit,\n        onLoadMore: () -> Unit,\n        onRefresh: () -> Unit,\n        onReadPositionIndexChanged: (ActivityPubTimelineItem) -> Unit,\n    ) {\n        Box(modifier = Modifier.fillMaxSize()) {\n            if (uiState.items.isEmpty()) {\n                if (uiState.showPagingLoadingPlaceholder) {\n                    StatusListPlaceholder()\n                } else {\n                    InitErrorContent(\n                        errorMessage = uiState.pageErrorContent\n                            ?: textOf(LocalizedString.unknownError),\n                        onRetryClick = onRefresh,\n                    )\n                }\n            } else {\n                Box(modifier = Modifier.fillMaxSize()) {\n                    val state = rememberLoadableInlineVideoLazyColumnState(\n                        onRefresh = onRefresh,\n                        onLoadMore = onLoadMore,\n                        initialFirstVisibleItemIndex = uiState.initialShowIndex,\n                    )\n                    val lazyListState = state.lazyListState\n                    ObserveScrollStopedPosition(lazyListState) {\n                        uiState.items.getOrNull(it)\n                            ?.let { item ->\n                                onReadPositionIndexChanged(item)\n                            }\n                    }\n                    ObserveForFeedsConnection(\n                        listState = lazyListState,\n                        onRefresh = onRefresh,\n                    )\n                    LaunchedEffect(uiState.jumpToStatusId) {\n                        if (!uiState.jumpToStatusId.isNullOrEmpty()) {\n                            val jumpToIndex =\n                                uiState.items.getIndexByIdOrNull(uiState.jumpToStatusId)\n                            if (jumpToIndex >= 0 && jumpToIndex < uiState.items.size) {\n                                onJumpedToStatus()\n                                state.lazyListState.animateScrollToItem(jumpToIndex)\n                            }\n                        }\n                    }\n                    LoadableInlineVideoLazyColumn(\n                        modifier = Modifier.fillMaxSize(),\n                        state = state,\n                        onLoadPrevious = {\n                            onLoadPrevious()\n                        },\n                        refreshing = uiState.refreshing,\n                        loadState = uiState.loadMoreState,\n                    ) {\n                        itemsIndexed(\n                            items = uiState.items,\n                            key = { _, item ->\n                                (item as ActivityPubTimelineItem.StatusItem).status.status.id\n                            },\n                        ) { index, item ->\n                            FeedsStatusNode(\n                                modifier = Modifier.fillMaxWidth(),\n                                status = (item as ActivityPubTimelineItem.StatusItem).status,\n                                composedStatusInteraction = composedStatusInteraction,\n                                indexInList = index,\n                            )\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/content/timeline/ActivityPubTimelineViewModel.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.content.timeline\n\nimport com.zhangke.framework.composable.emitTextMessageFromThrowable\nimport com.zhangke.framework.composable.toTextStringOrNull\nimport com.zhangke.framework.coroutines.invokeOnCancel\nimport com.zhangke.framework.ktx.launchInViewModel\nimport com.zhangke.framework.lifecycle.SubViewModel\nimport com.zhangke.framework.utils.LoadState\nimport com.zhangke.fread.activitypub.app.ActivityPubAccountManager\nimport com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubStatusAdapter\nimport com.zhangke.fread.activitypub.app.internal.auth.LoggedAccountProvider\nimport com.zhangke.fread.activitypub.app.internal.model.ActivityPubLoggedAccount\nimport com.zhangke.fread.activitypub.app.internal.model.ActivityPubStatusSourceType\nimport com.zhangke.fread.activitypub.app.internal.repo.status.ActivityPubStatusReadStateRepo\nimport com.zhangke.fread.activitypub.app.internal.repo.status.ActivityPubTimelineStatusRepo\nimport com.zhangke.fread.common.adapter.StatusUiStateAdapter\nimport com.zhangke.fread.common.config.FreadConfigManager\nimport com.zhangke.fread.common.config.TimelineDefaultPosition\nimport com.zhangke.fread.common.status.StatusUpdater\nimport com.zhangke.fread.commonbiz.shared.feeds.IInteractiveHandler\nimport com.zhangke.fread.commonbiz.shared.feeds.InteractiveHandleResult\nimport com.zhangke.fread.commonbiz.shared.feeds.InteractiveHandler\nimport com.zhangke.fread.commonbiz.shared.usecase.RefactorToNewStatusUseCase\nimport com.zhangke.fread.status.StatusProvider\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.richtext.preParseStatus\nimport com.zhangke.fread.status.richtext.preParseStatusList\nimport com.zhangke.fread.status.status.model.Status\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.update\n\nclass ActivityPubTimelineViewModel(\n    private val statusProvider: StatusProvider,\n    statusUpdater: StatusUpdater,\n    private val statusUiStateAdapter: StatusUiStateAdapter,\n    private val statusAdapter: ActivityPubStatusAdapter,\n    refactorToNewStatus: RefactorToNewStatusUseCase,\n    private val timelineRepo: ActivityPubTimelineStatusRepo,\n    private val statusReadStateRepo: ActivityPubStatusReadStateRepo,\n    private val accountManager: ActivityPubAccountManager,\n    private val loggedAccountProvider: LoggedAccountProvider,\n    private val freadConfigManager: FreadConfigManager,\n    private val locator: PlatformLocator,\n    private val type: ActivityPubStatusSourceType,\n    private val listId: String?,\n) : SubViewModel(), IInteractiveHandler by InteractiveHandler(\n    statusProvider = statusProvider,\n    statusUpdater = statusUpdater,\n    statusUiStateAdapter = statusUiStateAdapter,\n    refactorToNewStatus = refactorToNewStatus,\n) {\n\n    private val _uiState = MutableStateFlow(ActivityPubTimelineUiState.default())\n    val uiState = _uiState.asStateFlow()\n\n    private var initFeedsJob: Job? = null\n    private var refreshJob: Job? = null\n    private var loadMoreJob: Job? = null\n    private var loadPreviousJob: Job? = null\n\n    init {\n        initInteractiveHandler(\n            coroutineScope = viewModelScope,\n            onInteractiveHandleResult = { result ->\n                result.handle()\n            }\n        )\n        initFeeds()\n    }\n\n    private fun initFeeds() {\n        // 1. load local first page(100 item) data\n        // 2. load previous page of this local page data from server\n        // 3. position item of latest read item, if can't position, move to top.\n        initFeedsJob?.cancel()\n        initFeedsJob = launchInViewModel {\n            _uiState.update {\n                it.copy(\n                    items = emptyList(),\n                    showPagingLoadingPlaceholder = true,\n                )\n            }\n            val minId = maybeLoadLocalFeeds()\n            if (minId.isNullOrEmpty()) {\n                timelineRepo.getFresherStatus(\n                    locator = locator,\n                    type = type,\n                    listId = listId,\n                )\n            } else {\n                timelineRepo.loadPreviousPageStatus(\n                    locator = locator,\n                    type = type,\n                    minId = minId,\n                    listId = listId,\n                )\n            }.map { status ->\n                status.preParseStatusList()\n                val account = getAccount()\n                status.toTimelineItems(account)\n            }.onFailure { t ->\n                if (_uiState.value.items.isEmpty()) {\n                    _uiState.update {\n                        it.copy(\n                            showPagingLoadingPlaceholder = false,\n                            pageErrorContent = t.toTextStringOrNull(),\n                        )\n                    }\n                } else {\n                    mutableErrorMessageFlow.emitTextMessageFromThrowable(t)\n                }\n            }.onSuccess { list ->\n                _uiState.update {\n                    it.copy(\n                        items = list.appendItems(it.items),\n                        showPagingLoadingPlaceholder = false,\n                    )\n                }\n            }\n        }\n        initFeedsJob!!.invokeOnCancel {\n            _uiState.update {\n                it.copy(showPagingLoadingPlaceholder = false)\n            }\n        }\n    }\n\n    /**\n     * @return minId\n     */\n    private suspend fun maybeLoadLocalFeeds(): String? {\n        if (freadConfigManager.getTimelineDefaultPosition() == TimelineDefaultPosition.NEWEST) return null\n        val localStatus =\n            if (type == ActivityPubStatusSourceType.TIMELINE_LOCAL || type == ActivityPubStatusSourceType.TIMELINE_PUBLIC) {\n                emptyList()\n            } else {\n                timelineRepo.getStatusFromLocal(\n                    locator = locator,\n                    type = type,\n                    listId = listId,\n                ).map {\n                    it.preParseStatus()\n                    it\n                }\n            }\n        if (localStatus.isNotEmpty()) {\n            val latestReadStatus = statusReadStateRepo.getLatestReadId(locator, type, listId)\n            val initialIndex = localStatus.indexOfFirst { it.id == latestReadStatus }\n            val account = getAccount()\n            _uiState.update {\n                it.copy(\n                    items = localStatus.toTimelineItems(account),\n                    initialShowIndex = if (initialIndex in localStatus.indices) initialIndex else 0,\n                    showPagingLoadingPlaceholder = false,\n                )\n            }\n        }\n        return localStatus.firstOrNull()?.id\n    }\n\n    fun onRefresh() {\n        loadPreviousJob?.cancel()\n        loadMoreJob?.cancel()\n        refreshJob?.cancel()\n        refreshJob = launchInViewModel {\n            _uiState.update { it.copy(refreshing = true, showPagingLoadingPlaceholder = true) }\n            val account = locator.accountUri?.let { loggedAccountProvider.getAccount(it) }\n            timelineRepo.getFresherStatus(\n                locator = locator,\n                type = type,\n                listId = listId,\n            ).map {\n                it.preParseStatusList()\n                it.toTimelineItems(account)\n            }.onFailure { t ->\n                _uiState.update {\n                    it.copy(\n                        refreshing = false,\n                        showPagingLoadingPlaceholder = false\n                    )\n                }\n                mutableErrorMessageFlow.emitTextMessageFromThrowable(t)\n            }.onSuccess { list ->\n                _uiState.update {\n                    it.copy(\n                        items = list,\n                        refreshing = false,\n                        showPagingLoadingPlaceholder = false,\n                        jumpToStatusId = list.firstOrNull()?.statusId,\n                    )\n                }\n            }\n        }\n        refreshJob?.invokeOnCancel {\n            _uiState.update { it.copy(refreshing = false) }\n        }\n    }\n\n    fun onJumpedToStatus() {\n        _uiState.update { it.copy(jumpToStatusId = null) }\n    }\n\n    fun onLoadPreviousPage() {\n        if (refreshJob?.isActive == true) return\n        if (initFeedsJob?.isActive == true) return\n        if (loadPreviousJob?.isActive == true) return\n        val minId = uiState.value.items.getStatusIdOrNull(0) ?: return\n        val account = locator.accountUri?.let { loggedAccountProvider.getAccount(it) }\n        loadPreviousJob = launchInViewModel {\n            timelineRepo.loadPreviousPageStatus(\n                locator = locator,\n                type = type,\n                minId = minId,\n                listId = listId,\n            ).map {\n                it.preParseStatusList()\n                it.toTimelineItems(account)\n            }.onSuccess { list ->\n                _uiState.update {\n                    it.copy(\n                        items = list.appendItems(it.items),\n                    )\n                }\n            }\n        }\n    }\n\n    fun onLoadMore() {\n        val maxId = uiState.value\n            .items\n            .lastOrNull { it is ActivityPubTimelineItem.StatusItem }\n            ?.let { it as ActivityPubTimelineItem.StatusItem }\n            ?.status\n            ?.status\n            ?.id ?: return\n        loadMoreJob?.cancel()\n        loadMoreJob = launchInViewModel {\n            _uiState.update { it.copy(loadMoreState = LoadState.Loading) }\n            val account = locator.accountUri?.let { loggedAccountProvider.getAccount(it) }\n            timelineRepo.loadMore(\n                locator = locator,\n                type = type,\n                maxId = maxId,\n                listId = listId,\n            ).map {\n                it.preParseStatusList()\n                it.toTimelineItems(account)\n            }.onFailure { t ->\n                _uiState.update { it.copy(loadMoreState = LoadState.Failed(t.toTextStringOrNull())) }\n            }.onSuccess { list ->\n                _uiState.update {\n                    it.copy(\n                        items = it.items.appendItems(list),\n                        loadMoreState = LoadState.Idle,\n                    )\n                }\n            }\n        }\n        loadMoreJob!!.invokeOnCancel {\n            _uiState.update { it.copy(loadMoreState = LoadState.Idle) }\n        }\n    }\n\n    fun updateMaxReadStatus(item: ActivityPubTimelineItem) {\n        val statusId = (item as ActivityPubTimelineItem.StatusItem).status.status.id\n        launchInViewModel {\n            statusReadStateRepo.updateLatestReadId(\n                locator = locator,\n                type = type,\n                listId = listId,\n                latestReadId = statusId,\n            )\n        }\n    }\n\n    private suspend fun InteractiveHandleResult.handle() {\n        when (this) {\n            is InteractiveHandleResult.UpdateStatus -> {\n                _uiState.update { state ->\n                    state.copy(items = state.items.updateStatus(this.status))\n                }\n                timelineRepo.updateStatus(\n                    locator = locator,\n                    type = type,\n                    status = this.status.status,\n                    listId = listId,\n                )\n            }\n\n            is InteractiveHandleResult.DeleteStatus -> {\n                _uiState.update { state ->\n                    state.copy(\n                        items = state.items.filter {\n                            when (it) {\n                                is ActivityPubTimelineItem.StatusItem -> it.status.status.id != this.statusId\n                            }\n                        }\n                    )\n                }\n                timelineRepo.deleteStatus(statusId)\n            }\n\n            is InteractiveHandleResult.UpdateFollowState -> {}\n        }\n    }\n\n    private fun List<ActivityPubTimelineItem>.appendItems(\n        items: List<ActivityPubTimelineItem>,\n    ): List<ActivityPubTimelineItem> {\n        if (items.isEmpty()) return this\n        val newList = this.toMutableList()\n        val idSet = this.map { it.statusId }\n        val pendingAddItems = items.filter { it.statusId !in idSet }\n        newList.addAll(pendingAddItems)\n        return newList\n    }\n\n    private val ActivityPubTimelineItem.statusId: String\n        get() {\n            return when (this) {\n                is ActivityPubTimelineItem.StatusItem -> this.status.status.id\n            }\n        }\n\n    private fun List<Status>.toTimelineItems(\n        loggedAccount: ActivityPubLoggedAccount?,\n    ): List<ActivityPubTimelineItem> {\n        return this.map {\n            ActivityPubTimelineItem.StatusItem(\n                statusAdapter.toStatusUiState(\n                    status = it,\n                    locator = locator,\n                    loggedAccount = loggedAccount,\n                ),\n            )\n        }\n    }\n\n    private fun getAccount(): ActivityPubLoggedAccount? {\n        return locator.accountUri?.let { loggedAccountProvider.getAccount(it) }\n    }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/content/timeline/ActivityPubUiState.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.content.timeline\n\nimport com.zhangke.framework.composable.TextString\nimport com.zhangke.framework.utils.LoadState\nimport com.zhangke.fread.status.model.StatusUiState\n\ndata class ActivityPubTimelineUiState(\n    val items: List<ActivityPubTimelineItem>,\n    val showPagingLoadingPlaceholder: Boolean,\n    val pageErrorContent: TextString?,\n    val initialShowIndex: Int,\n    val jumpToStatusId: String?,\n    val refreshing: Boolean,\n    val loadMoreState: LoadState,\n) {\n\n    companion object {\n\n        fun default() = ActivityPubTimelineUiState(\n            items = emptyList(),\n            initialShowIndex = 0,\n            jumpToStatusId = null,\n            showPagingLoadingPlaceholder = false,\n            pageErrorContent = null,\n            refreshing = false,\n            loadMoreState = LoadState.Idle,\n        )\n    }\n}\n\nsealed interface ActivityPubTimelineItem {\n\n    data class StatusItem(\n        val status: StatusUiState,\n    ) : ActivityPubTimelineItem\n}\n\nfun List<ActivityPubTimelineItem>.updateStatus(\n    status: StatusUiState,\n): List<ActivityPubTimelineItem> {\n    return map {\n        if (it is ActivityPubTimelineItem.StatusItem && it.status.status.intrinsicBlog.id == status.status.intrinsicBlog.id) {\n            ActivityPubTimelineItem.StatusItem(status)\n        } else {\n            it\n        }\n    }\n}\n\nfun List<ActivityPubTimelineItem>.getStatusIdOrNull(index: Int): String? {\n    return this.getOrNull(index)?.let {\n        when (it) {\n            is ActivityPubTimelineItem.StatusItem -> it.status.status.id\n        }\n    }\n}\n\nfun List<ActivityPubTimelineItem>.getIndexByIdOrNull(id: String): Int {\n    return this.indexOfFirst {\n        when (it) {\n            is ActivityPubTimelineItem.StatusItem -> it.status.status.id == id\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/explorer/ExplorerContainerTab.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.explorer\n\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.pager.HorizontalPager\nimport androidx.compose.foundation.pager.rememberPagerState\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.layout.onSizeChanged\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.unit.dp\nimport com.zhangke.framework.composable.FreadTabRow\nimport com.zhangke.framework.composable.LocalContentPadding\nimport com.zhangke.framework.composable.plusTopPadding\nimport com.zhangke.framework.nav.BaseTab\nimport com.zhangke.framework.nav.TabOptions\nimport com.zhangke.framework.utils.pxToDp\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.platform.BlogPlatform\nimport kotlinx.coroutines.launch\n\nclass ExplorerContainerTab(\n    private val locator: PlatformLocator,\n    private val platform: BlogPlatform,\n) : BaseTab() {\n\n    override val options: TabOptions?\n        @Composable get() = null\n\n    @Composable\n    override fun Content() {\n        super.Content()\n        val tabs = remember {\n            listOf(\n                ExplorerTab(\n                    locator = locator,\n                    platform = platform,\n                    feedsTabType = ExplorerFeedsTabType.STATUS,\n                ),\n                ExplorerTab(\n                    locator = locator,\n                    platform = platform,\n                    feedsTabType = ExplorerFeedsTabType.HASHTAG,\n                ),\n                ExplorerTab(\n                    locator = locator,\n                    platform = platform,\n                    feedsTabType = ExplorerFeedsTabType.USERS,\n                ),\n            )\n        }\n        val density = LocalDensity.current\n        val coroutineScope = rememberCoroutineScope()\n        val pagerState = rememberPagerState(initialPage = 0) { tabs.size }\n        var tabBarHeight by remember { mutableStateOf(24.dp) }\n        CompositionLocalProvider(\n            LocalContentPadding provides plusTopPadding(tabBarHeight),\n        ) {\n            HorizontalPager(\n                modifier = Modifier.fillMaxSize(),\n                state = pagerState,\n            ) { pageIndex ->\n                with(tabs[pageIndex]) {\n                    Content()\n                }\n            }\n        }\n        Column {\n            Spacer(\n                modifier = Modifier.fillMaxWidth()\n                    .padding(top = LocalContentPadding.current.calculateTopPadding())\n            )\n            FreadTabRow(\n                modifier = Modifier.fillMaxWidth()\n                    .onSizeChanged { tabBarHeight = it.height.pxToDp(density) },\n                selectedTabIndex = pagerState.currentPage,\n                tabCount = tabs.size,\n                tabContent = {\n                    Text(\n                        text = tabs[it].options.title,\n                        maxLines = 1,\n                    )\n                },\n                onTabClick = {\n                    coroutineScope.launch {\n                        pagerState.scrollToPage(it)\n                    }\n                },\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/explorer/ExplorerContainerViewModel.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.explorer\n\nimport com.zhangke.framework.lifecycle.ContainerViewModel\nimport com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubAccountEntityAdapter\nimport com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubStatusAdapter\nimport com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubTagAdapter\nimport com.zhangke.fread.activitypub.app.internal.auth.ActivityPubClientManager\nimport com.zhangke.fread.activitypub.app.internal.auth.LoggedAccountProvider\nimport com.zhangke.fread.common.adapter.StatusUiStateAdapter\nimport com.zhangke.fread.common.status.StatusUpdater\nimport com.zhangke.fread.commonbiz.shared.usecase.RefactorToNewStatusUseCase\nimport com.zhangke.fread.status.StatusProvider\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.platform.BlogPlatform\n\nclass ExplorerContainerViewModel (\n    private val clientManager: ActivityPubClientManager,\n    private val loggedAccountProvider: LoggedAccountProvider,\n    private val activityPubStatusAdapter: ActivityPubStatusAdapter,\n    private val accountAdapter: ActivityPubAccountEntityAdapter,\n    private val hashtagAdapter: ActivityPubTagAdapter,\n    private val statusProvider: StatusProvider,\n    private val statusUpdater: StatusUpdater,\n    private val statusUiStateAdapter: StatusUiStateAdapter,\n    private val refactorToNewStatus: RefactorToNewStatusUseCase,\n) : ContainerViewModel<ExplorerViewModel, ExplorerContainerViewModel.Params>() {\n\n    override fun createSubViewModel(params: Params): ExplorerViewModel {\n        return ExplorerViewModel(\n            clientManager = clientManager,\n            loggedAccountProvider = loggedAccountProvider,\n            activityPubStatusAdapter = activityPubStatusAdapter,\n            accountAdapter = accountAdapter,\n            hashtagAdapter = hashtagAdapter,\n            statusProvider = statusProvider,\n            statusUpdater = statusUpdater,\n            statusUiStateAdapter = statusUiStateAdapter,\n            refactorToNewStatus = refactorToNewStatus,\n            locator = params.locator,\n            platform = params.platform,\n            type = params.type,\n        )\n    }\n\n    fun getViewModel(\n        locator: PlatformLocator,\n        platform: BlogPlatform,\n        type: ExplorerFeedsTabType,\n    ): ExplorerViewModel {\n        return obtainSubViewModel(Params(locator, platform, type))\n    } class Params(\n        val locator: PlatformLocator,\n        val platform: BlogPlatform,\n        val type: ExplorerFeedsTabType,\n    ) : SubViewModelParams() {\n\n        override val key: String\n            get() = locator.toString() + platform + type\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/explorer/ExplorerFeedsTabType.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.explorer\n\nenum class ExplorerFeedsTabType {\n\n    STATUS,\n    HASHTAG,\n    USERS,\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/explorer/ExplorerItem.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.explorer\n\nimport com.zhangke.fread.status.author.BlogAuthor\nimport com.zhangke.fread.status.model.Hashtag\nimport com.zhangke.fread.status.model.StatusUiState\n\nsealed interface ExplorerItem {\n\n    val id: String\n\n    data class ExplorerStatus(val status: StatusUiState) : ExplorerItem {\n        override val id: String\n            get() = status.status.id\n    }\n\n    data class ExplorerHashtag(val hashtag: Hashtag) : ExplorerItem {\n        override val id: String\n            get() = hashtag.name\n    }\n\n    data class ExplorerUser(val user: BlogAuthor, val following: Boolean) : ExplorerItem {\n        override val id: String\n            get() = user.uri.toString()\n    }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/explorer/ExplorerTab.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.explorer\n\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.unit.Dp\nimport com.zhangke.framework.composable.ConsumeFlow\nimport com.zhangke.framework.composable.ConsumeSnackbarFlow\nimport com.zhangke.framework.composable.LocalSnackbarHostState\nimport com.zhangke.framework.composable.currentOrThrow\nimport com.zhangke.framework.composable.textString\nimport com.zhangke.framework.controller.CommonLoadableUiState\nimport com.zhangke.framework.loadable.lazycolumn.LoadableInlineVideoLazyColumn\nimport com.zhangke.framework.loadable.lazycolumn.rememberLoadableInlineVideoLazyColumnState\nimport com.zhangke.framework.nav.BaseTab\nimport com.zhangke.framework.nav.LocalNavBackStack\nimport com.zhangke.framework.nav.TabOptions\nimport com.zhangke.fread.common.composable.ErrorContent\nimport com.zhangke.fread.common.composable.ErrorType\nimport com.zhangke.fread.commonbiz.shared.composable.FeedsStatusNode\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.platform.BlogPlatform\nimport com.zhangke.fread.status.ui.ComposedStatusInteraction\nimport com.zhangke.fread.status.ui.RecommendAuthorUi\nimport com.zhangke.fread.status.ui.StatusListPlaceholder\nimport com.zhangke.fread.status.ui.common.ObserveScrollInProgressForConnection\nimport com.zhangke.fread.status.ui.hashtag.HashtagUi\nimport org.jetbrains.compose.resources.stringResource\nimport org.koin.compose.viewmodel.koinViewModel\n\nclass ExplorerTab(\n    private val locator: PlatformLocator,\n    private val platform: BlogPlatform,\n    private val feedsTabType: ExplorerFeedsTabType,\n) : BaseTab() {\n\n    override val options: TabOptions\n        @Composable get() = TabOptions(\n            title = when (feedsTabType) {\n                ExplorerFeedsTabType.STATUS -> stringResource(LocalizedString.activity_pub_explorer_tab_status_title)\n                ExplorerFeedsTabType.USERS -> stringResource(LocalizedString.activity_pub_explorer_tab_users_title)\n                ExplorerFeedsTabType.HASHTAG -> stringResource(LocalizedString.activity_pub_explorer_tab_hashtag_title)\n            }\n        )\n\n    @Composable\n    override fun Content() {\n        super.Content()\n        val backStack = LocalNavBackStack.currentOrThrow\n        val viewModel = koinViewModel<ExplorerContainerViewModel>()\n            .getViewModel(locator, platform, feedsTabType)\n        val uiState by viewModel.uiState.collectAsState()\n        val snackbarHostState = LocalSnackbarHostState.current\n        ConsumeSnackbarFlow(snackbarHostState, viewModel.errorMessageFlow)\n        ConsumeFlow(viewModel.openScreenFlow) {\n            backStack.add(it)\n        }\n        ExplorerFeedsTabContent(\n            uiState = uiState,\n            onRefresh = viewModel::onRefresh,\n            onLoadMore = viewModel::onLoadMore,\n            composedStatusInteraction = viewModel.composedStatusInteraction,\n        )\n    }\n\n    @Composable\n    private fun ExplorerFeedsTabContent(\n        uiState: CommonLoadableUiState<ExplorerItem>,\n        onRefresh: () -> Unit,\n        onLoadMore: () -> Unit,\n        composedStatusInteraction: ComposedStatusInteraction,\n    ) {\n        val errorMessage = uiState.errorMessage?.let { textString(it) }\n        var containerHeight: Dp? by remember {\n            mutableStateOf(null)\n        }\n        val density = LocalDensity.current\n        if (uiState.initializing) {\n            StatusListPlaceholder()\n        } else if (uiState.dataList.isEmpty()) {\n            ErrorContent(\n                modifier = Modifier.fillMaxSize(),\n                type = ErrorType.Network,\n                errorMessage = errorMessage,\n                onRetryClick = onRefresh,\n            )\n        } else {\n            val state = rememberLoadableInlineVideoLazyColumnState(\n                onRefresh = onRefresh,\n                onLoadMore = onLoadMore,\n            )\n            ObserveScrollInProgressForConnection(state.lazyListState)\n            LoadableInlineVideoLazyColumn(\n                modifier = Modifier.fillMaxSize(),\n                state = state,\n                refreshing = uiState.refreshing,\n                loadState = uiState.loadMoreState,\n            ) {\n                itemsIndexed(\n                    items = uiState.dataList,\n                ) { index, item ->\n                    ExplorerItemUi(\n                        modifier = Modifier.fillMaxWidth(),\n                        item = item,\n                        locator = locator,\n                        composedStatusInteraction = composedStatusInteraction,\n                        indexInList = index,\n                    )\n                }\n            }\n        }\n    }\n\n    @Composable\n    private fun ExplorerItemUi(\n        modifier: Modifier,\n        item: ExplorerItem,\n        locator: PlatformLocator,\n        indexInList: Int,\n        composedStatusInteraction: ComposedStatusInteraction,\n    ) {\n        when (item) {\n            is ExplorerItem.ExplorerStatus -> {\n                FeedsStatusNode(\n                    modifier = modifier,\n                    status = item.status,\n                    composedStatusInteraction = composedStatusInteraction,\n                    indexInList = indexInList,\n                )\n            }\n\n            is ExplorerItem.ExplorerUser -> {\n                RecommendAuthorUi(\n                    modifier = modifier,\n                    locator = locator,\n                    author = item.user,\n                    following = item.following,\n                    composedStatusInteraction = composedStatusInteraction,\n                )\n            }\n\n            is ExplorerItem.ExplorerHashtag -> {\n                HashtagUi(\n                    modifier = modifier,\n                    tag = item.hashtag,\n                    onClick = {\n                        composedStatusInteraction.onHashtagClick(locator, it)\n                    },\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/explorer/ExplorerUiState.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.explorer\n\nclass ExplorerUiState {\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/explorer/ExplorerViewModel.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.explorer\n\nimport com.zhangke.framework.controller.CommonLoadableController\nimport com.zhangke.framework.controller.CommonLoadableUiState\nimport com.zhangke.framework.ktx.launchInViewModel\nimport com.zhangke.framework.lifecycle.SubViewModel\nimport com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubAccountEntityAdapter\nimport com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubStatusAdapter\nimport com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubTagAdapter\nimport com.zhangke.fread.activitypub.app.internal.auth.ActivityPubClientManager\nimport com.zhangke.fread.activitypub.app.internal.auth.LoggedAccountProvider\nimport com.zhangke.fread.common.adapter.StatusUiStateAdapter\nimport com.zhangke.fread.common.status.StatusUpdater\nimport com.zhangke.fread.commonbiz.shared.feeds.IInteractiveHandler\nimport com.zhangke.fread.commonbiz.shared.feeds.InteractiveHandleResult\nimport com.zhangke.fread.commonbiz.shared.feeds.InteractiveHandler\nimport com.zhangke.fread.commonbiz.shared.usecase.RefactorToNewStatusUseCase\nimport com.zhangke.fread.status.StatusProvider\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.model.StatusUiState\nimport com.zhangke.fread.status.platform.BlogPlatform\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.update\n\nclass ExplorerViewModel(\n    private val clientManager: ActivityPubClientManager,\n    private val loggedAccountProvider: LoggedAccountProvider,\n    private val activityPubStatusAdapter: ActivityPubStatusAdapter,\n    private val accountAdapter: ActivityPubAccountEntityAdapter,\n    private val hashtagAdapter: ActivityPubTagAdapter,\n    private val statusProvider: StatusProvider,\n    statusUpdater: StatusUpdater,\n    private val statusUiStateAdapter: StatusUiStateAdapter,\n    refactorToNewStatus: RefactorToNewStatusUseCase,\n    private val locator: PlatformLocator,\n    private val platform: BlogPlatform,\n    private val type: ExplorerFeedsTabType,\n) : SubViewModel(), IInteractiveHandler by InteractiveHandler(\n    statusProvider = statusProvider,\n    statusUpdater = statusUpdater,\n    statusUiStateAdapter = statusUiStateAdapter,\n    refactorToNewStatus = refactorToNewStatus,\n) {\n\n    companion object {\n\n        private const val LIMIT = 50\n    }\n\n    private val loadController = CommonLoadableController<ExplorerItem>(\n        viewModelScope,\n        onPostSnackMessage = {\n            launchInViewModel {\n                mutableErrorMessageFlow.emit(it)\n            }\n        },\n    )\n\n    val uiState: StateFlow<CommonLoadableUiState<ExplorerItem>> get() = loadController.uiState\n\n    init {\n        initInteractiveHandler(\n            coroutineScope = viewModelScope,\n            onInteractiveHandleResult = { interactiveResult ->\n                when (interactiveResult) {\n                    is InteractiveHandleResult.UpdateStatus -> {\n                        loadController.mutableUiState.update { state ->\n                            val dataList = state.dataList.updateStatus(interactiveResult.status)\n                            state.copy(dataList = dataList)\n                        }\n                    }\n\n                    is InteractiveHandleResult.DeleteStatus -> {\n                        loadController.mutableUiState.update { state ->\n                            state.copy(\n                                dataList = state.dataList.filter {\n                                    if (it is ExplorerItem.ExplorerStatus) {\n                                        it.id != interactiveResult.statusId\n                                    } else {\n                                        true\n                                    }\n                                }\n                            )\n                        }\n                    }\n\n                    is InteractiveHandleResult.UpdateFollowState -> {\n                        val authorUri = interactiveResult.userUri\n                        loadController.mutableUiState.update { state ->\n                            val dataList = state.dataList.map {\n                                if (it is ExplorerItem.ExplorerUser && it.user.uri == authorUri) {\n                                    it.copy(following = interactiveResult.following)\n                                } else {\n                                    it\n                                }\n                            }\n                            state.copy(dataList = dataList)\n                        }\n                    }\n                }\n            },\n        )\n        launchInViewModel {\n            loadController.initData(\n                getDataFromServer = { getExplorer(maxId = null, offset = 0) },\n                getDataFromLocal = null,\n            )\n        }\n    }\n\n    fun onRefresh() {\n        loadController.onRefresh(false) {\n            getExplorer(null, 0)\n        }\n    }\n\n    fun onLoadMore() {\n        val dataList = uiState.value.dataList\n        if (dataList.isEmpty()) return\n        loadController.onLoadMore {\n            getExplorer(\n                maxId = dataList.last().id,\n                offset = loadController.uiState.value.dataList.size,\n            )\n        }\n    }\n\n    private suspend fun getExplorer(maxId: String?, offset: Int): Result<List<ExplorerItem>> {\n        val client = clientManager.getClient(locator)\n        return when (type) {\n            ExplorerFeedsTabType.STATUS -> {\n                val loggedAccount = locator.accountUri?.let { loggedAccountProvider.getAccount(it) }\n                client.timelinesRepo\n                    .publicTimelines(limit = LIMIT, maxId = maxId)\n                    .map { list ->\n                        list.map {\n                            activityPubStatusAdapter.toStatusUiState(\n                                entity = it,\n                                platform = platform,\n                                locator = locator,\n                                loggedAccount = loggedAccount,\n                            )\n                        }\n                    }.map { list ->\n                        list.map { ExplorerItem.ExplorerStatus(it) }\n                    }\n            }\n\n            ExplorerFeedsTabType.USERS -> {\n                if ((offset > 0) || !maxId.isNullOrEmpty()) {\n                    return Result.success(emptyList())\n                }\n                client.accountRepo\n                    .getSuggestions()\n                    .map { list -> list.map { accountAdapter.toAuthor(it.account) } }\n                    .map { list -> list.map { ExplorerItem.ExplorerUser(it, false) } }\n            }\n\n            ExplorerFeedsTabType.HASHTAG -> {\n                client.instanceRepo\n                    .getTrendsTags(limit = LIMIT, offset = offset)\n                    .map { list -> list.map { hashtagAdapter.adapt(it) } }\n                    .map { list -> list.map { ExplorerItem.ExplorerHashtag(it) } }\n            }\n        }\n    }\n\n    private fun List<ExplorerItem>.updateStatus(newStatus: StatusUiState): List<ExplorerItem> {\n        return map { item ->\n            if (item is ExplorerItem.ExplorerStatus && item.status.status.intrinsicBlog.id == newStatus.status.intrinsicBlog.id) {\n                item.copy(status = newStatus)\n            } else {\n                item\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/filters/edit/EditFilterScreen.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.filters.edit\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Delete\nimport androidx.compose.material.icons.filled.Save\nimport androidx.compose.material3.Checkbox\nimport androidx.compose.material3.DropdownMenuItem\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.OutlinedTextField\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.SnackbarHost\nimport androidx.compose.material3.SnackbarHostState\nimport androidx.compose.material3.Switch\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateListOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.ExperimentalComposeUiApi\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.DpOffset\nimport androidx.compose.ui.unit.dp\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.composable.BackHandler\nimport com.zhangke.framework.composable.ConsumeFlow\nimport com.zhangke.framework.composable.ConsumeSnackbarFlow\nimport com.zhangke.framework.composable.DatePickerDialog\nimport com.zhangke.framework.composable.FreadDialog\nimport com.zhangke.framework.composable.PopupMenu\nimport com.zhangke.framework.composable.SimpleIconButton\nimport com.zhangke.framework.composable.Toolbar\nimport com.zhangke.framework.composable.currentOrThrow\nimport com.zhangke.framework.composable.rememberFutureDatePickerState\nimport com.zhangke.framework.composable.rememberSnackbarHostState\nimport com.zhangke.framework.nav.LocalNavBackStack\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.ui.utils.getScreenWidth\nimport kotlinx.datetime.Clock\nimport kotlinx.datetime.Instant\nimport kotlinx.serialization.Serializable\nimport org.jetbrains.compose.resources.stringResource\nimport kotlin.time.Duration.Companion.days\nimport kotlin.time.Duration.Companion.hours\nimport kotlin.time.Duration.Companion.minutes\nimport kotlin.time.ExperimentalTime\n\n@Serializable\ndata class EditFilterScreenKey(\n    val locator: PlatformLocator,\n    val id: String? = null,\n) : NavKey\n\n@OptIn(ExperimentalComposeUiApi::class, ExperimentalTime::class)\n@Composable\nfun EditFilterScreen(viewModel: EditFilterViewModel, id: String?) {\n    val backStack = LocalNavBackStack.currentOrThrow\n    ConsumeFlow(HiddenKeywordScreenNavKey.keywordsListFlow.flow) {\n        viewModel.onKeywordChanged(it)\n    }\n    val uiState by viewModel.uiState.collectAsState()\n    val snackBarHostState = rememberSnackbarHostState()\n\n    var showBackDialog by remember {\n        mutableStateOf(false)\n    }\n\n    fun onBack() {\n        if (uiState.hasInputtedSomething) {\n            showBackDialog = true\n        } else {\n            backStack.removeLastOrNull()\n        }\n    }\n\n    BackHandler(true) { onBack() }\n\n    EditFilterContent(\n        uiState = uiState,\n        id = id,\n        snackBarHostState = snackBarHostState,\n        onBackClick = { onBack() },\n        onTitleChanged = viewModel::onTitleChanged,\n        onExpiredDateSelected = viewModel::onExpiredDateSelected,\n        onKeywordClick = {\n            backStack.add(HiddenKeywordScreenNavKey(uiState.keywordList))\n        },\n        onContextChanged = viewModel::onContextChanged,\n        onWarningCheckChanged = viewModel::onWarningCheckChanged,\n        onDeleteClick = viewModel::onDeleteClick,\n        onSubmitClick = viewModel::onSubmitClick,\n    )\n    ConsumeSnackbarFlow(hostState = snackBarHostState, messageTextFlow = viewModel.snackBarFlow)\n    ConsumeFlow(viewModel.finishPageFlow) {\n        backStack.removeLastOrNull()\n    }\n\n    if (showBackDialog) {\n        FreadDialog(\n            onDismissRequest = { showBackDialog = false },\n            contentText = stringResource(LocalizedString.activity_pub_filter_edit_back_dialog),\n            onNegativeClick = { showBackDialog = false },\n            onPositiveClick = {\n                showBackDialog = false\n                backStack.removeLastOrNull()\n            }\n        )\n    }\n}\n\n@OptIn(ExperimentalTime::class)\n@Composable\nprivate fun EditFilterContent(\n    uiState: EditFilterUiState,\n    id: String?,\n    snackBarHostState: SnackbarHostState,\n    onBackClick: () -> Unit,\n    onTitleChanged: (String) -> Unit,\n    onExpiredDateSelected: (Instant?) -> Unit,\n    onKeywordClick: () -> Unit,\n    onContextChanged: (List<FilterContext>) -> Unit,\n    onWarningCheckChanged: (Boolean) -> Unit,\n    onDeleteClick: () -> Unit,\n    onSubmitClick: () -> Unit,\n) {\n    Scaffold(\n        topBar = {\n            Toolbar(\n                title = stringResource(LocalizedString.activity_pub_filter_edit_title),\n                onBackClick = onBackClick,\n                actions = {\n                    if (id.isNullOrEmpty().not()) {\n                        DeleteMenuItem(onDeleteClick = onDeleteClick)\n                    }\n                    SimpleIconButton(\n                        onClick = onSubmitClick,\n                        imageVector = Icons.Default.Save,\n                        contentDescription = \"Save\",\n                    )\n                },\n            )\n        },\n        snackbarHost = {\n            SnackbarHost(hostState = snackBarHostState)\n        },\n    ) { innerPadding ->\n        Column(\n            modifier = Modifier\n                .fillMaxWidth()\n                .padding(innerPadding),\n        ) {\n            OutlinedTextField(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .padding(start = 16.dp, top = 24.dp, end = 16.dp),\n                value = uiState.title,\n                onValueChange = onTitleChanged,\n                maxLines = 1,\n                placeholder = {\n                    Text(text = stringResource(LocalizedString.activity_pub_filter_edit_input_title_label))\n                },\n                label = {\n                    Text(text = stringResource(LocalizedString.activity_pub_filter_edit_input_title_label))\n                },\n            )\n\n            DurationItem(\n                uiState = uiState,\n                onExpiredDateSelected = onExpiredDateSelected,\n            )\n\n            LinedItem(\n                modifier = Modifier\n                    .clickable {\n                        onKeywordClick()\n                    }\n                    .fillMaxWidth()\n                    .padding(horizontal = 16.dp),\n                title = stringResource(LocalizedString.activity_pub_filter_edit_keyword_list_title),\n                subtitle = stringResource(\n                    LocalizedString.activity_pub_filter_edit_keyword_list_desc,\n                    uiState.keywordCount\n                ),\n            )\n            ContextItem(\n                uiState = uiState,\n                onContextChanged = onContextChanged,\n            )\n            WarningItem(\n                uiState = uiState,\n                onCheckChanged = onWarningCheckChanged,\n            )\n        }\n    }\n}\n\n@OptIn(ExperimentalMaterial3Api::class, ExperimentalTime::class)\n@Composable\nprivate fun DurationItem(\n    uiState: EditFilterUiState,\n    onExpiredDateSelected: (Instant?) -> Unit,\n) {\n    val screenWidth = getScreenWidth() * 0.5F\n    var showDurationPopup by remember {\n        mutableStateOf(false)\n    }\n    var showDurationSelector by remember {\n        mutableStateOf(false)\n    }\n    Box(modifier = Modifier.fillMaxWidth()) {\n        LinedItem(\n            modifier = Modifier\n                .clickable {\n                    showDurationPopup = true\n                }\n                .fillMaxWidth()\n                .padding(horizontal = 16.dp),\n            title = stringResource(LocalizedString.activity_pub_filter_edit_duration),\n            subtitle = uiState.getExpiresDateDesc(),\n        )\n        PopupMenu(\n            offset = DpOffset(screenWidth, 0.dp),\n            expanded = showDurationPopup,\n            onDismissRequest = { showDurationPopup = false },\n        ) {\n            DropdownMenuItem(\n                text = { Text(text = stringResource(LocalizedString.activity_pub_filter_edit_duration_permanent)) },\n                onClick = {\n                    showDurationPopup = false\n                    onExpiredDateSelected(null)\n                },\n            )\n            DropdownMenuItem(\n                text = { Text(text = stringResource(LocalizedString.activity_pub_filter_edit_duration_thirty_minutes)) },\n                onClick = {\n                    showDurationPopup = false\n                    onExpiredDateSelected(Clock.System.now().plus(30.minutes))\n                },\n            )\n            DropdownMenuItem(\n                text = { Text(text = stringResource(LocalizedString.activity_pub_filter_edit_duration_one_hour)) },\n                onClick = {\n                    showDurationPopup = false\n                    onExpiredDateSelected(Clock.System.now().plus(1.hours))\n                },\n            )\n            DropdownMenuItem(\n                text = { Text(text = stringResource(LocalizedString.activity_pub_filter_edit_duration_twelve_hours)) },\n                onClick = {\n                    showDurationPopup = false\n                    onExpiredDateSelected(Clock.System.now().plus(12.hours))\n                },\n            )\n            DropdownMenuItem(\n                text = { Text(text = stringResource(LocalizedString.activity_pub_filter_edit_duration_one_day)) },\n                onClick = {\n                    showDurationPopup = false\n                    onExpiredDateSelected(Clock.System.now().plus(1.days))\n                },\n            )\n            DropdownMenuItem(\n                text = { Text(text = stringResource(LocalizedString.activity_pub_filter_edit_duration_three_day)) },\n                onClick = {\n                    showDurationPopup = false\n                    onExpiredDateSelected(Clock.System.now().plus(3.days))\n                },\n            )\n            DropdownMenuItem(\n                text = { Text(text = stringResource(LocalizedString.activity_pub_filter_edit_duration_one_week)) },\n                onClick = {\n                    showDurationPopup = false\n                    onExpiredDateSelected(Clock.System.now().plus(7.days))\n                },\n            )\n            DropdownMenuItem(\n                text = { Text(text = stringResource(LocalizedString.activity_pub_filter_edit_duration_custom)) },\n                onClick = {\n                    showDurationPopup = false\n                    showDurationSelector = true\n                },\n            )\n        }\n        val pickerState = rememberFutureDatePickerState(\n            initialSelectedDateMillis = uiState.expiresDate?.toEpochMilliseconds()\n        )\n        DatePickerDialog(\n            datePickerState = pickerState,\n            visible = showDurationSelector,\n            onDismissRequest = { showDurationSelector = false },\n            onConfirmClick = {\n                showDurationSelector = false\n                pickerState.selectedDateMillis\n                    ?.let { Instant.fromEpochMilliseconds(it) }\n                    ?.let(onExpiredDateSelected)\n            },\n        )\n    }\n}\n\n@Composable\nprivate fun WarningItem(\n    uiState: EditFilterUiState,\n    onCheckChanged: (Boolean) -> Unit,\n) {\n    Row(\n        modifier = Modifier\n            .fillMaxWidth()\n            .padding(start = 16.dp, end = 8.dp),\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        LinedItem(\n            modifier = Modifier.weight(1F),\n            title = stringResource(LocalizedString.activity_pub_filter_edit_warning_title),\n            subtitle = stringResource(LocalizedString.activity_pub_filter_edit_warning_desc),\n        )\n        Switch(\n            checked = uiState.filterByWarn,\n            onCheckedChange = {\n                onCheckChanged(it)\n            },\n        )\n    }\n}\n\n@Composable\nprivate fun ContextItem(\n    uiState: EditFilterUiState,\n    onContextChanged: (List<FilterContext>) -> Unit,\n) {\n    var showSelector by remember {\n        mutableStateOf(false)\n    }\n    LinedItem(\n        modifier = Modifier\n            .clickable { showSelector = true }\n            .fillMaxWidth()\n            .padding(horizontal = 16.dp),\n        title = stringResource(LocalizedString.activity_pub_filter_edit_context_title),\n        subtitle = uiState.contextList.map { it.title }.joinToString().ifEmpty {\n            stringResource(LocalizedString.activity_pub_filter_edit_empty_context)\n        },\n    )\n    if (showSelector) {\n        ContextSelector(\n            selectedContext = uiState.contextList,\n            onContextSelected = onContextChanged,\n            onDismissRequest = { showSelector = false },\n        )\n    }\n}\n\n@Composable\nprivate fun ContextSelector(\n    selectedContext: List<FilterContext>,\n    onContextSelected: (List<FilterContext>) -> Unit,\n    onDismissRequest: () -> Unit,\n) {\n    val currentSelected = remember(selectedContext) {\n        mutableStateListOf<FilterContext>().also { it.addAll(selectedContext) }\n    }\n    FreadDialog(\n        title = stringResource(LocalizedString.activity_pub_filter_edit_context_selector_title),\n        onDismissRequest = onDismissRequest,\n        onNegativeClick = onDismissRequest,\n        content = {\n            Column {\n                FilterContext.entries.forEach { context ->\n                    val selected = currentSelected.contains(context)\n                    Box(\n                        modifier = Modifier\n                            .fillMaxWidth()\n                            .padding(vertical = 8.dp, horizontal = 16.dp),\n                    ) {\n                        Text(\n                            modifier = Modifier.align(Alignment.CenterStart),\n                            text = context.title,\n                            style = MaterialTheme.typography.titleMedium,\n                        )\n                        Checkbox(\n                            modifier = Modifier.align(Alignment.CenterEnd),\n                            checked = selected,\n                            onCheckedChange = {\n                                if (it) {\n                                    currentSelected += context\n                                } else {\n                                    currentSelected -= context\n                                }\n                            },\n                        )\n                    }\n                }\n            }\n        },\n        onPositiveClick = {\n            onDismissRequest()\n            onContextSelected(currentSelected)\n        },\n    )\n}\n\n@Composable\nprivate fun LinedItem(\n    modifier: Modifier,\n    title: String,\n    subtitle: String,\n) {\n    Column(\n        modifier = modifier,\n    ) {\n        Spacer(modifier = Modifier.height(16.dp))\n        Text(\n            text = title,\n            style = MaterialTheme.typography.titleMedium,\n        )\n        Text(\n            modifier = Modifier.padding(top = 4.dp),\n            text = subtitle,\n            style = MaterialTheme.typography.bodyMedium,\n        )\n        Spacer(modifier = Modifier.height(16.dp))\n    }\n}\n\n@Composable\nprivate fun DeleteMenuItem(onDeleteClick: () -> Unit) {\n    var showConfirmDialog by remember {\n        mutableStateOf(false)\n    }\n    SimpleIconButton(\n        onClick = { showConfirmDialog = true },\n        imageVector = Icons.Default.Delete,\n        contentDescription = \"Delete\"\n    )\n    if (showConfirmDialog) {\n        FreadDialog(\n            contentText = stringResource(LocalizedString.activity_pub_filter_edit_delete_content),\n            onDismissRequest = { showConfirmDialog = false },\n            onNegativeClick = { showConfirmDialog = false },\n            onPositiveClick = {\n                showConfirmDialog = false\n                onDeleteClick()\n            },\n        )\n    }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/filters/edit/EditFilterUiState.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.filters.edit\n\nimport androidx.compose.runtime.Composable\nimport com.zhangke.framework.utils.Parcelize\nimport com.zhangke.framework.utils.PlatformParcelable\nimport com.zhangke.framework.utils.PlatformSerializable\nimport com.zhangke.fread.common.utils.formatDefault\nimport com.zhangke.fread.localization.LocalizedString\nimport kotlinx.serialization.Serializable\nimport org.jetbrains.compose.resources.stringResource\nimport kotlin.time.ExperimentalTime\nimport kotlinx.datetime.Instant\n\ndata class EditFilterUiState @OptIn(ExperimentalTime::class) constructor(\n    val title: String,\n    val expiresDate: Instant?,\n    val keywordList: List<Keyword>,\n    val contextList: List<FilterContext>,\n    val filterByWarn: Boolean,\n    val hasInputtedSomething: Boolean,\n) {\n\n    val keywordCount: Int\n        get() = keywordList.filter { !it.deleted }.size\n\n    @OptIn(ExperimentalTime::class)\n    private val expiresDateString: String by lazy {\n        if (expiresDate == null) return@lazy \"\"\n        // val dateFormat = DateFormat.getDateInstance(DateFormat.MEDIUM, Locale.getDefault())\n        // val timeFormat = DateFormat.getTimeInstance(DateFormat.MEDIUM, Locale.getDefault())\n        // dateFormat.format(expiresDate) + \" \" + timeFormat.format(expiresDate)\n        expiresDate.formatDefault()\n    }\n\n    @OptIn(ExperimentalTime::class)\n    @Composable\n    fun getExpiresDateDesc(): String {\n        if (expiresDate == null) {\n            return stringResource(LocalizedString.activity_pub_filter_edit_duration_permanent)\n        }\n        return stringResource(\n            LocalizedString.activity_pub_filter_edit_duration_finish_subtitle,\n            expiresDateString,\n        )\n    }\n\n    @Parcelize\n    @Serializable\n    data class Keyword(\n        val keyword: String,\n        val id: String? = null,\n        val deleted: Boolean = false,\n        val wholeWord: Boolean = true,\n    ) : PlatformSerializable, PlatformParcelable\n\n    companion object {\n\n        @OptIn(ExperimentalTime::class)\n        fun default(): EditFilterUiState {\n            return EditFilterUiState(\n                title = \"\",\n                expiresDate = null,\n                keywordList = emptyList(),\n                contextList = FilterContext.entries,\n                filterByWarn = true,\n                hasInputtedSomething = false,\n            )\n        }\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/filters/edit/EditFilterViewModel.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.filters.edit\n\nimport androidx.lifecycle.ViewModel\nimport com.zhangke.activitypub.entities.ActivityPubCreateFilterEntity\nimport com.zhangke.activitypub.entities.ActivityPubFilterEntity\nimport com.zhangke.activitypub.entities.ActivityPubFilterKeywordEntity\nimport com.zhangke.framework.composable.TextString\nimport com.zhangke.framework.composable.emitTextMessageFromThrowable\nimport com.zhangke.framework.date.DateParser\nimport com.zhangke.framework.ktx.launchInViewModel\nimport com.zhangke.fread.activitypub.app.internal.auth.ActivityPubClientManager\nimport com.zhangke.fread.common.utils.getCurrentTimeMillis\nimport com.zhangke.fread.status.model.PlatformLocator\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.SharedFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.update\nimport kotlin.time.ExperimentalTime\nimport kotlinx.datetime.Instant\n\nclass EditFilterViewModel(\n    private val clientManager: ActivityPubClientManager,\n    private val locator: PlatformLocator,\n    private val id: String?,\n) : ViewModel() {\n\n    private val _uiState = MutableStateFlow(EditFilterUiState.default())\n    val uiState: StateFlow<EditFilterUiState> = _uiState\n\n    private val _snackBarFlow = MutableSharedFlow<TextString>()\n    val snackBarFlow: SharedFlow<TextString> = _snackBarFlow\n\n    private val _finishPageFlow = MutableSharedFlow<Unit>()\n    val finishPageFlow: SharedFlow<Unit> = _finishPageFlow\n\n    private var submitJob: Job? = null\n\n    init {\n        loadFilter()\n    }\n\n    fun onTitleChanged(title: String) {\n        _uiState.update { it.copy(title = title, hasInputtedSomething = true) }\n    }\n\n    @OptIn(ExperimentalTime::class)\n    fun onExpiredDateSelected(date: Instant?) {\n        _uiState.update { it.copy(expiresDate = date, hasInputtedSomething = true) }\n    }\n\n    fun onKeywordChanged(keywordList: List<EditFilterUiState.Keyword>) {\n        _uiState.update { it.copy(keywordList = keywordList, hasInputtedSomething = true) }\n    }\n\n    fun onContextChanged(contextList: List<FilterContext>) {\n        _uiState.update { it.copy(contextList = contextList, hasInputtedSomething = true) }\n    }\n\n    fun onWarningCheckChanged(checked: Boolean) {\n        _uiState.update { it.copy(filterByWarn = checked, hasInputtedSomething = true) }\n    }\n\n    fun onDeleteClick() {\n        val filterId = id ?: return\n        launchInViewModel {\n            clientManager.getClient(locator).accountRepo\n                .deleteFilter(filterId)\n                .onSuccess {\n                    _finishPageFlow.emit(Unit)\n                }.onFailure {\n                    _snackBarFlow.emitTextMessageFromThrowable(it)\n                }\n        }\n    }\n\n    fun onSubmitClick() {\n        if (submitJob?.isActive == true) return\n        submitJob = launchInViewModel {\n            val request = _uiState.value.toRequest()\n            val accountRepo = clientManager.getClient(locator).accountRepo\n            if (id.isNullOrEmpty()) {\n                accountRepo.createFilters(request)\n            } else {\n                accountRepo.updateFilters(id, request)\n            }.onSuccess {\n                _finishPageFlow.emit(Unit)\n            }.onFailure {\n                _snackBarFlow.emitTextMessageFromThrowable(it)\n            }\n        }\n    }\n\n    private fun EditFilterUiState.toRequest(): ActivityPubCreateFilterEntity {\n        val expiresIn = if (expiresDate == null) {\n            null\n        } else {\n            (expiresDate.toEpochMilliseconds() - getCurrentTimeMillis()) / 1000\n        }\n        return ActivityPubCreateFilterEntity(\n            title = this.title,\n            context = this.contextList.map { it.contextName },\n            filterAction = if (this.filterByWarn) {\n                ActivityPubFilterEntity.FILTER_ACTION_WARN\n            } else {\n                ActivityPubFilterEntity.FILTER_ACTION_KEYWORDS\n            },\n            expiresIn = expiresIn?.toInt()?.coerceAtLeast(0),\n            keywordsAttributes = this.keywordList.map { keyword ->\n                ActivityPubCreateFilterEntity.KeywordAttribute(\n                    id = keyword.id,\n                    keyword = keyword.keyword.trim(),\n                    wholeWord = keyword.wholeWord,\n                    destroy = keyword.deleted,\n                )\n            },\n        )\n    }\n\n    private fun loadFilter() {\n        if (id != null) {\n            launchInViewModel {\n                clientManager.getClient(locator)\n                    .accountRepo\n                    .getFilter(id)\n                    .onFailure {\n                        _snackBarFlow.emitTextMessageFromThrowable(it)\n                    }.onSuccess {\n                        _uiState.value = it.toFilter()\n                    }\n            }\n        }\n    }\n\n    private fun ActivityPubFilterEntity.toFilter(): EditFilterUiState {\n        return EditFilterUiState(\n            title = this.title,\n            expiresDate = expiresAt?.let(DateParser::parseAll),\n            keywordList = this.keywords?.map { it.toKeyword() } ?: emptyList(),\n            contextList = this.context.mapNotNull { FilterContext.fromContext(it) },\n            filterByWarn = this.filterAction == ActivityPubFilterEntity.FILTER_ACTION_WARN,\n            hasInputtedSomething = false,\n        )\n    }\n\n    private fun ActivityPubFilterKeywordEntity.toKeyword(): EditFilterUiState.Keyword {\n        return EditFilterUiState.Keyword(\n            id = this.id,\n            keyword = this.keyword,\n            wholeWord = this.wholeWord,\n        )\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/filters/edit/FilterContext.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.filters.edit\n\nimport androidx.compose.runtime.Composable\nimport com.zhangke.fread.localization.LocalizedString\nimport org.jetbrains.compose.resources.stringResource\n\nenum class FilterContext(val contextName: String) {\n\n    HOME_LIST(\"home\"),\n    NOTIFICATION(\"notifications\"),\n    TIMELINE(\"public\"),\n    POST_REPLY(\"thread\"),\n    USER_INFO(\"account\");\n\n    val title: String\n        @Composable get() = when (this) {\n            HOME_LIST -> stringResource(LocalizedString.activity_pub_filter_edit_context_home)\n            NOTIFICATION -> stringResource(LocalizedString.activity_pub_filter_edit_context_notification)\n            TIMELINE -> stringResource(LocalizedString.activity_pub_filter_edit_context_timeline)\n            POST_REPLY -> stringResource(LocalizedString.activity_pub_filter_edit_context_thread)\n            USER_INFO -> stringResource(LocalizedString.activity_pub_filter_edit_context_account)\n        }\n\n    companion object {\n\n        fun fromContext(context: String): FilterContext? {\n            return FilterContext.entries.firstOrNull { it.contextName == context }\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/filters/edit/HiddenKeywordScreen.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.filters.edit\n\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Add\nimport androidx.compose.material.icons.filled.Delete\nimport androidx.compose.material3.Checkbox\nimport androidx.compose.material3.FloatingActionButton\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.OutlinedTextField\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateListOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.ExperimentalComposeUiApi\nimport androidx.compose.ui.Modifier\nimport com.zhangke.framework.composable.BackHandler\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.unit.dp\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.composable.FreadDialog\nimport com.zhangke.framework.composable.SimpleIconButton\nimport com.zhangke.framework.composable.Toolbar\nimport com.zhangke.framework.composable.currentOrThrow\nimport com.zhangke.framework.nav.LocalNavBackStack\nimport com.zhangke.framework.nav.ScreenEventFlow\nimport com.zhangke.fread.localization.LocalizedString\nimport kotlinx.coroutines.launch\nimport kotlinx.serialization.Serializable\nimport org.jetbrains.compose.resources.stringResource\n\n@Serializable\ndata class HiddenKeywordScreenNavKey(\n    val addedKeywords: List<EditFilterUiState.Keyword>,\n) : NavKey {\n\n    companion object {\n        val keywordsListFlow = ScreenEventFlow<List<EditFilterUiState.Keyword>>()\n    }\n}\n\n@OptIn(ExperimentalComposeUiApi::class)\n@Composable\nfun HiddenKeywordScreen(addedKeywords: List<EditFilterUiState.Keyword>) {\n    val backStack = LocalNavBackStack.currentOrThrow\n    val keywordsList = remember(addedKeywords) {\n        mutableStateListOf<EditFilterUiState.Keyword>().also { it.addAll(addedKeywords) }\n    }\n    val showingKeywordList = keywordsList.filter { !it.deleted }\n    var pendingEditKeyword: EditFilterUiState.Keyword? by remember {\n        mutableStateOf(null)\n    }\n    var showEditDialog by remember { mutableStateOf(false) }\n    val coroutineScope = rememberCoroutineScope()\n    BackHandler(true) {\n        coroutineScope.launch {\n            HiddenKeywordScreenNavKey.keywordsListFlow.emit(keywordsList)\n            backStack.removeLastOrNull()\n        }\n    }\n    Scaffold(\n        topBar = {\n            Toolbar(\n                title = stringResource(LocalizedString.activity_pub_filter_edit_keyword_title),\n                onBackClick = {\n                    coroutineScope.launch {\n                        HiddenKeywordScreenNavKey.keywordsListFlow.emit(keywordsList)\n                        backStack.removeLastOrNull()\n                    }\n                },\n            )\n        },\n        floatingActionButton = {\n            FloatingActionButton(\n                containerColor = MaterialTheme.colorScheme.surface,\n                onClick = {\n                    pendingEditKeyword = null\n                    showEditDialog = true\n                },\n            ) {\n                Icon(imageVector = Icons.Default.Add, contentDescription = \"Add\")\n            }\n        },\n    ) { innerPadding ->\n        LazyColumn(\n            modifier = Modifier\n                .padding(innerPadding)\n                .fillMaxSize(),\n        ) {\n            items(showingKeywordList) { keyword ->\n                Row(\n                    modifier = Modifier.fillMaxWidth()\n                        .padding(start = 16.dp, top = 16.dp, end = 8.dp),\n                    verticalAlignment = Alignment.CenterVertically,\n                ) {\n                    Column(\n                        modifier = Modifier.weight(1f),\n                    ) {\n                        Text(\n                            modifier = Modifier.fillMaxWidth(),\n                            text = keyword.keyword,\n                            textAlign = TextAlign.Start,\n                            style = MaterialTheme.typography.titleMedium,\n                        )\n\n                        Row(\n                            modifier = Modifier.fillMaxWidth(),\n                            verticalAlignment = Alignment.CenterVertically,\n                        ) {\n                            Checkbox(\n                                checked = keyword.wholeWord,\n                                onCheckedChange = {\n                                    keywordsList[keywordsList.indexOf(keyword)] =\n                                        keyword.copy(wholeWord = it)\n                                },\n                            )\n                            Text(\n                                text = stringResource(LocalizedString.activity_pub_filter_edit_whole_word),\n                                style = MaterialTheme.typography.labelMedium,\n                                color = MaterialTheme.colorScheme.onSurfaceVariant,\n                            )\n                        }\n                    }\n\n                    Spacer(modifier = Modifier.width(8.dp))\n\n                    var showDeleteConfirmDialog by remember {\n                        mutableStateOf(false)\n                    }\n                    SimpleIconButton(\n                        modifier = Modifier,\n                        onClick = {\n                            showDeleteConfirmDialog = true\n                        },\n                        imageVector = Icons.Default.Delete,\n                        contentDescription = \"Delete\",\n                    )\n                    if (showDeleteConfirmDialog) {\n                        FreadDialog(\n                            contentText = stringResource(LocalizedString.activity_pub_filter_edit_keyword_remove_keyword_dialog_content),\n                            onDismissRequest = {\n                                showDeleteConfirmDialog = false\n                            },\n                            onNegativeClick = {\n                                showDeleteConfirmDialog = false\n                            },\n                            onPositiveClick = {\n                                showDeleteConfirmDialog = false\n                                keywordsList[keywordsList.indexOf(keyword)] =\n                                    keyword.copy(deleted = true)\n                            },\n                        )\n                    }\n                }\n            }\n        }\n    }\n    if (showEditDialog) {\n        EditKeywordDialog(\n            keyword = pendingEditKeyword,\n            onConfirmClick = {\n                if (pendingEditKeyword == null) {\n                    keywordsList.add(EditFilterUiState.Keyword(keyword = it))\n                } else {\n                    if (keywordsList.contains(pendingEditKeyword)) {\n                        keywordsList[keywordsList.indexOf(pendingEditKeyword)] =\n                            pendingEditKeyword!!.copy(keyword = it)\n                    }\n                }\n                pendingEditKeyword = null\n            },\n            onDismissRequest = { showEditDialog = false },\n        )\n    }\n}\n\n@Composable\nprivate fun EditKeywordDialog(\n    keyword: EditFilterUiState.Keyword?,\n    onConfirmClick: (String) -> Unit,\n    onDismissRequest: () -> Unit,\n) {\n    var inputtingKeyword by remember(keyword) {\n        mutableStateOf(keyword?.keyword.orEmpty())\n    }\n    FreadDialog(\n        title = stringResource(LocalizedString.activity_pub_filter_edit_keyword_dialog_title),\n        onDismissRequest = onDismissRequest,\n        content = {\n            OutlinedTextField(\n                modifier = Modifier\n                    .padding(16.dp)\n                    .fillMaxWidth(),\n                value = inputtingKeyword,\n                onValueChange = {\n                    inputtingKeyword = it\n                },\n                maxLines = 1,\n                label = {\n                    Text(text = stringResource(LocalizedString.activity_pub_filter_edit_keyword_dialog_hint))\n                },\n                placeholder = {\n                    Text(text = stringResource(LocalizedString.activity_pub_filter_edit_keyword_dialog_hint))\n                },\n            )\n        },\n        onNegativeClick = onDismissRequest,\n        onPositiveClick = {\n            onDismissRequest()\n            if (inputtingKeyword.isNotEmpty()) {\n                onConfirmClick(inputtingKeyword)\n            }\n        },\n    )\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/filters/list/FiltersListScreen.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.filters.list\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Add\nimport androidx.compose.material3.FloatingActionButton\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.SnackbarHost\nimport androidx.compose.material3.SnackbarHostState\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.composable.currentOrThrow\nimport com.zhangke.framework.composable.ConsumeSnackbarFlow\nimport com.zhangke.framework.composable.Toolbar\nimport com.zhangke.framework.composable.freadPlaceholder\nimport com.zhangke.framework.composable.rememberSnackbarHostState\nimport com.zhangke.framework.composable.textString\nimport com.zhangke.framework.nav.LocalNavBackStack\nimport com.zhangke.fread.activitypub.app.internal.screen.filters.edit.EditFilterScreenKey\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.model.PlatformLocator\nimport kotlinx.serialization.Serializable\nimport org.jetbrains.compose.resources.stringResource\n\n@Serializable\ndata class FiltersListScreenKey(val locator: PlatformLocator) : NavKey\n\n@Composable\nfun FiltersListScreen(viewModel: FiltersListViewModel, locator: PlatformLocator) {\n    val backStack = LocalNavBackStack.currentOrThrow\n    val uiState by viewModel.uiState.collectAsState()\n    val snackBarHostState = rememberSnackbarHostState()\n    FiltersListContent(\n        uiState = uiState,\n        snackBarHostState = snackBarHostState,\n        onBackClick = backStack::removeLastOrNull,\n        onItemClick = {\n            backStack.add(EditFilterScreenKey(locator, it.id))\n        },\n        onAddClick = {\n            backStack.add(EditFilterScreenKey(locator, null))\n        },\n    )\n    LaunchedEffect(Unit) {\n        viewModel.onPageResume()\n    }\n    ConsumeSnackbarFlow(snackBarHostState, viewModel.snackBarFlow)\n}\n\n@Composable\nprivate fun FiltersListContent(\n    uiState: FiltersListUiState,\n    snackBarHostState: SnackbarHostState,\n    onBackClick: () -> Unit,\n    onItemClick: (FilterItemUiState) -> Unit,\n    onAddClick: () -> Unit,\n) {\n    Scaffold(\n        snackbarHost = {\n            SnackbarHost(snackBarHostState)\n        },\n        topBar = {\n            Toolbar(\n                title = stringResource(LocalizedString.activity_pub_filters_list_page_title),\n                onBackClick = onBackClick,\n            )\n        },\n        floatingActionButton = {\n            FloatingActionButton(\n                containerColor = MaterialTheme.colorScheme.surface,\n                onClick = {\n                    onAddClick()\n                },\n            ) {\n                Icon(imageVector = Icons.Default.Add, contentDescription = \"Add\")\n            }\n        },\n    ) { innerPadding ->\n        val state = rememberLazyListState()\n        LazyColumn(\n            modifier = Modifier\n                .fillMaxSize()\n                .padding(innerPadding),\n            state = state,\n        ) {\n            if (uiState.initializing && uiState.list.isEmpty()) {\n                items(30) {\n                    FilterItemPlaceholder()\n                }\n            } else if (uiState.list.isNotEmpty()) {\n                items(uiState.list) {\n                    FilterItem(\n                        filterEntity = it,\n                        onItemClick = onItemClick,\n                    )\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun FilterItemPlaceholder() {\n    Column(\n        modifier = Modifier\n            .fillMaxWidth()\n            .padding(16.dp)\n    ) {\n        Box(\n            modifier = Modifier\n                .size(width = 100.dp, height = 18.dp)\n                .freadPlaceholder(true)\n        )\n\n        Box(\n            modifier = Modifier\n                .padding(top = 4.dp)\n                .size(width = 180.dp, height = 16.dp)\n                .freadPlaceholder(true)\n        )\n    }\n}\n\n@Composable\nprivate fun FilterItem(\n    filterEntity: FilterItemUiState,\n    onItemClick: (FilterItemUiState) -> Unit,\n) {\n    Column(\n        modifier = Modifier\n            .clickable { onItemClick(filterEntity) }\n            .fillMaxWidth()\n            .padding(16.dp)\n    ) {\n        Text(\n            text = filterEntity.title,\n            style = MaterialTheme.typography.titleMedium,\n            maxLines = 1,\n            overflow = TextOverflow.Ellipsis,\n        )\n        Text(\n            modifier = Modifier.padding(top = 4.dp),\n            text = textString(text = filterEntity.validateDescription),\n            style = MaterialTheme.typography.bodyMedium,\n            maxLines = 1,\n            overflow = TextOverflow.Ellipsis,\n        )\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/filters/list/FiltersListUiState.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.filters.list\n\nimport com.zhangke.framework.composable.TextString\n\ndata class FiltersListUiState(\n    val initializing: Boolean,\n    val list: List<FilterItemUiState>,\n) {\n\n    companion object {\n\n        fun default(): FiltersListUiState {\n            return FiltersListUiState(\n                initializing = false,\n                list = emptyList(),\n            )\n        }\n    }\n}\n\ndata class FilterItemUiState(\n    val id: String,\n    val title: String,\n    val validateDescription: TextString,\n)\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/filters/list/FiltersListViewModel.kt",
    "content": "@file:OptIn(ExperimentalTime::class)\n\npackage com.zhangke.fread.activitypub.app.internal.screen.filters.list\n\nimport androidx.lifecycle.ViewModel\nimport com.zhangke.activitypub.entities.ActivityPubFilterEntity\nimport com.zhangke.framework.composable.TextString\nimport com.zhangke.framework.composable.emitTextMessageFromThrowable\nimport com.zhangke.framework.composable.textOf\nimport com.zhangke.framework.date.DateParser\nimport com.zhangke.framework.ktx.launchInViewModel\nimport com.zhangke.fread.activitypub.app.internal.auth.ActivityPubClientManager\nimport com.zhangke.fread.common.utils.getCurrentTimeMillis\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.model.PlatformLocator\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.SharedFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.update\nimport kotlin.time.ExperimentalTime\n\nclass FiltersListViewModel (\n    private val clientManager: ActivityPubClientManager,\n    private val locator: PlatformLocator,\n) : ViewModel() {\n\n    private val _uiState = MutableStateFlow(FiltersListUiState.default())\n    val uiState: StateFlow<FiltersListUiState> = _uiState\n\n    private val _snackBarFlow = MutableSharedFlow<TextString>()\n    val snackBarFlow: SharedFlow<TextString> = _snackBarFlow\n\n    init {\n        launchInViewModel {\n            _uiState.update { it.copy(initializing = true) }\n            fetchFilters()\n                .onSuccess { list ->\n                    _uiState.update { it.copy(initializing = false, list = list) }\n                }.onFailure { t ->\n                    _uiState.update { it.copy(initializing = false) }\n                    _snackBarFlow.emitTextMessageFromThrowable(t)\n                }\n        }\n    }\n\n    fun onPageResume() {\n        launchInViewModel {\n            fetchFilters()\n                .onSuccess { list ->\n                    _uiState.update { it.copy(list = list) }\n                }.onFailure { t ->\n                    _snackBarFlow.emitTextMessageFromThrowable(t)\n                }\n        }\n    }\n\n    private suspend fun fetchFilters(): Result<List<FilterItemUiState>> {\n        return clientManager.getClient(locator)\n            .accountRepo\n            .getFilters()\n            .map { list -> list.map { it.toUiState() } }\n    }\n\n    private fun ActivityPubFilterEntity.toUiState(): FilterItemUiState {\n        val expiresAtDate = this.expiresAt?.let(DateParser::parseAll)\n        val validateDescription = if (expiresAtDate != null && expiresAtDate.toEpochMilliseconds() < getCurrentTimeMillis()){\n            textOf(LocalizedString.activity_pub_filters_expired)\n        }else{\n            textOf(LocalizedString.activity_pub_filters_active)\n        }\n        return FilterItemUiState(\n            id = this.id,\n            title = this.title,\n            validateDescription = validateDescription,\n        )\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/hashtag/HashtagTimelineContainerViewModel.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.hashtag\n\nimport com.zhangke.framework.lifecycle.ContainerViewModel\nimport com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubStatusAdapter\nimport com.zhangke.fread.activitypub.app.internal.auth.ActivityPubClientManager\nimport com.zhangke.fread.activitypub.app.internal.auth.LoggedAccountProvider\nimport com.zhangke.fread.activitypub.app.internal.repo.platform.ActivityPubPlatformRepo\nimport com.zhangke.fread.common.adapter.StatusUiStateAdapter\nimport com.zhangke.fread.common.status.StatusUpdater\nimport com.zhangke.fread.commonbiz.shared.usecase.RefactorToNewStatusUseCase\nimport com.zhangke.fread.status.StatusProvider\nimport com.zhangke.fread.status.model.PlatformLocator\n\nclass HashtagTimelineContainerViewModel (\n    private val clientManager: ActivityPubClientManager,\n    private val statusProvider: StatusProvider,\n    private val statusUpdater: StatusUpdater,\n    private val statusAdapter: ActivityPubStatusAdapter,\n    private val platformRepo: ActivityPubPlatformRepo,\n    private val statusUiStateAdapter: StatusUiStateAdapter,\n    private val refactorToNewStatus: RefactorToNewStatusUseCase,\n    private val loggedAccountProvider: LoggedAccountProvider,\n) : ContainerViewModel<HashtagTimelineViewModel, HashtagTimelineContainerViewModel.Params>() {\n\n    override fun createSubViewModel(params: Params): HashtagTimelineViewModel {\n        return HashtagTimelineViewModel(\n            clientManager = clientManager,\n            statusProvider = statusProvider,\n            statusUpdater = statusUpdater,\n            statusAdapter = statusAdapter,\n            platformRepo = platformRepo,\n            statusUiStateAdapter = statusUiStateAdapter,\n            refactorToNewStatus = refactorToNewStatus,\n            locator = params.locator,\n            hashtag = params.tag,\n            loggedAccountProvider = loggedAccountProvider,\n        )\n    }\n\n    fun getViewModel(locator: PlatformLocator, tag: String): HashtagTimelineViewModel {\n        val params = Params(locator, tag)\n        return obtainSubViewModel(params)\n    } class Params(\n        val locator: PlatformLocator,\n        val tag: String,\n    ) : SubViewModelParams() {\n\n        override val key: String\n            get() = locator.toString() + tag\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/hashtag/HashtagTimelineScreen.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.hashtag\n\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material3.ButtonDefaults\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.FilledTonalButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.SnackbarHost\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.contentColorFor\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.blur.BlurController\nimport com.zhangke.framework.blur.LocalBlurController\nimport com.zhangke.framework.blur.applyBlurEffect\nimport com.zhangke.framework.blur.blurEffectContainerColor\nimport com.zhangke.framework.blur.rememberBlurController\nimport com.zhangke.framework.composable.AlertConfirmDialog\nimport com.zhangke.framework.composable.ConsumeSnackbarFlow\nimport com.zhangke.framework.composable.LocalContentPadding\nimport com.zhangke.framework.composable.LocalSnackbarHostState\nimport com.zhangke.framework.composable.SingleRowTopAppBar\nimport com.zhangke.framework.composable.TextString\nimport com.zhangke.framework.composable.Toolbar\nimport com.zhangke.framework.composable.TopAppBarColors\nimport com.zhangke.framework.composable.currentOrThrow\nimport com.zhangke.framework.composable.plusContentPadding\nimport com.zhangke.framework.composable.rememberSnackbarHostState\nimport com.zhangke.framework.nav.LocalNavBackStack\nimport com.zhangke.fread.commonbiz.shared.composable.FeedsContent\nimport com.zhangke.fread.commonbiz.shared.feeds.CommonFeedsUiState\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.ui.ComposedStatusInteraction\nimport kotlinx.coroutines.flow.SharedFlow\nimport kotlinx.serialization.Serializable\nimport org.jetbrains.compose.resources.stringResource\n\n@Serializable\ndata class HashtagTimelineScreenKey(\n    val locator: PlatformLocator,\n    val hashtag: String,\n) : NavKey\n\n@Composable\nfun HashtagTimelineScreen(\n    viewModel: HashtagTimelineContainerViewModel,\n    locator: PlatformLocator,\n    hashtag: String,\n) {\n    val backStack = LocalNavBackStack.currentOrThrow\n    val viewModel = viewModel.getViewModel(locator, hashtag)\n    val hashtagTimelineUiState by viewModel.hashtagTimelineUiState.collectAsState()\n    val statusUiState by viewModel.uiState.collectAsState()\n    HashtagTimelineContent(\n        hashtagTimelineUiState = hashtagTimelineUiState,\n        statusUiState = statusUiState,\n        messageFlow = viewModel.errorMessageFlow,\n        openScreenFlow = viewModel.openScreenFlow,\n        newStatusNotifyFlow = viewModel.newStatusNotifyFlow,\n        onBackClick = backStack::removeLastOrNull,\n        onRefresh = viewModel::onRefresh,\n        onLoadMore = viewModel::onLoadMore,\n        composedStatusInteraction = viewModel.composedStatusInteraction,\n        onFollowClick = viewModel::onFollowClick,\n        onUnfollowClick = viewModel::onUnfollowClick,\n    )\n    ConsumeSnackbarFlow(LocalSnackbarHostState.current, viewModel.errorMessageFlow)\n}\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nprivate fun HashtagTimelineContent(\n    hashtagTimelineUiState: HashtagTimelineUiState,\n    statusUiState: CommonFeedsUiState,\n    messageFlow: SharedFlow<TextString>,\n    openScreenFlow: SharedFlow<NavKey>,\n    newStatusNotifyFlow: SharedFlow<Unit>,\n    onBackClick: () -> Unit,\n    onRefresh: () -> Unit,\n    onLoadMore: () -> Unit,\n    composedStatusInteraction: ComposedStatusInteraction,\n    onFollowClick: () -> Unit,\n    onUnfollowClick: () -> Unit,\n) {\n    val snackbarHostState = rememberSnackbarHostState()\n    val topBarColor = MaterialTheme.colorScheme.surface\n    val blurController = rememberBlurController()\n    CompositionLocalProvider(\n        LocalBlurController provides blurController\n    ) {\n        Scaffold(\n            modifier = Modifier,\n            snackbarHost = {\n                SnackbarHost(snackbarHostState)\n            },\n            topBar = {\n                SingleRowTopAppBar(\n                    modifier = Modifier.fillMaxWidth()\n                        .applyBlurEffect(containerColor = topBarColor),\n                    colors = TopAppBarColors.default(\n                        containerColor = blurEffectContainerColor(containerColor = topBarColor),\n                    ),\n                    title = {\n                        Column {\n                            Text(\n                                hashtagTimelineUiState.hashTag,\n                                maxLines = 1,\n                                overflow = TextOverflow.Ellipsis,\n                            )\n                            Text(\n                                modifier = Modifier\n                                    .padding(top = 2.dp),\n                                text = hashtagTimelineUiState.description,\n                                style = MaterialTheme.typography.bodySmall,\n                                color = MaterialTheme.colorScheme.onSurfaceVariant,\n                            )\n                        }\n                    },\n                    navigationIcon = {\n                        Toolbar.BackButton(onBackClick = onBackClick)\n                    },\n                    actions = {\n                        FollowHashtagButton(\n                            modifier = Modifier.padding(end = 8.dp),\n                            uiState = hashtagTimelineUiState,\n                            onFollowClick = onFollowClick,\n                            onUnfollowClick = onUnfollowClick,\n                        )\n                    },\n                )\n            },\n        ) { innerPadding ->\n            CompositionLocalProvider(\n                LocalContentPadding provides plusContentPadding(innerPadding),\n            ) {\n                FeedsContent(\n                    uiState = statusUiState,\n                    openScreenFlow = openScreenFlow,\n                    newStatusNotifyFlow = newStatusNotifyFlow,\n                    composedStatusInteraction = composedStatusInteraction,\n                    onRefresh = onRefresh,\n                    onLoadMore = onLoadMore,\n                    nestedScrollConnection = null,\n                )\n            }\n        }\n    }\n    ConsumeSnackbarFlow(snackbarHostState, messageFlow)\n}\n\n@Composable\nprivate fun FollowHashtagButton(\n    modifier: Modifier,\n    uiState: HashtagTimelineUiState,\n    onFollowClick: () -> Unit,\n    onUnfollowClick: () -> Unit,\n) {\n    var showUnfollowDialog by remember { mutableStateOf(false) }\n    val containerColor = if (uiState.following) {\n        MaterialTheme.colorScheme.secondaryContainer\n    } else {\n        MaterialTheme.colorScheme.primaryContainer\n    }\n    FilledTonalButton(\n        modifier = modifier,\n        onClick = {\n            if (uiState.following) {\n                showUnfollowDialog = true\n            } else {\n                onFollowClick()\n            }\n        },\n        colors = ButtonDefaults.outlinedButtonColors(\n            containerColor = containerColor,\n            contentColor = MaterialTheme.colorScheme.contentColorFor(containerColor),\n        ),\n    ) {\n        Text(\n            text = if (uiState.following) {\n                stringResource(LocalizedString.statusUiUserDetailRelationshipFollowing)\n            } else {\n                stringResource(LocalizedString.statusUiUserDetailRelationshipNotFollow)\n            },\n        )\n    }\n    if (showUnfollowDialog) {\n        AlertConfirmDialog(\n            content = stringResource(LocalizedString.activity_pub_hashtag_unfollow_dialog_message),\n            onConfirm = onUnfollowClick,\n            onDismissRequest = { showUnfollowDialog = false },\n        )\n    }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/hashtag/HashtagTimelineUiState.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.hashtag\n\nimport com.zhangke.fread.status.model.PlatformLocator\n\ndata class HashtagTimelineUiState(\n    val locator: PlatformLocator,\n    val hashTag: String,\n    val following: Boolean,\n    val description: String,\n)\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/hashtag/HashtagTimelineViewModel.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.hashtag\n\nimport com.zhangke.activitypub.entities.ActivityPubTagEntity\nimport com.zhangke.framework.composable.emitTextMessageFromThrowable\nimport com.zhangke.framework.ktx.launchInViewModel\nimport com.zhangke.framework.lifecycle.SubViewModel\nimport com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubStatusAdapter\nimport com.zhangke.fread.activitypub.app.internal.auth.ActivityPubClientManager\nimport com.zhangke.fread.activitypub.app.internal.auth.LoggedAccountProvider\nimport com.zhangke.fread.activitypub.app.internal.repo.platform.ActivityPubPlatformRepo\nimport com.zhangke.fread.common.adapter.StatusUiStateAdapter\nimport com.zhangke.fread.common.feeds.model.RefreshResult\nimport com.zhangke.fread.common.status.StatusConfigurationDefault\nimport com.zhangke.fread.common.status.StatusUpdater\nimport com.zhangke.fread.common.utils.getCurrentInstant\nimport com.zhangke.fread.commonbiz.shared.feeds.FeedsViewModelController\nimport com.zhangke.fread.commonbiz.shared.feeds.IFeedsViewModelController\nimport com.zhangke.fread.commonbiz.shared.usecase.RefactorToNewStatusUseCase\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.StatusProvider\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.model.StatusUiState\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.update\nimport kotlinx.coroutines.launch\nimport kotlinx.datetime.LocalDateTime\nimport kotlinx.datetime.TimeZone\nimport kotlinx.datetime.toInstant\nimport kotlinx.datetime.toLocalDateTime\nimport org.jetbrains.compose.resources.getString\nimport kotlin.time.ExperimentalTime\n\nclass HashtagTimelineViewModel(\n    private val clientManager: ActivityPubClientManager,\n    private val statusProvider: StatusProvider,\n    private val statusUpdater: StatusUpdater,\n    private val statusAdapter: ActivityPubStatusAdapter,\n    private val platformRepo: ActivityPubPlatformRepo,\n    private val loggedAccountProvider: LoggedAccountProvider,\n    statusUiStateAdapter: StatusUiStateAdapter,\n    refactorToNewStatus: RefactorToNewStatusUseCase,\n    private val locator: PlatformLocator,\n    private val hashtag: String,\n) : SubViewModel(), IFeedsViewModelController by FeedsViewModelController(\n    statusProvider = statusProvider,\n    statusUpdater = statusUpdater,\n    statusUiStateAdapter = statusUiStateAdapter,\n    refactorToNewStatus = refactorToNewStatus,\n) {\n\n    private val _hashtagTimelineUiState = MutableStateFlow(\n        HashtagTimelineUiState(\n            locator = locator,\n            hashTag = hashtag,\n            following = false,\n            description = \"\",\n        )\n    )\n    val hashtagTimelineUiState = _hashtagTimelineUiState.asStateFlow()\n\n    init {\n        initController(\n            coroutineScope = viewModelScope,\n            locatorResolver = { locator },\n            loadFirstPageLocalFeeds = { Result.success(emptyList()) },\n            loadNewFromServerFunction = ::loadNewFromServer,\n            loadMoreFunction = ::loadMore,\n            onStatusUpdate = {},\n        )\n        initFeeds(false)\n        launchInViewModel {\n            clientManager.getClient(locator)\n                .accountRepo\n                .getTagInformation(hashtag)\n                .onSuccess {\n                    _hashtagTimelineUiState.value = _hashtagTimelineUiState.value.copy(\n                        following = it.following,\n                        description = buildDescription(it),\n                    )\n                }\n        }\n    }\n\n    private suspend fun loadNewFromServer(): Result<RefreshResult> {\n        return loadHashtagTimeline().map {\n            RefreshResult(\n                newStatus = it,\n                deletedStatus = emptyList(),\n            )\n        }\n    }\n\n    private suspend fun loadMore(maxId: String?): Result<List<StatusUiState>> {\n        return loadHashtagTimeline(maxId)\n    }\n\n    private suspend fun buildDescription(hashTag: ActivityPubTagEntity): String {\n        val todayTimeInMillis = getTodayTimeInMillis()\n        var posts = 0\n        var participants = 0\n        var todayPosts = 0\n        hashTag.history.forEach {\n            posts += it.uses\n            participants += it.accounts\n            if ((it.day * 1000) >= todayTimeInMillis) {\n                todayPosts += it.uses\n            }\n        }\n        return getString(\n            LocalizedString.activity_pub_hashtag_timeline_description,\n            posts.toString(),\n            participants.toString(),\n            todayPosts.toString(),\n        )\n    }\n\n    @OptIn(ExperimentalTime::class)\n    private fun getTodayTimeInMillis(): Long {\n        val timeZone = TimeZone.currentSystemDefault()\n        val today = getCurrentInstant().toLocalDateTime(timeZone)\n        return LocalDateTime(today.year, today.month, today.dayOfMonth, 0, 0, 0)\n            .toInstant(timeZone)\n            .toEpochMilliseconds()\n    }\n\n    fun onFollowClick() {\n        launchInViewModel {\n            clientManager.getClient(locator)\n                .accountRepo\n                .followTag(hashtag)\n                .handle()\n        }\n    }\n\n    fun onUnfollowClick() {\n        launchInViewModel {\n            clientManager.getClient(locator)\n                .accountRepo\n                .unfollowTag(hashtag)\n                .handle()\n        }\n    }\n\n    private suspend fun Result<ActivityPubTagEntity>.handle() {\n        this.onSuccess { newEntity ->\n            _hashtagTimelineUiState.update { state ->\n                state.copy(\n                    following = newEntity.following,\n                    description = buildDescription(newEntity),\n                )\n            }\n        }.onFailure { e ->\n            viewModelScope.launch {\n                mutableErrorMessageFlow.emitTextMessageFromThrowable(e)\n            }\n        }\n    }\n\n    private suspend fun loadHashtagTimeline(maxId: String? = null): Result<List<StatusUiState>> {\n        val platformResult = platformRepo.getPlatform(locator)\n        if (platformResult.isFailure) {\n            return Result.failure(platformResult.exceptionOrNull()!!)\n        }\n        val platform = platformResult.getOrThrow()\n        val account = locator.accountUri?.let { loggedAccountProvider.getAccount(it) }\n        return clientManager.getClient(locator)\n            .timelinesRepo\n            .getTagTimeline(\n                hashtag = hashtag,\n                limit = StatusConfigurationDefault.config.loadFromServerLimit,\n                maxId = maxId,\n            ).map { list ->\n                list.map {\n                    statusAdapter.toStatusUiState(\n                        entity = it,\n                        platform = platform,\n                        locator = locator,\n                        loggedAccount = account,\n                    )\n                }\n            }\n    }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/instance/InstanceDetailScreen.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.instance\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.MoreVert\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport androidx.navigation3.runtime.NavKey\nimport com.seiko.imageloader.ui.AutoSizeImage\nimport com.zhangke.framework.composable.PopupMenu\nimport com.zhangke.framework.composable.SimpleIconButton\nimport com.zhangke.framework.composable.currentOrThrow\nimport com.zhangke.framework.composable.freadPlaceholder\nimport com.zhangke.framework.composable.noRippleClick\nimport com.zhangke.framework.composable.rememberSnackbarHostState\nimport com.zhangke.framework.nav.ContentPaddingsHorizontalPagerWithTab\nimport com.zhangke.framework.nav.LocalNavBackStack\nimport com.zhangke.framework.network.FormalBaseUrl\nimport com.zhangke.framework.utils.WebFinger\nimport com.zhangke.fread.activitypub.app.internal.screen.instance.about.ServerAboutTab\nimport com.zhangke.fread.activitypub.app.internal.screen.instance.tags.ServerTrendsTagsTab\nimport com.zhangke.fread.activitypub.app.internal.screen.user.UserDetailScreenKey\nimport com.zhangke.fread.common.browser.LocalActivityBrowserLauncher\nimport com.zhangke.fread.common.handler.LocalTextHandler\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.richtext.buildRichText\nimport com.zhangke.fread.status.ui.action.DropDownCopyLinkItem\nimport com.zhangke.fread.status.ui.action.DropDownOpenInBrowserItem\nimport com.zhangke.fread.status.ui.common.DetailPageScaffold\nimport com.zhangke.fread.status.ui.richtext.FreadRichText\nimport kotlinx.coroutines.launch\nimport kotlinx.serialization.Serializable\nimport org.jetbrains.compose.resources.stringResource\n\n@Serializable\ndata class InstanceDetailScreenKey(\n    val locator: PlatformLocator,\n    val baseUrl: FormalBaseUrl,\n) : NavKey\n\n@Composable\nfun InstanceDetailScreen(locator: PlatformLocator, viewModel: InstanceDetailViewModel) {\n    val uiState by viewModel.uiState.collectAsState()\n    val backStack = LocalNavBackStack.currentOrThrow\n    InstanceDetailContent(\n        uiState = uiState,\n        locator = locator,\n        onBackClick = { backStack.removeLastOrNull() },\n        onUserClick = { _, webFinger, userId ->\n            backStack.add(\n                UserDetailScreenKey(\n                    locator = locator,\n                    webFinger = webFinger,\n                    userId = userId,\n                )\n            )\n        }\n    )\n}\n\n@Composable\nprivate fun InstanceDetailContent(\n    uiState: InstanceDetailUiState,\n    locator: PlatformLocator,\n    onBackClick: () -> Unit,\n    onUserClick: (PlatformLocator, WebFinger, String?) -> Unit,\n) {\n    val browserLauncher = LocalActivityBrowserLauncher.current\n    val contentCanScrollBackward = remember { mutableStateOf(false) }\n    val coroutineScope = rememberCoroutineScope()\n    DetailPageScaffold(\n        modifier = Modifier.fillMaxSize(),\n        snackbarHostState = rememberSnackbarHostState(),\n        title = buildRichText(uiState.instance?.title.orEmpty()),\n        loading = uiState.loading,\n        avatar = uiState.instance?.thumbnail?.url.orEmpty(),\n        banner = uiState.instance?.thumbnail?.url,\n        privateNote = null,\n        topBarActions = {\n            val baseUrl = uiState.baseUrl\n            InstanceDetailActions(baseUrl)\n        },\n        contentCanScrollBackward = contentCanScrollBackward,\n        description = buildRichText(uiState.instance?.description.orEmpty()),\n        handleLine = {\n            Text(\n                text = uiState.baseUrl.toString(),\n                style = MaterialTheme.typography.labelMedium,\n            )\n        },\n        followInfoLine = {\n            InstanceModLine(\n                uiState = uiState,\n                onUserClick = onUserClick,\n            )\n        },\n        onBackClick = onBackClick,\n        onUrlClick = {\n            coroutineScope.launch {\n                browserLauncher.launchWebTabInApp(it, locator)\n            }\n        },\n        onMaybeHashtagClick = {},\n        onBannerClick = {},\n        onAvatarClick = {},\n        topDetailContentAction = null,\n        bottomArea = null,\n    ) { progress ->\n        if (uiState.instance != null) {\n            val tabs = remember(uiState) {\n                listOf(\n                    ServerAboutTab(\n                        baseUrl = uiState.baseUrl,\n                        rules = uiState.instance.rules,\n                        contentCanScrollBackward = contentCanScrollBackward,\n                    ),\n                    ServerTrendsTagsTab(\n                        baseUrl = uiState.baseUrl,\n                        contentCanScrollBackward = contentCanScrollBackward,\n                    ),\n                )\n            }\n            ContentPaddingsHorizontalPagerWithTab(\n                tabList = tabs,\n                blurEnabled = progress >= 1F,\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun InstanceModLine(\n    uiState: InstanceDetailUiState,\n    onUserClick: (PlatformLocator, WebFinger, String?) -> Unit,\n) {\n    val instance = uiState.instance\n    val loading = uiState.loading\n    Column {\n        val languageString = instance?.languages?.joinToString(\", \").orEmpty()\n        Text(\n            modifier = Modifier\n                .padding(top = 4.dp)\n                .freadPlaceholder(visible = loading),\n            text = stringResource(\n                LocalizedString.activity_pub_instance_detail_language_label,\n                languageString\n            ),\n            maxLines = 3,\n            style = MaterialTheme.typography.bodyMedium,\n        )\n        Text(\n            modifier = Modifier\n                .padding(top = 4.dp)\n                .freadPlaceholder(visible = loading),\n            text = stringResource(\n                LocalizedString.activity_pub_instance_detail_active_month_label,\n                instance?.usage?.users?.activeMonth.toString()\n            ),\n            style = MaterialTheme.typography.bodyMedium,\n        )\n\n        if (instance?.contact != null) {\n            Row(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .padding(top = 6.dp)\n                    .freadPlaceholder(visible = loading)\n                    .noRippleClick {\n                        val account = uiState.modAccount ?: return@noRippleClick\n                        val role =\n                            PlatformLocator(accountUri = account.uri, baseUrl = uiState.baseUrl)\n                        onUserClick(role, account.webFinger, account.userId)\n                    },\n                verticalAlignment = Alignment.CenterVertically,\n            ) {\n                Text(\n                    modifier = Modifier\n                        .background(\n                            MaterialTheme.colorScheme.primaryContainer,\n                            RoundedCornerShape(4.dp),\n                        )\n                        .padding(horizontal = 6.dp),\n                    text = \"MOD\",\n                    fontSize = 10.sp,\n                    color = MaterialTheme.colorScheme.onPrimary,\n                )\n\n                AutoSizeImage(\n                    instance.contact?.account?.avatar.orEmpty(),\n                    modifier = Modifier\n                        .padding(start = 6.dp)\n                        .size(22.dp)\n                        .clip(CircleShape),\n                    contentDescription = \"Mod avatar\",\n                )\n                FreadRichText(\n                    modifier = Modifier\n                        .padding(start = 4.dp),\n                    content = instance.contact?.account?.displayName.orEmpty(),\n                    emojis = uiState.modAccount?.emojis ?: emptyList(),\n                    onUrlClick = {},\n                )\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun InstanceDetailActions(baseUrl: FormalBaseUrl) {\n    val activityTextHandler = LocalTextHandler.current\n    val browserLauncher = LocalActivityBrowserLauncher.current\n    var showMorePopup by remember {\n        mutableStateOf(false)\n    }\n    SimpleIconButton(\n        onClick = { showMorePopup = true },\n        imageVector = Icons.Default.MoreVert,\n        contentDescription = \"More Options\"\n    )\n    PopupMenu(\n        expanded = showMorePopup,\n        onDismissRequest = { showMorePopup = false },\n    ) {\n        DropDownOpenInBrowserItem {\n            showMorePopup = false\n            browserLauncher.launchBySystemBrowser(baseUrl.toString())\n        }\n\n        DropDownCopyLinkItem {\n            showMorePopup = false\n            activityTextHandler.copyText(baseUrl.toString())\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/instance/InstanceDetailUiState.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.instance\n\nimport com.zhangke.activitypub.entities.ActivityPubInstanceEntity\nimport com.zhangke.framework.network.FormalBaseUrl\nimport com.zhangke.fread.status.author.BlogAuthor\n\ndata class InstanceDetailUiState(\n    val loading: Boolean,\n    val baseUrl: FormalBaseUrl,\n    val instance: ActivityPubInstanceEntity?,\n    val modAccount: BlogAuthor?,\n)\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/instance/InstanceDetailViewModel.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.instance\n\nimport androidx.lifecycle.ViewModel\nimport com.zhangke.framework.ktx.launchInViewModel\nimport com.zhangke.framework.network.FormalBaseUrl\nimport com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubAccountEntityAdapter\nimport com.zhangke.fread.activitypub.app.internal.repo.platform.ActivityPubPlatformRepo\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asStateFlow\n\nclass InstanceDetailViewModel (\n    private val platformRepo: ActivityPubPlatformRepo,\n    private val authorAdapter: ActivityPubAccountEntityAdapter,\n    private val serverBaseUrl: FormalBaseUrl,\n) : ViewModel() {\n\n    private val _uiState = MutableStateFlow(\n        InstanceDetailUiState(\n            loading = false,\n            baseUrl = serverBaseUrl,\n            instance = null,\n            modAccount = null,\n        )\n    )\n    val uiState = _uiState.asStateFlow()\n\n    init {\n        _uiState.value = _uiState.value.copy(\n            loading = true,\n            baseUrl = serverBaseUrl\n        )\n        launchInViewModel {\n            val platform = platformRepo.getInstanceEntity(serverBaseUrl).getOrNull()\n            _uiState.value = _uiState.value.copy(\n                loading = false,\n                instance = platform,\n                modAccount = platform?.contact?.account?.let { authorAdapter.toAuthor(it) }\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/instance/about/ServerAboutPage.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.instance.about\n\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.text.selection.SelectionContainer\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material3.HorizontalDivider\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.MutableState\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport com.zhangke.activitypub.entities.ActivityPubAnnouncementEntity\nimport com.zhangke.activitypub.entities.ActivityPubInstanceEntity\nimport com.zhangke.framework.composable.LocalContentPadding\nimport com.zhangke.framework.nav.BaseTab\nimport com.zhangke.framework.nav.TabOptions\nimport com.zhangke.framework.network.FormalBaseUrl\nimport com.zhangke.fread.common.browser.LocalActivityBrowserLauncher\nimport com.zhangke.fread.common.browser.launchWebTabInApp\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.ui.richtext.FreadRichText\nimport org.jetbrains.compose.resources.stringResource\nimport org.koin.compose.viewmodel.koinViewModel\n\nclass ServerAboutTab(\n    val baseUrl: FormalBaseUrl,\n    val rules: List<ActivityPubInstanceEntity.Rule> = emptyList(),\n    val contentCanScrollBackward: MutableState<Boolean>,\n) : BaseTab() {\n\n    override val options: TabOptions\n        @Composable\n        get() = TabOptions(\n            title = stringResource(LocalizedString.activity_pub_about),\n        )\n\n    @Composable\n    override fun Content() {\n        super.Content()\n        val viewModel: ServerAboutViewModel = koinViewModel()\n        LaunchedEffect(viewModel) {\n            viewModel.rules = rules\n            viewModel.baseUrl = baseUrl\n            viewModel.onPageResume()\n        }\n        val uiState by viewModel.uiState.collectAsState()\n        ServerAboutPageContent(\n            uiState = uiState,\n            contentCanScrollBackward = contentCanScrollBackward,\n            baseUrl = baseUrl,\n        )\n    }\n\n    @Composable\n    private fun ServerAboutPageContent(\n        uiState: ServerAboutUiState,\n        contentCanScrollBackward: MutableState<Boolean>,\n        baseUrl: FormalBaseUrl,\n    ) {\n        val scrollState = rememberScrollState()\n        contentCanScrollBackward.value = scrollState.value > 0\n        Column(\n            modifier = Modifier\n                .fillMaxSize()\n                .padding(LocalContentPadding.current)\n                .verticalScroll(scrollState),\n        ) {\n            if (uiState.announcement.isNotEmpty()) {\n                ServerAboutAnnouncementSection(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .padding(start = 16.dp, top = 16.dp, end = 16.dp, bottom = 8.dp),\n                    announcementList = uiState.announcement,\n                    baseUrl = baseUrl,\n                )\n            }\n            if (uiState.rules.isNotEmpty()) {\n                ServerAboutRulesSection(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .padding(start = 16.dp, top = 6.dp, end = 16.dp, bottom = 36.dp),\n                    ruleList = uiState.rules,\n                )\n            }\n        }\n    }\n\n    @Composable\n    private fun ServerAboutAnnouncementSection(\n        modifier: Modifier = Modifier,\n        announcementList: List<ActivityPubAnnouncementEntity>,\n        baseUrl: FormalBaseUrl,\n    ) {\n        Column(modifier = modifier) {\n            announcementList.forEach {\n                ServerAboutAnnouncement(\n                    modifier = Modifier.fillMaxWidth(),\n                    entity = it,\n                    baseUrl = baseUrl,\n                )\n            }\n        }\n    }\n\n    @Composable\n    private fun ServerAboutAnnouncement(\n        modifier: Modifier = Modifier,\n        entity: ActivityPubAnnouncementEntity,\n        baseUrl: FormalBaseUrl,\n    ) {\n        val browserLauncher = LocalActivityBrowserLauncher.current\n        val coroutineScope = rememberCoroutineScope()\n        SelectionContainer {\n            FreadRichText(\n                modifier = modifier,\n                content = entity.content,\n                onUrlClick = {\n                    val locator = PlatformLocator(accountUri = null, baseUrl = baseUrl)\n                    browserLauncher.launchWebTabInApp(coroutineScope, it, locator)\n                },\n            )\n        }\n    }\n\n    @Composable\n    private fun ServerAboutRulesSection(\n        modifier: Modifier = Modifier,\n        ruleList: List<ActivityPubInstanceEntity.Rule>,\n    ) {\n        SelectionContainer {\n            Column(modifier = modifier) {\n                Text(\n                    modifier = Modifier,\n                    text = stringResource(LocalizedString.activity_pub_about_rule_title),\n                    fontWeight = FontWeight.Bold,\n                    style = MaterialTheme.typography.titleLarge,\n                )\n                Spacer(modifier = Modifier.height(4.dp))\n                HorizontalDivider()\n                Spacer(modifier = Modifier.height(4.dp))\n                ruleList.forEachIndexed { index, rule ->\n                    ServerAboutRule(\n                        modifier = Modifier,\n                        rule = rule,\n                        index = index,\n                    )\n                    Spacer(modifier = Modifier.height(4.dp))\n                    if (index != ruleList.lastIndex) {\n                        HorizontalDivider()\n                        Spacer(modifier = Modifier.height(4.dp))\n                    }\n                }\n                Spacer(modifier = Modifier.height(80.dp))\n            }\n        }\n    }\n\n    @Composable\n    private fun ServerAboutRule(\n        modifier: Modifier = Modifier,\n        rule: ActivityPubInstanceEntity.Rule,\n        index: Int,\n    ) {\n        Row(modifier = modifier) {\n            Text(\n                text = \"${index + 1}.\",\n                fontSize = 18.sp,\n                fontWeight = FontWeight.Bold,\n                color = Color.Black,\n            )\n\n            Text(\n                modifier = Modifier.padding(start = 6.dp),\n                text = rule.text,\n                fontSize = 14.sp,\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/instance/about/ServerAboutUiState.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.instance.about\n\nimport com.zhangke.activitypub.entities.ActivityPubAnnouncementEntity\nimport com.zhangke.activitypub.entities.ActivityPubInstanceEntity\nimport com.zhangke.fread.activitypub.app.internal.model.ActivityPubInstanceRule\n\ndata class ServerAboutUiState(\n    val announcement: List<ActivityPubAnnouncementEntity>,\n    val rules: List<ActivityPubInstanceEntity.Rule>,\n)\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/instance/about/ServerAboutViewModel.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.instance.about\n\nimport androidx.lifecycle.ViewModel\nimport com.zhangke.activitypub.entities.ActivityPubInstanceEntity\nimport com.zhangke.framework.ktx.launchInViewModel\nimport com.zhangke.framework.network.FormalBaseUrl\nimport com.zhangke.fread.activitypub.app.internal.repo.account.ActivityPubLoggedAccountRepo\nimport com.zhangke.fread.activitypub.app.internal.usecase.GetInstanceAnnouncementUseCase\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.update\n\nclass ServerAboutViewModel (\n    private val accountRepo: ActivityPubLoggedAccountRepo,\n    private val getInstanceAnnouncementUseCase: GetInstanceAnnouncementUseCase,\n) : ViewModel() {\n\n    lateinit var baseUrl: FormalBaseUrl\n\n    var rules: List<ActivityPubInstanceEntity.Rule> = emptyList()\n\n    private val _uiState = MutableStateFlow(ServerAboutUiState(emptyList(), emptyList()))\n    val uiState: StateFlow<ServerAboutUiState> = _uiState\n\n    fun onPageResume() {\n        _uiState.update {\n            it.copy(rules = rules)\n        }\n        requestAnnouncement()\n    }\n\n    private fun requestAnnouncement() {\n        launchInViewModel {\n            if (accountRepo.queryAll().isEmpty()) return@launchInViewModel\n            getInstanceAnnouncementUseCase(baseUrl)\n                .onSuccess { announcements ->\n                    _uiState.update {\n                        it.copy(announcement = announcements)\n                    }\n                }\n        }\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/instance/tags/ServerTrendsTagsPage.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.instance.tags\n\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.MutableState\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport com.zhangke.framework.composable.LocalContentPadding\nimport com.zhangke.framework.composable.currentOrThrow\nimport com.zhangke.framework.nav.BaseTab\nimport com.zhangke.framework.nav.LocalNavBackStack\nimport com.zhangke.framework.nav.TabOptions\nimport com.zhangke.framework.network.FormalBaseUrl\nimport com.zhangke.fread.activitypub.app.internal.screen.hashtag.HashtagTimelineScreenKey\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.model.Hashtag\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.ui.hashtag.HashtagUi\nimport org.jetbrains.compose.resources.stringResource\nimport org.koin.compose.viewmodel.koinViewModel\n\nclass ServerTrendsTagsTab(\n    val baseUrl: FormalBaseUrl,\n    val contentCanScrollBackward: MutableState<Boolean>,\n) : BaseTab() {\n\n    override val options: TabOptions\n        @Composable\n        get() = TabOptions(\n            title = stringResource(LocalizedString.activity_pub_trends_tag),\n        )\n\n    @Composable\n    override fun Content() {\n        super.Content()\n        val backStack = LocalNavBackStack.currentOrThrow\n        val viewModel = koinViewModel<ServerTrendsTagsViewModel>()\n        viewModel.baseUrl = baseUrl\n        val uiState by viewModel.uiState.collectAsState()\n        LaunchedEffect(Unit) {\n            viewModel.onPageResume()\n        }\n        ServerTrendsTagsContent(\n            uiState = uiState,\n            contentCanScrollBackward = contentCanScrollBackward,\n            onHashtagClick = { tag ->\n                val locator = PlatformLocator(accountUri = null, baseUrl = baseUrl)\n                backStack.add(\n                    HashtagTimelineScreenKey(\n                        locator = locator,\n                        hashtag = tag.name.removePrefix(\"#\"),\n                    )\n                )\n            },\n        )\n    }\n\n    @Composable\n    private fun ServerTrendsTagsContent(\n        uiState: ServerTrendsTagsUiState,\n        contentCanScrollBackward: MutableState<Boolean>,\n        onHashtagClick: (Hashtag) -> Unit,\n    ) {\n        val listState = rememberLazyListState()\n        val canScrollBackward by remember {\n            derivedStateOf {\n                listState.firstVisibleItemIndex != 0 || listState.firstVisibleItemScrollOffset != 0\n            }\n        }\n        contentCanScrollBackward.value = canScrollBackward\n        LazyColumn(\n            modifier = Modifier.fillMaxSize(),\n            state = listState,\n            contentPadding = LocalContentPadding.current,\n        ) {\n            items(uiState.list) { item ->\n                HashtagUi(\n                    tag = item,\n                    onClick = onHashtagClick,\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/instance/tags/ServerTrendsTagsUiState.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.instance.tags\n\nimport com.zhangke.fread.status.model.Hashtag\n\ndata class ServerTrendsTagsUiState(\n    val list: List<Hashtag>\n)\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/instance/tags/ServerTrendsTagsViewModel.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.instance.tags\n\nimport androidx.lifecycle.ViewModel\nimport com.zhangke.framework.ktx.launchInViewModel\nimport com.zhangke.framework.network.FormalBaseUrl\nimport com.zhangke.fread.activitypub.app.internal.usecase.GetServerTrendTagsUseCase\nimport com.zhangke.fread.status.model.PlatformLocator\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.update\n\nclass ServerTrendsTagsViewModel (\n    private val getServerTrendsTags: GetServerTrendTagsUseCase,\n) : ViewModel() {\n\n    lateinit var baseUrl: FormalBaseUrl\n\n    private val _uiState = MutableStateFlow(ServerTrendsTagsUiState(emptyList()))\n    val uiState: StateFlow<ServerTrendsTagsUiState> = _uiState\n\n    fun onPageResume() {\n        launchInViewModel {\n            getServerTrendsTags(PlatformLocator(accountUri = null, baseUrl = baseUrl))\n                .onSuccess { list ->\n                    _uiState.update { it.copy(list = list) }\n                }.onFailure {\n\n                }\n        }\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/list/CreatedListsScreen.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.list\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Add\nimport androidx.compose.material3.FloatingActionButton\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.SnackbarHost\nimport androidx.compose.material3.SnackbarHostState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.vector.rememberVectorPainter\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.composable.currentOrThrow\nimport com.zhangke.activitypub.entities.ActivityPubListEntity\nimport com.zhangke.framework.architect.json.globalJson\nimport com.zhangke.framework.composable.ConsumeSnackbarFlow\nimport com.zhangke.framework.composable.DefaultFailed\nimport com.zhangke.framework.composable.Toolbar\nimport com.zhangke.framework.composable.rememberSnackbarHostState\nimport com.zhangke.framework.nav.LocalNavBackStack\nimport com.zhangke.fread.activitypub.app.internal.screen.list.add.AddListScreenNavKey\nimport com.zhangke.fread.activitypub.app.internal.screen.list.edit.EditListScreenNavKey\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.model.PlatformLocator\nimport kotlinx.serialization.Serializable\nimport org.jetbrains.compose.resources.stringResource\n\n@Serializable\ndata class CreatedListsScreenKey(val locator: PlatformLocator) : NavKey\n\n@Composable\nfun CreatedListsScreen(\n    viewModel: CreatedListsViewModel,\n    locator: PlatformLocator,\n) {\n    val backStack = LocalNavBackStack.currentOrThrow\n    val uiState by viewModel.uiState.collectAsState()\n    val snackBarState = rememberSnackbarHostState()\n    CreatedListsContent(\n        uiState = uiState,\n        snackBarState = snackBarState,\n        onBackClick = backStack::removeLastOrNull,\n        onRetryClick = viewModel::onRetryClick,\n        onListClick = {\n            backStack.add(\n                EditListScreenNavKey(\n                    locator = locator,\n                    serializedList = globalJson.encodeToString(it)\n                )\n            )\n        },\n        onAddListClick = { backStack.add(AddListScreenNavKey(locator)) },\n    )\n    LaunchedEffect(Unit) { viewModel.onPageResume() }\n    ConsumeSnackbarFlow(snackBarState, viewModel.snackBarFlow)\n}\n\n@Composable\nprivate fun CreatedListsContent(\n    uiState: CreatedListsUiState,\n    snackBarState: SnackbarHostState,\n    onBackClick: () -> Unit,\n    onAddListClick: () -> Unit,\n    onListClick: (ActivityPubListEntity) -> Unit,\n    onRetryClick: () -> Unit,\n) {\n    Scaffold(\n        topBar = {\n            Toolbar(\n                title = stringResource(LocalizedString.activity_pub_created_list_title),\n                onBackClick = onBackClick,\n            )\n        },\n        snackbarHost = {\n            SnackbarHost(snackBarState)\n        },\n        floatingActionButton = {\n            FloatingActionButton(\n                containerColor = MaterialTheme.colorScheme.surface,\n                onClick = onAddListClick,\n            ) {\n                Icon(\n                    painter = rememberVectorPainter(image = Icons.Default.Add),\n                    contentDescription = \"Create List\",\n                )\n            }\n        },\n    ) { innerPadding ->\n        Box(\n            modifier = Modifier.fillMaxSize().padding(innerPadding),\n        ) {\n            when {\n                uiState.lists.isNotEmpty() -> {\n                    LazyColumn(\n                        modifier = Modifier.fillMaxSize(),\n                    ) {\n                        items(uiState.lists) {\n                            ListItem(\n                                modifier = Modifier.fillMaxWidth()\n                                    .clickable { onListClick(it) },\n                                list = it,\n                            )\n                        }\n                    }\n                }\n\n                uiState.loading -> {\n                    Column(\n                        modifier = Modifier.fillMaxSize(),\n                    ) {\n                        repeat(30) {\n                            ListItemPlaceholder()\n                        }\n                    }\n                }\n\n                uiState.pageError != null -> {\n                    DefaultFailed(\n                        modifier = Modifier.fillMaxSize(),\n                        exception = uiState.pageError,\n                        onRetryClick = onRetryClick,\n                    )\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/list/CreatedListsUiState.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.list\n\nimport com.zhangke.activitypub.entities.ActivityPubListEntity\n\ndata class CreatedListsUiState(\n    val lists: List<ActivityPubListEntity>,\n    val loading: Boolean,\n    val pageError: Throwable?,\n) {\n\n    companion object {\n\n        fun default(\n            loading: Boolean = false,\n        ): CreatedListsUiState {\n            return CreatedListsUiState(\n                loading = loading,\n                pageError = null,\n                lists = emptyList(),\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/list/CreatedListsViewModel.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.list\n\nimport androidx.lifecycle.ViewModel\nimport com.zhangke.framework.composable.TextString\nimport com.zhangke.framework.ktx.launchInViewModel\nimport com.zhangke.fread.activitypub.app.internal.usecase.content.GetUserCreatedListUseCase\nimport com.zhangke.fread.status.model.PlatformLocator\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asSharedFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.update\nclass CreatedListsViewModel (\n    private val getUserCreatedList: GetUserCreatedListUseCase,\n    private val locator: PlatformLocator,\n) : ViewModel() {\n\n    private val _uiState = MutableStateFlow(CreatedListsUiState.default())\n    val uiState = _uiState.asStateFlow()\n\n    private val _snackBarFlow = MutableSharedFlow<TextString>()\n    val snackBarFlow = _snackBarFlow.asSharedFlow()\n\n    private var getListJob: Job? = null\n\n    init {\n        getUserLists()\n    }\n\n    fun onRetryClick() {\n        getUserLists()\n    }\n\n    fun onPageResume() {\n        getUserLists()\n    }\n\n    private fun getUserLists() {\n        if (getListJob?.isActive == true) return\n        _uiState.update { it.copy(loading = true) }\n        getListJob = launchInViewModel {\n            getUserCreatedList(locator)\n                .onSuccess { list ->\n                    _uiState.update {\n                        it.copy(loading = false, lists = list)\n                    }\n                }.onFailure { t ->\n                    _uiState.update {\n                        it.copy(loading = false, pageError = t)\n                    }\n                }\n        }\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/list/ListDetailPageContent.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.list\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.focusable\nimport androidx.compose.foundation.gestures.awaitEachGesture\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Add\nimport androidx.compose.material.icons.filled.Delete\nimport androidx.compose.material.icons.filled.Save\nimport androidx.compose.material3.CircularProgressIndicator\nimport androidx.compose.material3.DropdownMenuItem\nimport androidx.compose.material3.FloatingActionButton\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.OutlinedTextField\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.SnackbarHost\nimport androidx.compose.material3.SnackbarHostState\nimport androidx.compose.material3.Switch\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.vector.rememberVectorPainter\nimport androidx.compose.ui.input.pointer.PointerEventType\nimport androidx.compose.ui.input.pointer.pointerInput\nimport androidx.compose.ui.text.input.TextFieldValue\nimport androidx.compose.ui.unit.DpOffset\nimport androidx.compose.ui.unit.dp\nimport com.zhangke.activitypub.entities.ActivityPubAccountEntity\nimport com.zhangke.framework.composable.DefaultFailed\nimport com.zhangke.framework.composable.FreadDialog\nimport com.zhangke.framework.composable.PopupMenu\nimport com.zhangke.framework.composable.Toolbar\nimport com.zhangke.framework.composable.freadPlaceholder\nimport com.zhangke.framework.composable.noRippleClick\nimport com.zhangke.fread.activitypub.app.internal.screen.list.edit.ListRepliesPolicy\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.ui.BlogAuthorAvatar\nimport org.jetbrains.compose.resources.stringResource\n\n@Composable\ninternal fun ListDetailPageContent(\n    name: TextFieldValue,\n    repliesPolicy: ListRepliesPolicy,\n    exclusive: Boolean,\n    showLoadingCover: Boolean,\n    accountList: List<ActivityPubAccountEntity>,\n    snackBarState: SnackbarHostState,\n    accountsLoading: Boolean,\n    loadAccountsError: Throwable?,\n    showDeleteIcon: Boolean,\n    onBackClick: () -> Unit,\n    onSaveClick: () -> Unit,\n    onAddUserClick: () -> Unit,\n    onExclusiveChangeRequest: (Boolean) -> Unit,\n    onRemoveAccount: (ActivityPubAccountEntity) -> Unit,\n    onRetryLoadAccountsClick: () -> Unit,\n    onPolicySelect: (ListRepliesPolicy) -> Unit,\n    onNameChangedRequest: (TextFieldValue) -> Unit,\n    onDeleteClick: () -> Unit = {},\n) {\n    Scaffold(\n        topBar = {\n            Toolbar(\n                title = stringResource(LocalizedString.activity_pub_add_list_title),\n                onBackClick = onBackClick,\n                actions = {\n                    if (showDeleteIcon) {\n                        var showDeleteDialog by remember { mutableStateOf(false) }\n                        if (showDeleteDialog) {\n                            FreadDialog(\n                                onDismissRequest = { showDeleteDialog = false },\n                                contentText = stringResource(LocalizedString.activity_pub_list_delete_confirm),\n                                onNegativeClick = { showDeleteDialog = false },\n                                onPositiveClick = {\n                                    onDeleteClick()\n                                    showDeleteDialog = false\n                                },\n                            )\n                        }\n                        Toolbar.DeleteButton(\n                            onDeleteClick = { showDeleteDialog = true },\n                        )\n                    }\n                    IconButton(\n                        onClick = onSaveClick,\n                    ) {\n                        Icon(\n                            imageVector = Icons.Default.Save,\n                            contentDescription = \"Save\",\n                        )\n                    }\n                }\n            )\n        },\n        floatingActionButton = {\n            FloatingActionButton(\n                containerColor = MaterialTheme.colorScheme.surface,\n                onClick = onAddUserClick,\n            ) {\n                Icon(\n                    painter = rememberVectorPainter(image = Icons.Default.Add),\n                    contentDescription = \"Add User\",\n                )\n            }\n        },\n        snackbarHost = { SnackbarHost(snackBarState) },\n    ) { innerPadding ->\n        Box(\n            modifier = Modifier.fillMaxSize()\n                .padding(innerPadding),\n        ) {\n            LazyColumn(\n                modifier = Modifier.fillMaxSize(),\n            ) {\n                item {\n                    ListDetailSetting(\n                        name = name,\n                        repliesPolicy = repliesPolicy,\n                        exclusive = exclusive,\n                        onExclusiveChangeRequest = onExclusiveChangeRequest,\n                        onNameChangedRequest = onNameChangedRequest,\n                        onPolicySelect = onPolicySelect,\n                    )\n                }\n                if (accountList.isNotEmpty()) {\n                    items(accountList) {\n                        AccountItem(\n                            account = it,\n                            showRemoveIcon = true,\n                            onRemoveAccount = onRemoveAccount,\n                        )\n                    }\n                } else if (accountsLoading) {\n                    items(20) {\n                        AccountPlaceholder()\n                    }\n                } else if (loadAccountsError != null) {\n                    item {\n                        Box(\n                            modifier = Modifier.fillMaxWidth()\n                                .height(300.dp)\n                        ) {\n                            DefaultFailed(\n                                modifier = Modifier.fillMaxSize(),\n                                exception = loadAccountsError,\n                                onRetryClick = onRetryLoadAccountsClick,\n                            )\n                        }\n                    }\n                }\n            }\n            if (showLoadingCover) {\n                Box(\n                    modifier = Modifier.fillMaxSize()\n                        .background(MaterialTheme.colorScheme.surface.copy(alpha = 0.6F))\n                        .noRippleClick { }\n                ) {\n                    CircularProgressIndicator(\n                        modifier = Modifier.align(Alignment.Center).size(64.dp)\n                    )\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun ListDetailSetting(\n    name: TextFieldValue,\n    repliesPolicy: ListRepliesPolicy,\n    exclusive: Boolean,\n    onExclusiveChangeRequest: (Boolean) -> Unit,\n    onPolicySelect: (ListRepliesPolicy) -> Unit,\n    onNameChangedRequest: (TextFieldValue) -> Unit,\n) {\n    Column(modifier = Modifier.fillMaxSize()) {\n        OutlinedTextField(\n            modifier = Modifier\n                .fillMaxWidth()\n                .padding(start = 16.dp, top = 16.dp, end = 16.dp),\n            value = name,\n            onValueChange = { onNameChangedRequest(it) },\n            label = {\n                Text(\n                    text = stringResource(LocalizedString.activity_pub_add_list_name)\n                )\n            },\n        )\n        var showPolicySelector by remember { mutableStateOf(false) }\n        Box(\n            modifier = Modifier.fillMaxWidth()\n                .pointerInput(Unit) {\n                    awaitEachGesture {\n                        while (true) {\n                            val event = awaitPointerEvent()\n                            event.changes.forEach { it.consume() }\n                            if (event.type == PointerEventType.Release) {\n                                showPolicySelector = true\n                            }\n                        }\n                    }\n                },\n        ) {\n            OutlinedTextField(\n                modifier = Modifier\n                    .focusable(false)\n                    .fillMaxWidth()\n                    .padding(start = 16.dp, top = 16.dp, end = 16.dp),\n                value = repliesPolicy.showName,\n                readOnly = true,\n                onValueChange = { },\n                label = {\n                    Text(text = stringResource(LocalizedString.activity_pub_add_list_replies))\n                },\n            )\n            PopupMenu(\n                modifier = Modifier.align(Alignment.BottomStart),\n                expanded = showPolicySelector,\n                offset = DpOffset(16.dp, 0.dp),\n                onDismissRequest = { showPolicySelector = false },\n            ) {\n                ListRepliesPolicy.entries.forEach { policy ->\n                    DropdownMenuItem(\n                        text = { Text(text = policy.showName) },\n                        onClick = {\n                            showPolicySelector = false\n                            onPolicySelect(policy)\n                        },\n                    )\n                }\n            }\n        }\n        Row(\n            modifier = Modifier.fillMaxWidth()\n                .padding(start = 16.dp, top = 16.dp, end = 8.dp)\n        ) {\n            Column(modifier = Modifier.weight(1F)) {\n                Text(\n                    text = stringResource(LocalizedString.activity_pub_add_list_hide_in_timeline),\n                    style = MaterialTheme.typography.titleMedium,\n                )\n                Text(\n                    modifier = Modifier.padding(top = 1.dp),\n                    text = stringResource(LocalizedString.activity_pub_add_list_hide_in_timeline_desc),\n                    style = MaterialTheme.typography.bodyMedium,\n                )\n            }\n            Switch(\n                modifier = Modifier.padding(start = 8.dp),\n                checked = exclusive,\n                onCheckedChange = {\n                    onExclusiveChangeRequest(it)\n                },\n            )\n        }\n\n        Text(\n            modifier = Modifier.padding(start = 16.dp, top = 16.dp),\n            text = stringResource(LocalizedString.activity_pub_add_list_replies_list),\n            style = MaterialTheme.typography.titleMedium,\n        )\n    }\n}\n\n@Composable\ninternal fun AccountItem(\n    account: ActivityPubAccountEntity,\n    showRemoveIcon: Boolean,\n    modifier: Modifier = Modifier,\n    onRemoveAccount: (ActivityPubAccountEntity) -> Unit = {},\n) {\n    Row(\n        modifier = modifier.fillMaxWidth()\n            .padding(start = 16.dp, top = 8.dp, end = 8.dp, bottom = 8.dp),\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        BlogAuthorAvatar(\n            modifier = Modifier.size(42.dp),\n            imageUrl = account.avatar,\n        )\n        Column(\n            modifier = Modifier.weight(1F).padding(start = 8.dp),\n        ) {\n            Text(\n                text = account.displayName,\n                style = MaterialTheme.typography.titleMedium,\n            )\n            Text(\n                text = account.acct,\n                style = MaterialTheme.typography.bodyMedium,\n                color = MaterialTheme.colorScheme.onSurfaceVariant,\n            )\n        }\n        if (showRemoveIcon) {\n            var showRemoveConfirmDialog by remember { mutableStateOf(false) }\n            IconButton(\n                modifier = Modifier.padding(start = 8.dp),\n                onClick = { showRemoveConfirmDialog = true },\n            ) {\n                Icon(\n                    imageVector = Icons.Default.Delete,\n                    contentDescription = \"Remove\",\n                )\n            }\n            if (showRemoveConfirmDialog) {\n                FreadDialog(\n                    onDismissRequest = { showRemoveConfirmDialog = false },\n                    contentText = stringResource(LocalizedString.activity_pub_add_list_remove_user_message),\n                    onNegativeClick = { showRemoveConfirmDialog = false },\n                    onPositiveClick = {\n                        showRemoveConfirmDialog = false\n                        onRemoveAccount(account)\n                    },\n                )\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun AccountPlaceholder() {\n    Row(\n        modifier = Modifier.fillMaxWidth()\n            .padding(start = 16.dp, top = 8.dp, end = 8.dp, bottom = 8.dp),\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        Box(modifier = Modifier.size(42.dp).clip(CircleShape).freadPlaceholder(true))\n\n        Column(\n            modifier = Modifier.weight(1F).padding(start = 8.dp),\n        ) {\n            Box(modifier = Modifier.height(16.dp).width(100.dp).freadPlaceholder(true))\n            Box(\n                modifier = Modifier.padding(top = 2.dp).height(14.dp).width(100.dp)\n                    .freadPlaceholder(true)\n            )\n        }\n    }\n}\n\nprivate val ListRepliesPolicy.showName: String\n    @Composable get() {\n        return when (this) {\n            ListRepliesPolicy.FOLLOWING -> stringResource(LocalizedString.activity_pub_add_list_replies_followers)\n            ListRepliesPolicy.LIST -> stringResource(LocalizedString.activity_pub_add_list_replies_list)\n            ListRepliesPolicy.NONE -> stringResource(LocalizedString.activity_pub_add_list_replies_non)\n        }\n    }\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/list/ListItem.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.list\n\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.filled.ListAlt\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport com.zhangke.activitypub.entities.ActivityPubListEntity\nimport com.zhangke.framework.composable.freadPlaceholder\n\n@Composable\ninternal fun ListItem(\n    modifier: Modifier,\n    list: ActivityPubListEntity,\n) {\n    Row(\n        modifier = modifier.padding(vertical = 16.dp, horizontal = 16.dp),\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        Icon(\n            imageVector = Icons.AutoMirrored.Filled.ListAlt,\n            contentDescription = null,\n        )\n        Text(\n            modifier = Modifier.padding(start = 8.dp),\n            text = list.title,\n        )\n    }\n}\n\n@Composable\ninternal fun ListItemPlaceholder() {\n    Row(\n        modifier = Modifier.padding(vertical = 16.dp, horizontal = 16.dp),\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        Box(\n            modifier = Modifier\n                .size(24.dp)\n                .freadPlaceholder(visible = true),\n        )\n        Box(\n            modifier = Modifier\n                .padding(start = 8.dp)\n                .width(100.dp)\n                .height(16.dp)\n                .freadPlaceholder(visible = true),\n        )\n    }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/list/add/AddListScreen.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.list.add\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.composable.currentOrThrow\nimport com.zhangke.framework.composable.ConsumeFlow\nimport com.zhangke.framework.composable.ConsumeSnackbarFlow\nimport com.zhangke.framework.composable.rememberSnackbarHostState\nimport com.zhangke.framework.nav.LocalNavBackStack\nimport com.zhangke.fread.activitypub.app.internal.screen.list.ListDetailPageContent\nimport com.zhangke.fread.activitypub.app.internal.screen.user.search.SearchUserScreenNavKey\nimport com.zhangke.fread.status.model.PlatformLocator\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class AddListScreenNavKey(val locator: PlatformLocator) : NavKey\n\n@Composable\nfun AddListScreen(viewModel: AddListViewModel, locator: PlatformLocator) {\n    val backStack = LocalNavBackStack.currentOrThrow\n    val snackbarHostState = rememberSnackbarHostState()\n    val uiState by viewModel.uiState.collectAsState()\n    ConsumeFlow(SearchUserScreenNavKey.accountSelectedFlow.flow) {\n        viewModel.onAddAccount(it)\n    }\n    ListDetailPageContent(\n        name = uiState.name,\n        snackBarState = snackbarHostState,\n        exclusive = uiState.exclusive,\n        repliesPolicy = uiState.repliesPolicy,\n        showLoadingCover = uiState.showLoadingCover,\n        accountList = uiState.accountList,\n        accountsLoading = false,\n        loadAccountsError = null,\n        showDeleteIcon = false,\n        onSaveClick = viewModel::onSaveClick,\n        onExclusiveChangeRequest = viewModel::onExclusiveChanged,\n        onPolicySelect = viewModel::onPolicySelect,\n        onBackClick = { backStack.removeLastOrNull() },\n        onRemoveAccount = viewModel::onRemoveAccount,\n        onRetryLoadAccountsClick = {},\n        onNameChangedRequest = viewModel::onNameChangeRequest,\n        onAddUserClick = {\n            backStack.add(SearchUserScreenNavKey(locator, onlyFollowing = false))\n        },\n    )\n    ConsumeSnackbarFlow(snackbarHostState, viewModel.snackBarFlow)\n    ConsumeFlow(viewModel.finishPageFlow) { backStack.removeLastOrNull() }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/list/add/AddListUiState.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.list.add\n\nimport androidx.compose.ui.text.input.TextFieldValue\nimport com.zhangke.activitypub.entities.ActivityPubAccountEntity\nimport com.zhangke.fread.activitypub.app.internal.screen.list.edit.ListRepliesPolicy\n\ndata class AddListUiState(\n    val name: TextFieldValue,\n    val repliesPolicy: ListRepliesPolicy,\n    val exclusive: Boolean,\n    val showLoadingCover: Boolean,\n    val accountList: List<ActivityPubAccountEntity>,\n    val contentHasChanged: Boolean,\n) {\n\n    companion object {\n\n        fun default(): AddListUiState {\n            return AddListUiState(\n                name = TextFieldValue(\"\"),\n                repliesPolicy = ListRepliesPolicy.LIST,\n                exclusive = false,\n                showLoadingCover = false,\n                accountList = emptyList(),\n                contentHasChanged = false,\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/list/add/AddListViewModel.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.list.add\n\nimport androidx.compose.ui.text.input.TextFieldValue\nimport androidx.lifecycle.ViewModel\nimport com.zhangke.activitypub.entities.ActivityPubAccountEntity\nimport com.zhangke.framework.composable.TextString\nimport com.zhangke.framework.composable.emitInViewModel\nimport com.zhangke.framework.composable.emitTextMessageFromThrowable\nimport com.zhangke.framework.composable.textOf\nimport com.zhangke.framework.ktx.launchInViewModel\nimport com.zhangke.fread.activitypub.app.internal.auth.ActivityPubClientManager\nimport com.zhangke.fread.activitypub.app.internal.screen.list.edit.ListRepliesPolicy\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.model.PlatformLocator\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asSharedFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.update\nclass AddListViewModel (\n    private val clientManager: ActivityPubClientManager,\n    private val locator: PlatformLocator,\n) : ViewModel() {\n\n    private val _uiState = MutableStateFlow(AddListUiState.default())\n    val uiState = _uiState.asStateFlow()\n\n    private val _snackBarFlow = MutableSharedFlow<TextString>()\n    val snackBarFlow = _snackBarFlow.asSharedFlow()\n\n    private val _finishPageFlow = MutableSharedFlow<Unit>()\n    val finishPageFlow = _finishPageFlow.asSharedFlow()\n\n    fun onPolicySelect(policy: ListRepliesPolicy) {\n        _uiState.update { it.copy(repliesPolicy = policy) }\n        checkContentHasChanged()\n    }\n\n    fun onNameChangeRequest(name: TextFieldValue) {\n        _uiState.update { state ->\n            state.copy(name = name)\n        }\n        checkContentHasChanged()\n    }\n\n    fun onExclusiveChanged(exclusive: Boolean) {\n        _uiState.update { state ->\n            state.copy(exclusive = exclusive)\n        }\n        checkContentHasChanged()\n    }\n\n    fun onAddAccount(entity: ActivityPubAccountEntity) {\n        if (_uiState.value.accountList.any { it.id == entity.id }) return\n        _uiState.update { state ->\n            state.copy(accountList = state.accountList + entity)\n        }\n        checkContentHasChanged()\n    }\n\n    fun onRemoveAccount(entity: ActivityPubAccountEntity) {\n        _uiState.update { state ->\n            state.copy(accountList = state.accountList - entity)\n        }\n        checkContentHasChanged()\n    }\n\n    fun onSaveClick() {\n        val currentUiState = _uiState.value\n        if (!currentUiState.contentHasChanged) return\n        if (currentUiState.name.text.isEmpty()) {\n            _snackBarFlow.emitInViewModel(textOf(LocalizedString.activity_pub_add_list_name_is_empty))\n            return\n        }\n        launchInViewModel {\n            _uiState.update { it.copy(showLoadingCover = true) }\n            val listsRepo = clientManager.getClient(locator).listsRepo\n            listsRepo.createList(\n                title = currentUiState.name.text,\n                repliesPolicy = currentUiState.repliesPolicy.apiName,\n                exclusive = currentUiState.exclusive,\n            ).onSuccess { entity ->\n                if (currentUiState.accountList.isNotEmpty()) {\n                    listsRepo.postAccountInList(\n                        listId = entity.id,\n                        accountIds = currentUiState.accountList.map { it.id },\n                    ).onSuccess {\n                        _uiState.update { it.copy(showLoadingCover = false) }\n                        _finishPageFlow.emit(Unit)\n                    }.onFailure {\n                        _uiState.update { it.copy(showLoadingCover = false) }\n                        _snackBarFlow.emitTextMessageFromThrowable(it)\n                    }\n                } else {\n                    _uiState.update { it.copy(showLoadingCover = false) }\n                    _finishPageFlow.emit(Unit)\n                }\n            }.onFailure {\n                _uiState.update { it.copy(showLoadingCover = false) }\n                _snackBarFlow.emitTextMessageFromThrowable(it)\n            }\n        }\n    }\n\n    private fun checkContentHasChanged() {\n        var hasChanged = false\n        val currentUiState = _uiState.value\n        if (currentUiState.name.text.isNotEmpty()) {\n            hasChanged = true\n        }\n        if (currentUiState.repliesPolicy != ListRepliesPolicy.LIST) {\n            hasChanged = true\n        }\n        if (currentUiState.exclusive) {\n            hasChanged = true\n        }\n        if (currentUiState.accountList.isNotEmpty()) {\n            hasChanged = true\n        }\n        if (hasChanged != currentUiState.contentHasChanged) {\n            _uiState.update { it.copy(contentHasChanged = hasChanged) }\n        }\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/list/edit/EditListScreen.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.list.edit\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.ExperimentalComposeUiApi\nimport com.zhangke.framework.composable.BackHandler\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.composable.ConsumeFlow\nimport com.zhangke.framework.composable.ConsumeSnackbarFlow\nimport com.zhangke.framework.composable.FreadDialog\nimport com.zhangke.framework.composable.currentOrThrow\nimport com.zhangke.framework.composable.rememberSnackbarHostState\nimport com.zhangke.framework.nav.LocalNavBackStack\nimport com.zhangke.fread.activitypub.app.internal.screen.list.ListDetailPageContent\nimport com.zhangke.fread.activitypub.app.internal.screen.user.search.SearchUserScreenNavKey\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.model.PlatformLocator\nimport kotlinx.serialization.Serializable\nimport org.jetbrains.compose.resources.stringResource\n\n@Serializable\ndata class EditListScreenNavKey(\n    val locator: PlatformLocator,\n    val serializedList: String,\n) : NavKey\n\n@OptIn(ExperimentalComposeUiApi::class)\n@Composable\nfun EditListScreen(\n    locator: PlatformLocator,\n    viewModel: EditListViewModel,\n) {\n    val backStack = LocalNavBackStack.currentOrThrow\n    val uiState by viewModel.uiState.collectAsState()\n    val snackBarState = rememberSnackbarHostState()\n    var showBackReminder by remember { mutableStateOf(false) }\n    fun onBack() {\n        if (uiState.contentHasChanged) {\n            showBackReminder = true\n        } else {\n            backStack.removeLastOrNull()\n        }\n    }\n    BackHandler(true) { onBack() }\n    if (showBackReminder) {\n        FreadDialog(\n            onDismissRequest = { showBackReminder = false },\n            contentText = stringResource(LocalizedString.activity_pub_add_list_back_reminder),\n            onNegativeClick = { showBackReminder = false },\n            onPositiveClick = {\n                showBackReminder = false\n                backStack.removeLastOrNull()\n            },\n        )\n    }\n    ConsumeFlow(SearchUserScreenNavKey.accountSelectedFlow.flow) {\n        viewModel.onAddUser(it)\n    }\n    ListDetailPageContent(\n        name = uiState.name,\n        repliesPolicy = uiState.repliesPolicy,\n        exclusive = uiState.exclusive,\n        showLoadingCover = uiState.showLoadingCover,\n        accountList = uiState.accountList,\n        snackBarState = snackBarState,\n        showDeleteIcon = true,\n        accountsLoading = uiState.accountsLoading,\n        loadAccountsError = uiState.loadAccountsError,\n        onNameChangedRequest = viewModel::onNameChangeRequest,\n        onExclusiveChangeRequest = viewModel::onExclusiveChanged,\n        onRemoveAccount = viewModel::onRemoveAccount,\n        onAddUserClick = {\n            val searchUserScreen = SearchUserScreenNavKey(locator, true)\n            backStack.add(searchUserScreen)\n        },\n        onSaveClick = viewModel::onSaveClick,\n        onBackClick = { onBack() },\n        onRetryLoadAccountsClick = viewModel::onRetryLoadAccountsClick,\n        onPolicySelect = viewModel::onPolicySelect,\n        onDeleteClick = viewModel::onDeleteClick,\n    )\n    ConsumeSnackbarFlow(snackBarState, viewModel.snackBarFlow)\n    ConsumeFlow(viewModel.finishPageFlow) { backStack.removeLastOrNull() }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/list/edit/EditListUiState.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.list.edit\n\nimport androidx.compose.ui.text.input.TextFieldValue\nimport com.zhangke.activitypub.entities.ActivityPubAccountEntity\nimport com.zhangke.activitypub.entities.ActivityPubListEntity\n\ndata class EditListUiState(\n    val accountsLoading: Boolean,\n    val loadAccountsError: Throwable?,\n    val name: TextFieldValue,\n    val repliesPolicy: ListRepliesPolicy,\n    val exclusive: Boolean,\n    val showLoadingCover: Boolean,\n    val accountList: List<ActivityPubAccountEntity>,\n    val contentHasChanged: Boolean,\n) {\n\n    companion object {\n\n        fun default(list: ActivityPubListEntity): EditListUiState {\n            return EditListUiState(\n                accountsLoading = false,\n                loadAccountsError = null,\n                name = TextFieldValue(list.title),\n                repliesPolicy = ListRepliesPolicy.fromName(list.repliesPolicy),\n                exclusive = list.exclusive,\n                showLoadingCover = false,\n                accountList = emptyList(),\n                contentHasChanged = false,\n            )\n        }\n    }\n}\n\nenum class ListRepliesPolicy(val apiName: String) {\n\n    FOLLOWING(\"followed\"),\n    LIST(\"list\"),\n    NONE(\"none\");\n\n    companion object {\n\n        fun fromName(name: String): ListRepliesPolicy {\n            return entries.first { it.apiName == name }\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/list/edit/EditListViewModel.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.list.edit\n\nimport androidx.compose.ui.text.input.TextFieldValue\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.zhangke.activitypub.entities.ActivityPubAccountEntity\nimport com.zhangke.activitypub.entities.ActivityPubListEntity\nimport com.zhangke.framework.architect.json.globalJson\nimport com.zhangke.framework.composable.TextString\nimport com.zhangke.framework.composable.emitInViewModel\nimport com.zhangke.framework.composable.emitTextMessageFromThrowable\nimport com.zhangke.framework.composable.textOf\nimport com.zhangke.framework.ktx.launchInViewModel\nimport com.zhangke.framework.utils.exceptionOrThrow\nimport com.zhangke.fread.activitypub.app.internal.auth.ActivityPubClientManager\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.model.PlatformLocator\nimport kotlinx.coroutines.async\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asSharedFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.update\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.supervisorScope\nclass EditListViewModel (\n    private val clientManager: ActivityPubClientManager,\n    private val locator: PlatformLocator,\n    private val serializedList: String,\n) : ViewModel() {\n\n    private val entity: ActivityPubListEntity = globalJson.decodeFromString(serializedList)\n\n    private val _uiState = MutableStateFlow(EditListUiState.default(entity))\n    val uiState = _uiState.asStateFlow()\n\n    private val _snackBarFlow = MutableSharedFlow<TextString>()\n    val snackBarFlow = _snackBarFlow.asSharedFlow()\n\n    private val _finishPageFlow = MutableSharedFlow<Unit>()\n    val finishPageFlow = _finishPageFlow.asSharedFlow()\n\n    private var originalAccountList: List<ActivityPubAccountEntity> = emptyList()\n\n    init {\n        launchInViewModel { getListDetail() }\n    }\n\n    private suspend fun getListDetail() {\n        getAccountList()\n    }\n\n    fun onRetryLoadAccountsClick() {\n        launchInViewModel { getAccountList() }\n    }\n\n    fun onPolicySelect(policy: ListRepliesPolicy) {\n        _uiState.update { it.copy(repliesPolicy = policy) }\n        checkContentHasChanged()\n    }\n\n    fun onDeleteClick() {\n        launchInViewModel {\n            clientManager.getClient(locator)\n                .listsRepo\n                .deleteList(entity.id)\n                .onSuccess {\n                    _finishPageFlow.emit(Unit)\n                }.onFailure { t ->\n                    _snackBarFlow.emitTextMessageFromThrowable(t)\n                }\n        }\n    }\n\n    fun onNameChangeRequest(name: TextFieldValue) {\n        _uiState.update { state ->\n            state.copy(name = name)\n        }\n        checkContentHasChanged()\n    }\n\n    fun onExclusiveChanged(exclusive: Boolean) {\n        _uiState.update { state ->\n            state.copy(exclusive = exclusive)\n        }\n        checkContentHasChanged()\n    }\n\n    fun onRemoveAccount(accountEntity: ActivityPubAccountEntity) {\n        if (!originalAccountList.any { it.id == accountEntity.id }) {\n            _uiState.update { it.copy(accountList = it.accountList - accountEntity) }\n            return\n        }\n        _uiState.update { it.copy(showLoadingCover = true) }\n        launchInViewModel {\n            clientManager.getClient(locator)\n                .listsRepo\n                .deleteAccountsInList(\n                    listId = entity.id,\n                    accounts = listOf(accountEntity.id),\n                ).onSuccess {\n                    _uiState.update { state ->\n                        state.copy(\n                            showLoadingCover = false,\n                            accountList = state.accountList - accountEntity,\n                        )\n                    }\n                    originalAccountList = originalAccountList - accountEntity\n                }.onFailure { t ->\n                    _uiState.update { state ->\n                        state.copy(showLoadingCover = false)\n                    }\n                    _snackBarFlow.emitTextMessageFromThrowable(t)\n                }\n        }\n    }\n\n    fun onSaveClick() {\n        if (_uiState.value.name.text.isEmpty()) {\n            _snackBarFlow.emitInViewModel(textOf(LocalizedString.activity_pub_add_list_name_is_empty))\n            return\n        }\n        _uiState.update { it.copy(showLoadingCover = true) }\n        viewModelScope.launch {\n            supervisorScope {\n                val settingDeferred = async { updateSettingPart() }\n                val accountDeferred = async { updateAccountList() }\n                val settingResult = settingDeferred.await()\n                val accountResult = accountDeferred.await()\n                _uiState.update { it.copy(showLoadingCover = false) }\n                if (settingResult.isFailure) {\n                    _snackBarFlow.emitTextMessageFromThrowable(settingResult.exceptionOrThrow())\n                } else if (accountResult.isFailure) {\n                    _snackBarFlow.emitTextMessageFromThrowable(accountResult.exceptionOrThrow())\n                } else {\n                    _finishPageFlow.emit(Unit)\n                }\n            }\n        }\n    }\n\n    fun onAddUser(user: ActivityPubAccountEntity) {\n        if (_uiState.value.accountList.any { it.id == entity.id }) return\n        _uiState.update { it.copy(accountList = it.accountList + user) }\n    }\n\n    private suspend fun updateSettingPart(): Result<Unit> {\n        if (!checkSettingHasChanged()) return Result.success(Unit)\n        return clientManager.getClient(locator).listsRepo\n            .updateList(\n                listId = entity.id,\n                title = uiState.value.name.text,\n                repliesPolicy = uiState.value.repliesPolicy.apiName,\n                exclusive = uiState.value.exclusive,\n            ).map { }\n    }\n\n    private suspend fun updateAccountList(): Result<Unit> {\n        if (!checkAccountHasChanged()) return Result.success(Unit)\n        val ids = originalAccountList.map { it.id }.toSet()\n        val newAccounts = _uiState.value.accountList.filter { !ids.contains(it.id) }\n        return clientManager.getClient(locator).listsRepo\n            .postAccountInList(\n                listId = entity.id,\n                accountIds = newAccounts.map { it.id },\n            ).map { }\n    }\n\n    private fun checkContentHasChanged() {\n        val hasChanged = checkSettingHasChanged() || checkAccountHasChanged()\n        if (_uiState.value.contentHasChanged != hasChanged) {\n            _uiState.update { it.copy(contentHasChanged = hasChanged) }\n        }\n    }\n\n    private fun checkSettingHasChanged(): Boolean {\n        if (entity.title != uiState.value.name.text) {\n            return true\n        }\n        if (entity.repliesPolicy != uiState.value.repliesPolicy.apiName) {\n            return true\n        }\n        if (entity.exclusive != uiState.value.exclusive) {\n            return true\n        }\n        return false\n    }\n\n    private fun checkAccountHasChanged(): Boolean {\n        if (originalAccountList.size != _uiState.value.accountList.size) {\n            return true\n        }\n        val ids = originalAccountList.map { it.id }.toSet()\n        for (entity in _uiState.value.accountList) {\n            if (!ids.contains(entity.id)) return true\n        }\n        return false\n    }\n\n    private suspend fun getAccountList() {\n        _uiState.update { it.copy(accountsLoading = true) }\n        clientManager.getClient(locator)\n            .listsRepo\n            .getAccountsInList(entity.id)\n            .onSuccess { list ->\n                originalAccountList = list\n                _uiState.update { state ->\n                    state.copy(\n                        accountsLoading = false,\n                        accountList = list,\n                    )\n                }\n            }.onFailure { t ->\n                _uiState.update { state ->\n                    state.copy(\n                        accountsLoading = false,\n                        loadAccountsError = t,\n                    )\n                }\n            }\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/search/SearchStatusScreen.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.search\n\nimport androidx.compose.runtime.Composable\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.fread.commonbiz.shared.screen.search.AbstractSearchStatusScreen\nimport com.zhangke.fread.status.model.PlatformLocator\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class SearchStatusScreenNavKey(\n    val locator: PlatformLocator,\n    val userId: String,\n) : NavKey\n\n@Composable\nfun SearchStatusScreen(viewModel: SearchStatusViewModel) {\n    AbstractSearchStatusScreen(viewModel)\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/search/SearchStatusViewModel.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.search\n\nimport com.zhangke.framework.ktx.launchInViewModel\nimport com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubStatusAdapter\nimport com.zhangke.fread.activitypub.app.internal.auth.ActivityPubClientManager\nimport com.zhangke.fread.activitypub.app.internal.auth.LoggedAccountProvider\nimport com.zhangke.fread.activitypub.app.internal.model.ActivityPubLoggedAccount\nimport com.zhangke.fread.activitypub.app.internal.repo.platform.ActivityPubPlatformRepo\nimport com.zhangke.fread.common.adapter.StatusUiStateAdapter\nimport com.zhangke.fread.common.status.StatusUpdater\nimport com.zhangke.fread.commonbiz.shared.screen.search.AbstractSearchStatusViewModel\nimport com.zhangke.fread.commonbiz.shared.usecase.RefactorToNewStatusUseCase\nimport com.zhangke.fread.status.StatusProvider\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.model.StatusUiState\nimport com.zhangke.fread.status.platform.BlogPlatform\nclass SearchStatusViewModel (\n    private val clientManager: ActivityPubClientManager,\n    statusProvider: StatusProvider,\n    private val platformRepo: ActivityPubPlatformRepo,\n    loggedAccountProvider: LoggedAccountProvider,\n    statusUiStateAdapter: StatusUiStateAdapter,\n    private val statusAdapter: ActivityPubStatusAdapter,\n    refactorToNewStatus: RefactorToNewStatusUseCase,\n    statusUpdater: StatusUpdater,\n    private val locator: PlatformLocator,\n    private val userId: String,\n) : AbstractSearchStatusViewModel(\n    statusProvider = statusProvider,\n    statusUiStateAdapter = statusUiStateAdapter,\n    statusUpdater = statusUpdater,\n    refactorToNewStatus = refactorToNewStatus,\n) {\n\n    private var platform: BlogPlatform? = null\n    private var loggedAccount: ActivityPubLoggedAccount? = loggedAccountProvider.getAccount(locator)\n    private var maxId: String? = null\n\n    init {\n        launchInViewModel { platform = loadPlatform().getOrNull() }\n    }\n\n    override suspend fun performSearch(\n        query: String,\n        loadMore: Boolean\n    ): Result<List<StatusUiState>> {\n        if (!loadMore) {\n            this.maxId = null\n        }\n        if (loadMore && maxId == null) {\n            return Result.success(emptyList())\n        }\n        val platformResult = loadPlatform()\n        if (platformResult.isFailure) {\n            return Result.failure(platformResult.exceptionOrNull()!!)\n        }\n        val platform = platformResult.getOrThrow()\n        return clientManager.getClient(locator)\n            .searchRepo\n            .queryStatus(\n                query = query,\n                accountId = userId,\n                maxId = maxId,\n                limit = 20,\n            ).map { statuses ->\n                statuses.map {\n                    statusAdapter.toStatusUiState(\n                        entity = it,\n                        platform = platform,\n                        locator = locator,\n                        loggedAccount = loggedAccount,\n                    )\n                }\n            }.onSuccess {\n                this.maxId = it.lastOrNull()?.status?.id\n            }\n    }\n\n    private suspend fun loadPlatform(): Result<BlogPlatform> {\n        if (platform != null) return Result.success(platform!!)\n        return platformRepo.getPlatform(locator)\n            .onSuccess { platform = it }\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/status/post/PostStatusScreen.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.status.post\n\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material3.SnackbarHostState\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.ExperimentalComposeUiApi\nimport androidx.compose.ui.Modifier\nimport com.zhangke.framework.composable.BackHandler\nimport androidx.compose.ui.text.input.TextFieldValue\nimport androidx.compose.ui.unit.dp\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.composable.ConsumeFlow\nimport com.zhangke.framework.composable.ConsumeSnackbarFlow\nimport com.zhangke.framework.composable.FreadDialog\nimport com.zhangke.framework.composable.LoadableLayout\nimport com.zhangke.framework.composable.LoadableState\nimport com.zhangke.framework.composable.currentOrThrow\nimport com.zhangke.framework.composable.rememberSnackbarHostState\nimport com.zhangke.framework.composable.requireSuccessData\nimport com.zhangke.framework.nav.LocalNavBackStack\nimport com.zhangke.framework.nav.popIfNotRoot\nimport com.zhangke.framework.nav.replaceTopOrAdd\nimport com.zhangke.framework.toast.toast\nimport com.zhangke.framework.utils.Locale\nimport com.zhangke.framework.utils.PlatformUri\nimport com.zhangke.framework.utils.TextFieldUtils\nimport com.zhangke.fread.activitypub.app.internal.screen.status.post.composable.PostStatusBottomBar\nimport com.zhangke.fread.activitypub.app.internal.screen.status.post.composable.PostStatusPoll\nimport com.zhangke.fread.activitypub.app.internal.utils.DeleteTextUtil\nimport com.zhangke.fread.common.utils.MentionTextUtil\nimport com.zhangke.fread.commonbiz.shared.screen.publish.PublishPostMedia\nimport com.zhangke.fread.commonbiz.shared.screen.publish.PublishPostMediaAttachment\nimport com.zhangke.fread.commonbiz.shared.screen.publish.PublishPostScaffold\nimport com.zhangke.fread.commonbiz.shared.screen.publish.composable.PostStatusVisibilityUi\nimport com.zhangke.fread.commonbiz.shared.screen.publish.composable.PostStatusWarning\nimport com.zhangke.fread.commonbiz.shared.screen.publish.multi.MultiAccountPublishingScreenKey\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.account.LoggedAccount\nimport com.zhangke.fread.status.model.QuoteApprovalPolicy\nimport com.zhangke.fread.status.model.StatusVisibility\nimport com.zhangke.fread.status.ui.common.SelectAccountDialog\nimport com.zhangke.fread.status.ui.embed.UnavailableQuoteInEmbedding\nimport com.zhangke.fread.status.ui.embed.embedBorder\nimport com.zhangke.fread.status.ui.publish.BlogInQuoting\nimport com.zhangke.fread.status.ui.publish.PublishBlogStyle\nimport com.zhangke.fread.status.uri.FormalUri\nimport kotlinx.serialization.Serializable\nimport org.jetbrains.compose.resources.stringResource\nimport kotlin.time.Duration\n\n@Serializable\ndata class PostStatusScreenKey(\n    val accountUri: FormalUri,\n    val defaultContent: String? = null,\n    val editBlogJsonString: String? = null,\n    val replyingBlogJsonString: String? = null,\n    val quoteBlogJsonString: String? = null,\n) : NavKey\n\n@OptIn(ExperimentalComposeUiApi::class)\n@Composable\nfun PostStatusScreen(viewModel: PostStatusViewModel) {\n    val backStack = LocalNavBackStack.currentOrThrow\n    val loadableUiState by viewModel.uiState.collectAsState()\n    var showExitDialog by remember { mutableStateOf(false) }\n\n    val snackMessageState = rememberSnackbarHostState()\n\n    fun onBack() {\n        if (loadableUiState !is LoadableState.Success) {\n            backStack.popIfNotRoot()\n            return\n        }\n        if (loadableUiState.requireSuccessData().hasInputtedData()) {\n            showExitDialog = true\n            return\n        }\n        backStack.popIfNotRoot()\n    }\n    LoadableLayout(\n        modifier = Modifier.fillMaxSize(),\n        state = loadableUiState,\n    ) { uiState ->\n        PostStatusScreenContent(\n            uiState = uiState,\n            snackMessageState = snackMessageState,\n            onSwitchAccount = viewModel::onSwitchAccountClick,\n            onContentChanged = viewModel::onContentChanged,\n            onCloseClick = { onBack() },\n            onPostClick = viewModel::onPostClick,\n            onSensitiveClick = viewModel::onSensitiveClick,\n            onMediaSelected = viewModel::onMediaSelected,\n            onQuoteApprovalPolicySelect = viewModel::onQuoteApprovalPolicySelect,\n            onLanguageSelected = viewModel::onLanguageSelected,\n            onDeleteClick = viewModel::onMediaDeleteClick,\n            onDescriptionInputted = viewModel::onDescriptionInputted,\n            onPollClicked = viewModel::onPollClicked,\n            onPollContentChanged = viewModel::onPollContentChanged,\n            onAddPollItemClick = viewModel::onAddPollItemClick,\n            onRemovePollClick = viewModel::onRemovePollClick,\n            onRemovePollItemClick = viewModel::onRemovePollItemClick,\n            onPollStyleSelect = viewModel::onPollStyleSelect,\n            onWarningContentChanged = viewModel::onWarningContentChanged,\n            onVisibilityChanged = viewModel::onVisibilityChanged,\n            onDurationSelect = viewModel::onDurationSelect,\n            onAddAccountClick = {\n                val multiAccPublishScreen =\n                    MultiAccountPublishingScreenKey.create(listOf(uiState.account))\n                if (uiState.hasInputtedData()) {\n                    backStack.add(multiAccPublishScreen)\n                } else {\n                    backStack.replaceTopOrAdd(multiAccPublishScreen)\n                }\n            },\n        )\n    }\n    val successMessage = stringResource(LocalizedString.postStatusSuccess)\n    ConsumeFlow(viewModel.publishSuccessFlow) {\n        toast(successMessage)\n        backStack.popIfNotRoot()\n    }\n    BackHandler(true) {\n        onBack()\n    }\n    if (showExitDialog) {\n        FreadDialog(\n            onDismissRequest = { showExitDialog = false },\n            content = {\n                Text(text = stringResource(LocalizedString.postStatusExitDialogContent))\n            },\n            onNegativeClick = {\n                showExitDialog = false\n            },\n            onPositiveClick = {\n                showExitDialog = false\n                backStack.popIfNotRoot()\n            },\n        )\n    }\n    ConsumeSnackbarFlow(snackMessageState, viewModel.snackMessage)\n}\n\n@Composable\nprivate fun PostStatusScreenContent(\n    uiState: PostStatusUiState,\n    snackMessageState: SnackbarHostState,\n    onSwitchAccount: (LoggedAccount) -> Unit,\n    onContentChanged: (TextFieldValue) -> Unit,\n    onCloseClick: () -> Unit,\n    onPostClick: () -> Unit,\n    onSensitiveClick: () -> Unit,\n    onMediaSelected: (List<PlatformUri>) -> Unit,\n    onDeleteClick: (PublishPostMedia) -> Unit,\n    onQuoteApprovalPolicySelect: (QuoteApprovalPolicy) -> Unit,\n    onDescriptionInputted: (PublishPostMedia, String) -> Unit,\n    onLanguageSelected: (Locale) -> Unit,\n    onPollClicked: () -> Unit,\n    onPollContentChanged: (Int, String) -> Unit,\n    onRemovePollClick: () -> Unit,\n    onRemovePollItemClick: (Int) -> Unit,\n    onAddPollItemClick: () -> Unit,\n    onPollStyleSelect: (multiple: Boolean) -> Unit,\n    onWarningContentChanged: (TextFieldValue) -> Unit,\n    onVisibilityChanged: (StatusVisibility) -> Unit,\n    onDurationSelect: (Duration) -> Unit,\n    onAddAccountClick: () -> Unit,\n) {\n    var showAccountSwitchPopup by remember { mutableStateOf(false) }\n    PublishPostScaffold(\n        account = uiState.account,\n        snackBarHostState = snackMessageState,\n        content = uiState.content,\n        publishEnabled = uiState.publishEnabled,\n        showSwitchAccountIcon = uiState.accountChangeable && uiState.availableAccountList.size > 1,\n        showAddAccountIcon = uiState.showAddAccountIcon,\n        publishing = uiState.publishing,\n        replyingBlog = uiState.replyToBlog,\n        onContentChanged = onContentChanged,\n        onPublishClick = onPostClick,\n        onBackClick = onCloseClick,\n        onSwitchAccountClick = { showAccountSwitchPopup = true },\n        onAddAccountClick = onAddAccountClick,\n        contentWarning = {\n            if (uiState.sensitive) {\n                PostStatusWarning(\n                    modifier = Modifier.fillMaxWidth()\n                        .padding(start = 16.dp, top = 16.dp, end = 16.dp),\n                    warning = uiState.warningContent,\n                    onValueChanged = onWarningContentChanged,\n                )\n            }\n        },\n        postSettingLabel = {\n            if (uiState.rules.supportsQuotePost) {\n                PublishInteractionSettingLabel(\n                    modifier = Modifier,\n                    visibility = uiState.visibility,\n                    quoteApprovalPolicy = uiState.quoteApprovalPolicy,\n                    visibilityChangeable = uiState.visibilityChangeable,\n                    quoteApprovalPolicyChangeable = uiState.quoteApprovalPolicyChangeable,\n                    onVisibilitySelect = onVisibilityChanged,\n                    onQuoteApprovalPolicySelect = onQuoteApprovalPolicySelect,\n                )\n            } else {\n                PostStatusVisibilityUi(\n                    modifier = Modifier,\n                    visibility = uiState.visibility,\n                    changeable = uiState.visibilityChangeable,\n                    onVisibilitySelect = onVisibilityChanged,\n                )\n            }\n        },\n        bottomPanel = {\n            PostStatusBottomBar(\n                uiState = uiState,\n                onSensitiveClick = onSensitiveClick,\n                onMediaSelected = onMediaSelected,\n                onLanguageSelected = onLanguageSelected,\n                onPollClicked = onPollClicked,\n                onEmojiPick = {\n                    onContentChanged(\n                        TextFieldUtils.insertText(\n                            value = uiState.content,\n                            insertText = \" :${it.shortcode}: \",\n                        )\n                    )\n                },\n                onMentionClick = {\n                    onContentChanged(\n                        MentionTextUtil.insertMention(\n                            text = uiState.content,\n                            insertText = it.acct,\n                        )\n                    )\n                },\n                onDeleteEmojiClick = {\n                    onContentChanged(DeleteTextUtil.deleteText(uiState.content))\n                },\n            )\n        },\n        attachment = { style ->\n            StatusAttachment(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .padding(bottom = 16.dp),\n                uiState = uiState,\n                style = style,\n                onDeleteClick = onDeleteClick,\n                onDescriptionInputted = onDescriptionInputted,\n                onPollContentChanged = onPollContentChanged,\n                onRemovePollClick = onRemovePollClick,\n                onRemovePollItemClick = onRemovePollItemClick,\n                onAddPollItemClick = onAddPollItemClick,\n                onPollStyleSelect = onPollStyleSelect,\n                onDurationSelect = onDurationSelect,\n            )\n        },\n    )\n\n    if (showAccountSwitchPopup) {\n        SelectAccountDialog(\n            accountList = uiState.availableAccountList,\n            onDismissRequest = { showAccountSwitchPopup = false },\n            selectedAccounts = listOf(uiState.account),\n            onAccountClicked = { account ->\n                onSwitchAccount(account)\n            },\n        )\n    }\n}\n\n@Composable\nprivate fun StatusAttachment(\n    modifier: Modifier,\n    uiState: PostStatusUiState,\n    style: PublishBlogStyle,\n    onDeleteClick: (PublishPostMedia) -> Unit,\n    onDescriptionInputted: (PublishPostMedia, String) -> Unit,\n    onPollContentChanged: (Int, String) -> Unit,\n    onRemovePollClick: () -> Unit,\n    onRemovePollItemClick: (Int) -> Unit,\n    onAddPollItemClick: () -> Unit,\n    onPollStyleSelect: (multiple: Boolean) -> Unit,\n    onDurationSelect: (Duration) -> Unit,\n) {\n    if (uiState.quotingBlog != null) {\n        BlogInQuoting(\n            modifier = Modifier.fillMaxWidth().padding(16.dp),\n            blog = uiState.quotingBlog,\n            style = style.statusStyle,\n        )\n    } else if (uiState.unavailableQuote != null) {\n        UnavailableQuoteInEmbedding(\n            modifier = Modifier.fillMaxWidth()\n                .padding(16.dp)\n                .embedBorder()\n                .padding(16.dp),\n            unavailableQuote = uiState.unavailableQuote,\n            onContentClick = {},\n        )\n    } else {\n        val attachment = uiState.attachment ?: return\n        when (attachment) {\n            is PostStatusAttachment.Image -> {\n                PublishPostMediaAttachment(\n                    modifier = modifier\n                        .padding(horizontal = 16.dp),\n                    medias = attachment.imageList,\n                    mediaAltMaxCharacters = uiState.rules.altMaxCharacters,\n                    onAltChanged = onDescriptionInputted,\n                    onDeleteClick = onDeleteClick,\n                )\n            }\n\n            is PostStatusAttachment.Video -> {\n                PublishPostMediaAttachment(\n                    modifier = modifier\n                        .padding(horizontal = 16.dp),\n                    medias = listOf(attachment.video),\n                    mediaAltMaxCharacters = uiState.rules.altMaxCharacters,\n                    onAltChanged = onDescriptionInputted,\n                    onDeleteClick = onDeleteClick,\n                )\n            }\n\n            is PostStatusAttachment.Poll -> {\n                PostStatusPoll(\n                    modifier = modifier,\n                    poll = attachment,\n                    rules = uiState.rules,\n                    onPollContentChanged = onPollContentChanged,\n                    onRemovePollClick = onRemovePollClick,\n                    onRemoveItemClick = onRemovePollItemClick,\n                    onAddPollItemClick = onAddPollItemClick,\n                    onPollStyleSelect = onPollStyleSelect,\n                    onDurationSelect = onDurationSelect,\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/status/post/PostStatusScreenRoute.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.status.post\n\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.architect.json.fromJson\nimport com.zhangke.framework.architect.json.globalJson\nimport com.zhangke.fread.status.blog.Blog\nimport com.zhangke.fread.status.uri.FormalUri\nimport kotlinx.serialization.json.Json\nimport kotlinx.serialization.serializer\n\nobject PostStatusScreenRoute {\n\n    fun buildReplyScreen(\n        accountUri: FormalUri,\n        blog: Blog,\n    ): NavKey {\n        return PostStatusScreenKey(\n            accountUri = accountUri,\n            replyingBlogJsonString = globalJson.encodeToString(serializer(), blog)\n        )\n    }\n\n    fun buildEditBlogRoute(\n        accountUri: FormalUri,\n        blog: Blog,\n    ): NavKey {\n        return PostStatusScreenKey(\n            accountUri = accountUri,\n            editBlogJsonString = globalJson.encodeToString(serializer(), blog),\n        )\n    }\n\n    fun buildQuoteBlogScreen(\n        accountUri: FormalUri,\n        quoteBlog: Blog,\n    ): NavKey {\n        return PostStatusScreenKey(\n            accountUri = accountUri,\n            quoteBlogJsonString = globalJson.encodeToString(serializer(), quoteBlog),\n        )\n    }\n\n    fun buildParams(\n        accountUri: FormalUri,\n        defaultContent: String?,\n        editBlog: String?,\n        replyToBlogJsonString: String?,\n        quoteBlogJsonString: String? = null,\n    ): PostStatusScreenParams {\n        val replyToBlog = replyToBlogJsonString?.let {\n            runCatching { globalJson.fromJson<Blog>(it) }.getOrNull()\n        }\n        if (replyToBlog != null) {\n            return PostStatusScreenParams.ReplyStatusParams(\n                accountUri = accountUri,\n                replyingToBlog = replyToBlog,\n            )\n        }\n        if (!editBlog.isNullOrEmpty()) {\n            val blog = runCatching { Json.decodeFromString<Blog>(editBlog) }.getOrNull()\n            if (blog != null) {\n                return PostStatusScreenParams.EditStatusParams(accountUri, blog)\n            }\n        }\n        if (!quoteBlogJsonString.isNullOrEmpty()) {\n            val quoteBlog =\n                runCatching { Json.decodeFromString<Blog>(quoteBlogJsonString) }.getOrNull()\n            if (quoteBlog != null) {\n                return PostStatusScreenParams.QuoteBlogParams(accountUri, quoteBlog)\n            }\n        }\n        return PostStatusScreenParams.PostStatusParams(accountUri, defaultContent)\n    }\n}\n\nsealed interface PostStatusScreenParams {\n\n    val accountUri: FormalUri?\n\n    data class PostStatusParams(\n        override val accountUri: FormalUri?,\n        val defaultContent: String?,\n    ) : PostStatusScreenParams\n\n    data class ReplyStatusParams(\n        override val accountUri: FormalUri?,\n        val replyingToBlog: Blog,\n    ) : PostStatusScreenParams\n\n    data class EditStatusParams(\n        override val accountUri: FormalUri?,\n        val blog: Blog,\n    ) : PostStatusScreenParams\n\n    data class QuoteBlogParams(\n        override val accountUri: FormalUri?,\n        val quoteBlog: Blog,\n    ) : PostStatusScreenParams\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/status/post/PostStatusUiState.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.status.post\n\nimport androidx.compose.ui.text.input.TextFieldValue\nimport com.zhangke.activitypub.entities.ActivityPubAccountEntity\nimport com.zhangke.framework.composable.LoadableState\nimport com.zhangke.framework.utils.ContentProviderFile\nimport com.zhangke.framework.utils.Locale\nimport com.zhangke.framework.utils.getDefaultLocale\nimport com.zhangke.fread.activitypub.app.internal.model.ActivityPubLoggedAccount\nimport com.zhangke.fread.activitypub.app.internal.screen.status.post.composable.GroupedCustomEmojiCell\nimport com.zhangke.fread.commonbiz.shared.screen.publish.PublishPostMedia\nimport com.zhangke.fread.status.blog.Blog\nimport com.zhangke.fread.status.blog.BlogEmbed\nimport com.zhangke.fread.status.model.QuoteApprovalPolicy\nimport com.zhangke.fread.status.model.StatusVisibility\nimport kotlin.time.Duration\n\ndata class PostStatusUiState(\n    val account: ActivityPubLoggedAccount,\n    val availableAccountList: List<ActivityPubLoggedAccount>,\n    val accountChangeable: Boolean,\n    val content: TextFieldValue,\n    val attachment: PostStatusAttachment?,\n    val visibility: StatusVisibility,\n    val visibilityChangeable: Boolean,\n    val quoteApprovalPolicyChangeable: Boolean,\n    val sensitive: Boolean,\n    val warningContent: TextFieldValue,\n    val replyToBlog: Blog?,\n    val quotingBlog: Blog?,\n    val unavailableQuote: BlogEmbed.UnavailableQuote?,\n    val emojiList: List<GroupedCustomEmojiCell>,\n    val language: Locale,\n    val rules: PostBlogRules,\n    val publishing: Boolean,\n    val mentionState: LoadableState<List<ActivityPubAccountEntity>>,\n    val quoteApprovalPolicy: QuoteApprovalPolicy,\n) {\n\n    val showAddAccountIcon: Boolean get() = accountChangeable && replyToBlog == null\n\n    val isQuotingBlogMode: Boolean get() = quotingBlog != null || unavailableQuote != null\n\n    val publishEnabled: Boolean\n        get() {\n            if (publishing) return false\n            if (content.text.isEmpty() && attachment == null) return false\n            return true\n        }\n\n    fun hasInputtedData(): Boolean {\n        if (content.text.isNotEmpty()) return true\n        if (attachment != null) return true\n        if (sensitive && warningContent.text.isNotEmpty()) return true\n        return false\n    }\n\n    companion object {\n\n        fun default(\n            account: ActivityPubLoggedAccount,\n            allLoggedAccount: List<ActivityPubLoggedAccount>,\n            visibility: StatusVisibility,\n            replyingToBlog: Blog? = null,\n            quoteBlog: Blog? = null,\n            content: TextFieldValue = TextFieldValue(\"\"),\n            sensitive: Boolean = false,\n            warningContent: TextFieldValue = TextFieldValue(\"\"),\n            language: Locale? = null,\n            accountChangeable: Boolean = true,\n            visibilityChangeable: Boolean = true,\n            attachment: PostStatusAttachment? = null,\n            quoteApprovalPolicyChangeable: Boolean = true,\n            unavailableQuote: BlogEmbed.UnavailableQuote? = null,\n        ): PostStatusUiState {\n            return PostStatusUiState(\n                account = account,\n                availableAccountList = allLoggedAccount,\n                content = content,\n                attachment = attachment,\n                visibility = visibility,\n                sensitive = sensitive,\n                replyToBlog = replyingToBlog,\n                quotingBlog = quoteBlog,\n                warningContent = warningContent,\n                emojiList = emptyList(),\n                language = language ?: getDefaultLocale(),\n                rules = PostBlogRules.default(),\n                accountChangeable = accountChangeable,\n                visibilityChangeable = visibilityChangeable,\n                publishing = false,\n                mentionState = LoadableState.idle(),\n                quoteApprovalPolicy = QuoteApprovalPolicy.PUBLIC,\n                quoteApprovalPolicyChangeable = quoteApprovalPolicyChangeable,\n                unavailableQuote = unavailableQuote,\n            )\n        }\n    }\n}\n\nsealed interface PostStatusAttachment {\n\n    data class Image(val imageList: List<PostStatusMediaAttachmentFile>) : PostStatusAttachment\n\n    data class Video(val video: PostStatusMediaAttachmentFile) : PostStatusAttachment\n\n    data class Poll(\n        val optionList: List<String>,\n        val multiple: Boolean,\n        val duration: Duration,\n    ) : PostStatusAttachment\n\n    val asImageOrNull: Image? get() = this as? Image\n\n    val asVideoOrNull: Video? get() = this as? Video\n\n    val asPollAttachment: Poll get() = this as Poll\n\n    val asPollAttachmentOrNull: Poll? get() = this as? Poll\n}\n\nsealed interface PostStatusMediaAttachmentFile : PublishPostMedia {\n\n    val previewUri: String\n\n    data class LocalFile(\n        val file: ContentProviderFile,\n        override val alt: String?,\n    ) : PostStatusMediaAttachmentFile {\n\n        override val isVideo: Boolean\n            get() = file.isVideo\n\n        override val previewUri: String\n            get() = file.uri.toString()\n\n        override val uri: String\n            get() = file.uri.toString()\n\n    }\n\n    data class RemoteFile(\n        val id: String,\n        val url: String,\n        val originalAlt: String?,\n        override val alt: String?,\n        override val isVideo: Boolean,\n    ) : PostStatusMediaAttachmentFile {\n\n        override val previewUri: String\n            get() = url\n\n        override val uri: String\n            get() = url\n    }\n}\n\ndata class PostBlogRules(\n    val maxCharacters: Int,\n    val maxMediaCount: Int,\n    val maxPollOptions: Int,\n    val altMaxCharacters: Int,\n    val supportsQuotePost: Boolean,\n) {\n    companion object {\n\n        fun default(\n            maxCharacters: Int = 1000,\n            maxMediaCount: Int = 4,\n            maxPollOptions: Int = 4,\n            altMaxCharacters: Int = 1500,\n            supportsQuotePost: Boolean = false,\n        ): PostBlogRules {\n            return PostBlogRules(\n                maxCharacters = maxCharacters,\n                maxMediaCount = maxMediaCount,\n                maxPollOptions = maxPollOptions,\n                altMaxCharacters = altMaxCharacters,\n                supportsQuotePost = supportsQuotePost,\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/status/post/PostStatusViewModel.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.status.post\n\nimport androidx.compose.ui.text.input.TextFieldValue\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.zhangke.activitypub.entities.ActivityPubPreferencesEntity\nimport com.zhangke.framework.collections.remove\nimport com.zhangke.framework.collections.removeIndex\nimport com.zhangke.framework.collections.updateIndex\nimport com.zhangke.framework.composable.LoadableState\nimport com.zhangke.framework.composable.TextString\nimport com.zhangke.framework.composable.emitInViewModel\nimport com.zhangke.framework.composable.emitTextMessageFromThrowable\nimport com.zhangke.framework.composable.requireSuccessData\nimport com.zhangke.framework.composable.successDataOrNull\nimport com.zhangke.framework.composable.textOf\nimport com.zhangke.framework.composable.updateOnSuccess\nimport com.zhangke.framework.composable.updateToFailed\nimport com.zhangke.framework.coroutines.invokeOnCancel\nimport com.zhangke.framework.ktx.ifNullOrEmpty\nimport com.zhangke.framework.ktx.launchInViewModel\nimport com.zhangke.framework.utils.ContentProviderFile\nimport com.zhangke.framework.utils.Locale\nimport com.zhangke.framework.utils.PlatformUri\nimport com.zhangke.framework.utils.initLocale\nimport com.zhangke.fread.activitypub.app.internal.adapter.toQuoteApprovalPolicy\nimport com.zhangke.fread.activitypub.app.internal.adapter.toStatusVisibility\nimport com.zhangke.fread.activitypub.app.internal.auth.ActivityPubClientManager\nimport com.zhangke.fread.activitypub.app.internal.model.ActivityPubLoggedAccount\nimport com.zhangke.fread.activitypub.app.internal.screen.status.post.usecase.GenerateInitPostStatusUiStateUseCase\nimport com.zhangke.fread.activitypub.app.internal.screen.status.post.usecase.PublishPostUseCase\nimport com.zhangke.fread.activitypub.app.internal.usecase.emoji.GetCustomEmojiUseCase\nimport com.zhangke.fread.activitypub.app.internal.usecase.platform.GetInstancePostStatusRulesUseCase\nimport com.zhangke.fread.common.utils.MentionTextUtil\nimport com.zhangke.fread.common.utils.PlatformUriHelper\nimport com.zhangke.fread.commonbiz.shared.screen.publish.PublishPostMedia\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.account.LoggedAccount\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.model.QuoteApprovalPolicy\nimport com.zhangke.fread.status.model.StatusVisibility\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.async\nimport kotlinx.coroutines.awaitAll\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.SharedFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.distinctUntilChanged\nimport kotlinx.coroutines.flow.mapNotNull\nimport kotlinx.coroutines.launch\nimport kotlin.time.Duration\nimport kotlin.time.Duration.Companion.days\n\nclass PostStatusViewModel (\n    private val getCustomEmoji: GetCustomEmojiUseCase,\n    private val getInstancePostStatusRules: GetInstancePostStatusRulesUseCase,\n    private val generateInitPostStatusUiState: GenerateInitPostStatusUiStateUseCase,\n    private val clientManager: ActivityPubClientManager,\n    private val publishPost: PublishPostUseCase,\n    private val screenParams: PostStatusScreenParams,\n    private val platformUriHelper: PlatformUriHelper,\n) : ViewModel() {\n\n    private val _uiState = MutableStateFlow(LoadableState.loading<PostStatusUiState>())\n    val uiState: StateFlow<LoadableState<PostStatusUiState>> = _uiState.asStateFlow()\n\n    private val _snackMessage = MutableSharedFlow<TextString>()\n    val snackMessage: SharedFlow<TextString> get() = _snackMessage\n\n    private val _publishSuccessFlow = MutableSharedFlow<Unit>()\n    val publishSuccessFlow: SharedFlow<Unit> get() = _publishSuccessFlow\n\n    private var searchMentionUserJob: Job? = null\n\n    init {\n        launchInViewModel {\n            generateInitPostStatusUiState(screenParams)\n                .onSuccess {\n                    _uiState.value = LoadableState.success(it)\n                }.onFailure {\n                    _uiState.updateToFailed(it)\n                }\n        }\n        loadPostingSettings()\n    }\n\n    private fun loadPostingSettings() {\n        launchInViewModel {\n            _uiState.mapNotNull { it.successDataOrNull()?.account }\n                .distinctUntilChanged()\n                .mapNotNull { it.locator }\n                .collect { locator ->\n                    getCustomEmoji(locator)\n                        .onSuccess {\n                            _uiState.updateOnSuccess { state -> state.copy(emojiList = it) }\n                        }.onFailure {\n                            _snackMessage.emitTextMessageFromThrowable(it)\n                        }\n                    getInstancePostStatusRules(locator)\n                        .onSuccess {\n                            _uiState.updateOnSuccess { state -> state.copy(rules = it) }\n                        }.onFailure {\n                            _snackMessage.emitTextMessageFromThrowable(it)\n                        }\n                    if (screenParams !is PostStatusScreenParams.EditStatusParams) {\n                        clientManager.getClient(locator)\n                            .accountRepo\n                            .getPreferences()\n                            .onSuccess { preferences ->\n                                _uiState.updateOnSuccess { state ->\n                                    fillDefaultSetting(state, preferences)\n                                }\n                            }\n                    }\n                }\n        }\n    }\n\n    private fun fillDefaultSetting(\n        state: PostStatusUiState,\n        preferences: ActivityPubPreferencesEntity,\n    ): PostStatusUiState {\n        val keepInitVisibility = state.replyToBlog != null || state.isQuotingBlogMode\n        return state.copy(\n            visibility = if (keepInitVisibility) {\n                state.visibility\n            } else {\n                preferences.postingDefaultVisibility.toStatusVisibility()\n            },\n            quoteApprovalPolicy = preferences.postingDefaultQuotePolicy?.toQuoteApprovalPolicy()\n                ?: state.quoteApprovalPolicy,\n            language = preferences.postingDefaultLanguage?.let { initLocale(it) }\n                ?: state.language,\n        )\n    }\n\n    fun onSwitchAccountClick(account: LoggedAccount) {\n        _uiState.updateOnSuccess {\n            it.copy(account = account as ActivityPubLoggedAccount)\n        }\n    }\n\n    fun onContentChanged(inputtedText: TextFieldValue) {\n        _uiState.updateOnSuccess {\n            it.copy(content = inputtedText)\n        }\n        maybeSearchAccountForMention(inputtedText)\n    }\n\n    private fun maybeSearchAccountForMention(content: TextFieldValue) {\n        val account = _uiState.value.successDataOrNull()?.account ?: return\n        searchMentionUserJob?.cancel()\n        val platformLocator =\n            PlatformLocator(baseUrl = account.platform.baseUrl, accountUri = account.uri)\n        val mentionText = MentionTextUtil.findTypingMentionName(content)?.removePrefix(\"@\")\n        if (mentionText == null || mentionText.length < 2) {\n            _uiState.updateOnSuccess {\n                it.copy(mentionState = LoadableState.idle())\n            }\n            return\n        }\n        searchMentionUserJob = launchInViewModel {\n            _uiState.updateOnSuccess {\n                it.copy(mentionState = LoadableState.loading())\n            }\n            clientManager.getClient(locator = platformLocator)\n                .accountRepo\n                .search(\n                    query = mentionText,\n                    limit = 10,\n                    resolve = false,\n                )\n                .onSuccess { list ->\n                    _uiState.updateOnSuccess {\n                        it.copy(mentionState = LoadableState.success(list))\n                    }\n                }.onFailure {\n                    _uiState.updateOnSuccess {\n                        it.copy(mentionState = LoadableState.idle())\n                    }\n                }\n        }\n        searchMentionUserJob?.invokeOnCancel {\n            _uiState.updateOnSuccess {\n                it.copy(mentionState = LoadableState.idle())\n            }\n        }\n    }\n\n    fun onSensitiveClick() {\n        _uiState.updateOnSuccess {\n            it.copy(sensitive = !it.sensitive)\n        }\n    }\n\n    fun onMediaSelected(list: List<PlatformUri>) {\n        if (list.isEmpty()) return\n        viewModelScope.launch {\n            val fileList = list.map {\n                async { platformUriHelper.read(it) }\n            }.awaitAll().filterNotNull()\n            val videoFile = fileList.firstOrNull { it.isVideo }\n            if (videoFile != null) {\n                onAddVideo(videoFile)\n            } else {\n                onAddImageList(fileList)\n            }\n        }\n    }\n\n    private fun onAddVideo(file: ContentProviderFile) {\n        val attachmentFile = buildLocalAttachmentFile(file)\n        _uiState.updateOnSuccess {\n            it.copy(attachment = PostStatusAttachment.Video(attachmentFile))\n        }\n    }\n\n    private fun onAddImageList(uriList: List<ContentProviderFile>) {\n        val imageList = mutableListOf<PostStatusMediaAttachmentFile>()\n        _uiState.value\n            .requireSuccessData()\n            .attachment\n            ?.asImageOrNull\n            ?.imageList\n            ?.let { imageList += it }\n        uriList.forEach { uri ->\n            imageList += buildLocalAttachmentFile(uri)\n        }\n        _uiState.updateOnSuccess {\n            it.copy(attachment = PostStatusAttachment.Image(imageList))\n        }\n    }\n\n    private fun buildLocalAttachmentFile(\n        file: ContentProviderFile,\n    ) = PostStatusMediaAttachmentFile.LocalFile(\n        file = file,\n        alt = null,\n    )\n\n    fun onMediaDeleteClick(image: PublishPostMedia) {\n        val attachment = _uiState.value\n            .requireSuccessData()\n            .attachment ?: return\n        val imageAttachment = attachment.asImageOrNull\n        if (imageAttachment != null) {\n            _uiState.updateOnSuccess { state ->\n                state.copy(\n                    attachment = PostStatusAttachment.Image(imageAttachment.imageList.remove { it == image })\n                )\n            }\n            return\n        }\n        val videoAttachment = attachment.asVideoOrNull\n        if (videoAttachment != null) {\n            _uiState.updateOnSuccess { state ->\n                state.copy(attachment = null)\n            }\n        }\n    }\n\n    fun onQuoteApprovalPolicySelect(policy: QuoteApprovalPolicy) {\n        _uiState.updateOnSuccess { it.copy(quoteApprovalPolicy = policy) }\n    }\n\n    fun onDescriptionInputted(file: PublishPostMedia, description: String) {\n        _uiState.updateOnSuccess { state ->\n            val imageList = state.attachment?.asImageOrNull?.imageList ?: emptyList()\n            val newImageList = imageList.map {\n                if (it == file) {\n                    when (it) {\n                        is PostStatusMediaAttachmentFile.LocalFile -> it.copy(alt = description)\n                        is PostStatusMediaAttachmentFile.RemoteFile -> it.copy(alt = description)\n                    }\n                } else {\n                    it\n                }\n            }\n            state.copy(attachment = PostStatusAttachment.Image(newImageList))\n        }\n    }\n\n    fun onLanguageSelected(locale: Locale) {\n        _uiState.updateOnSuccess { state ->\n            state.copy(language = locale)\n        }\n    }\n\n    fun onPollClicked() {\n        if (_uiState.value.successDataOrNull()?.attachment is PostStatusAttachment.Poll) return\n        _uiState.updateOnSuccess { state ->\n            state.copy(\n                attachment = PostStatusAttachment.Poll(\n                    optionList = listOf(\"\", \"\"),\n                    multiple = false,\n                    duration = 1.days,\n                )\n            )\n        }\n    }\n\n    fun onPollContentChanged(index: Int, content: String) {\n        _uiState.updateOnSuccess { state ->\n            val pollAttachment = state.attachment!!.asPollAttachment\n            state.copy(\n                attachment = pollAttachment.copy(\n                    optionList = pollAttachment.optionList.updateIndex(index) { content }\n                )\n            )\n        }\n    }\n\n    fun onAddPollItemClick() {\n        _uiState.updateOnSuccess { state ->\n            val pollAttachment = state.attachment!!.asPollAttachment\n            state.copy(\n                attachment = pollAttachment.copy(\n                    optionList = pollAttachment.optionList.plus(\"\")\n                )\n            )\n        }\n    }\n\n    fun onRemovePollItemClick(index: Int) {\n        _uiState.updateOnSuccess { state ->\n            val pollAttachment = state.attachment!!.asPollAttachment\n            state.copy(\n                attachment = pollAttachment.copy(\n                    optionList = pollAttachment.optionList.removeIndex(index)\n                )\n            )\n        }\n    }\n\n    fun onRemovePollClick() {\n        _uiState.updateOnSuccess { state ->\n            state.copy(attachment = null)\n        }\n    }\n\n    fun onPollStyleSelect(multiple: Boolean) {\n        _uiState.updateOnSuccess { state ->\n            val poll = state.attachment!!.asPollAttachment\n            state.copy(\n                attachment = poll.copy(\n                    multiple = multiple\n                )\n            )\n        }\n    }\n\n    fun onWarningContentChanged(content: TextFieldValue) {\n        _uiState.updateOnSuccess {\n            it.copy(warningContent = content)\n        }\n    }\n\n    fun onVisibilityChanged(visibility: StatusVisibility) {\n        _uiState.updateOnSuccess {\n            it.copy(visibility = visibility)\n        }\n    }\n\n    fun onDurationSelect(duration: Duration) {\n        _uiState.updateOnSuccess { state ->\n            val pollAttachment = state.attachment!!.asPollAttachment\n            state.copy(\n                attachment = pollAttachment.copy(duration = duration)\n            )\n        }\n    }\n\n    fun onPostClick() {\n        val currentUiState = _uiState.value.requireSuccessData()\n        val account = currentUiState.account\n        val attachment = currentUiState.attachment\n        if (currentUiState.content.text.isEmpty() && currentUiState.attachment == null) {\n            _snackMessage.emitInViewModel(textOf(LocalizedString.postStatusContentIsEmpty))\n            return\n        }\n        if (attachment is PostStatusAttachment.Poll) {\n            if (currentUiState.content.text.isEmpty()) {\n                _snackMessage.emitInViewModel(textOf(LocalizedString.postStatusContentIsEmpty))\n                return\n            }\n            for (option in attachment.optionList) {\n                if (option.isEmpty()) {\n                    _snackMessage.emitInViewModel(textOf(LocalizedString.post_status_poll_is_empty))\n                    return\n                }\n            }\n        }\n        launchInViewModel {\n            _uiState.updateOnSuccess { it.copy(publishing = true) }\n            publishPost(\n                account = account,\n                uiState = currentUiState,\n                editingBlogId = (screenParams as? PostStatusScreenParams.EditStatusParams)?.blog?.id,\n            ).onSuccess {\n                _uiState.updateOnSuccess { it.copy(publishing = false) }\n                _publishSuccessFlow.emit(Unit)\n            }.onFailure { t ->\n                _uiState.updateOnSuccess { it.copy(publishing = false) }\n                val errorMessage = textOf(\n                    LocalizedString.postStatusFailed,\n                    t.message.ifNullOrEmpty { \"unknown error\" }.take(180),\n                )\n                _snackMessage.emit(errorMessage)\n            }\n        }\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/status/post/PublishInteractionSettingLabel.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.status.post\n\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Public\nimport androidx.compose.material.icons.outlined.Group\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.ModalBottomSheet\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.dp\nimport com.zhangke.framework.composable.noRippleClick\nimport com.zhangke.framework.composable.rememberTransientModalBottomSheetState\nimport com.zhangke.fread.commonbiz.shared.screen.publish.PublishSettingLabel\nimport com.zhangke.fread.commonbiz.shared.screen.publish.composable.InteractionOption\nimport com.zhangke.fread.commonbiz.shared.screen.publish.multi.describeStringId\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.model.QuoteApprovalPolicy\nimport com.zhangke.fread.status.model.StatusVisibility\nimport kotlinx.coroutines.launch\nimport org.jetbrains.compose.resources.stringResource\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun PublishInteractionSettingLabel(\n    modifier: Modifier,\n    visibility: StatusVisibility,\n    visibilityChangeable: Boolean,\n    quoteApprovalPolicy: QuoteApprovalPolicy,\n    quoteApprovalPolicyChangeable: Boolean,\n    onVisibilitySelect: (StatusVisibility) -> Unit,\n    onQuoteApprovalPolicySelect: (QuoteApprovalPolicy) -> Unit,\n) {\n    var sheetVisibility by remember { mutableStateOf(false) }\n    val noLimit =\n        visibility == StatusVisibility.PUBLIC && quoteApprovalPolicy == QuoteApprovalPolicy.PUBLIC\n    PublishSettingLabel(\n        modifier = modifier.noRippleClick(\n            enabled = visibilityChangeable || quoteApprovalPolicyChangeable,\n        ) {\n            sheetVisibility = true\n        },\n        label = if (noLimit) {\n            stringResource(LocalizedString.sharedPublishInteractionNoLimit)\n        } else {\n            stringResource(LocalizedString.sharedPublishInteractionLimited)\n        },\n        icon = if (noLimit) Icons.Default.Public else Icons.Outlined.Group,\n    )\n    val coroutineScope = rememberCoroutineScope()\n    val state = rememberTransientModalBottomSheetState(skipPartiallyExpanded = true)\n    if (sheetVisibility) {\n        ModalBottomSheet(\n            onDismissRequest = {\n                coroutineScope.launch {\n                    state.hide()\n                    sheetVisibility = false\n                }\n            },\n            sheetState = state,\n        ) {\n            Column(\n                modifier = Modifier.fillMaxWidth()\n                    .padding(start = 16.dp, top = 16.dp, end = 16.dp, bottom = 32.dp)\n                    .verticalScroll(rememberScrollState()),\n            ) {\n                Text(\n                    modifier = Modifier.padding(top = 26.dp),\n                    text = stringResource(LocalizedString.sharedPublishInteractionDialogQuoteTitle),\n                    style = MaterialTheme.typography.titleMedium.copy(\n                        fontWeight = FontWeight.SemiBold,\n                    ),\n                )\n\n                InteractionOption(\n                    modifier = Modifier.padding(top = 16.dp).fillMaxWidth()\n                        .noRippleClick(enabled = quoteApprovalPolicyChangeable) {\n                            onQuoteApprovalPolicySelect(QuoteApprovalPolicy.PUBLIC)\n                        },\n                    text = QuoteApprovalPolicy.PUBLIC.label,\n                    selected = quoteApprovalPolicy == QuoteApprovalPolicy.PUBLIC,\n                )\n\n                InteractionOption(\n                    modifier = Modifier.padding(top = 16.dp).fillMaxWidth()\n                        .noRippleClick(enabled = quoteApprovalPolicyChangeable) {\n                            onQuoteApprovalPolicySelect(QuoteApprovalPolicy.FOLLOWERS)\n                        },\n                    text = QuoteApprovalPolicy.FOLLOWERS.label,\n                    selected = quoteApprovalPolicy == QuoteApprovalPolicy.FOLLOWERS,\n                )\n\n                InteractionOption(\n                    modifier = Modifier.padding(top = 16.dp).fillMaxWidth()\n                        .noRippleClick(enabled = quoteApprovalPolicyChangeable) {\n                            onQuoteApprovalPolicySelect(QuoteApprovalPolicy.NOBODY)\n                        },\n                    text = QuoteApprovalPolicy.NOBODY.label,\n                    selected = quoteApprovalPolicy == QuoteApprovalPolicy.NOBODY,\n                )\n\n\n                Text(\n                    modifier = Modifier.padding(top = 26.dp),\n                    text = stringResource(LocalizedString.status_ui_visibility),\n                    style = MaterialTheme.typography.titleMedium.copy(\n                        fontWeight = FontWeight.SemiBold,\n                    ),\n                )\n\n                InteractionOption(\n                    modifier = Modifier.padding(top = 16.dp).fillMaxWidth()\n                        .noRippleClick(enabled = visibilityChangeable) {\n                            onVisibilitySelect(StatusVisibility.PUBLIC)\n                        },\n                    text = stringResource(StatusVisibility.PUBLIC.describeStringId),\n                    selected = visibility == StatusVisibility.PUBLIC,\n                )\n\n                InteractionOption(\n                    modifier = Modifier.padding(top = 16.dp).fillMaxWidth()\n                        .noRippleClick(enabled = visibilityChangeable) {\n                            onVisibilitySelect(StatusVisibility.UNLISTED)\n                        },\n                    text = stringResource(StatusVisibility.UNLISTED.describeStringId),\n                    selected = visibility == StatusVisibility.UNLISTED,\n                )\n\n                InteractionOption(\n                    modifier = Modifier.padding(top = 16.dp).fillMaxWidth()\n                        .noRippleClick(enabled = visibilityChangeable) {\n                            onVisibilitySelect(StatusVisibility.PRIVATE)\n                        },\n                    text = stringResource(StatusVisibility.PRIVATE.describeStringId),\n                    selected = visibility == StatusVisibility.PRIVATE,\n                )\n\n                InteractionOption(\n                    modifier = Modifier.padding(top = 16.dp).fillMaxWidth()\n                        .noRippleClick(enabled = visibilityChangeable) {\n                            onVisibilitySelect(StatusVisibility.DIRECT)\n                        },\n                    text = stringResource(StatusVisibility.DIRECT.describeStringId),\n                    selected = visibility == StatusVisibility.DIRECT,\n                )\n            }\n        }\n    }\n}\n\nprivate val QuoteApprovalPolicy.label: String\n    @Composable\n    get() = when (this) {\n        QuoteApprovalPolicy.PUBLIC -> stringResource(LocalizedString.status_ui_quote_approval_public)\n        QuoteApprovalPolicy.FOLLOWERS -> stringResource(LocalizedString.status_ui_quote_approval_follower)\n        QuoteApprovalPolicy.NOBODY -> stringResource(LocalizedString.status_ui_quote_approval_nobody)\n        QuoteApprovalPolicy.FOLLOWING -> \"Following\"\n    }\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/status/post/adapter/CustomEmojiAdapter.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.status.post.adapter\n\nimport com.zhangke.fread.activitypub.app.internal.model.CustomEmoji\nimport com.zhangke.fread.activitypub.app.internal.screen.status.post.composable.GroupedCustomEmojiCell\n\nclass CustomEmojiAdapter () {\n\n    fun toEmojiCell(\n        customEmojiList: List<CustomEmoji>\n    ): List<GroupedCustomEmojiCell> {\n        return customEmojiList.filter { it.visibleInPicker }\n            .groupBy { it.category }\n            .entries\n            .map { GroupedCustomEmojiCell(it.key, it.value) }\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/status/post/composable/CustomEmojiPicker.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.status.post.composable\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.lazy.grid.GridCells\nimport androidx.compose.foundation.lazy.grid.LazyVerticalGrid\nimport androidx.compose.foundation.lazy.grid.items\nimport androidx.compose.foundation.pager.HorizontalPager\nimport androidx.compose.foundation.pager.rememberPagerState\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.vector.rememberVectorPainter\nimport androidx.compose.ui.unit.dp\nimport com.seiko.imageloader.ui.AutoSizeImage\nimport com.zhangke.framework.composable.FreadTabRow\nimport com.zhangke.framework.composable.icons.Tofu\nimport com.zhangke.framework.composable.noRippleClick\nimport com.zhangke.fread.activitypub.app.internal.model.CustomEmoji\nimport kotlinx.coroutines.launch\n\n@Composable\nfun CustomEmojiPicker(\n    modifier: Modifier,\n    emojiList: List<GroupedCustomEmojiCell>,\n    onEmojiPick: (CustomEmoji) -> Unit,\n) {\n    if (emojiList.isEmpty()) return\n    val coroutineScope = rememberCoroutineScope()\n    Surface(modifier = modifier) {\n        Column(modifier = Modifier.fillMaxWidth()) {\n            val pagerState = rememberPagerState {\n                emojiList.size\n            }\n\n            var selectedTabIndex by remember {\n                mutableIntStateOf(0)\n            }\n\n            LaunchedEffect(pagerState.currentPage) {\n                selectedTabIndex = pagerState.currentPage\n            }\n\n            FreadTabRow(\n                selectedTabIndex = selectedTabIndex,\n                tabCount = emojiList.size,\n                tabContent = {\n                    Text(text = emojiList[it].title)\n                },\n                onTabClick = {\n                    selectedTabIndex = it\n                    coroutineScope.launch {\n                        pagerState.animateScrollToPage(it)\n                    }\n                },\n            )\n\n            HorizontalPager(\n                modifier = Modifier\n                    .fillMaxWidth(),\n                state = pagerState,\n            ) { pageIndex ->\n                CustomEmojiPickerPage(\n                    emojis = emojiList[pageIndex].emojiList,\n                    onEmojiClick = onEmojiPick,\n                )\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun CustomEmojiPickerPage(\n    emojis: List<CustomEmoji>,\n    onEmojiClick: (CustomEmoji) -> Unit,\n) {\n    if (emojis.isEmpty()) return\n    LazyVerticalGrid(\n        modifier = Modifier.fillMaxWidth(),\n        horizontalArrangement = Arrangement.Center,\n        columns = GridCells.Fixed(7),\n    ) {\n        items(emojis) { emoji ->\n            Box(\n                modifier = Modifier\n                    .padding(vertical = 8.dp)\n                    .noRippleClick { onEmojiClick(emoji) },\n                contentAlignment = Alignment.Center,\n            ) {\n                val placePainter = rememberVectorPainter(Icons.Default.Tofu)\n                AutoSizeImage(\n                    emoji.url,\n                    modifier = Modifier.size(26.dp),\n                    errorPainter = { placePainter },\n                    placeholderPainter = { placePainter },\n                    contentDescription = emoji.shortcode,\n                )\n            }\n        }\n    }\n}\n\ndata class GroupedCustomEmojiCell(\n    val title: String,\n    val emojiList: List<CustomEmoji>,\n)\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/status/post/composable/PostStatusBottomBar.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.status.post.composable\n\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.ime\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.layout.widthIn\nimport androidx.compose.foundation.lazy.LazyRow\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Close\nimport androidx.compose.material.icons.filled.EmojiEmotions\nimport androidx.compose.material.icons.filled.KeyboardArrowDown\nimport androidx.compose.material.icons.filled.Poll\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.ExperimentalComposeUiApi\nimport androidx.compose.ui.Modifier\nimport com.zhangke.framework.composable.BackHandler\nimport androidx.compose.ui.unit.dp\nimport com.zhangke.activitypub.entities.ActivityPubAccountEntity\nimport com.zhangke.framework.composable.SimpleIconButton\nimport com.zhangke.framework.composable.freadPlaceholder\nimport com.zhangke.framework.composable.requireSuccessData\nimport com.zhangke.framework.utils.Locale\nimport com.zhangke.framework.utils.PlatformUri\nimport com.zhangke.framework.utils.initLocale\nimport com.zhangke.framework.utils.languageCode\nimport com.zhangke.fread.activitypub.app.internal.model.CustomEmoji\nimport com.zhangke.fread.activitypub.app.internal.screen.status.post.PostStatusUiState\nimport com.zhangke.fread.commonbiz.shared.screen.publish.PublishPostFeaturesPanel\nimport com.zhangke.fread.commonbiz.shared.screen.publish.SensitiveIconButton\nimport com.zhangke.fread.commonbiz.shared.screen.publish.bottomPaddingAsBottomBar\nimport com.zhangke.fread.status.ui.BlogAuthorAvatar\n\n@OptIn(ExperimentalComposeUiApi::class)\n@Composable\ninternal fun PostStatusBottomBar(\n    uiState: PostStatusUiState,\n    onSensitiveClick: () -> Unit,\n    onPollClicked: () -> Unit,\n    onMediaSelected: (List<PlatformUri>) -> Unit,\n    onLanguageSelected: (Locale) -> Unit,\n    onEmojiPick: (CustomEmoji) -> Unit,\n    onMentionClick: (ActivityPubAccountEntity) -> Unit,\n    onDeleteEmojiClick: () -> Unit,\n) {\n    var showEmojiPicker by remember { mutableStateOf(false) }\n    PublishPostFeaturesPanel(\n        modifier = Modifier.fillMaxWidth().bottomPaddingAsBottomBar(),\n        contentLength = uiState.content.text.length,\n        maxContentLimit = uiState.rules.maxCharacters,\n        mediaAvailableCount = uiState.rules.maxMediaCount,\n        onMediaSelected = onMediaSelected,\n        selectedLanguages = listOf(uiState.language.languageCode),\n        mediaSelectEnabled = !uiState.isQuotingBlogMode,\n        maxLanguageCount = 1,\n        onLanguageSelected = { onLanguageSelected(initLocale(it.first())) },\n        actions = {\n            SimpleIconButton(\n                modifier = Modifier\n                    .align(Alignment.CenterVertically)\n                    .padding(start = 4.dp),\n                onClick = onPollClicked,\n                enabled = !uiState.isQuotingBlogMode,\n                imageVector = Icons.Default.Poll,\n                contentDescription = \"Add Poll\",\n            )\n            if (uiState.emojiList.isNotEmpty()) {\n                SimpleIconButton(\n                    modifier = Modifier\n                        .align(Alignment.CenterVertically)\n                        .padding(start = 4.dp),\n                    onClick = { showEmojiPicker = true },\n                    imageVector = Icons.Default.EmojiEmotions,\n                    contentDescription = \"Pick Emoji\",\n                )\n            }\n            SensitiveIconButton(onSensitiveClick = onSensitiveClick)\n        },\n        floatingBar = { BottomBarMentions(uiState, onMentionClick) },\n    )\n\n    val bottomPaddingByIme = WindowInsets.ime.asPaddingValues().calculateBottomPadding()\n    val bottomEmojiBarHeight = 48.dp\n    AnimatedVisibility(\n        visible = showEmojiPicker,\n        modifier = Modifier.padding(bottom = bottomPaddingByIme),\n    ) {\n        BackHandler(showEmojiPicker) {\n            showEmojiPicker = false\n        }\n        Box(\n            modifier = Modifier\n                .fillMaxWidth()\n                .height(280.dp)\n        ) {\n            CustomEmojiPicker(\n                modifier = Modifier\n                    .fillMaxSize()\n                    .padding(start = 16.dp, end = 16.dp, bottom = bottomEmojiBarHeight),\n                emojiList = uiState.emojiList,\n                onEmojiPick = onEmojiPick,\n            )\n            Surface(\n                modifier = Modifier\n                    .height(bottomEmojiBarHeight)\n                    .align(Alignment.BottomCenter)\n            ) {\n                Box(\n                    modifier = Modifier.fillMaxSize(),\n                ) {\n                    SimpleIconButton(\n                        modifier = Modifier\n                            .padding(start = 16.dp)\n                            .align(Alignment.CenterStart),\n                        onClick = { showEmojiPicker = false },\n                        imageVector = Icons.Default.KeyboardArrowDown,\n                        contentDescription = \"Hide keyboard\",\n                    )\n                    SimpleIconButton(\n                        modifier = Modifier\n                            .padding(end = 16.dp)\n                            .align(Alignment.CenterEnd),\n                        onClick = onDeleteEmojiClick,\n                        imageVector = Icons.Default.Close,\n                        contentDescription = \"Delete emoji\",\n                    )\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun BottomBarMentions(\n    uiState: PostStatusUiState,\n    onMentionClick: (ActivityPubAccountEntity) -> Unit,\n) {\n    val mentionState = uiState.mentionState\n    if (mentionState.isIdle || mentionState.isFailed) return\n    Spacer(modifier = Modifier.height(8.dp))\n    if (mentionState.isLoading) {\n        LazyRow(\n            modifier = Modifier.fillMaxWidth(),\n            contentPadding = PaddingValues(horizontal = 6.dp)\n        ) {\n            items(10) {\n                MentionedItem(null, onMentionClick)\n                Spacer(modifier = Modifier.width(8.dp))\n            }\n        }\n    } else if (mentionState.isSuccess) {\n        LazyRow(\n            modifier = Modifier.fillMaxWidth(),\n            contentPadding = PaddingValues(horizontal = 6.dp)\n        ) {\n            items(mentionState.requireSuccessData()) {\n                MentionedItem(it, onMentionClick)\n                Spacer(modifier = Modifier.width(8.dp))\n            }\n        }\n    }\n    Spacer(modifier = Modifier.height(8.dp))\n}\n\n@Composable\nprivate fun MentionedItem(\n    account: ActivityPubAccountEntity?,\n    onMentionClick: (ActivityPubAccountEntity) -> Unit,\n) {\n    Row(\n        modifier = Modifier\n            .border(\n                width = 1.dp,\n                shape = RoundedCornerShape(12),\n                color = MaterialTheme.colorScheme.outline,\n            )\n            .clickable(account != null) { account?.let(onMentionClick) }\n            .padding(vertical = 6.dp),\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        Spacer(modifier = Modifier.width(6.dp))\n        BlogAuthorAvatar(\n            modifier = Modifier.size(18.dp),\n            imageUrl = account?.avatar,\n        )\n        Text(\n            modifier = Modifier\n                .padding(start = 4.dp)\n                .widthIn(min = 80.dp)\n                .freadPlaceholder(account?.acct.isNullOrEmpty()),\n            text = account?.acct.orEmpty(),\n            style = MaterialTheme.typography.labelMedium,\n        )\n        Spacer(modifier = Modifier.width(6.dp))\n    }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/status/post/composable/PostStatusPoll.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.status.post.composable\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Delete\nimport androidx.compose.material.icons.rounded.Add\nimport androidx.compose.material.icons.rounded.Remove\nimport androidx.compose.material3.DividerDefaults\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.RadioButton\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextField\nimport androidx.compose.material3.TextFieldDefaults\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.produceState\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.unit.dp\nimport com.zhangke.framework.composable.DurationSelector\nimport com.zhangke.framework.composable.FreadDialog\nimport com.zhangke.framework.composable.SimpleIconButton\nimport com.zhangke.framework.utils.formattedString\nimport com.zhangke.fread.activitypub.app.internal.screen.status.post.PostBlogRules\nimport com.zhangke.fread.activitypub.app.internal.screen.status.post.PostStatusAttachment\nimport com.zhangke.fread.localization.LocalizedString\nimport org.jetbrains.compose.resources.stringResource\nimport kotlin.time.Duration\n\n@Composable\ninternal fun PostStatusPoll(\n    modifier: Modifier,\n    poll: PostStatusAttachment.Poll,\n    rules: PostBlogRules,\n    onRemovePollClick: () -> Unit,\n    onRemoveItemClick: (Int) -> Unit,\n    onAddPollItemClick: () -> Unit,\n    onPollContentChanged: (Int, String) -> Unit,\n    onPollStyleSelect: (multiple: Boolean) -> Unit,\n    onDurationSelect: (Duration) -> Unit,\n) {\n    Column(modifier = modifier.padding(start = 16.dp, end = 8.dp)) {\n        poll.optionList.forEachIndexed { index, option ->\n            Row(modifier = Modifier.fillMaxWidth()) {\n                TextField(\n                    modifier = Modifier\n                        .weight(1F)\n                        .border(\n                            width = 1.dp,\n                            shape = RoundedCornerShape(4.dp),\n                            color = MaterialTheme.colorScheme.outline,\n                        )\n                        .align(Alignment.CenterVertically),\n                    value = option,\n                    onValueChange = {\n                        onPollContentChanged(index, it)\n                    },\n                    maxLines = 1,\n                    singleLine = true,\n                    placeholder = {\n                        Text(\n                            text = stringResource(LocalizedString.post_status_poll_item_hint, index + 1),\n                            style = MaterialTheme.typography.bodyMedium,\n                        )\n                    },\n                    textStyle = MaterialTheme.typography.bodyMedium,\n                    colors = TextFieldDefaults.colors(\n                        focusedIndicatorColor = Color.Transparent,\n                        unfocusedIndicatorColor = Color.Transparent,\n                        disabledIndicatorColor = Color.Transparent,\n                        errorIndicatorColor = Color.Transparent,\n                        focusedContainerColor = Color.Transparent,\n                        unfocusedContainerColor = Color.Transparent,\n                    ),\n                )\n                if (index == poll.optionList.lastIndex) {\n                    if (poll.optionList.size < rules.maxPollOptions) {\n                        SimpleIconButton(\n                            modifier = Modifier.align(Alignment.CenterVertically),\n                            onClick = onAddPollItemClick,\n                            imageVector = Icons.Rounded.Add,\n                            contentDescription = \"\",\n                        )\n                    }\n                } else {\n                    SimpleIconButton(\n                        modifier = Modifier.align(Alignment.CenterVertically),\n                        onClick = {\n                            onRemoveItemClick(index)\n                        },\n                        imageVector = Icons.Rounded.Remove,\n                        enabled = poll.optionList.size > 2,\n                        contentDescription = \"\",\n                    )\n                }\n            }\n            Spacer(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .height(6.dp)\n            )\n        }\n        Row(\n            modifier = Modifier\n                .padding(start = 8.dp)\n                .fillMaxWidth()\n                .height(38.dp)\n        ) {\n            var durationDialogVisible by remember {\n                mutableStateOf(false)\n            }\n            Column(\n                modifier = Modifier\n                    .fillMaxHeight()\n                    .clickable {\n                        durationDialogVisible = true\n                    }\n            ) {\n                Text(\n                    text = stringResource(LocalizedString.post_status_poll_duration),\n                    style = MaterialTheme.typography.labelMedium,\n                )\n                Box(modifier = Modifier.weight(1F))\n                val durationString by produceState(\"\", poll.duration) {\n                    value = poll.duration.formattedString()\n                }\n                Text(\n                    modifier = Modifier.padding(top = 2.dp),\n                    text = durationString,\n                    style = MaterialTheme.typography.labelLarge,\n                    color = MaterialTheme.colorScheme.primary,\n                )\n            }\n            if (durationDialogVisible) {\n                DurationSelector(\n                    defaultDuration = poll.duration,\n                    onDismissRequest = { durationDialogVisible = false },\n                    onDurationSelect = onDurationSelect,\n                )\n            }\n            Box(\n                modifier = Modifier\n                    .padding(start = 8.dp, top = 6.dp, end = 16.dp, bottom = 4.dp)\n                    .width(1.dp)\n                    .fillMaxHeight()\n                    .background(DividerDefaults.color)\n            )\n            var showChooseStyleDialog by remember {\n                mutableStateOf(false)\n            }\n            Column(modifier = Modifier\n                .fillMaxHeight()\n                .clickable { showChooseStyleDialog = true }) {\n                Text(\n                    text = stringResource(LocalizedString.post_status_poll_function_title),\n                    style = MaterialTheme.typography.labelSmall,\n                )\n                Box(modifier = Modifier.weight(1F))\n                Text(\n                    modifier = Modifier.padding(top = 2.dp),\n                    text = if (poll.multiple) {\n                        stringResource(LocalizedString.post_status_poll_multiple)\n                    } else {\n                        stringResource(LocalizedString.post_status_poll_single)\n                    },\n                    style = MaterialTheme.typography.labelLarge,\n                    color = MaterialTheme.colorScheme.primary,\n                )\n            }\n            if (showChooseStyleDialog) {\n                ChoosePollStyleDialog(\n                    defaultMultiple = poll.multiple,\n                    onDismissRequest = { showChooseStyleDialog = false },\n                    onSelect = onPollStyleSelect,\n                )\n            }\n\n            Box(\n                modifier = Modifier\n                    .height(1.dp)\n                    .weight(1F)\n            )\n\n            SimpleIconButton(\n                modifier = Modifier.align(Alignment.CenterVertically),\n                onClick = onRemovePollClick,\n                imageVector = Icons.Filled.Delete,\n                contentDescription = \"Remove poll\",\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun ChoosePollStyleDialog(\n    defaultMultiple: Boolean,\n    onDismissRequest: () -> Unit,\n    onSelect: (multiple: Boolean) -> Unit,\n) {\n    var multiple by remember(defaultMultiple) {\n        mutableStateOf(defaultMultiple)\n    }\n    FreadDialog(\n        onDismissRequest = onDismissRequest,\n        title = stringResource(LocalizedString.post_status_poll_style_select_dialog_title),\n        onNegativeClick = onDismissRequest,\n        onPositiveClick = {\n            onDismissRequest()\n            onSelect(multiple)\n        },\n        content = {\n            Column(\n                modifier = Modifier\n                    .padding(start = 80.dp, top = 16.dp, end = 80.dp, bottom = 16.dp)\n                    .fillMaxWidth()\n            ) {\n                Row(modifier = Modifier.fillMaxWidth()) {\n                    Text(\n                        modifier = Modifier.align(Alignment.CenterVertically),\n                        text = stringResource(LocalizedString.post_status_poll_single)\n                    )\n                    Box(modifier = Modifier.weight(1F))\n                    RadioButton(\n                        modifier = Modifier.align(Alignment.CenterVertically),\n                        selected = !multiple,\n                        onClick = { multiple = false },\n                    )\n                }\n                Row(\n                    modifier = Modifier\n                        .padding(top = 16.dp)\n                        .fillMaxWidth()\n                ) {\n                    Text(\n                        modifier = Modifier.align(Alignment.CenterVertically),\n                        text = stringResource(LocalizedString.post_status_poll_multiple)\n                    )\n                    Box(modifier = Modifier.weight(1F))\n                    RadioButton(\n                        modifier = Modifier.align(Alignment.CenterVertically),\n                        selected = multiple,\n                        onClick = { multiple = true },\n                    )\n                }\n            }\n        }\n    )\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/status/post/usecase/GenerateInitPostStatusUiStateUseCase.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.status.post.usecase\n\nimport androidx.compose.ui.text.TextRange\nimport androidx.compose.ui.text.input.TextFieldValue\nimport com.zhangke.framework.date.DateParser\nimport com.zhangke.framework.ktx.ifNullOrEmpty\nimport com.zhangke.framework.utils.initLocale\nimport com.zhangke.fread.activitypub.app.ActivityPubAccountManager\nimport com.zhangke.fread.activitypub.app.internal.model.ActivityPubLoggedAccount\nimport com.zhangke.fread.activitypub.app.internal.screen.status.post.PostStatusAttachment\nimport com.zhangke.fread.activitypub.app.internal.screen.status.post.PostStatusMediaAttachmentFile\nimport com.zhangke.fread.activitypub.app.internal.screen.status.post.PostStatusScreenParams\nimport com.zhangke.fread.activitypub.app.internal.screen.status.post.PostStatusUiState\nimport com.zhangke.fread.common.utils.getCurrentTimeMillis\nimport com.zhangke.fread.status.blog.Blog\nimport com.zhangke.fread.status.blog.BlogEmbed\nimport com.zhangke.fread.status.blog.BlogMedia\nimport com.zhangke.fread.status.blog.BlogMediaType\nimport com.zhangke.fread.status.model.StatusVisibility\nimport com.zhangke.fread.status.richtext.parser.HtmlParser\nimport kotlin.time.Duration.Companion.days\nimport kotlin.time.Duration.Companion.milliseconds\nimport kotlin.time.ExperimentalTime\n\nclass GenerateInitPostStatusUiStateUseCase (\n    private val accountManager: ActivityPubAccountManager,\n) {\n\n    suspend operator fun invoke(\n        screenParams: PostStatusScreenParams,\n    ): Result<PostStatusUiState> {\n        val allLoggedAccount = accountManager.getAllLoggedAccount()\n        val defaultAccount = allLoggedAccount.pickDefaultAccount(screenParams)\n            ?: return Result.failure(IllegalStateException(\"Not login!\"))\n        return when (screenParams) {\n            is PostStatusScreenParams.PostStatusParams -> PostStatusUiState.default(\n                account = defaultAccount,\n                allLoggedAccount = allLoggedAccount,\n                visibility = StatusVisibility.PUBLIC,\n                replyingToBlog = null,\n                content = if (screenParams.defaultContent.isNullOrEmpty()) {\n                    TextFieldValue(\"\")\n                } else {\n                    TextFieldValue(\n                        text = screenParams.defaultContent,\n                        selection = TextRange(screenParams.defaultContent.length),\n                    )\n                },\n            )\n\n            is PostStatusScreenParams.ReplyStatusParams -> buildReplyUiState(\n                allLoggedAccount = allLoggedAccount,\n                defaultAccount = defaultAccount,\n                replyParams = screenParams,\n            )\n\n            is PostStatusScreenParams.EditStatusParams -> buildEditPostUiState(\n                defaultAccount = defaultAccount,\n                allLoggedAccount = allLoggedAccount,\n                editParams = screenParams,\n            )\n\n            is PostStatusScreenParams.QuoteBlogParams -> buildQuotingPostUiState(\n                defaultAccount = defaultAccount,\n                allLoggedAccount = allLoggedAccount,\n                quotingParams = screenParams,\n            )\n        }.let { Result.success(it) }\n    }\n\n    private fun List<ActivityPubLoggedAccount>.pickDefaultAccount(\n        screenParams: PostStatusScreenParams\n    ): ActivityPubLoggedAccount? {\n        return if (screenParams.accountUri != null) {\n            this.firstOrNull { it.uri == screenParams.accountUri } ?: this.firstOrNull()\n        } else {\n            this.firstOrNull()\n        }\n    }\n\n    private fun buildReplyUiState(\n        defaultAccount: ActivityPubLoggedAccount,\n        allLoggedAccount: List<ActivityPubLoggedAccount>,\n        replyParams: PostStatusScreenParams.ReplyStatusParams,\n    ): PostStatusUiState {\n        val replyWebFinger = replyParams.replyingToBlog.author.webFinger\n        val initialContent = if (defaultAccount.platform.baseUrl.host == replyWebFinger.host) {\n            \"@${replyWebFinger.name} \"\n        } else {\n            \"$replyWebFinger \"\n        }\n        return PostStatusUiState.default(\n            account = defaultAccount,\n            allLoggedAccount = allLoggedAccount,\n            content = buildTextFieldValue(initialContent),\n            visibility = replyParams.replyingToBlog.visibility,\n            replyingToBlog = replyParams.replyingToBlog,\n            accountChangeable = false,\n            quoteBlog = null,\n        )\n    }\n\n    private fun buildEditPostUiState(\n        defaultAccount: ActivityPubLoggedAccount,\n        allLoggedAccount: List<ActivityPubLoggedAccount>,\n        editParams: PostStatusScreenParams.EditStatusParams,\n    ): PostStatusUiState {\n        val blog = editParams.blog\n        val quotingBlog = blog.embeds.firstNotNullOfOrNull { it as? BlogEmbed.Blog }?.blog\n        return PostStatusUiState.default(\n            account = defaultAccount,\n            allLoggedAccount = allLoggedAccount,\n            content = buildTextFieldValue(HtmlParser.parseToPlainText(blog.content)),\n            visibility = blog.visibility,\n            sensitive = editParams.blog.sensitive,\n            language = editParams.blog.language?.let { initLocale(it) },\n            warningContent = buildTextFieldValue(HtmlParser.parseToPlainText(editParams.blog.spoilerText)),\n            replyingToBlog = null,\n            visibilityChangeable = false,\n            accountChangeable = false,\n            quoteApprovalPolicyChangeable = false,\n            attachment = blog.generateAttachment(),\n            quoteBlog = quotingBlog,\n            unavailableQuote = blog.embeds.firstNotNullOfOrNull { it as? BlogEmbed.UnavailableQuote },\n        )\n    }\n\n    private fun buildQuotingPostUiState(\n        defaultAccount: ActivityPubLoggedAccount,\n        allLoggedAccount: List<ActivityPubLoggedAccount>,\n        quotingParams: PostStatusScreenParams.QuoteBlogParams,\n    ): PostStatusUiState {\n        return PostStatusUiState.default(\n            account = defaultAccount,\n            allLoggedAccount = allLoggedAccount,\n            content = buildTextFieldValue(\"\"),\n            visibility = StatusVisibility.PUBLIC,\n            quoteBlog = quotingParams.quoteBlog,\n            replyingToBlog = null,\n            visibilityChangeable = true,\n            accountChangeable = false,\n        )\n    }\n\n    @OptIn(ExperimentalTime::class)\n    private fun Blog.generateAttachment(): PostStatusAttachment? {\n        if (mediaList.isNotEmpty()) {\n            if (mediaList.first().type == BlogMediaType.VIDEO) {\n                return PostStatusAttachment.Video(mediaList.first().toAttachmentFile())\n            }\n            return PostStatusAttachment.Image(\n                mediaList.map { it.toAttachmentFile() }\n            )\n        }\n        val poll = poll\n        if (poll != null) {\n            val duration = poll.expiresAt\n                ?.let { DateParser.parseAll(it) }\n                ?.toEpochMilliseconds()\n                ?.let { it - getCurrentTimeMillis() }\n                ?.takeIf { it > 0 }\n                ?.milliseconds\n            return PostStatusAttachment.Poll(\n                optionList = poll.options.map { it.title },\n                multiple = poll.multiple,\n                duration = duration ?: 1.days,\n            )\n        }\n        return null\n    }\n\n    private fun BlogMedia.toAttachmentFile(): PostStatusMediaAttachmentFile.RemoteFile {\n        return PostStatusMediaAttachmentFile.RemoteFile(\n            id = this.id,\n            url = this.previewUrl.ifNullOrEmpty { this.url },\n            alt = this.description,\n            originalAlt = this.description,\n            isVideo = this.type == BlogMediaType.VIDEO,\n        )\n    }\n\n    private fun buildTextFieldValue(text: String): TextFieldValue {\n        return TextFieldValue(\n            text = text,\n            selection = TextRange(text.length)\n        )\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/status/post/usecase/PublishPostUseCase.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.status.post.usecase\n\nimport com.zhangke.activitypub.entities.ActivityPubEditStatusEntity\nimport com.zhangke.activitypub.entities.ActivityPubPostStatusRequestEntity\nimport com.zhangke.activitypub.entities.ActivityPubStatusVisibilityEntity\nimport com.zhangke.framework.utils.Locale\nimport com.zhangke.framework.utils.isO3LanguageCode\nimport com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubStatusAdapter\nimport com.zhangke.fread.activitypub.app.internal.adapter.PostStatusAttachmentAdapter\nimport com.zhangke.fread.activitypub.app.internal.adapter.apCode\nimport com.zhangke.fread.activitypub.app.internal.adapter.toEntityVisibility\nimport com.zhangke.fread.activitypub.app.internal.auth.ActivityPubClientManager\nimport com.zhangke.fread.activitypub.app.internal.model.ActivityPubLoggedAccount\nimport com.zhangke.fread.activitypub.app.internal.screen.status.post.PostStatusAttachment\nimport com.zhangke.fread.activitypub.app.internal.screen.status.post.PostStatusMediaAttachmentFile\nimport com.zhangke.fread.activitypub.app.internal.screen.status.post.PostStatusUiState\nimport com.zhangke.fread.activitypub.app.internal.usecase.media.UploadMediaAttachmentUseCase\nimport com.zhangke.fread.common.status.StatusUpdater\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.model.QuoteApprovalPolicy\nimport com.zhangke.fread.status.model.StatusVisibility\nimport kotlinx.coroutines.async\nimport kotlinx.coroutines.awaitAll\nimport kotlinx.coroutines.supervisorScope\n\nclass PublishPostUseCase (\n    private val clientManager: ActivityPubClientManager,\n    private val uploadMediaAttachment: UploadMediaAttachmentUseCase,\n    private val attachmentAdapter: PostStatusAttachmentAdapter,\n    private val statusUpdater: StatusUpdater,\n    private val statusEntityAdapter: ActivityPubStatusAdapter,\n) {\n\n    suspend operator fun invoke(\n        account: ActivityPubLoggedAccount,\n        content: String,\n        attachment: PostStatusAttachment?,\n        sensitive: Boolean,\n        warningContent: String?,\n        visibility: StatusVisibility,\n        language: Locale,\n        replyToBlogId: String? = null,\n        editingBlogId: String? = null,\n        quotingBlogId: String? = null,\n        quoteApprovalPolicy: QuoteApprovalPolicy? = null,\n    ): Result<Unit> {\n        val locator = account.locator\n        val statusRepo = clientManager.getClient(locator).statusRepo\n        val medias = handleMedias(locator, attachment).let {\n            if (it.isFailure) return Result.failure(it.exceptionOrNull()!!)\n            it.getOrThrow()\n        }\n        return if (!editingBlogId.isNullOrEmpty()) {\n            statusRepo.editStatus(\n                id = editingBlogId,\n                status = content,\n                mediaIds = medias,\n                mediaAttributes = buildMediaAttributes(attachment),\n                poll = attachment?.asPollAttachmentOrNull?.let(attachmentAdapter::toPollRequest),\n                sensitive = sensitive,\n                spoilerText = if (sensitive) warningContent else null,\n                language = language.isO3LanguageCode,\n            )\n        } else {\n            val request = ActivityPubPostStatusRequestEntity(\n                status = content,\n                mediaIds = medias,\n                poll = attachment?.asPollAttachmentOrNull?.let(attachmentAdapter::toPollRequest),\n                isSensitive = sensitive,\n                spoilerText = if (sensitive) warningContent else null,\n                replyToId = replyToBlogId,\n                visibility = visibility.toEntityVisibility().code,\n                language = language.isO3LanguageCode,\n                quotedStatusId = quotingBlogId,\n                quoteApprovalPolicy = quoteApprovalPolicy?.apCode,\n            )\n            statusRepo.publishBlog(request)\n        }.map {\n            statusUpdater.update(\n                statusEntityAdapter.toStatusUiState(\n                    entity = it,\n                    platform = account.platform,\n                    locator = locator,\n                    loggedAccount = account,\n                )\n            )\n        }\n    }\n\n    suspend operator fun invoke(\n        account: ActivityPubLoggedAccount,\n        uiState: PostStatusUiState,\n        editingBlogId: String?,\n    ): Result<Unit> {\n        return invoke(\n            account = account,\n            content = uiState.content.text,\n            attachment = uiState.attachment,\n            sensitive = uiState.sensitive,\n            warningContent = uiState.warningContent.text,\n            visibility = uiState.visibility,\n            language = uiState.language,\n            replyToBlogId = uiState.replyToBlog?.id,\n            editingBlogId = editingBlogId,\n            quotingBlogId = uiState.quotingBlog?.id,\n            quoteApprovalPolicy = uiState.quoteApprovalPolicy,\n        )\n    }\n\n    private suspend fun handleMedias(\n        locator: PlatformLocator,\n        attachment: PostStatusAttachment?,\n    ): Result<List<String>> {\n        if (attachment == null) return Result.success(emptyList())\n        val mediaIdList = mutableListOf<String>()\n        val localFiles = mutableListOf<PostStatusMediaAttachmentFile.LocalFile>()\n        if (attachment is PostStatusAttachment.Image) {\n            for (file in attachment.imageList) {\n                when (file) {\n                    is PostStatusMediaAttachmentFile.LocalFile -> localFiles.add(file)\n                    is PostStatusMediaAttachmentFile.RemoteFile -> mediaIdList.add(file.id)\n                }\n            }\n        } else if (attachment is PostStatusAttachment.Video) {\n            when (val file = attachment.video) {\n                is PostStatusMediaAttachmentFile.LocalFile -> localFiles.add(file)\n                is PostStatusMediaAttachmentFile.RemoteFile -> mediaIdList.add(file.id)\n            }\n        }\n        if (localFiles.isNotEmpty()) {\n            val uploadResultList = supervisorScope {\n                localFiles.map { async { uploadMediaWithAlt(locator, it) } }.awaitAll()\n            }\n            if (uploadResultList.any { it.isFailure }) {\n                return Result.failure(uploadResultList.first { it.isFailure }.exceptionOrNull()!!)\n            }\n            uploadResultList.map { it.getOrThrow() }.forEach { mediaIdList.add(it) }\n        }\n        return Result.success(mediaIdList)\n    }\n\n    private fun buildMediaAttributes(\n        attachment: PostStatusAttachment?,\n    ): List<ActivityPubEditStatusEntity.MediaAttributes> {\n        if (attachment == null) return emptyList()\n        val mediaAttributes = mutableListOf<ActivityPubEditStatusEntity.MediaAttributes>()\n        val mediaFileList = when (attachment) {\n            is PostStatusAttachment.Image -> {\n                attachment.imageList\n            }\n\n            is PostStatusAttachment.Video -> {\n                listOf(attachment.video)\n            }\n\n            else -> emptyList()\n        }\n        mediaFileList.mapNotNull { it as? PostStatusMediaAttachmentFile.RemoteFile }.map {\n            mediaAttributes += ActivityPubEditStatusEntity.MediaAttributes(\n                id = it.id,\n                description = it.alt,\n            )\n        }\n        return mediaAttributes\n    }\n\n    private suspend fun uploadMediaWithAlt(\n        locator: PlatformLocator,\n        file: PostStatusMediaAttachmentFile.LocalFile,\n    ): Result<String> {\n        return uploadMediaAttachment(locator, file.file)\n            .mapCatching { mediaId ->\n                if (file.alt.isNullOrEmpty()) {\n                    mediaId\n                } else {\n                    updateAlt(locator, mediaId, file.alt).getOrThrow()\n                    mediaId\n                }\n            }\n    }\n\n    private suspend fun updateAlt(\n        locator: PlatformLocator,\n        fileId: String,\n        alt: String?,\n    ): Result<Unit> {\n        return clientManager.getClient(locator).mediaRepo\n            .updateMedia(id = fileId, description = alt)\n            .map { }\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/trending/TrendingStatusSubViewModel.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.trending\n\nimport com.zhangke.framework.lifecycle.SubViewModel\nimport com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubStatusAdapter\nimport com.zhangke.fread.activitypub.app.internal.auth.ActivityPubClientManager\nimport com.zhangke.fread.activitypub.app.internal.auth.LoggedAccountProvider\nimport com.zhangke.fread.activitypub.app.internal.repo.platform.ActivityPubPlatformRepo\nimport com.zhangke.fread.common.adapter.StatusUiStateAdapter\nimport com.zhangke.fread.common.feeds.model.RefreshResult\nimport com.zhangke.fread.common.status.StatusConfigurationDefault\nimport com.zhangke.fread.common.status.StatusUpdater\nimport com.zhangke.fread.commonbiz.shared.feeds.FeedsViewModelController\nimport com.zhangke.fread.commonbiz.shared.feeds.IFeedsViewModelController\nimport com.zhangke.fread.commonbiz.shared.usecase.RefactorToNewStatusUseCase\nimport com.zhangke.fread.status.StatusProvider\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.model.StatusUiState\n\nclass TrendingStatusSubViewModel(\n    private val statusProvider: StatusProvider,\n    private val clientManager: ActivityPubClientManager,\n    statusUpdater: StatusUpdater,\n    private val statusAdapter: ActivityPubStatusAdapter,\n    statusUiStateAdapter: StatusUiStateAdapter,\n    refactorToNewStatus: RefactorToNewStatusUseCase,\n    private val platformRepo: ActivityPubPlatformRepo,\n    private val locator: PlatformLocator,\n    private val loggedAccountProvider: LoggedAccountProvider,\n) : SubViewModel(), IFeedsViewModelController by FeedsViewModelController(\n    statusProvider = statusProvider,\n    statusUpdater = statusUpdater,\n    statusUiStateAdapter = statusUiStateAdapter,\n    refactorToNewStatus = refactorToNewStatus,\n) {\n\n    init {\n        initController(\n            coroutineScope = viewModelScope,\n            locatorResolver = { locator },\n            loadFirstPageLocalFeeds = ::loadFirstPageLocalFeeds,\n            loadNewFromServerFunction = ::loadNewFromServer,\n            loadMoreFunction = ::loadMore,\n            onStatusUpdate = {},\n        )\n        initFeeds(false)\n    }\n\n    private fun loadFirstPageLocalFeeds(): Result<List<StatusUiState>> {\n        return Result.success(emptyList())\n    }\n\n    private suspend fun loadNewFromServer(): Result<RefreshResult> {\n        return getServerTrending(0).map {\n            RefreshResult(\n                newStatus = it,\n                deletedStatus = emptyList(),\n                useOldData = false,\n            )\n        }\n    }\n\n    private suspend fun loadMore(maxId: String): Result<List<StatusUiState>> {\n        val offset = uiState.value.feeds.size\n        if (offset == 0) return Result.success(emptyList())\n        return getServerTrending(offset)\n    }\n\n    private suspend fun getServerTrending(offset: Int): Result<List<StatusUiState>> {\n        val platformResult = platformRepo.getPlatform(locator)\n        if (platformResult.isFailure) {\n            return Result.failure(platformResult.exceptionOrNull()!!)\n        }\n        val platform = platformResult.getOrThrow()\n        val loggedAccount = locator.accountUri?.let { loggedAccountProvider.getAccount(it) }\n        return clientManager.getClient(locator)\n            .instanceRepo\n            .getTrendsStatuses(\n                limit = StatusConfigurationDefault.config.loadFromServerLimit,\n                offset = offset,\n            ).map { list ->\n                list.map {\n                    statusAdapter.toStatusUiState(\n                        entity = it,\n                        platform = platform,\n                        loggedAccount = loggedAccount,\n                        locator = locator,\n                    )\n                }\n            }\n    }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/trending/TrendingStatusTab.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.trending\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.rememberCoroutineScope\nimport com.zhangke.framework.composable.ConsumeSnackbarFlow\nimport com.zhangke.framework.composable.LocalSnackbarHostState\nimport com.zhangke.framework.nav.BaseTab\nimport com.zhangke.framework.nav.TabOptions\nimport com.zhangke.fread.activitypub.app.internal.composable.ActivityPubTabNames\nimport com.zhangke.fread.commonbiz.shared.composable.FeedsContent\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.ui.common.LocalNestedTabConnection\nimport org.koin.compose.viewmodel.koinViewModel\n\ninternal class TrendingStatusTab(private val locator: PlatformLocator) : BaseTab() {\n\n    override val options: TabOptions\n        @Composable get() = TabOptions(\n            title = ActivityPubTabNames.trending\n        )\n\n    @Composable\n    override fun Content() {\n        super.Content()\n        val snackbarHostState = LocalSnackbarHostState.current\n        val viewModel = koinViewModel<TrendingStatusViewModel>().getSubViewModel(locator)\n        val uiState by viewModel.uiState.collectAsState()\n        val mainTabConnection = LocalNestedTabConnection.current\n        val coroutineScope = rememberCoroutineScope()\n        FeedsContent(\n            uiState = uiState,\n            openScreenFlow = viewModel.openScreenFlow,\n            newStatusNotifyFlow = viewModel.newStatusNotifyFlow,\n            composedStatusInteraction = viewModel.composedStatusInteraction,\n            onRefresh = viewModel::onRefresh,\n            onLoadMore = viewModel::onLoadMore,\n            observeScrollToTopEvent = true,\n            nestedScrollConnection = null,\n            onImmersiveEvent = {\n                if (it) {\n                    mainTabConnection.openImmersiveMode(coroutineScope)\n                } else {\n                    mainTabConnection.closeImmersiveMode(coroutineScope)\n                }\n            },\n            onScrollInProgress = {\n                mainTabConnection.updateContentScrollInProgress(it)\n            },\n        )\n        LaunchedEffect(mainTabConnection.refreshFlow) {\n            mainTabConnection.refreshFlow.collect {\n                viewModel.onRefresh()\n            }\n        }\n        ConsumeSnackbarFlow(snackbarHostState, viewModel.errorMessageFlow)\n    }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/trending/TrendingStatusViewModel.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.trending\n\nimport com.zhangke.framework.lifecycle.ContainerViewModel\nimport com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubStatusAdapter\nimport com.zhangke.fread.activitypub.app.internal.auth.ActivityPubClientManager\nimport com.zhangke.fread.activitypub.app.internal.auth.LoggedAccountProvider\nimport com.zhangke.fread.activitypub.app.internal.repo.platform.ActivityPubPlatformRepo\nimport com.zhangke.fread.common.adapter.StatusUiStateAdapter\nimport com.zhangke.fread.common.status.StatusUpdater\nimport com.zhangke.fread.commonbiz.shared.usecase.RefactorToNewStatusUseCase\nimport com.zhangke.fread.status.StatusProvider\nimport com.zhangke.fread.status.model.PlatformLocator\n\nclass TrendingStatusViewModel (\n    private val statusProvider: StatusProvider,\n    private val clientManager: ActivityPubClientManager,\n    private val statusUpdater: StatusUpdater,\n    private val statusAdapter: ActivityPubStatusAdapter,\n    private val statusUiStateAdapter: StatusUiStateAdapter,\n    private val platformRepo: ActivityPubPlatformRepo,\n    private val refactorToNewStatus: RefactorToNewStatusUseCase,\n    private val loggedAccountProvider: LoggedAccountProvider,\n) : ContainerViewModel<TrendingStatusSubViewModel, TrendingStatusViewModel.Params>() {\n\n    override fun createSubViewModel(params: Params): TrendingStatusSubViewModel {\n        return TrendingStatusSubViewModel(\n            statusProvider = statusProvider,\n            clientManager = clientManager,\n            statusUpdater = statusUpdater,\n            statusAdapter = statusAdapter,\n            refactorToNewStatus = refactorToNewStatus,\n            statusUiStateAdapter = statusUiStateAdapter,\n            platformRepo = platformRepo,\n            loggedAccountProvider = loggedAccountProvider,\n            locator = params.locator,\n        )\n    }\n\n    fun getSubViewModel(\n        locator: PlatformLocator,\n    ): TrendingStatusSubViewModel {\n        val params = Params(locator)\n        return obtainSubViewModel(params)\n    } class Params(val locator: PlatformLocator) : SubViewModelParams() {\n        override val key: String\n            get() = locator.toString()\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/user/UserDetailContainerViewModel.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.user\n\nimport com.zhangke.framework.lifecycle.ContainerViewModel\nimport com.zhangke.framework.utils.WebFinger\nimport com.zhangke.fread.activitypub.app.ActivityPubAccountManager\nimport com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubAccountEntityAdapter\nimport com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubCustomEmojiEntityAdapter\nimport com.zhangke.fread.activitypub.app.internal.auth.ActivityPubClientManager\nimport com.zhangke.fread.activitypub.app.internal.uri.UserUriTransformer\nimport com.zhangke.fread.activitypub.app.internal.usecase.ActivityPubAccountLogoutUseCase\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.uri.FormalUri\n\nclass UserDetailContainerViewModel (\n    private val accountManager: ActivityPubAccountManager,\n    private val userUriTransformer: UserUriTransformer,\n    private val clientManager: ActivityPubClientManager,\n    private val accountEntityAdapter: ActivityPubAccountEntityAdapter,\n    private val emojiEntityAdapter: ActivityPubCustomEmojiEntityAdapter,\n    private val accountLogout: ActivityPubAccountLogoutUseCase,\n) : ContainerViewModel<UserDetailViewModel, UserDetailContainerViewModel.Params>() {\n\n    override fun createSubViewModel(params: Params): UserDetailViewModel {\n        return UserDetailViewModel(\n            accountManager = accountManager,\n            userUriTransformer = userUriTransformer,\n            clientManager = clientManager,\n            emojiEntityAdapter = emojiEntityAdapter,\n            accountEntityAdapter = accountEntityAdapter,\n            accountLogout = accountLogout,\n            locator = params.locator,\n            userUri = params.userUri,\n            webFinger = params.webFinger,\n            userId = params.userId,\n        )\n    }\n\n    fun getViewModel(\n        locator: PlatformLocator,\n        userUri: FormalUri?,\n        webFinger: WebFinger?,\n        userId: String?,\n    ): UserDetailViewModel {\n        return obtainSubViewModel(Params(locator, userUri, webFinger, userId))\n    } class Params(\n        val locator: PlatformLocator,\n        val userUri: FormalUri?,\n        val webFinger: WebFinger?,\n        val userId: String?,\n    ) : SubViewModelParams() {\n\n        override val key: String\n            get() = locator.toString() + userUri + webFinger + userId\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/user/UserDetailScreen.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.user\n\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.text.selection.SelectionContainer\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.filled.Logout\nimport androidx.compose.material.icons.automirrored.filled.VolumeOff\nimport androidx.compose.material.icons.automirrored.outlined.ListAlt\nimport androidx.compose.material.icons.filled.AlternateEmail\nimport androidx.compose.material.icons.filled.Block\nimport androidx.compose.material.icons.filled.Bookmarks\nimport androidx.compose.material.icons.filled.Campaign\nimport androidx.compose.material.icons.filled.Edit\nimport androidx.compose.material.icons.filled.Favorite\nimport androidx.compose.material.icons.filled.FilterAlt\nimport androidx.compose.material.icons.filled.MoreVert\nimport androidx.compose.material.icons.filled.Search\nimport androidx.compose.material.icons.filled.Tag\nimport androidx.compose.material.icons.filled.VisibilityOff\nimport androidx.compose.material3.Button\nimport androidx.compose.material3.Card\nimport androidx.compose.material3.CardDefaults\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.FilledTonalButton\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.MenuDefaults\nimport androidx.compose.material3.ModalBottomSheet\nimport androidx.compose.material3.OutlinedTextField\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.MutableState\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.activitypub.entities.ActivityPubAccountEntity\nimport com.zhangke.framework.composable.AlertConfirmDialog\nimport com.zhangke.framework.composable.ConsumeFlow\nimport com.zhangke.framework.composable.ConsumeSnackbarFlow\nimport com.zhangke.framework.composable.FreadDialog\nimport com.zhangke.framework.composable.PopupMenu\nimport com.zhangke.framework.composable.SimpleIconButton\nimport com.zhangke.framework.composable.TextString\nimport com.zhangke.framework.composable.currentOrThrow\nimport com.zhangke.framework.composable.rememberSnackbarHostState\nimport com.zhangke.framework.composable.rememberTransientModalBottomSheetState\nimport com.zhangke.framework.date.DateParser\nimport com.zhangke.framework.nav.ContentPaddingsHorizontalPagerWithTab\nimport com.zhangke.framework.nav.LocalNavBackStack\nimport com.zhangke.framework.nav.Tab\nimport com.zhangke.framework.network.FormalBaseUrl\nimport com.zhangke.framework.utils.WebFinger\nimport com.zhangke.fread.activitypub.app.internal.screen.account.EditAccountInfoScreenNavKey\nimport com.zhangke.fread.activitypub.app.internal.screen.filters.list.FiltersListScreenKey\nimport com.zhangke.fread.activitypub.app.internal.screen.hashtag.HashtagTimelineScreenKey\nimport com.zhangke.fread.activitypub.app.internal.screen.list.CreatedListsScreenKey\nimport com.zhangke.fread.activitypub.app.internal.screen.search.SearchStatusScreenNavKey\nimport com.zhangke.fread.activitypub.app.internal.screen.user.list.UserListScreenKey\nimport com.zhangke.fread.activitypub.app.internal.screen.user.list.UserListType\nimport com.zhangke.fread.activitypub.app.internal.screen.user.status.StatusListScreenKey\nimport com.zhangke.fread.activitypub.app.internal.screen.user.status.StatusListTabStatusListScreen\nimport com.zhangke.fread.activitypub.app.internal.screen.user.status.StatusListType\nimport com.zhangke.fread.activitypub.app.internal.screen.user.tags.TagListScreenKey\nimport com.zhangke.fread.activitypub.app.internal.screen.user.timeline.UserTimelineTab\nimport com.zhangke.fread.activitypub.app.internal.screen.user.timeline.UserTimelineTabType\nimport com.zhangke.fread.common.browser.LocalActivityBrowserLauncher\nimport com.zhangke.fread.common.browser.launchWebTabInApp\nimport com.zhangke.fread.common.handler.LocalTextHandler\nimport com.zhangke.fread.common.utils.formatDate\nimport com.zhangke.fread.commonbiz.shared.screen.ImageViewerImage\nimport com.zhangke.fread.commonbiz.shared.screen.ImageViewerScreenNavKey\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.model.Emoji\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.model.Relationships\nimport com.zhangke.fread.status.richtext.RichText\nimport com.zhangke.fread.status.ui.action.DropDownCopyLinkItem\nimport com.zhangke.fread.status.ui.action.DropDownOpenInBrowserItem\nimport com.zhangke.fread.status.ui.action.DropDownOpenOriginalInstanceItem\nimport com.zhangke.fread.status.ui.action.ModalDropdownMenuItem\nimport com.zhangke.fread.status.ui.common.DetailPageScaffold\nimport com.zhangke.fread.status.ui.common.LocalNestedTabConnection\nimport com.zhangke.fread.status.ui.common.NestedTabConnection\nimport com.zhangke.fread.status.ui.common.RelationshipStateButton\nimport com.zhangke.fread.status.ui.common.UserFollowLine\nimport com.zhangke.fread.status.ui.richtext.FreadRichText\nimport com.zhangke.fread.status.ui.user.UserHandleLine\nimport com.zhangke.fread.status.uri.FormalUri\nimport com.zhangke.fread.statusui.ic_status_forward\nimport kotlinx.coroutines.flow.SharedFlow\nimport kotlinx.coroutines.launch\nimport kotlinx.serialization.Serializable\nimport org.jetbrains.compose.resources.stringResource\nimport org.jetbrains.compose.resources.vectorResource\n\n@Serializable\ndata class UserDetailScreenKey(\n    val locator: PlatformLocator,\n    val userUri: FormalUri? = null,\n    val webFinger: WebFinger? = null,\n    val userId: String? = null,\n) : NavKey\n\n@Composable\nfun UserDetailScreen(\n    viewModel: UserDetailContainerViewModel,\n    locator: PlatformLocator,\n    userUri: FormalUri? = null,\n    webFinger: WebFinger? = null,\n    userId: String? = null,\n) {\n    val backstack = LocalNavBackStack.currentOrThrow\n    val browserLauncher = LocalActivityBrowserLauncher.current\n    val activityTextHandler = LocalTextHandler.current\n    val viewModel = viewModel.getViewModel(locator, userUri, webFinger, userId)\n    val uiState by viewModel.uiState.collectAsState()\n    val coroutineScope = rememberCoroutineScope()\n    UserDetailContent(\n        uiState = uiState,\n        messageFlow = viewModel.messageFlow,\n        userId = userId,\n        onFavouritesClick = {\n            backstack.add(\n                StatusListScreenKey(\n                    locator = locator,\n                    type = StatusListType.FAVOURITES\n                )\n            )\n        },\n        onSearchClick = {\n            uiState.accountUiState?.account?.id?.let { userId ->\n                backstack.add(\n                    SearchStatusScreenNavKey(\n                        locator = uiState.locator,\n                        userId = userId,\n                    )\n                )\n            }\n        },\n        onBookmarksClick = {\n            backstack.add(StatusListScreenKey(locator = locator, type = StatusListType.BOOKMARKS))\n        },\n        onBackClick = backstack::removeLastOrNull,\n        onFollowAccountClick = viewModel::onFollowClick,\n        onUnfollowAccountClick = viewModel::onUnfollowClick,\n        onCancelFollowRequestClick = viewModel::onCancelFollowRequestClick,\n        onUnblockClick = viewModel::onUnblockClick,\n        onBlockClick = viewModel::onBlockClick,\n        onBlockDomainClick = viewModel::onBlockDomainClick,\n        onUnblockDomainClick = viewModel::onUnblockDomainClick,\n        onAvatarClick = {\n            uiState.accountUiState\n                ?.account\n                ?.avatar\n                ?.let {\n                    val screenKey = ImageViewerScreenNavKey(\n                        selectedIndex = 0,\n                        imageList = listOf(ImageViewerImage(url = it)),\n                    )\n                    backstack.add(screenKey)\n                }\n        },\n        onBannerClick = {\n            uiState.accountUiState\n                ?.account\n                ?.header\n                ?.let {\n                    val screen = ImageViewerScreenNavKey(\n                        selectedIndex = 0,\n                        imageList = listOf(ImageViewerImage(url = it)),\n                    )\n                    backstack.add(screen)\n                }\n        },\n        onOpenInBrowserClick = {\n            uiState.accountUiState?.account?.url?.let {\n                browserLauncher.launchWebTabInApp(\n                    scope = coroutineScope,\n                    url = it,\n                    checkAppSupportPage = false,\n                )\n            }\n        },\n        onCopyLinkClick = {\n            uiState.accountUiState?.account?.url?.let {\n                activityTextHandler.copyText(it)\n            }\n        },\n        onOpenOriginalInstanceClick = {\n            uiState.accountUiState?.account?.url?.let { FormalBaseUrl.parse(it) }?.let {\n                browserLauncher.launchWebTabInApp(\n                    scope = coroutineScope,\n                    url = it.toString(),\n                    locator = locator,\n                    checkAppSupportPage = true,\n                )\n            }\n        },\n        onEditClick = {\n            uiState.userInsight\n                ?.let {\n                    backstack.add(\n                        EditAccountInfoScreenNavKey(\n                            baseUrl = it.baseUrl.toString(),\n                            accountUri = it.uri.toString(),\n                        )\n                    )\n                }\n        },\n        onFollowerClick = {\n            if (uiState.userInsight != null) {\n                val key = UserListScreenKey(\n                    type = UserListType.FOLLOWERS,\n                    locator = uiState.locator,\n                    userUri = uiState.userInsight!!.uri,\n                    userId = uiState.accountUiState?.account?.id ?: userId,\n                )\n                backstack.add(key)\n            }\n        },\n        onFollowingClick = {\n            if (uiState.userInsight != null) {\n                val screen = UserListScreenKey(\n                    type = UserListType.FOLLOWING,\n                    locator = uiState.locator,\n                    userUri = uiState.userInsight!!.uri,\n                    userId = uiState.accountUiState?.account?.id ?: userId,\n                )\n                backstack.add(screen)\n            }\n        },\n        onNewNoteSet = viewModel::onNewNoteSet,\n        onMaybeHashtagClick = {\n            backstack.add(\n                HashtagTimelineScreenKey(\n                    locator = uiState.locator,\n                    hashtag = it.removePrefix(\"#\"),\n                )\n            )\n        },\n        onUnmuteUserClick = viewModel::onUnmuteUserClick,\n        onMuteUserClick = viewModel::onMuteUserClick,\n        onMuteUserListClick = {\n            backstack.add(\n                UserListScreenKey(\n                    locator = locator,\n                    type = UserListType.MUTED,\n                    userId = uiState.accountUiState?.account?.id ?: userId,\n                )\n            )\n        },\n        onBlockedUserListClick = {\n            backstack.add(\n                UserListScreenKey(\n                    locator = locator,\n                    type = UserListType.BLOCKED,\n                    userId = uiState.accountUiState?.account?.id ?: userId,\n                )\n            )\n        },\n        onFollowedHashtagsListClick = {\n            backstack.add(TagListScreenKey(locator))\n        },\n        onFilterClick = {\n            backstack.add(FiltersListScreenKey(uiState.locator))\n        },\n        onCreatedListClick = {\n            backstack.add(CreatedListsScreenKey(uiState.locator))\n        },\n        onLogoutClick = {\n            viewModel.onLogoutClick()\n        },\n    )\n    ConsumeFlow(viewModel.finishPageFlow) {\n        backstack.removeLastOrNull()\n    }\n}\n\n@Composable\nprivate fun UserDetailContent(\n    uiState: UserDetailUiState,\n    messageFlow: SharedFlow<TextString>,\n    userId: String?,\n    onBackClick: () -> Unit,\n    onSearchClick: () -> Unit,\n    onFavouritesClick: () -> Unit,\n    onBookmarksClick: () -> Unit,\n    onBannerClick: () -> Unit,\n    onAvatarClick: () -> Unit,\n    onMuteUserClick: () -> Unit,\n    onUnmuteUserClick: () -> Unit,\n    onUnblockClick: () -> Unit,\n    onFollowAccountClick: () -> Unit,\n    onUnfollowAccountClick: () -> Unit,\n    onCancelFollowRequestClick: () -> Unit,\n    onBlockClick: () -> Unit,\n    onBlockDomainClick: () -> Unit,\n    onUnblockDomainClick: () -> Unit,\n    onOpenInBrowserClick: () -> Unit,\n    onCopyLinkClick: () -> Unit,\n    onOpenOriginalInstanceClick: () -> Unit,\n    onEditClick: () -> Unit,\n    onFollowerClick: () -> Unit,\n    onFollowingClick: () -> Unit,\n    onNewNoteSet: (String) -> Unit,\n    onMaybeHashtagClick: (String) -> Unit,\n    onMuteUserListClick: () -> Unit,\n    onBlockedUserListClick: () -> Unit,\n    onFollowedHashtagsListClick: () -> Unit,\n    onFilterClick: () -> Unit,\n    onCreatedListClick: () -> Unit,\n    onLogoutClick: () -> Unit,\n) {\n    val accountUiState = uiState.accountUiState\n    val account = accountUiState?.account\n    val browserLauncher = LocalActivityBrowserLauncher.current\n    val contentCanScrollBackward = remember { mutableStateOf(false) }\n    val snackBarHost = rememberSnackbarHostState()\n    val coroutineScope = rememberCoroutineScope()\n    ConsumeSnackbarFlow(hostState = snackBarHost, messageTextFlow = messageFlow)\n    DetailPageScaffold(\n        modifier = Modifier.fillMaxSize(),\n        snackbarHostState = snackBarHost,\n        title = accountUiState?.userName ?: RichText.empty,\n        avatar = account?.avatar.orEmpty(),\n        banner = account?.header,\n        description = accountUiState?.description,\n        privateNote = uiState.personalNote,\n        loading = uiState.loading,\n        contentCanScrollBackward = contentCanScrollBackward,\n        onBannerClick = onBannerClick,\n        onAvatarClick = onAvatarClick,\n        onUrlClick = {\n            browserLauncher.launchWebTabInApp(coroutineScope, it, uiState.locator)\n        },\n        onMaybeHashtagClick = onMaybeHashtagClick,\n        onBackClick = onBackClick,\n        topBarActions = {\n            ToolbarActions(\n                uiState = uiState,\n                onFavouritesClick = onFavouritesClick,\n                onBlockClick = onBlockClick,\n                onSearchClick = onSearchClick,\n                onBookmarksClick = onBookmarksClick,\n                onBlockDomainClick = onBlockDomainClick,\n                onUnblockDomainClick = onUnblockDomainClick,\n                onOpenInBrowserClick = onOpenInBrowserClick,\n                onOpenOriginalInstanceClick = onOpenOriginalInstanceClick,\n                onNewNoteSet = onNewNoteSet,\n                onCopyLinkClick = onCopyLinkClick,\n                onMuteUserClick = onMuteUserClick,\n                onUnmuteUserClick = onUnmuteUserClick,\n                onMuteUserListClick = onMuteUserListClick,\n                onBlockedUserListClick = onBlockedUserListClick,\n                onFollowedHashtagsListClick = onFollowedHashtagsListClick,\n                onFilterClick = onFilterClick,\n                onCreatedListClick = onCreatedListClick,\n                onLogoutClick = onLogoutClick,\n            )\n        },\n        handleLine = {\n            UserHandleLine(\n                modifier = Modifier,\n                handle = account?.prettyAcct.orEmpty(),\n                bot = account?.bot == true,\n                followedBy = uiState.relationships?.followedBy == true\n            )\n        },\n        followInfoLine = {\n            UserFollowLine(\n                modifier = Modifier,\n                followersCount = account?.followersCount?.toLong(),\n                followingCount = account?.followingCount?.toLong(),\n                statusesCount = account?.statusesCount?.toLong(),\n                onFollowerClick = onFollowerClick,\n                onFollowingClick = onFollowingClick,\n            )\n        },\n        topDetailContentAction = {\n            if (uiState.isAccountOwner) {\n                FilledTonalButton(\n                    onClick = onEditClick,\n                ) {\n                    Text(\n                        text = stringResource(LocalizedString.statusUiEditProfile)\n                    )\n                }\n            } else if (uiState.relationships != null) {\n                RelationshipStateButton(\n                    modifier = Modifier,\n                    relationship = uiState.relationships,\n                    onFollowClick = onFollowAccountClick,\n                    onUnfollowClick = onUnfollowAccountClick,\n                    onCancelFollowRequestClick = onCancelFollowRequestClick,\n                    onUnblockClick = onUnblockClick,\n                )\n            }\n        },\n        bottomArea = if (uiState.accountUiState?.account != null) {\n            {\n                UserAboutCard(\n                    locator = uiState.locator,\n                    account = uiState.accountUiState.account,\n                    emojis = uiState.accountUiState.emojis,\n                )\n            }\n        } else {\n            null\n        },\n    ) { progress ->\n        if (uiState.userInsight != null) {\n            val tabs: List<Tab> = remember(\n                uiState.userInsight,\n                uiState.locator,\n                uiState.isAccountOwner,\n            ) {\n                buildTabList(\n                    webFinger = uiState.userInsight.webFinger,\n                    locator = uiState.locator,\n                    userId = userId,\n                    isAccountOwner = uiState.isAccountOwner,\n                    contentCanScrollBackward = contentCanScrollBackward,\n                )\n            }\n            val nestedTabConnection = remember { NestedTabConnection() }\n            CompositionLocalProvider(\n                LocalNestedTabConnection provides nestedTabConnection,\n            ) {\n                val contentScrollInProgress by nestedTabConnection.contentScrollInpProgress.collectAsState()\n                ContentPaddingsHorizontalPagerWithTab(\n                    tabList = tabs,\n                    blurEnabled = progress >= 1F,\n                    pagerUserScrollEnabled = !contentScrollInProgress,\n                )\n            }\n        }\n    }\n}\n\nprivate fun buildTabList(\n    locator: PlatformLocator,\n    webFinger: WebFinger,\n    isAccountOwner: Boolean,\n    userId: String?,\n    contentCanScrollBackward: MutableState<Boolean>,\n): List<Tab> {\n    val tabList = mutableListOf<Tab>()\n    tabList += UserTimelineTab(\n        tabType = UserTimelineTabType.POSTS,\n        locator = locator,\n        userWebFinger = webFinger,\n        contentCanScrollBackward = contentCanScrollBackward,\n        userId = userId,\n    )\n    tabList += UserTimelineTab(\n        tabType = UserTimelineTabType.REPLIES,\n        locator = locator,\n        userWebFinger = webFinger,\n        contentCanScrollBackward = contentCanScrollBackward,\n        userId = userId,\n    )\n    tabList += UserTimelineTab(\n        tabType = UserTimelineTabType.MEDIA,\n        locator = locator,\n        userWebFinger = webFinger,\n        contentCanScrollBackward = contentCanScrollBackward,\n        userId = userId,\n    )\n    if (isAccountOwner) {\n        tabList += StatusListTabStatusListScreen(\n            locator = locator,\n            type = StatusListType.FAVOURITES,\n            contentCanScrollBackward = contentCanScrollBackward,\n        )\n        tabList += StatusListTabStatusListScreen(\n            locator = locator,\n            type = StatusListType.BOOKMARKS,\n            contentCanScrollBackward = contentCanScrollBackward,\n        )\n    }\n    return tabList\n}\n\n@Composable\nprivate fun ToolbarActions(\n    uiState: UserDetailUiState,\n    onSearchClick: () -> Unit,\n    onFavouritesClick: () -> Unit,\n    onBookmarksClick: () -> Unit,\n    onBlockClick: () -> Unit,\n    onBlockDomainClick: () -> Unit,\n    onUnblockDomainClick: () -> Unit,\n    onOpenInBrowserClick: () -> Unit,\n    onCopyLinkClick: () -> Unit,\n    onOpenOriginalInstanceClick: () -> Unit,\n    onNewNoteSet: (String) -> Unit,\n    onMuteUserClick: () -> Unit,\n    onUnmuteUserClick: () -> Unit,\n    onMuteUserListClick: () -> Unit,\n    onBlockedUserListClick: () -> Unit,\n    onFollowedHashtagsListClick: () -> Unit,\n    onFilterClick: () -> Unit,\n    onCreatedListClick: () -> Unit,\n    onLogoutClick: () -> Unit,\n) {\n    val accountUiState = uiState.accountUiState ?: return\n    SimpleIconButton(\n        onClick = onSearchClick,\n        imageVector = Icons.Default.Search,\n        contentDescription = stringResource(LocalizedString.search),\n    )\n    if (uiState.isAccountOwner) {\n        SimpleIconButton(\n            onClick = onCreatedListClick,\n            imageVector = Icons.AutoMirrored.Outlined.ListAlt,\n            contentDescription = stringResource(LocalizedString.activity_pub_created_list_title),\n        )\n    }\n    var showMorePopup by remember { mutableStateOf(false) }\n    SimpleIconButton(\n        onClick = { showMorePopup = true },\n        imageVector = Icons.Default.MoreVert,\n        contentDescription = \"More Options\"\n    )\n    var showBlockUserConfirmDialog by remember {\n        mutableStateOf(false)\n    }\n    var showBlockDomainConfirmDialog by remember {\n        mutableStateOf(false)\n    }\n    var showMuteDialog by remember {\n        mutableStateOf(false)\n    }\n    PopupMenu(\n        expanded = showMorePopup,\n        onDismissRequest = { showMorePopup = false },\n    ) {\n        DropDownOpenInBrowserItem {\n            showMorePopup = false\n            onOpenInBrowserClick()\n        }\n        DropDownCopyLinkItem {\n            showMorePopup = false\n            onCopyLinkClick()\n        }\n        DropDownOpenOriginalInstanceItem {\n            showMorePopup = false\n            onOpenOriginalInstanceClick()\n        }\n        val isAccountOwner = uiState.isAccountOwner\n        if (isAccountOwner) {\n            SelfAccountActions(\n                onBookmarksClick = {\n                    showMorePopup = false\n                    onBookmarksClick()\n                },\n                onFavouritesClick = {\n                    showMorePopup = false\n                    onFavouritesClick()\n                },\n                onBlockedUserListClick = {\n                    showMorePopup = false\n                    onBlockedUserListClick()\n                },\n                onMuteUserListClick = {\n                    showMorePopup = false\n                    onMuteUserListClick()\n                },\n                onFollowedHashtagsListClick = {\n                    showMorePopup = false\n                    onFollowedHashtagsListClick()\n                },\n                onFilterClick = {\n                    showMorePopup = false\n                    onFilterClick()\n                },\n                onLogoutClick = {\n                    showMorePopup = false\n                    onLogoutClick()\n                },\n            )\n        }\n        val relationship = uiState.relationships\n        if (!isAccountOwner && relationship != null) {\n            OtherAccountActions(\n                uiState = uiState,\n                account = accountUiState,\n                relationship = relationship,\n                onNewNoteSet = onNewNoteSet,\n                onDismissMorePopupRequest = {\n                    showMorePopup = false\n                },\n                onShowBlockUserConfirmDialog = {\n                    showBlockUserConfirmDialog = true\n                },\n                onShowBlockDomainConfirmDialog = {\n                    showBlockDomainConfirmDialog = true\n                },\n                onUnblockDomainClick = onUnblockDomainClick,\n                onUnmuteClick = onUnmuteUserClick,\n                onShowMuteDialogClick = {\n                    showMuteDialog = true\n                },\n            )\n        }\n    }\n    if (showBlockUserConfirmDialog) {\n        AlertConfirmDialog(\n            content = stringResource(LocalizedString.activity_pub_user_detail_dialog_content_block),\n            onConfirm = {\n                showBlockUserConfirmDialog = false\n                onBlockClick()\n            },\n            onDismissRequest = { showBlockUserConfirmDialog = false },\n        )\n    }\n    if (showBlockDomainConfirmDialog) {\n        AlertConfirmDialog(\n            content = stringResource(LocalizedString.activity_pub_user_detail_dialog_content_block_domain),\n            onConfirm = {\n                showBlockDomainConfirmDialog = false\n                onBlockDomainClick()\n            },\n            onDismissRequest = { showBlockDomainConfirmDialog = false },\n        )\n    }\n    if (showMuteDialog) {\n        MuteUserBottomSheetDialog(\n            account = accountUiState,\n            onDismissRequest = { showMuteDialog = false },\n            onConfirmClick = {\n                showMuteDialog = false\n                onMuteUserClick()\n            },\n        )\n    }\n}\n\n@Composable\nprivate fun EditPrivateNoteItem(\n    note: String,\n    onDismissRequest: () -> Unit,\n    onNewNoteSet: (String) -> Unit,\n) {\n    var showEditDialog by remember {\n        mutableStateOf(false)\n    }\n    ModalDropdownMenuItem(\n        text = stringResource(LocalizedString.activity_pub_user_detail_menu_edit_private_note),\n        imageVector = Icons.Default.Edit,\n        onClick = {\n            showEditDialog = true\n        },\n    )\n    if (showEditDialog) {\n        var inputtingNote by remember { mutableStateOf(note) }\n        FreadDialog(\n            onDismissRequest = {\n                onDismissRequest()\n                showEditDialog = false\n            },\n            title = stringResource(LocalizedString.activity_pub_user_detail_menu_edit_private_note),\n            content = {\n                OutlinedTextField(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .padding(start = 16.dp, top = 8.dp, bottom = 8.dp, end = 16.dp),\n                    value = inputtingNote,\n                    onValueChange = {\n                        inputtingNote = it\n                    },\n                    label = {\n                        Text(\n                            text = stringResource(LocalizedString.activity_pub_user_detail_menu_edit_private_note)\n                        )\n                    },\n                    placeholder = {\n                        Text(\n                            text = stringResource(LocalizedString.activity_pub_user_detail_menu_edit_private_note_dialog_hint)\n                        )\n                    },\n                )\n            },\n            onNegativeClick = {\n                onDismissRequest()\n                showEditDialog = false\n            },\n            onPositiveClick = {\n                onDismissRequest()\n                showEditDialog = false\n                onNewNoteSet(inputtingNote)\n            },\n        )\n    }\n}\n\n@Composable\nprivate fun SelfAccountActions(\n    onBookmarksClick: () -> Unit,\n    onFavouritesClick: () -> Unit,\n    onBlockedUserListClick: () -> Unit,\n    onMuteUserListClick: () -> Unit,\n    onFollowedHashtagsListClick: () -> Unit,\n    onFilterClick: () -> Unit,\n    onLogoutClick: () -> Unit,\n) {\n    ModalDropdownMenuItem(\n        text = stringResource(LocalizedString.activity_pub_favourites_list_title),\n        onClick = onFavouritesClick,\n        imageVector = Icons.Default.Favorite,\n    )\n    ModalDropdownMenuItem(\n        text = stringResource(LocalizedString.activity_pub_bookmarks_list_title),\n        onClick = onBookmarksClick,\n        imageVector = Icons.Default.Bookmarks,\n    )\n    ModalDropdownMenuItem(\n        text = stringResource(LocalizedString.activity_pub_followed_tags_screen_title),\n        onClick = onFollowedHashtagsListClick,\n        imageVector = Icons.Default.Tag,\n    )\n    ModalDropdownMenuItem(\n        text = stringResource(LocalizedString.activity_pub_user_menu_muted_user_list),\n        imageVector = Icons.AutoMirrored.Filled.VolumeOff,\n        onClick = onMuteUserListClick,\n    )\n    ModalDropdownMenuItem(\n        text = stringResource(LocalizedString.activity_pub_user_menu_blocked_user_list),\n        imageVector = Icons.Default.Block,\n        onClick = onBlockedUserListClick,\n    )\n    ModalDropdownMenuItem(\n        text = stringResource(LocalizedString.activity_pub_filters_list_page_title),\n        imageVector = Icons.Default.FilterAlt,\n        onClick = onFilterClick,\n    )\n    var showLogoutDialog by remember { mutableStateOf(false) }\n    ModalDropdownMenuItem(\n        text = stringResource(LocalizedString.statusUiLogout),\n        imageVector = Icons.AutoMirrored.Filled.Logout,\n        colors = MenuDefaults.itemColors(\n            textColor = MaterialTheme.colorScheme.error,\n            leadingIconColor = MaterialTheme.colorScheme.error,\n        ),\n        onClick = { showLogoutDialog = true },\n    )\n    if (showLogoutDialog) {\n        FreadDialog(\n            onDismissRequest = { showLogoutDialog = false },\n            contentText = stringResource(LocalizedString.statusUiLogoutDialogContent),\n            onPositiveClick = {\n                showLogoutDialog = false\n                onLogoutClick()\n            },\n            onNegativeClick = { showLogoutDialog = false },\n        )\n    }\n}\n\n@Composable\nprivate fun OtherAccountActions(\n    uiState: UserDetailUiState,\n    account: UserDetailAccountUiState,\n    relationship: Relationships,\n    onNewNoteSet: (String) -> Unit,\n    onUnmuteClick: () -> Unit,\n    onDismissMorePopupRequest: () -> Unit,\n    onUnblockDomainClick: () -> Unit,\n    onShowBlockUserConfirmDialog: () -> Unit,\n    onShowBlockDomainConfirmDialog: () -> Unit,\n    onShowMuteDialogClick: () -> Unit,\n) {\n    EditPrivateNoteItem(\n        note = uiState.personalNote.orEmpty(),\n        onDismissRequest = onDismissMorePopupRequest,\n        onNewNoteSet = onNewNoteSet,\n    )\n    val fixedName = account.account.displayName.take(10)\n    val muteOrUnmuteText = if (relationship.muting) {\n        stringResource(LocalizedString.activity_pub_user_detail_menu_unmute_user, fixedName)\n    } else {\n        stringResource(LocalizedString.activity_pub_user_detail_menu_mute_user, fixedName)\n    }\n    ModalDropdownMenuItem(\n        text = muteOrUnmuteText,\n        imageVector = Icons.AutoMirrored.Filled.VolumeOff,\n        onClick = {\n            onDismissMorePopupRequest()\n            if (relationship.muting) {\n                onUnmuteClick()\n            } else {\n                onShowMuteDialogClick()\n            }\n        }\n    )\n    if (!relationship.blocking) {\n        ModalDropdownMenuItem(\n            text = stringResource(LocalizedString.activity_pub_user_detail_menu_block, fixedName),\n            imageVector = Icons.Default.Block,\n            onClick = {\n                onDismissMorePopupRequest()\n                onShowBlockUserConfirmDialog()\n            },\n        )\n    }\n    val domainBlocked = uiState.domainBlocked\n    val host = uiState.userInsight!!.baseUrl.host\n    if (domainBlocked != null) {\n        val blockDomainLabel = if (domainBlocked) {\n            stringResource(LocalizedString.activity_pub_user_detail_menu_unblock_domain, host)\n        } else {\n            stringResource(LocalizedString.activity_pub_user_detail_menu_block_domain, host)\n        }\n        ModalDropdownMenuItem(\n            text = blockDomainLabel,\n            imageVector = Icons.Default.Block,\n            onClick = {\n                onDismissMorePopupRequest()\n                if (domainBlocked) {\n                    onUnblockDomainClick()\n                } else {\n                    onShowBlockDomainConfirmDialog()\n                }\n            }\n        )\n    }\n}\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nprivate fun MuteUserBottomSheetDialog(\n    account: UserDetailAccountUiState,\n    onDismissRequest: () -> Unit,\n    onConfirmClick: () -> Unit,\n) {\n    val coroutineScope = rememberCoroutineScope()\n    val state = rememberTransientModalBottomSheetState(skipPartiallyExpanded = true)\n    ModalBottomSheet(\n        sheetState = state,\n        onDismissRequest = onDismissRequest,\n    ) {\n        Column(\n            modifier = Modifier\n                .fillMaxWidth()\n                .padding(start = 16.dp, end = 16.dp, bottom = 8.dp),\n        ) {\n            Text(\n                modifier = Modifier.align(Alignment.CenterHorizontally),\n                text = stringResource(LocalizedString.activity_pub_mute_user_bottom_sheet_title),\n                style = MaterialTheme.typography.titleLarge,\n            )\n\n            FreadRichText(\n                modifier = Modifier\n                    .padding(top = 16.dp)\n                    .align(Alignment.CenterHorizontally),\n                maxLines = 1,\n                overflow = TextOverflow.Ellipsis,\n                richText = account.userName,\n                fontSize = 16.sp,\n            )\n\n            Text(\n                modifier = Modifier\n                    .padding(top = 6.dp)\n                    .align(Alignment.CenterHorizontally),\n                text = account.account.prettyAcct,\n                style = MaterialTheme.typography.labelMedium,\n                maxLines = 1,\n                overflow = TextOverflow.Ellipsis,\n            )\n\n            Spacer(modifier = Modifier.height(8.dp))\n\n            MuteUserRoleItem(\n                icon = Icons.Default.Campaign,\n                role = stringResource(LocalizedString.activity_pub_mute_user_bottom_sheet_role1)\n            )\n\n            MuteUserRoleItem(\n                icon = Icons.Default.VisibilityOff,\n                role = stringResource(LocalizedString.activity_pub_mute_user_bottom_sheet_role2)\n            )\n\n            MuteUserRoleItem(\n                icon = Icons.Default.AlternateEmail,\n                role = stringResource(LocalizedString.activity_pub_mute_user_bottom_sheet_role3)\n            )\n\n            MuteUserRoleItem(\n                icon = vectorResource(com.zhangke.fread.statusui.Res.drawable.ic_status_forward),\n                role = stringResource(LocalizedString.activity_pub_mute_user_bottom_sheet_role4)\n            )\n\n            Button(\n                modifier = Modifier\n                    .padding(top = 16.dp)\n                    .fillMaxWidth()\n                    .height(40.dp),\n                onClick = onConfirmClick,\n            ) {\n                Text(text = stringResource(LocalizedString.activity_pub_mute_user_bottom_sheet_btn_mute))\n            }\n            TextButton(\n                modifier = Modifier\n                    .align(Alignment.CenterHorizontally)\n                    .padding(top = 8.dp),\n                onClick = {\n                    coroutineScope.launch {\n                        state.hide()\n                        onDismissRequest()\n                    }\n                },\n            ) {\n                Text(text = stringResource(LocalizedString.cancel))\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun MuteUserRoleItem(\n    icon: ImageVector,\n    role: String,\n) {\n    Row(\n        modifier = Modifier\n            .fillMaxWidth()\n            .padding(vertical = 8.dp),\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        Icon(\n            imageVector = icon,\n            contentDescription = null,\n        )\n        Spacer(modifier = Modifier.width(16.dp))\n        Text(\n            modifier = Modifier.weight(1F),\n            text = role,\n            textAlign = TextAlign.Start,\n        )\n    }\n}\n\n@Composable\nprivate fun UserAboutCard(\n    account: ActivityPubAccountEntity,\n    locator: PlatformLocator,\n    emojis: List<Emoji>,\n) {\n    Card(\n        modifier = Modifier.fillMaxWidth(),\n        colors = CardDefaults.cardColors(\n            containerColor = MaterialTheme.colorScheme\n                .surfaceContainerHighest\n                .copy(alpha = 0.3F),\n        ),\n    ) {\n        SelectionContainer {\n            Column(\n                modifier = Modifier.fillMaxWidth()\n                    .padding(horizontal = 16.dp, vertical = 8.dp),\n            ) {\n                FieldLine(\n                    locator = locator,\n                    key = stringResource(LocalizedString.activity_pub_user_detail_join_date),\n                    value = DateParser.parseOrCurrent(account.createdAt).formatDate(),\n                    emojis = emojis,\n                )\n                for (field in account.fields) {\n                    FieldLine(\n                        locator = locator,\n                        key = field.name,\n                        value = field.value,\n                        emojis = emojis,\n                    )\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun FieldLine(\n    locator: PlatformLocator,\n    key: String,\n    value: String,\n    emojis: List<Emoji>,\n) {\n    val browserLauncher = LocalActivityBrowserLauncher.current\n    val coroutineScope = rememberCoroutineScope()\n    Row(\n        modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp),\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        Text(\n            modifier = Modifier.weight(2F),\n            text = key,\n            maxLines = 1,\n            fontWeight = FontWeight.SemiBold,\n            style = MaterialTheme.typography.labelMedium,\n        )\n        Spacer(modifier = Modifier.width(8.dp))\n        Box(\n            modifier = Modifier.weight(4F),\n            contentAlignment = Alignment.CenterEnd,\n        ) {\n            FreadRichText(\n                modifier = Modifier,\n                content = value,\n                emojis = emojis,\n                textAlign = TextAlign.End,\n                maxLines = 3,\n                fontSize = 14.sp,\n                onUrlClick = {\n                    browserLauncher.launchWebTabInApp(coroutineScope, it, locator)\n                },\n            )\n        }\n    }\n}\n\nprivate val ActivityPubAccountEntity.prettyAcct: String\n    get() {\n        val acct = this.acct\n        return if (acct.isNotEmpty() && !acct.contains('@')) {\n            \"@$acct\"\n        } else {\n            acct\n        }\n    }\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/user/UserDetailUiState.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.user\n\nimport com.zhangke.activitypub.entities.ActivityPubAccountEntity\nimport com.zhangke.fread.activitypub.app.internal.model.UserUriInsights\nimport com.zhangke.fread.status.model.Emoji\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.model.Relationships\nimport com.zhangke.fread.status.richtext.RichText\n\ndata class UserDetailUiState(\n    val locator: PlatformLocator,\n    val loading: Boolean,\n    val userInsight: UserUriInsights?,\n    val accountUiState: UserDetailAccountUiState?,\n    val personalNote: String?,\n    val relationships: Relationships?,\n    val domainBlocked: Boolean?,\n    val isAccountOwner: Boolean,\n)\n\ndata class UserDetailAccountUiState(\n    val account: ActivityPubAccountEntity,\n    val userName: RichText,\n    val description: RichText,\n    val emojis: List<Emoji>,\n)\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/user/UserDetailViewModel.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.user\n\nimport com.zhangke.activitypub.api.AccountsRepo\nimport com.zhangke.activitypub.entities.ActivityPubAccountEntity\nimport com.zhangke.activitypub.entities.ActivityPubRelationshipEntity\nimport com.zhangke.framework.composable.TextString\nimport com.zhangke.framework.composable.emitTextMessageFromThrowable\nimport com.zhangke.framework.composable.textOf\nimport com.zhangke.framework.ktx.launchInViewModel\nimport com.zhangke.framework.lifecycle.SubViewModel\nimport com.zhangke.framework.utils.WebFinger\nimport com.zhangke.fread.activitypub.app.ActivityPubAccountManager\nimport com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubAccountEntityAdapter\nimport com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubCustomEmojiEntityAdapter\nimport com.zhangke.fread.activitypub.app.internal.auth.ActivityPubClientManager\nimport com.zhangke.fread.activitypub.app.internal.model.UserUriInsights\nimport com.zhangke.fread.activitypub.app.internal.uri.UserUriTransformer\nimport com.zhangke.fread.activitypub.app.internal.usecase.ActivityPubAccountLogoutUseCase\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.richtext.buildRichText\nimport com.zhangke.fread.status.uri.FormalUri\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asSharedFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.update\n\nclass UserDetailViewModel(\n    private val accountManager: ActivityPubAccountManager,\n    private val accountEntityAdapter: ActivityPubAccountEntityAdapter,\n    private val userUriTransformer: UserUriTransformer,\n    private val clientManager: ActivityPubClientManager,\n    private val emojiEntityAdapter: ActivityPubCustomEmojiEntityAdapter,\n    private val accountLogout: ActivityPubAccountLogoutUseCase,\n    val locator: PlatformLocator,\n    val userUri: FormalUri?,\n    val webFinger: WebFinger?,\n    val userId: String?,\n) : SubViewModel() {\n\n    private val _uiState = MutableStateFlow(\n        UserDetailUiState(\n            locator = locator,\n            loading = false,\n            userInsight = null,\n            accountUiState = null,\n            domainBlocked = false,\n            isAccountOwner = false,\n            relationships = null,\n            personalNote = null,\n        )\n    )\n    val uiState = _uiState.asStateFlow()\n\n    private val _messageFlow = MutableSharedFlow<TextString>()\n    val messageFlow = _messageFlow.asSharedFlow()\n\n    private val _finishPageFlow = MutableSharedFlow<Unit>()\n    val finishPageFlow = _finishPageFlow.asSharedFlow()\n\n    init {\n        launchInViewModel {\n            val webFinger = userUri?.let(userUriTransformer::parse)?.webFinger ?: webFinger\n            if (webFinger == null) {\n                _messageFlow.emit(textOf(\"Invalid user.\"))\n                return@launchInViewModel\n            }\n            _uiState.update { it.copy(loading = true) }\n            val accountRepo = clientManager.getClient(locator).accountRepo\n            val accountResult = loadAccountInfo(accountRepo, webFinger)\n            if (accountResult.isFailure) {\n                _uiState.update { it.copy(loading = false) }\n                _messageFlow.emit(textOf(\"Failed to lookup ${webFinger}, ${accountResult.exceptionOrNull()!!.message}\"))\n                return@launchInViewModel\n            }\n            val account = accountResult.getOrThrow()!!\n            val userInsight = accountEntityAdapter.toUri(account).let(userUriTransformer::parse)!!\n            val isAccountOwner = accountManager.getAllLoggedAccount()\n                .any { loggedAccount ->\n                    loggedAccount.uri == userInsight.uri\n                }\n            _uiState.value = _uiState.value.copy(\n                userInsight = userInsight,\n                isAccountOwner = isAccountOwner,\n                accountUiState = account.toAccountUiState(),\n                loading = false,\n            )\n            loadRelationship(accountRepo, account.id)\n            loadDomainBlockState(accountRepo, userInsight)\n        }\n    }\n\n    private suspend fun loadAccountInfo(\n        accountRepo: AccountsRepo,\n        webFinger: WebFinger,\n    ): Result<ActivityPubAccountEntity?> {\n        if (!userId.isNullOrEmpty()) {\n            return accountRepo.getAccount(userId)\n        }\n        val resultOfWebFinger = accountRepo.lookup(webFinger.toString())\n        if (resultOfWebFinger.isSuccess) {\n            return resultOfWebFinger\n        }\n        return accountRepo.lookup(\"@${webFinger.name}\")\n    }\n\n    private suspend fun loadRelationship(\n        accountRepo: AccountsRepo,\n        accountId: String,\n    ) {\n        val relationshipEntityResult = accountRepo.getRelationships(listOf(accountId))\n        if (relationshipEntityResult.isFailure) {\n            return\n        }\n        val relationshipEntity = relationshipEntityResult.getOrThrow().firstOrNull() ?: return\n        _uiState.update { state ->\n            state.copy(\n                personalNote = relationshipEntity.note,\n                relationships = accountEntityAdapter.convertRelationship(relationshipEntity),\n            )\n        }\n    }\n\n    private suspend fun loadDomainBlockState(\n        accountRepo: AccountsRepo,\n        userUriInsights: UserUriInsights,\n    ) {\n        val blockedDomainList = accountRepo.getDomainBlocks().getOrNull() ?: return\n        val domainBlocked = blockedDomainList.firstOrNull { it == userUriInsights.baseUrl.host }\n        _uiState.value = _uiState.value.copy(\n            domainBlocked = domainBlocked != null\n        )\n    }\n\n    fun onFollowClick() {\n        performRelationshipAction { accountsRepo, accountId ->\n            accountsRepo.follow(accountId)\n        }\n    }\n\n    fun onUnfollowClick() {\n        performRelationshipAction { accountsRepo, accountId ->\n            accountsRepo.unfollow(accountId)\n        }\n    }\n\n    fun onCancelFollowRequestClick() {\n        performRelationshipAction { accountsRepo, accountId ->\n            accountsRepo.unfollow(accountId)\n        }\n    }\n\n    fun onBlockClick() {\n        performRelationshipAction { accountsRepo, accountId ->\n            accountsRepo.block(accountId)\n        }\n    }\n\n    fun onUnblockClick() {\n        performRelationshipAction { accountsRepo, accountId ->\n            accountsRepo.unblock(accountId)\n        }\n    }\n\n    fun onBlockDomainClick() {\n        val userUriInsights = _uiState.value.userInsight ?: return\n        launchInViewModel {\n            val accountRepo = clientManager.getClient(locator).accountRepo\n            accountRepo.blockDomain(userUriInsights.baseUrl.host)\n                .onFailure { e ->\n                    e.message?.let {\n                        _messageFlow.emit(textOf(it))\n                    }\n                }.onSuccess {\n                    _uiState.update {\n                        it.copy(domainBlocked = true)\n                    }\n                }\n        }\n    }\n\n    fun onUnblockDomainClick() {\n        val userUriInsights = _uiState.value.userInsight ?: return\n        launchInViewModel {\n            val accountRepo = clientManager.getClient(locator).accountRepo\n            accountRepo.unblockDomain(userUriInsights.baseUrl.host)\n                .onFailure { e ->\n                    e.message?.let {\n                        _messageFlow.emit(textOf(it))\n                    }\n                }.onSuccess {\n                    _uiState.update {\n                        it.copy(domainBlocked = false)\n                    }\n                }\n        }\n    }\n\n    fun onNewNoteSet(newNote: String) {\n        val privateNote = uiState.value.personalNote\n        if (newNote == privateNote) return\n        val accountId = uiState.value.accountUiState?.account?.id ?: return\n        launchInViewModel {\n            val accountRepo = clientManager.getClient(locator).accountRepo\n            accountRepo.updateNote(accountId, newNote)\n                .onSuccess { relationship ->\n                    _uiState.update {\n                        it.copy(\n                            personalNote = relationship.note,\n                            relationships = accountEntityAdapter.convertRelationship(relationship),\n                        )\n                    }\n                }.onFailure {\n                    _messageFlow.emitTextMessageFromThrowable(it)\n                }\n        }\n    }\n\n    fun onMuteUserClick() {\n        muteOrUnmute(true)\n    }\n\n    fun onUnmuteUserClick() {\n        muteOrUnmute(false)\n    }\n\n    fun onLogoutClick() {\n        val account = uiState.value.accountUiState?.account ?: return\n        val uriInsights = uiState.value.userInsight ?: return\n        launchInViewModel {\n            accountLogout(\n                baseUrl = locator.baseUrl,\n                accountUri = uriInsights.uri,\n                userId = account.id,\n            )\n            _finishPageFlow.emit(Unit)\n        }\n    }\n\n    private fun muteOrUnmute(mute: Boolean) {\n        val accountId = uiState.value.accountUiState?.account?.id ?: return\n        launchInViewModel {\n            val accountRepo = clientManager.getClient(locator).accountRepo\n            if (mute) {\n                accountRepo.mute(accountId)\n            } else {\n                accountRepo.unmute(accountId)\n            }.map { accountEntityAdapter.convertRelationship(it) }\n                .onSuccess { relationship ->\n                    _uiState.update { it.copy(relationships = relationship) }\n                }.onFailure {\n                    _messageFlow.emitTextMessageFromThrowable(it)\n                }\n        }\n    }\n\n    private fun performRelationshipAction(\n        action: suspend (accountsRepo: AccountsRepo, accountId: String) -> Result<ActivityPubRelationshipEntity>,\n    ) {\n        val accountId = _uiState.value.accountUiState?.account?.id ?: return\n        launchInViewModel {\n            val accountRepo = clientManager.getClient(locator).accountRepo\n            action(accountRepo, accountId)\n                .onFailure { e ->\n                    e.message?.let {\n                        _messageFlow.emit(textOf(it))\n                    }\n                }.onSuccess { relationship ->\n                    _uiState.update {\n                        it.copy(\n                            relationships = accountEntityAdapter.convertRelationship(relationship),\n                        )\n                    }\n                }\n        }\n    }\n\n    private fun ActivityPubAccountEntity.toAccountUiState(): UserDetailAccountUiState {\n        val customEmojis = emojis.map(emojiEntityAdapter::toEmoji)\n        return UserDetailAccountUiState(\n            account = this,\n            userName = buildRichText(\n                document = getFixedName(),\n                emojis = customEmojis,\n            ),\n            description = buildRichText(\n                document = note,\n                emojis = customEmojis,\n            ),\n            emojis = customEmojis,\n        )\n    }\n\n    private fun ActivityPubAccountEntity.getFixedName(): String {\n        if (displayName.isNotEmpty()) return displayName\n        if (username.isNotEmpty()) return username\n        return \"\"\n    }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/user/list/UserListScreen.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.user.list\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.RowScope\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.SnackbarHost\nimport androidx.compose.material3.SnackbarHostState\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.composable.currentOrThrow\nimport com.zhangke.framework.composable.ConsumeSnackbarFlow\nimport com.zhangke.framework.composable.StyledTextButton\nimport com.zhangke.framework.composable.TextButtonStyle\nimport com.zhangke.framework.composable.Toolbar\nimport com.zhangke.framework.composable.rememberSnackbarHostState\nimport com.zhangke.framework.loadable.lazycolumn.LoadableLazyColumn\nimport com.zhangke.framework.loadable.lazycolumn.rememberLoadableLazyColumnState\nimport com.zhangke.framework.nav.LocalNavBackStack\nimport com.zhangke.fread.activitypub.app.internal.screen.user.UserDetailScreenKey\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.author.BlogAuthor\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.ui.user.CommonUserPlaceHolder\nimport com.zhangke.fread.status.ui.user.CommonUserUi\nimport com.zhangke.fread.status.uri.FormalUri\nimport kotlinx.serialization.Serializable\nimport org.jetbrains.compose.resources.stringResource\n\n@Serializable\ndata class UserListScreenKey(\n    val locator: PlatformLocator,\n    val type: UserListType,\n    val statusId: String? = null,\n    val userUri: FormalUri? = null,\n    val userId: String? = null,\n) : NavKey\n\n@Composable\nfun UserListScreen(viewModel: UserListViewModel) {\n    val backStack = LocalNavBackStack.currentOrThrow\n    val uiState by viewModel.uiState.collectAsState()\n    val snackBarHostState = rememberSnackbarHostState()\n    UserListContent(\n        uiState = uiState,\n        snackBarHostState = snackBarHostState,\n        onRefresh = viewModel::onRefresh,\n        onLoadMore = viewModel::onLoadMore,\n        onUnblockClick = viewModel::onUnblockClick,\n        onUnmuteClick = viewModel::onUnmuteClick,\n        onBackClick = backStack::removeLastOrNull,\n        onFollowClick = viewModel::onFollowClick,\n    )\n    ConsumeSnackbarFlow(snackBarHostState, viewModel.snackMessageFlow)\n}\n\n@Composable\nprivate fun UserListContent(\n    uiState: UserListUiState,\n    snackBarHostState: SnackbarHostState,\n    onBackClick: () -> Unit,\n    onUnblockClick: (BlogAuthor) -> Unit,\n    onUnmuteClick: (BlogAuthor) -> Unit,\n    onRefresh: () -> Unit,\n    onLoadMore: () -> Unit,\n    onFollowClick: (BlogAuthorUiState) -> Unit,\n) {\n    val backStack = LocalNavBackStack.currentOrThrow\n    Scaffold(\n        topBar = {\n            Toolbar(\n                title = uiState.type.title,\n                onBackClick = onBackClick,\n            )\n        },\n        snackbarHost = {\n            SnackbarHost(snackBarHostState)\n        },\n    ) { innerPadding ->\n        Box(\n            modifier = Modifier\n                .fillMaxSize()\n                .padding(innerPadding)\n        ) {\n            if (uiState.userList.isNotEmpty()) {\n                val loadableState = rememberLoadableLazyColumnState(\n                    onRefresh = onRefresh,\n                    onLoadMore = onLoadMore,\n                )\n                LoadableLazyColumn(\n                    modifier = Modifier.fillMaxSize(),\n                    state = loadableState,\n                    refreshing = uiState.loading,\n                    loadState = uiState.loadMoreState,\n                ) {\n                    itemsIndexed(uiState.userList) { index, item ->\n                        CommonUserUi(\n                            modifier = Modifier.clickable {\n                                backStack.add(\n                                    UserDetailScreenKey(\n                                        locator = uiState.locator,\n                                        webFinger = item.author.webFinger,\n                                        userId = item.author.userId,\n                                    )\n                                )\n                            },\n                            user = item.author,\n                            showDivider = index < uiState.userList.lastIndex,\n                            actionButton = {\n                                StatusAction(\n                                    authorUiState = item,\n                                    type = uiState.type,\n                                    onUnblockClick = onUnblockClick,\n                                    onUnmuteClick = onUnmuteClick,\n                                    onFollowClick = onFollowClick,\n                                )\n                            },\n                        )\n                    }\n                }\n            } else if (uiState.loading) {\n                LazyColumn(\n                    modifier = Modifier.fillMaxSize(),\n                ) {\n                    items(30) {\n                        CommonUserPlaceHolder()\n                    }\n                }\n            } else {\n                Box(modifier = Modifier.fillMaxSize()) {\n                    Text(\n                        modifier = Modifier.align(Alignment.Center),\n                        text = stringResource(LocalizedString.activity_pub_user_list_empty),\n                    )\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun RowScope.StatusAction(\n    authorUiState: BlogAuthorUiState,\n    type: UserListType,\n    onUnblockClick: (BlogAuthor) -> Unit,\n    onUnmuteClick: (BlogAuthor) -> Unit,\n    onFollowClick: (BlogAuthorUiState) -> Unit,\n) {\n    val author = authorUiState.author\n    when (type) {\n        UserListType.BLOCKED -> {\n            Spacer(modifier = Modifier.width(6.dp))\n            StyledTextButton(\n                modifier = Modifier.align(Alignment.CenterVertically),\n                text = stringResource(LocalizedString.sharedUserListActionBlocked),\n                style = TextButtonStyle.STANDARD,\n                onClick = {\n                    onUnblockClick(author)\n                },\n            )\n        }\n\n        UserListType.MUTED -> {\n            Spacer(modifier = Modifier.width(6.dp))\n            StyledTextButton(\n                modifier = Modifier.align(Alignment.CenterVertically),\n                text = stringResource(LocalizedString.sharedUserListActionMuted),\n                style = TextButtonStyle.STANDARD,\n                onClick = {\n                    onUnmuteClick(author)\n                },\n            )\n        }\n\n        UserListType.REBLOGS, UserListType.FOLLOWERS, UserListType.FAVOURITES -> {\n            if (authorUiState.following == false) {\n                Spacer(modifier = Modifier.width(6.dp))\n                StyledTextButton(\n                    modifier = Modifier.align(Alignment.CenterVertically),\n                    text = stringResource(LocalizedString.statusUiFollow),\n                    style = TextButtonStyle.STANDARD,\n                    onClick = {\n                        onFollowClick(authorUiState)\n                    },\n                )\n            }\n        }\n\n        else -> {}\n    }\n}\n\nprivate val UserListType.title: String\n    @Composable get() = when (this) {\n        UserListType.FAVOURITES -> stringResource(LocalizedString.sharedUserListTitleLikes)\n        UserListType.REBLOGS -> stringResource(LocalizedString.sharedUserListTitleReblog)\n        UserListType.MUTED -> stringResource(LocalizedString.sharedUserListTitleMutes)\n        UserListType.BLOCKED -> stringResource(LocalizedString.sharedUserListTitleBlocks)\n        UserListType.FOLLOWERS -> stringResource(LocalizedString.sharedUserListTitleFollowers)\n        UserListType.FOLLOWING -> stringResource(LocalizedString.sharedUserListTitleFollowing)\n    }\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/user/list/UserListType.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.user.list\n\nimport com.zhangke.framework.utils.PlatformSerializable\nimport kotlinx.serialization.Serializable\n\n@Serializable\nenum class UserListType: PlatformSerializable {\n\n    FAVOURITES,\n    REBLOGS,\n    BLOCKED,\n    MUTED,\n    FOLLOWERS,\n    FOLLOWING,\n\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/user/list/UserListUiState.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.user.list\n\nimport com.zhangke.framework.utils.LoadState\nimport com.zhangke.fread.status.author.BlogAuthor\nimport com.zhangke.fread.status.model.PlatformLocator\n\ndata class UserListUiState(\n    val type: UserListType,\n    val locator: PlatformLocator,\n    val loading: Boolean,\n    val userList: List<BlogAuthorUiState>,\n    val loadMoreState: LoadState,\n)\n\ndata class BlogAuthorUiState(\n    val author: BlogAuthor,\n    val following: Boolean? = null,\n)\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/user/list/UserListViewModel.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.user.list\n\nimport androidx.lifecycle.ViewModel\nimport com.zhangke.activitypub.api.PagingResult\nimport com.zhangke.activitypub.entities.ActivityPubAccountEntity\nimport com.zhangke.framework.composable.TextString\nimport com.zhangke.framework.composable.emitTextMessageFromThrowable\nimport com.zhangke.framework.composable.textOf\nimport com.zhangke.framework.composable.toTextStringOrNull\nimport com.zhangke.framework.ktx.launchInViewModel\nimport com.zhangke.framework.utils.LoadState\nimport com.zhangke.framework.utils.exceptionOrThrow\nimport com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubAccountEntityAdapter\nimport com.zhangke.fread.activitypub.app.internal.auth.ActivityPubClientManager\nimport com.zhangke.fread.activitypub.app.internal.repo.WebFingerBaseUrlToUserIdRepo\nimport com.zhangke.fread.activitypub.app.internal.uri.UserUriTransformer\nimport com.zhangke.fread.common.status.StatusConfigurationDefault\nimport com.zhangke.fread.status.author.BlogAuthor\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.uri.FormalUri\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.asSharedFlow\nimport kotlinx.coroutines.flow.update\nclass UserListViewModel (\n    private val clientManager: ActivityPubClientManager,\n    private val userUriTransformer: UserUriTransformer,\n    private val webFingerBaseUrlToUserIdRepo: WebFingerBaseUrlToUserIdRepo,\n    private val accountEntityAdapter: ActivityPubAccountEntityAdapter,\n    private val locator: PlatformLocator,\n    private val type: UserListType,\n    private val statusId: String?,\n    private val userUri: FormalUri?,\n    private val userId: String?,\n) : ViewModel() {\n\n    private val _uiState = MutableStateFlow(\n        UserListUiState(\n            type = type,\n            locator = locator,\n            loading = false,\n            userList = emptyList(),\n            loadMoreState = LoadState.Idle,\n        )\n    )\n    val uiState: StateFlow<UserListUiState> = _uiState\n\n    private val mutableSnackMessageFlow = MutableSharedFlow<TextString>()\n    val snackMessageFlow = mutableSnackMessageFlow.asSharedFlow()\n\n    private var refreshJob: Job? = null\n    private var loadMoreJob: Job? = null\n\n    private var nextMaxId: String? = null\n\n    private var cachedUserId: String? = userId\n\n    init {\n        loadFirstPageUsers()\n    }\n\n    fun onRefresh() {\n        loadFirstPageUsers()\n    }\n\n    fun onLoadMore() {\n        loadNextPageUsers()\n    }\n\n    fun onFollowClick(authorUiState: BlogAuthorUiState) {\n\n    }\n\n    private fun loadFirstPageUsers() {\n        if (refreshJob?.isActive == true) return\n        loadMoreJob?.cancel()\n        _uiState.update { it.copy(loading = true) }\n        refreshJob = launchInViewModel {\n            fetchUserListFromServer()\n                .onSuccess {\n                    _uiState.update { state ->\n                        state.copy(\n                            loading = false,\n                            userList = it,\n                        )\n                    }\n                }.onFailure { t ->\n                    _uiState.update { it.copy(loading = false) }\n                    mutableSnackMessageFlow.emitTextMessageFromThrowable(t)\n                }\n        }\n    }\n\n    private fun loadNextPageUsers() {\n        if (refreshJob?.isActive == true) return\n        if (loadMoreJob?.isActive == true) return\n        if (nextMaxId.isNullOrEmpty()) return\n        loadMoreJob = launchInViewModel {\n            _uiState.update { it.copy(loadMoreState = LoadState.Loading) }\n            fetchUserListFromServer(nextMaxId)\n                .onSuccess {\n                    _uiState.update { state ->\n                        state.copy(\n                            loadMoreState = LoadState.Idle,\n                            userList = state.userList + it,\n                        )\n                    }\n                }.onFailure { t ->\n                    _uiState.update { it.copy(loadMoreState = LoadState.Failed(t.toTextStringOrNull())) }\n                }\n        }\n    }\n\n    private suspend fun fetchUserListFromServer(maxId: String? = null): Result<List<BlogAuthorUiState>> {\n        val client = clientManager.getClient(locator)\n        val pagingResult = when (type) {\n            UserListType.REBLOGS -> client.statusRepo.getReblogBy(\n                statusId = statusId!!,\n                maxId = maxId,\n                limit = StatusConfigurationDefault.config.loadFromServerLimit,\n            )\n\n            UserListType.FAVOURITES -> client.statusRepo.getFavouritesBy(\n                statusId = statusId!!,\n                maxId = maxId,\n                limit = StatusConfigurationDefault.config.loadFromServerLimit,\n            )\n\n            UserListType.BLOCKED -> client.accountRepo.getBlockedUserList(\n                maxId = nextMaxId,\n                limit = StatusConfigurationDefault.config.loadFromServerLimit,\n            )\n\n            UserListType.MUTED -> client.accountRepo.getMutedUserList(\n                maxId = nextMaxId,\n                limit = StatusConfigurationDefault.config.loadFromServerLimit,\n            )\n\n            UserListType.FOLLOWING -> getFollowUserList(maxId)\n\n            UserListType.FOLLOWERS -> getFollowUserList(maxId)\n        }\n        return pagingResult.map {\n            nextMaxId = it.pagingInfo.nextMaxId\n            it.data.toAuthors()\n        }\n    }\n\n    private suspend fun getFollowUserList(maxId: String?): Result<PagingResult<List<ActivityPubAccountEntity>>> {\n        val userIdResult = getPageTargetUserId()\n        if (userIdResult.isFailure) return Result.failure(userIdResult.exceptionOrThrow())\n        val userId = userIdResult.getOrThrow()\n        val accountRepo = clientManager.getClient(locator).accountRepo\n        return if (type == UserListType.FOLLOWERS) {\n            accountRepo.getFollowers(\n                id = userId,\n                limit = StatusConfigurationDefault.config.loadFromServerLimit,\n                maxId = maxId,\n            )\n        } else {\n            accountRepo.getFollowing(\n                id = userId,\n                limit = StatusConfigurationDefault.config.loadFromServerLimit,\n                maxId = maxId,\n            )\n        }\n    }\n\n    fun onUnblockClick(author: BlogAuthor) {\n        launchInViewModel {\n            val id = author.userId ?: getUserIdByUri(author.uri) ?: return@launchInViewModel\n            clientManager.getClient(locator)\n                .accountRepo\n                .unblock(id)\n                .onFailure {\n                    mutableSnackMessageFlow.emitTextMessageFromThrowable(it)\n                }.onSuccess {\n                    if (type == UserListType.BLOCKED) {\n                        _uiState.update { state ->\n                            state.copy(\n                                userList = state.userList.filterNot { it.author.uri == author.uri }\n                            )\n                        }\n                    }\n                }\n        }\n    }\n\n    fun onUnmuteClick(author: BlogAuthor) {\n        launchInViewModel {\n            val accountRepo = clientManager.getClient(locator).accountRepo\n            val authorId = author.userId ?: getUserIdByUri(author.uri) ?: return@launchInViewModel\n            accountRepo.unmute(authorId)\n                .onSuccess {\n                    if (type == UserListType.MUTED) {\n                        _uiState.update { state ->\n                            state.copy(\n                                userList = state.userList.filterNot { it.author.uri == author.uri }\n                            )\n                        }\n                    }\n                }.onFailure {\n                    mutableSnackMessageFlow.emitTextMessageFromThrowable(it)\n                }\n        }\n    }\n\n    private suspend fun getPageTargetUserId(): Result<String> {\n        if (!cachedUserId.isNullOrEmpty()) return Result.success(cachedUserId!!)\n        if (userUri == null) {\n            return Result.failure(IllegalStateException(\"User uri is null!\"))\n        }\n        val userId = cachedUserId ?: getUserIdByUri(userUri)?.also {\n            this.cachedUserId = it\n        }\n        if (userId == null) {\n            return Result.failure(IllegalStateException(\"Invalid user uri: $userUri\"))\n        }\n        return Result.success(userId)\n    }\n\n    private suspend fun getUserIdByUri(uri: FormalUri): String? {\n        val userInsight = userUriTransformer.parse(uri)\n        if (userInsight == null) {\n            mutableSnackMessageFlow.emit(textOf(\"Invalid user uri: $uri\"))\n            return null\n        }\n        val userIdResult = webFingerBaseUrlToUserIdRepo.getUserId(userInsight.webFinger, locator)\n        if (userIdResult.isFailure) {\n            mutableSnackMessageFlow.emitTextMessageFromThrowable(userIdResult.exceptionOrThrow())\n            return null\n        }\n        return userIdResult.getOrNull()\n    }\n\n    private fun List<ActivityPubAccountEntity>.toAuthors(): List<BlogAuthorUiState> {\n        return this.map { it.toAuthor() }\n    }\n\n    private fun ActivityPubAccountEntity.toAuthor(): BlogAuthorUiState {\n        return BlogAuthorUiState(\n            author = accountEntityAdapter.toAuthor(this),\n            following = null,\n        )\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/user/search/SearchUserScreen.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.user.search\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.foundation.text.KeyboardActions\nimport androidx.compose.foundation.text.KeyboardOptions\nimport androidx.compose.material3.CircularProgressIndicator\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.SnackbarHost\nimport androidx.compose.material3.SnackbarHostState\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextField\nimport androidx.compose.material3.TextFieldDefaults\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.text.input.ImeAction\nimport androidx.compose.ui.unit.dp\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.composable.currentOrThrow\nimport com.zhangke.activitypub.entities.ActivityPubAccountEntity\nimport com.zhangke.framework.composable.ConsumeSnackbarFlow\nimport com.zhangke.framework.composable.Toolbar\nimport com.zhangke.framework.composable.rememberSnackbarHostState\nimport com.zhangke.framework.nav.LocalNavBackStack\nimport com.zhangke.framework.nav.ScreenEventFlow\nimport com.zhangke.framework.utils.transparentIndicatorColors\nimport com.zhangke.fread.activitypub.app.internal.screen.list.AccountItem\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.model.PlatformLocator\nimport kotlinx.coroutines.launch\nimport kotlinx.serialization.Serializable\nimport org.jetbrains.compose.resources.stringResource\n\n@Serializable\ndata class SearchUserScreenNavKey(\n    val locator: PlatformLocator,\n    val onlyFollowing: Boolean,\n) : NavKey {\n\n    companion object {\n        val accountSelectedFlow = ScreenEventFlow<ActivityPubAccountEntity>()\n    }\n}\n\n@Composable\nfun SearchUserScreen(viewModel: SearchUserViewModel) {\n    val backStack = LocalNavBackStack.currentOrThrow\n    val snackbarHostState = rememberSnackbarHostState()\n    val uiState by viewModel.uiState.collectAsState()\n    val coroutineScope = rememberCoroutineScope()\n    SearchUserContent(\n        uiState = uiState,\n        snackbarHostState = snackbarHostState,\n        onAccountClicked = {\n            coroutineScope.launch {\n                SearchUserScreenNavKey.accountSelectedFlow.emit(it)\n                backStack.removeLastOrNull()\n            }\n        },\n        onQueryChange = viewModel::onQueryChange,\n        onSearchClick = viewModel::onSearchClick,\n        onBackClick = backStack::removeLastOrNull,\n    )\n    ConsumeSnackbarFlow(snackbarHostState, viewModel.snackBarMessage)\n}\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nprivate fun SearchUserContent(\n    uiState: SearchUserUiState,\n    snackbarHostState: SnackbarHostState,\n    onAccountClicked: (ActivityPubAccountEntity) -> Unit,\n    onQueryChange: (String) -> Unit,\n    onSearchClick: () -> Unit,\n    onBackClick: () -> Unit,\n) {\n    Scaffold(\n        topBar = {\n            TopAppBar(\n                title = {\n                    val focusRequester = remember { FocusRequester() }\n                    LaunchedEffect(Unit) { focusRequester.requestFocus() }\n                    TextField(\n                        modifier = Modifier.fillMaxWidth()\n                            .focusRequester(focusRequester),\n                        value = uiState.query,\n                        onValueChange = { onQueryChange(it) },\n                        placeholder = {\n                            Text(\n                                text = stringResource(LocalizedString.activity_pub_search_user_placeholder)\n                            )\n                        },\n                        keyboardActions = KeyboardActions(\n                            onSearch = { onSearchClick() }\n                        ),\n                        singleLine = true,\n                        maxLines = 1,\n                        keyboardOptions = KeyboardOptions.Default.copy(\n                            imeAction = ImeAction.Search\n                        ),\n                        colors = TextFieldDefaults.transparentIndicatorColors.copy(\n                            focusedContainerColor = Color.Transparent,\n                            unfocusedContainerColor = Color.Transparent,\n                        ),\n                        textStyle = MaterialTheme.typography.titleMedium,\n                    )\n                },\n                navigationIcon = {\n                    Toolbar.BackButton(onBackClick = onBackClick)\n                },\n            )\n        },\n        snackbarHost = {\n            SnackbarHost(snackbarHostState)\n        }\n    ) { innerPadding ->\n        Box(\n            modifier = Modifier.fillMaxSize()\n                .padding(innerPadding),\n        ) {\n            LazyColumn(\n                modifier = Modifier.fillMaxSize(),\n            ) {\n                items(uiState.accounts) {\n                    AccountItem(\n                        modifier = Modifier.clickable { onAccountClicked(it) },\n                        account = it,\n                        showRemoveIcon = false,\n                    )\n                }\n            }\n            if (uiState.searching) {\n                CircularProgressIndicator(\n                    modifier = Modifier.align(Alignment.Center).size(64.dp)\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/user/search/SearchUserUiState.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.user.search\n\nimport com.zhangke.activitypub.entities.ActivityPubAccountEntity\n\ndata class SearchUserUiState(\n    val query: String,\n    val searching: Boolean,\n    val accounts: List<ActivityPubAccountEntity>\n) {\n\n    companion object {\n\n        fun default(): SearchUserUiState {\n            return SearchUserUiState(\n                query = \"\",\n                searching = false,\n                accounts = emptyList(),\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/user/search/SearchUserViewModel.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.user.search\n\nimport androidx.lifecycle.ViewModel\nimport com.zhangke.framework.composable.TextString\nimport com.zhangke.framework.composable.emitTextMessageFromThrowable\nimport com.zhangke.framework.ktx.launchInViewModel\nimport com.zhangke.fread.activitypub.app.internal.auth.ActivityPubClientManager\nimport com.zhangke.fread.status.model.PlatformLocator\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asSharedFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.update\nclass SearchUserViewModel (\n    private val clientManager: ActivityPubClientManager,\n    private val locator: PlatformLocator,\n    private val onlyFollowing: Boolean,\n) : ViewModel() {\n\n    private val _uiState = MutableStateFlow(SearchUserUiState.default())\n    val uiState = _uiState.asStateFlow()\n\n    private val _snackBarMessage = MutableSharedFlow<TextString>()\n    val snackBarMessage = _snackBarMessage.asSharedFlow()\n\n    private var searchJob: Job? = null\n\n    fun onQueryChange(query: String) {\n        _uiState.update { it.copy(query = query) }\n    }\n\n    fun onSearchClick() {\n        searchJob?.cancel()\n        val query = _uiState.value.query\n        _uiState.update { it.copy(searching = true) }\n        searchJob = launchInViewModel {\n            clientManager.getClient(locator)\n                .accountRepo\n                .search(\n                    query = query,\n                    resolve = false,\n                    following = onlyFollowing,\n                ).onSuccess { accounts ->\n                    _uiState.update {\n                        it.copy(\n                            searching = false,\n                            accounts = accounts,\n                        )\n                    }\n                }.onFailure {\n                    _uiState.update {\n                        it.copy(searching = false)\n                    }\n                    _snackBarMessage.emitTextMessageFromThrowable(it)\n                }\n        }\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/user/status/StatusListContainerViewModel.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.user.status\n\nimport com.zhangke.framework.lifecycle.ContainerViewModel\nimport com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubStatusAdapter\nimport com.zhangke.fread.activitypub.app.internal.auth.ActivityPubClientManager\nimport com.zhangke.fread.activitypub.app.internal.auth.LoggedAccountProvider\nimport com.zhangke.fread.activitypub.app.internal.repo.platform.ActivityPubPlatformRepo\nimport com.zhangke.fread.common.adapter.StatusUiStateAdapter\nimport com.zhangke.fread.common.status.StatusUpdater\nimport com.zhangke.fread.commonbiz.shared.usecase.RefactorToNewStatusUseCase\nimport com.zhangke.fread.status.StatusProvider\nimport com.zhangke.fread.status.model.PlatformLocator\n\nclass StatusListContainerViewModel (\n    private val clientManager: ActivityPubClientManager,\n    private val statusAdapter: ActivityPubStatusAdapter,\n    private val statusProvider: StatusProvider,\n    private val statusUpdater: StatusUpdater,\n    private val platformRepo: ActivityPubPlatformRepo,\n    private val statusUiStateAdapter: StatusUiStateAdapter,\n    private val refactorToNewStatus: RefactorToNewStatusUseCase,\n    private val loggedAccountProvider: LoggedAccountProvider,\n) : ContainerViewModel<StatusListViewModel, StatusListContainerViewModel.ViewModelParams>() {\n\n    override fun createSubViewModel(params: ViewModelParams): StatusListViewModel {\n        return StatusListViewModel(\n            clientManager = clientManager,\n            statusAdapter = statusAdapter,\n            statusProvider = statusProvider,\n            statusUpdater = statusUpdater,\n            platformRepo = platformRepo,\n            statusUiStateAdapter = statusUiStateAdapter,\n            refactorToNewStatus = refactorToNewStatus,\n            loggedAccountProvider = loggedAccountProvider,\n            locator = params.locator,\n            type = params.type,\n        )\n    }\n\n    fun getViewModel(\n        locator: PlatformLocator,\n        type: StatusListType,\n    ): StatusListViewModel {\n        return obtainSubViewModel(ViewModelParams(locator, type))\n    } class ViewModelParams(\n        val locator: PlatformLocator,\n        val type: StatusListType,\n    ) : SubViewModelParams() {\n\n        override val key: String = locator.toString() + type\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/user/status/StatusListScreen.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.user.status\n\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.SnackbarHost\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.composable.LocalSnackbarHostState\nimport com.zhangke.framework.composable.Toolbar\nimport com.zhangke.framework.composable.currentOrThrow\nimport com.zhangke.framework.composable.rememberSnackbarHostState\nimport com.zhangke.framework.nav.LocalNavBackStack\nimport com.zhangke.fread.status.model.PlatformLocator\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class StatusListScreenKey(\n    val locator: PlatformLocator,\n    val type: StatusListType,\n) : NavKey\n\n@Composable\nfun StatusListScreen(locator: PlatformLocator, type: StatusListType) {\n    val tab = remember(locator, type) {\n        StatusListTabStatusListScreen(\n            locator = locator,\n            type = type,\n            contentCanScrollBackward = null,\n        )\n    }\n    val snackBarHostState = rememberSnackbarHostState()\n    val backStack = LocalNavBackStack.currentOrThrow\n    Scaffold(\n        topBar = {\n            Toolbar(\n                title = tab.options?.title.orEmpty(),\n                onBackClick = backStack::removeLastOrNull,\n            )\n        },\n        snackbarHost = {\n            SnackbarHost(hostState = snackBarHostState)\n        },\n    ) { innerPadding ->\n        Box(\n            modifier = Modifier\n                .padding(innerPadding)\n                .fillMaxSize()\n        ) {\n            CompositionLocalProvider(\n                LocalSnackbarHostState provides snackBarHostState\n            ) {\n                tab.Content()\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/user/status/StatusListTab.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.user.status\n\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.MutableState\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.ui.Modifier\nimport com.zhangke.framework.composable.ConsumeSnackbarFlow\nimport com.zhangke.framework.composable.LocalSnackbarHostState\nimport com.zhangke.framework.nav.BaseTab\nimport com.zhangke.framework.nav.TabOptions\nimport com.zhangke.fread.commonbiz.shared.composable.FeedsContent\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.model.PlatformLocator\nimport org.jetbrains.compose.resources.stringResource\nimport org.koin.compose.viewmodel.koinViewModel\n\nclass StatusListTabStatusListScreen(\n    private val locator: PlatformLocator,\n    private val type: StatusListType,\n    private val contentCanScrollBackward: MutableState<Boolean>?,\n) : BaseTab() {\n\n    override val options: TabOptions?\n        @Composable\n        get() = TabOptions(\n            title = when (type) {\n                StatusListType.BOOKMARKS -> stringResource(LocalizedString.statusUiBookmarks)\n                StatusListType.FAVOURITES -> stringResource(LocalizedString.statusUiLikes)\n            },\n        )\n\n    @Composable\n    override fun Content() {\n        super.Content()\n        val viewModel = koinViewModel<StatusListContainerViewModel>().getViewModel(locator, type)\n        val uiState by viewModel.uiState.collectAsState()\n        val snackBarHostState = LocalSnackbarHostState.current\n\n        Box(modifier = Modifier.fillMaxSize()) {\n            FeedsContent(\n                uiState = uiState,\n                openScreenFlow = viewModel.openScreenFlow,\n                newStatusNotifyFlow = viewModel.newStatusNotifyFlow,\n                onRefresh = viewModel::onRefresh,\n                onLoadMore = viewModel::onLoadMore,\n                contentCanScrollBackward = contentCanScrollBackward,\n                composedStatusInteraction = viewModel.composedStatusInteraction,\n                observeScrollToTopEvent = true,\n                onImmersiveEvent = {},\n                onScrollInProgress = {},\n            )\n        }\n\n        ConsumeSnackbarFlow(\n            hostState = snackBarHostState,\n            messageTextFlow = viewModel.errorMessageFlow,\n        )\n    }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/user/status/StatusListType.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.user.status\n\nimport com.zhangke.framework.utils.PlatformSerializable\nimport kotlinx.serialization.Serializable\n\n@Serializable\nenum class StatusListType: PlatformSerializable {\n\n    FAVOURITES,\n    BOOKMARKS,\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/user/status/StatusListViewModel.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.user.status\n\nimport com.zhangke.activitypub.api.AccountsRepo\nimport com.zhangke.activitypub.api.PagingResult\nimport com.zhangke.activitypub.entities.ActivityPubStatusEntity\nimport com.zhangke.framework.lifecycle.SubViewModel\nimport com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubStatusAdapter\nimport com.zhangke.fread.activitypub.app.internal.auth.ActivityPubClientManager\nimport com.zhangke.fread.activitypub.app.internal.auth.LoggedAccountProvider\nimport com.zhangke.fread.activitypub.app.internal.model.ActivityPubLoggedAccount\nimport com.zhangke.fread.activitypub.app.internal.repo.platform.ActivityPubPlatformRepo\nimport com.zhangke.fread.common.adapter.StatusUiStateAdapter\nimport com.zhangke.fread.common.feeds.model.RefreshResult\nimport com.zhangke.fread.common.status.StatusUpdater\nimport com.zhangke.fread.commonbiz.shared.feeds.FeedsViewModelController\nimport com.zhangke.fread.commonbiz.shared.feeds.IFeedsViewModelController\nimport com.zhangke.fread.commonbiz.shared.usecase.RefactorToNewStatusUseCase\nimport com.zhangke.fread.status.StatusProvider\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.model.StatusUiState\nimport com.zhangke.fread.status.platform.BlogPlatform\nimport com.zhangke.fread.status.richtext.preParseStatus\n\nclass StatusListViewModel(\n    private val clientManager: ActivityPubClientManager,\n    private val statusAdapter: ActivityPubStatusAdapter,\n    private val statusProvider: StatusProvider,\n    statusUpdater: StatusUpdater,\n    private val platformRepo: ActivityPubPlatformRepo,\n    private val statusUiStateAdapter: StatusUiStateAdapter,\n    private val refactorToNewStatus: RefactorToNewStatusUseCase,\n    private val loggedAccountProvider: LoggedAccountProvider,\n    private val locator: PlatformLocator,\n    val type: StatusListType,\n) : SubViewModel(), IFeedsViewModelController by FeedsViewModelController(\n    statusProvider = statusProvider,\n    statusUpdater = statusUpdater,\n    statusUiStateAdapter = statusUiStateAdapter,\n    refactorToNewStatus = refactorToNewStatus,\n) {\n\n    private var nextMaxId: String? = null\n\n    init {\n        initController(\n            coroutineScope = viewModelScope,\n            locatorResolver = { locator },\n            loadFirstPageLocalFeeds = {\n                Result.success(emptyList())\n            },\n            loadNewFromServerFunction = ::loadNewDataFromServer,\n            loadMoreFunction = { loadMoreDataFromServer() },\n            onStatusUpdate = {},\n        )\n        initFeeds(false)\n    }\n\n    private suspend fun loadNewDataFromServer(): Result<RefreshResult> {\n        nextMaxId = null\n        val accountRepo = clientManager.getClient(locator).accountRepo\n        val platformResult = platformRepo.getPlatform(locator)\n        if (platformResult.isFailure) {\n            return Result.failure(platformResult.exceptionOrNull()!!)\n        }\n        val platform = platformResult.getOrThrow()\n        val loggedAccount = locator.accountUri?.let { loggedAccountProvider.getAccount(it) }\n        return fetchStatuses(accountRepo)\n            .map { pagingResult ->\n                nextMaxId = pagingResult.pagingInfo.nextMaxId\n                RefreshResult(\n                    newStatus = pagingResult.data.map { it.toUiState(loggedAccount, platform) },\n                    deletedStatus = emptyList(),\n                )\n            }\n    }\n\n    private suspend fun loadMoreDataFromServer(): Result<List<StatusUiState>> {\n        val nextMaxId = nextMaxId\n        if (nextMaxId.isNullOrEmpty()) {\n            return Result.success(emptyList())\n        }\n        val accountRepo = clientManager.getClient(locator).accountRepo\n        val platformResult = platformRepo.getPlatform(locator)\n        if (platformResult.isFailure) {\n            return Result.failure(platformResult.exceptionOrNull()!!)\n        }\n        val platform = platformResult.getOrThrow()\n        val loggedAccount = locator.accountUri?.let { loggedAccountProvider.getAccount(it) }\n        return fetchStatuses(accountRepo, nextMaxId)\n            .map { pagingResult ->\n                this@StatusListViewModel.nextMaxId = pagingResult.pagingInfo.nextMaxId\n                pagingResult.data.map { it.toUiState(loggedAccount, platform) }\n            }\n    }\n\n    private suspend fun fetchStatuses(\n        accountRepo: AccountsRepo,\n        maxId: String? = null,\n    ): Result<PagingResult<List<ActivityPubStatusEntity>>> {\n        return if (type == StatusListType.FAVOURITES) {\n            accountRepo.getFavourites(maxId = maxId)\n        } else {\n            accountRepo.getBookmarks(maxId = maxId)\n        }\n    }\n\n    private suspend fun ActivityPubStatusEntity.toUiState(\n        loggedAccount: ActivityPubLoggedAccount?,\n        platform: BlogPlatform,\n    ): StatusUiState {\n        val status = statusAdapter.toStatusUiState(\n            entity = this,\n            platform = platform,\n            locator = locator,\n            loggedAccount = loggedAccount,\n        )\n        status.status.preParseStatus()\n        return status\n    }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/user/tags/TagListScreen.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.user.tags\n\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.SnackbarHost\nimport androidx.compose.material3.SnackbarHostState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.ui.Modifier\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.composable.ConsumeSnackbarFlow\nimport com.zhangke.framework.composable.Toolbar\nimport com.zhangke.framework.composable.currentOrThrow\nimport com.zhangke.framework.composable.rememberSnackbarHostState\nimport com.zhangke.framework.loadable.lazycolumn.LoadableLazyColumn\nimport com.zhangke.framework.loadable.lazycolumn.rememberLoadableLazyColumnState\nimport com.zhangke.framework.nav.LocalNavBackStack\nimport com.zhangke.fread.activitypub.app.internal.screen.hashtag.HashtagTimelineScreenKey\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.model.Hashtag\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.ui.hashtag.HashtagUi\nimport com.zhangke.fread.status.ui.hashtag.HashtagUiPlaceholder\nimport kotlinx.serialization.Serializable\nimport org.jetbrains.compose.resources.stringResource\n\n@Serializable\ndata class TagListScreenKey(\n    val locator: PlatformLocator,\n) : NavKey\n\n@Composable\nfun TagListScreen(viewModel: TagListViewModel) {\n    val backStack = LocalNavBackStack.currentOrThrow\n    val uiState by viewModel.uiState.collectAsState()\n    val snackbarHostState = rememberSnackbarHostState()\n    TagListContent(\n        uiState = uiState,\n        snackbarHostState = snackbarHostState,\n        onBackClick = backStack::removeLastOrNull,\n        onRefresh = viewModel::onRefresh,\n        onLoadMore = viewModel::onLoadMore,\n        onTagClick = { hashtag ->\n            backStack.add(\n                HashtagTimelineScreenKey(\n                    locator = uiState.locator,\n                    hashtag = hashtag.name.removePrefix(\"#\"),\n                )\n            )\n        },\n    )\n    ConsumeSnackbarFlow(snackbarHostState, viewModel.snackBarMessageFlow)\n}\n\n@Composable\nprivate fun TagListContent(\n    uiState: TagListUiState,\n    snackbarHostState: SnackbarHostState,\n    onBackClick: () -> Unit,\n    onRefresh: () -> Unit,\n    onLoadMore: () -> Unit,\n    onTagClick: (Hashtag) -> Unit,\n) {\n    Scaffold(\n        topBar = {\n            Toolbar(\n                title = stringResource(LocalizedString.activity_pub_followed_tags_screen_title),\n                onBackClick = onBackClick,\n            )\n        },\n        snackbarHost = {\n            SnackbarHost(hostState = snackbarHostState)\n        },\n    ) { innerPadding ->\n        val loadableState = rememberLoadableLazyColumnState(\n            onRefresh = onRefresh,\n            onLoadMore = onLoadMore,\n        )\n        LoadableLazyColumn(\n            modifier = Modifier\n                .fillMaxSize()\n                .padding(innerPadding),\n            state = loadableState,\n            refreshing = uiState.refreshing,\n            loadState = uiState.loadState,\n        ) {\n            if (uiState.tags.isNotEmpty()) {\n                items(uiState.tags) { tag ->\n                    HashtagUi(\n                        tag = tag,\n                        onClick = onTagClick,\n                    )\n                }\n            } else if (uiState.refreshing) {\n                items(20) {\n                    HashtagUiPlaceholder()\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/user/tags/TagListUiState.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.user.tags\n\nimport com.zhangke.framework.utils.LoadState\nimport com.zhangke.fread.status.model.Hashtag\nimport com.zhangke.fread.status.model.PlatformLocator\n\ndata class TagListUiState(\n    val locator: PlatformLocator,\n    val refreshing: Boolean,\n    val tags: List<Hashtag>,\n    val loadState: LoadState,\n) {\n\n    companion object {\n\n        fun default(locator: PlatformLocator) = TagListUiState(\n            locator = locator,\n            refreshing = false,\n            tags = emptyList(),\n            loadState = LoadState.Idle,\n        )\n    }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/user/tags/TagListViewModel.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.user.tags\n\nimport androidx.lifecycle.ViewModel\nimport com.zhangke.framework.composable.TextString\nimport com.zhangke.framework.composable.emitTextMessageFromThrowable\nimport com.zhangke.framework.composable.toTextStringOrNull\nimport com.zhangke.framework.ktx.launchInViewModel\nimport com.zhangke.framework.utils.LoadState\nimport com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubTagAdapter\nimport com.zhangke.fread.activitypub.app.internal.auth.ActivityPubClientManager\nimport com.zhangke.fread.status.model.Hashtag\nimport com.zhangke.fread.status.model.PlatformLocator\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.SharedFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.update\nclass TagListViewModel (\n    private val clientManager: ActivityPubClientManager,\n    private val activityPubTagAdapter: ActivityPubTagAdapter,\n    private val locator: PlatformLocator,\n) : ViewModel() {\n\n    private val _uiState = MutableStateFlow(TagListUiState.default(locator))\n    val uiState: StateFlow<TagListUiState> = _uiState\n\n    private val _snackBarMessageFlow = MutableSharedFlow<TextString>()\n    val snackBarMessageFlow: SharedFlow<TextString> = _snackBarMessageFlow\n\n    private var nextMaxId: String? = null\n    private var initializingJob: Job? = null\n    private var loadMoreJob: Job? = null\n\n    init {\n        initializingFollowedTags()\n    }\n\n    fun onRefresh() {\n        initializingFollowedTags()\n    }\n\n    private fun initializingFollowedTags() {\n        if (initializingJob?.isActive == true) return\n        loadMoreJob?.cancel()\n        launchInViewModel {\n            nextMaxId = null\n            _uiState.update { it.copy(refreshing = true) }\n            fetchFollowedTags()\n                .onFailure { t ->\n                    _uiState.update { it.copy(refreshing = false) }\n                    _snackBarMessageFlow.emitTextMessageFromThrowable(t)\n                }.onSuccess { list ->\n                    _uiState.update {\n                        it.copy(\n                            refreshing = false,\n                            tags = list,\n                        )\n                    }\n                }\n        }\n    }\n\n    fun onLoadMore() {\n        if (initializingJob?.isActive == true) return\n        if (loadMoreJob?.isActive == true) return\n        val nextMaxId = nextMaxId ?: return\n        if (nextMaxId.isEmpty()) return\n        launchInViewModel {\n            _uiState.update { it.copy(loadState = LoadState.Loading) }\n            fetchFollowedTags(nextMaxId)\n                .onFailure { t ->\n                    _uiState.update { it.copy(loadState = LoadState.Failed(t.toTextStringOrNull())) }\n                }.onSuccess { list ->\n                    _uiState.update {\n                        it.copy(\n                            loadState = LoadState.Idle,\n                            tags = it.tags + list,\n                        )\n                    }\n                }\n        }\n    }\n\n    private suspend fun fetchFollowedTags(maxId: String? = null): Result<List<Hashtag>> {\n        return clientManager.getClient(locator)\n            .accountRepo\n            .getFollowedTags(maxId = maxId)\n            .map { pagingResult ->\n                nextMaxId = pagingResult.pagingInfo.nextMaxId\n                pagingResult.data.map { activityPubTagAdapter.adapt(it) }\n            }\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/user/timeline/UserTimelineContainerViewModel.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.user.timeline\n\nimport com.zhangke.framework.lifecycle.ContainerViewModel\nimport com.zhangke.framework.utils.WebFinger\nimport com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubStatusAdapter\nimport com.zhangke.fread.activitypub.app.internal.auth.ActivityPubClientManager\nimport com.zhangke.fread.activitypub.app.internal.auth.LoggedAccountProvider\nimport com.zhangke.fread.activitypub.app.internal.repo.WebFingerBaseUrlToUserIdRepo\nimport com.zhangke.fread.activitypub.app.internal.repo.platform.ActivityPubPlatformRepo\nimport com.zhangke.fread.common.adapter.StatusUiStateAdapter\nimport com.zhangke.fread.common.status.StatusUpdater\nimport com.zhangke.fread.commonbiz.shared.usecase.RefactorToNewStatusUseCase\nimport com.zhangke.fread.status.StatusProvider\nimport com.zhangke.fread.status.model.PlatformLocator\n\nclass UserTimelineContainerViewModel (\n    private val statusProvider: StatusProvider,\n    private val webFingerBaseUrlToUserIdRepo: WebFingerBaseUrlToUserIdRepo,\n    private val statusUpdater: StatusUpdater,\n    private val statusUiStateAdapter: StatusUiStateAdapter,\n    private val platformRepo: ActivityPubPlatformRepo,\n    private val statusAdapter: ActivityPubStatusAdapter,\n    private val clientManager: ActivityPubClientManager,\n    private val refactorToNewStatus: RefactorToNewStatusUseCase,\n    private val loggedAccountProvider: LoggedAccountProvider,\n) : ContainerViewModel<UserTimelineViewModel, UserTimelineContainerViewModel.Params>() {\n\n    override fun createSubViewModel(params: Params): UserTimelineViewModel {\n        return UserTimelineViewModel(\n            statusProvider = statusProvider,\n            statusUpdater = statusUpdater,\n            webFingerBaseUrlToUserIdRepo = webFingerBaseUrlToUserIdRepo,\n            statusUiStateAdapter = statusUiStateAdapter,\n            platformRepo = platformRepo,\n            statusEntityAdapter = statusAdapter,\n            clientManager = clientManager,\n            refactorToNewStatus = refactorToNewStatus,\n            tabType = params.tabType,\n            locator = params.locator,\n            webFinger = params.webFinger,\n            loggedAccountProvider = loggedAccountProvider,\n            userId = params.userId,\n        )\n    }\n\n    fun getSubViewModel(\n        tabType: UserTimelineTabType,\n        locator: PlatformLocator,\n        webFinger: WebFinger,\n        userId: String?,\n    ): UserTimelineViewModel {\n        return obtainSubViewModel(\n            Params(tabType, locator, webFinger, userId)\n        )\n    } class Params(\n        val tabType: UserTimelineTabType,\n        val locator: PlatformLocator,\n        val webFinger: WebFinger,\n        val userId: String?,\n    ) : SubViewModelParams() {\n\n        override val key: String\n            get() = tabType.toString() + locator.toString() + webFinger + userId\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/user/timeline/UserTimelineTab.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.user.timeline\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.MutableState\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.remember\nimport com.zhangke.framework.composable.ConsumeSnackbarFlow\nimport com.zhangke.framework.composable.LocalSnackbarHostState\nimport com.zhangke.framework.nav.BaseTab\nimport com.zhangke.framework.nav.TabOptions\nimport com.zhangke.framework.utils.WebFinger\nimport com.zhangke.fread.commonbiz.shared.composable.FeedsContent\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.ui.common.LocalStatusSharedElementConfig\nimport org.jetbrains.compose.resources.stringResource\nimport org.koin.compose.viewmodel.koinViewModel\n\ninternal class UserTimelineTab(\n    private val tabType: UserTimelineTabType,\n    private val contentCanScrollBackward: MutableState<Boolean>,\n    private val locator: PlatformLocator,\n    private val userWebFinger: WebFinger,\n    private val userId: String?,\n) : BaseTab() {\n\n    override val options: TabOptions\n        @Composable get() {\n            val title = when (tabType) {\n                UserTimelineTabType.POSTS -> LocalizedString.activity_pub_user_detail_tab_post\n                UserTimelineTabType.REPLIES -> LocalizedString.activity_pub_user_detail_tab_replies\n                UserTimelineTabType.MEDIA -> LocalizedString.activity_pub_user_detail_tab_media\n            }\n            return TabOptions(title = stringResource(title))\n        }\n\n    @Composable\n    override fun Content() {\n        super.Content()\n        val viewModel = koinViewModel<UserTimelineContainerViewModel>().getSubViewModel(\n            tabType = tabType,\n            locator = locator,\n            webFinger = userWebFinger,\n            userId = userId,\n        )\n        val uiState by viewModel.uiState.collectAsState()\n\n        val preSharedElementConfig = LocalStatusSharedElementConfig.current\n        val sharedElementConfig = remember(preSharedElementConfig) {\n            preSharedElementConfig.copy(label = \"user-timeline\")\n        }\n        CompositionLocalProvider(\n            LocalStatusSharedElementConfig provides sharedElementConfig\n        ) {\n            FeedsContent(\n                uiState = uiState,\n                openScreenFlow = viewModel.openScreenFlow,\n                newStatusNotifyFlow = viewModel.newStatusNotifyFlow,\n                onRefresh = viewModel::onRefresh,\n                onLoadMore = viewModel::onLoadMore,\n                composedStatusInteraction = viewModel.composedStatusInteraction,\n                observeScrollToTopEvent = true,\n                contentCanScrollBackward = contentCanScrollBackward,\n                nestedScrollConnection = null,\n                onImmersiveEvent = {},\n                onScrollInProgress = {},\n            )\n        }\n        val snackbarHostState = LocalSnackbarHostState.current\n        ConsumeSnackbarFlow(snackbarHostState, viewModel.errorMessageFlow)\n    }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/user/timeline/UserTimelineTabType.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.user.timeline\n\nenum class UserTimelineTabType {\n\n    POSTS,\n    REPLIES,\n    MEDIA,\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/user/timeline/UserTimelineUiState.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.user.timeline\n\nimport com.zhangke.framework.composable.TextString\nimport com.zhangke.framework.utils.LoadState\nimport com.zhangke.fread.status.model.StatusUiState\n\ndata class UserTimelineUiState(\n    val feeds: List<StatusUiState>,\n    val showPagingLoadingPlaceholder: Boolean,\n    val pageErrorContent: TextString?,\n    val refreshing: Boolean,\n    val loadMoreState: LoadState,\n) {\n\n    companion object {\n\n        fun default() = UserTimelineUiState(\n            feeds = emptyList(),\n            showPagingLoadingPlaceholder = false,\n            pageErrorContent = null,\n            refreshing = false,\n            loadMoreState = LoadState.Idle,\n        )\n    }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/screen/user/timeline/UserTimelineViewModel.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.screen.user.timeline\n\nimport com.zhangke.activitypub.entities.ActivityPubStatusEntity\nimport com.zhangke.framework.lifecycle.SubViewModel\nimport com.zhangke.framework.utils.WebFinger\nimport com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubStatusAdapter\nimport com.zhangke.fread.activitypub.app.internal.auth.ActivityPubClientManager\nimport com.zhangke.fread.activitypub.app.internal.auth.LoggedAccountProvider\nimport com.zhangke.fread.activitypub.app.internal.model.ActivityPubLoggedAccount\nimport com.zhangke.fread.activitypub.app.internal.repo.WebFingerBaseUrlToUserIdRepo\nimport com.zhangke.fread.activitypub.app.internal.repo.platform.ActivityPubPlatformRepo\nimport com.zhangke.fread.common.adapter.StatusUiStateAdapter\nimport com.zhangke.fread.common.feeds.model.RefreshResult\nimport com.zhangke.fread.common.status.StatusUpdater\nimport com.zhangke.fread.commonbiz.shared.feeds.FeedsViewModelController\nimport com.zhangke.fread.commonbiz.shared.feeds.IFeedsViewModelController\nimport com.zhangke.fread.commonbiz.shared.usecase.RefactorToNewStatusUseCase\nimport com.zhangke.fread.status.StatusProvider\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.model.StatusUiState\nimport com.zhangke.fread.status.platform.BlogPlatform\nimport com.zhangke.fread.status.richtext.preParseStatus\n\nclass UserTimelineViewModel(\n    private val webFingerBaseUrlToUserIdRepo: WebFingerBaseUrlToUserIdRepo,\n    private val statusProvider: StatusProvider,\n    statusUpdater: StatusUpdater,\n    private val statusEntityAdapter: ActivityPubStatusAdapter,\n    private val platformRepo: ActivityPubPlatformRepo,\n    private val statusUiStateAdapter: StatusUiStateAdapter,\n    private val clientManager: ActivityPubClientManager,\n    private val refactorToNewStatus: RefactorToNewStatusUseCase,\n    private val loggedAccountProvider: LoggedAccountProvider,\n    val tabType: UserTimelineTabType,\n    val locator: PlatformLocator,\n    val webFinger: WebFinger,\n    val userId: String?,\n) : SubViewModel(), IFeedsViewModelController by FeedsViewModelController(\n    statusProvider = statusProvider,\n    statusUpdater = statusUpdater,\n    statusUiStateAdapter = statusUiStateAdapter,\n    refactorToNewStatus = refactorToNewStatus,\n) {\n\n    init {\n        initController(\n            coroutineScope = viewModelScope,\n            locatorResolver = { locator },\n            loadFirstPageLocalFeeds = {\n                Result.success(emptyList())\n            },\n            loadNewFromServerFunction = ::loadNewDataFromServer,\n            loadMoreFunction = ::loadMoreDataFromServer,\n            onStatusUpdate = {},\n        )\n        initFeeds(false)\n    }\n\n    private suspend fun loadNewDataFromServer(): Result<RefreshResult> {\n        return loadUserTimeline(null)\n            .map {\n                RefreshResult(\n                    newStatus = it,\n                    deletedStatus = emptyList(),\n                )\n            }\n    }\n\n    private suspend fun loadMoreDataFromServer(maxId: String): Result<List<StatusUiState>> {\n        return loadUserTimeline(maxId)\n    }\n\n    private suspend fun loadUserTimeline(maxId: String? = null): Result<List<StatusUiState>> {\n        val loggedAccount = locator.accountUri?.let { loggedAccountProvider.getAccount(it) }\n        val accountId = if (userId.isNullOrEmpty()) {\n            val accountIdResult =\n                webFingerBaseUrlToUserIdRepo.getUserId(webFinger, locator)\n            if (accountIdResult.isFailure) {\n                return Result.failure(accountIdResult.exceptionOrNull()!!)\n            }\n            accountIdResult.getOrThrow()\n        } else {\n            userId\n        }\n        val platformResult = platformRepo.getPlatform(locator)\n        if (platformResult.isFailure) {\n            return Result.failure(platformResult.exceptionOrNull()!!)\n        }\n        val platform = platformResult.getOrThrow()\n        return fetchStatus(\n            accountId = accountId,\n            maxId = maxId,\n        ).map { data ->\n            data.filter { it.id != maxId }.map { item ->\n                item.toUiState(loggedAccount, platform)\n            }\n        }\n    }\n\n    private suspend fun fetchStatus(\n        accountId: String,\n        maxId: String?,\n    ): Result<List<ActivityPubStatusEntity>> {\n        val showPinned = tabType == UserTimelineTabType.POSTS\n        val pinnedStatus = mutableListOf<ActivityPubStatusEntity>()\n        val accountRepo = clientManager.getClient(locator).accountRepo\n        if (showPinned && maxId == null) {\n            // first page\n            val pinnedStatusResult = accountRepo.getStatuses(\n                id = accountId,\n                pinned = true,\n                excludeReplies = true,\n                onlyMedia = false,\n            )\n            if (pinnedStatusResult.isFailure) {\n                return Result.failure(pinnedStatusResult.exceptionOrNull()!!)\n            }\n            pinnedStatus += pinnedStatusResult.getOrThrow().map { it.copy(pinned = true) }\n        }\n        return accountRepo.getStatuses(\n            id = accountId,\n            pinned = false,\n            maxId = maxId,\n            excludeReplies = tabType != UserTimelineTabType.REPLIES,\n            onlyMedia = tabType == UserTimelineTabType.MEDIA,\n        ).map {\n            pinnedStatus + it\n        }\n    }\n\n    private suspend fun ActivityPubStatusEntity.toUiState(\n        loggedAccount: ActivityPubLoggedAccount?,\n        platform: BlogPlatform,\n    ): StatusUiState {\n        val status = statusEntityAdapter.toStatusUiState(\n            entity = this,\n            locator = locator,\n            platform = platform,\n            loggedAccount = loggedAccount,\n        )\n        status.status.preParseStatus()\n        return status\n    }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/source/UserSourceTransformer.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.source\n\nimport com.zhangke.activitypub.entities.ActivityPubAccountEntity\nimport com.zhangke.framework.network.FormalBaseUrl\nimport com.zhangke.framework.utils.prettyHandle\nimport com.zhangke.fread.status.model.createActivityPubProtocol\nimport com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubAccountEntityAdapter\nimport com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubCustomEmojiEntityAdapter\nimport com.zhangke.fread.activitypub.app.internal.uri.UserUriTransformer\nimport com.zhangke.fread.activitypub.app.internal.usecase.emoji.MapCustomEmojiUseCase\nimport com.zhangke.fread.status.source.StatusSource\n\nclass UserSourceTransformer (\n    private val userUriTransformer: UserUriTransformer,\n    private val accountEntityAdapter: ActivityPubAccountEntityAdapter,\n    private val mapCustomEmoji: MapCustomEmojiUseCase,\n    private val emojiEntityAdapter: ActivityPubCustomEmojiEntityAdapter,\n) {\n\n    suspend fun createByUserEntity(entity: ActivityPubAccountEntity): StatusSource {\n        val webFinger = accountEntityAdapter.toWebFinger(entity)\n        val uri = userUriTransformer.build(webFinger, FormalBaseUrl.parse(entity.url)!!)\n        val emojis = entity.emojis.map(emojiEntityAdapter::toEmoji)\n        return StatusSource(\n            uri = uri,\n            name = entity.displayName,\n            handle = entity.acct.prettyHandle(),\n            description = mapCustomEmoji(entity.note, emojis),\n            thumbnail = entity.avatar,\n            protocol = createActivityPubProtocol(),\n        )\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/uri/ActivityPubUriHost.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.uri\n\nimport com.zhangke.fread.status.uri.FormalUri\n\nconst val ACTIVITY_PUB_HOST = \"activitypub.com\"\n\nfun createActivityPubUri(path: String, queries: Map<String, String>): FormalUri {\n    return FormalUri.create(\n        host = ACTIVITY_PUB_HOST,\n        path = path,\n        queries = queries,\n    )\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/uri/ActivityPubUriPath.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.uri\n\nobject ActivityPubUriPath {\n\n    const val USER = \"/user\"\n\n    const val TIMELINE = \"/timeline\"\n\n    const val PLATFORM = \"/platform\"\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/uri/PlatformUriTransformer.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.uri\n\nimport com.zhangke.framework.network.FormalBaseUrl\nimport com.zhangke.fread.activitypub.app.internal.model.PlatformUriInsights\nimport com.zhangke.fread.status.uri.FormalUri\n\nclass PlatformUriTransformer () {\n\n    companion object {\n        private const val QUERY_SERVER_BASE_URL = \"serverBaseUrl\"\n    }\n\n    fun build(serverBaseUrl: FormalBaseUrl): FormalUri {\n        val queries = mutableMapOf<String, String>()\n        queries[QUERY_SERVER_BASE_URL] = serverBaseUrl.toString()\n        return createActivityPubUri(\n            path = ActivityPubUriPath.PLATFORM,\n            queries = queries,\n        )\n    }\n\n    fun parse(uri: FormalUri): PlatformUriInsights? {\n        if (!uri.isActivityPubUri) return null\n        if (uri.path != ActivityPubUriPath.PLATFORM) return null\n        val serverBaseUrl = uri.queries[QUERY_SERVER_BASE_URL]\n        if (serverBaseUrl.isNullOrEmpty()) return null\n        return PlatformUriInsights(uri, FormalBaseUrl.parse(serverBaseUrl)!!)\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/uri/StatusProviderUriExts.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.uri\n\nimport com.zhangke.fread.status.uri.FormalUri\n\nval FormalUri.isActivityPubUri: Boolean get() = host == ACTIVITY_PUB_HOST\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/uri/UserUriTransformer.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.uri\n\nimport com.zhangke.framework.network.FormalBaseUrl\nimport com.zhangke.framework.utils.WebFinger\nimport com.zhangke.fread.activitypub.app.internal.model.UserUriInsights\nimport com.zhangke.fread.status.uri.FormalUri\n\nclass UserUriTransformer () {\n\n    companion object {\n\n        private const val QUERY_FINGER = \"finger\"\n        private const val QUERY_BASE_URL = \"baseUrl\"\n    }\n\n    fun parse(uri: FormalUri): UserUriInsights? {\n        if (!uri.isActivityPubUri) return null\n        if (uri.path != ActivityPubUriPath.USER) return null\n        val webFinger = uri.queries[QUERY_FINGER]?.let { WebFinger.create(it) } ?: return null\n        val baseUrl = uri.queries[QUERY_BASE_URL]?.let { FormalBaseUrl.parse(it) } ?: return null\n        return UserUriInsights(\n            uri = uri,\n            webFinger = webFinger,\n            baseUrl = baseUrl,\n        )\n    }\n\n    fun build(\n        webFinger: WebFinger,\n        baseUrl: FormalBaseUrl,\n    ): FormalUri {\n        val queries = mutableMapOf<String, String>()\n        queries[QUERY_FINGER] = webFinger.toString()\n        queries[QUERY_BASE_URL] = baseUrl.toString()\n        return createActivityPubUri(\n            path = ActivityPubUriPath.USER,\n            queries = queries,\n        )\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/usecase/ActivityPubAccountLogoutUseCase.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.usecase\n\nimport com.zhangke.framework.network.FormalBaseUrl\nimport com.zhangke.fread.activitypub.app.internal.auth.LoggedAccountProvider\nimport com.zhangke.fread.activitypub.app.internal.model.ActivityPubLoggedAccount\nimport com.zhangke.fread.activitypub.app.internal.push.ActivityPubPushManager\nimport com.zhangke.fread.activitypub.app.internal.repo.account.ActivityPubLoggedAccountRepo\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.uri.FormalUri\n\nclass ActivityPubAccountLogoutUseCase (\n    private val activityPubPushManager: ActivityPubPushManager,\n    private val accountRepo: ActivityPubLoggedAccountRepo,\n    private val loggedAccountProvider: LoggedAccountProvider,\n) {\n\n    suspend operator fun invoke(account: ActivityPubLoggedAccount) {\n        invoke(\n            baseUrl = account.platform.baseUrl,\n            accountUri = account.uri,\n            userId = account.userId,\n        )\n    }\n\n    suspend operator fun invoke(\n        baseUrl: FormalBaseUrl,\n        accountUri: FormalUri,\n        userId: String,\n    ) {\n        val role = PlatformLocator(baseUrl = baseUrl, accountUri = accountUri)\n        activityPubPushManager.unsubscribe(role, userId)\n        accountRepo.deleteByUri(accountUri)\n        loggedAccountProvider.removeAccount(accountUri)\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/usecase/GetDefaultBaseUrlUseCase.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.usecase\n\nimport com.zhangke.framework.network.FormalBaseUrl\nimport com.zhangke.fread.activitypub.app.internal.auth.LoggedAccountProvider\n\nclass GetDefaultBaseUrlUseCase (\n    private val loggedAccountProvider: LoggedAccountProvider,\n) {\n\n    companion object {\n\n        private val defaultBaseUrl = FormalBaseUrl.Companion.parse(\"https://mastodon.online\")!!\n    }\n\n    operator fun invoke(): FormalBaseUrl {\n        loggedAccountProvider.getAllAccounts().firstOrNull()?.let {\n            return it.platform.baseUrl\n        }\n        return defaultBaseUrl\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/usecase/GetInstanceAnnouncementUseCase.kt",
    "content": "@file:OptIn(ExperimentalTime::class)\n\npackage com.zhangke.fread.activitypub.app.internal.usecase\n\nimport com.zhangke.activitypub.entities.ActivityPubAnnouncementEntity\nimport com.zhangke.framework.date.DateParser\nimport com.zhangke.framework.network.FormalBaseUrl\nimport com.zhangke.fread.activitypub.app.internal.auth.ActivityPubClientManager\nimport com.zhangke.fread.common.utils.getCurrentTimeMillis\nimport com.zhangke.fread.status.model.PlatformLocator\nimport kotlin.time.ExperimentalTime\n\nclass GetInstanceAnnouncementUseCase (\n    private val clientManager: ActivityPubClientManager,\n) {\n\n    suspend operator fun invoke(\n        baseUrl: FormalBaseUrl,\n        justActive: Boolean = true,\n    ): Result<List<ActivityPubAnnouncementEntity>> {\n        val client = clientManager.getClient(PlatformLocator(baseUrl = baseUrl))\n        return client.instanceRepo.getAnnouncement()\n            .map {\n                if (justActive) {\n                    it.filter { item -> item.isActive() }\n                } else {\n                    it\n                }\n            }\n    }\n\n    private fun ActivityPubAnnouncementEntity.isActive(): Boolean {\n        val startDateTime =\n            startsAt?.let { DateParser.parseOrCurrent(it) }?.instant?.toEpochMilliseconds()\n        val endDateTime =\n            endsAt?.let { DateParser.parseOrCurrent(it) }?.instant?.toEpochMilliseconds()\n        if (startDateTime == null) return true\n        val currentDateTime = getCurrentTimeMillis()\n        if (currentDateTime < startDateTime) return false\n        if (endDateTime == null && allDay) return true\n        if (endDateTime == null) return true\n        return currentDateTime < endDateTime\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/usecase/GetServerTrendTagsUseCase.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.usecase\n\nimport com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubTagAdapter\nimport com.zhangke.fread.activitypub.app.internal.auth.ActivityPubClientManager\nimport com.zhangke.fread.status.model.Hashtag\nimport com.zhangke.fread.status.model.PlatformLocator\n\nclass GetServerTrendTagsUseCase (\n    private val clientManager: ActivityPubClientManager,\n    private val activityPubTagAdapter: ActivityPubTagAdapter,\n) {\n\n    suspend operator fun invoke(locator: PlatformLocator): Result<List<Hashtag>> {\n        return clientManager.getClient(locator)\n            .instanceRepo\n            .getTrendsTags(\n                limit = 10,\n                offset = 0,\n            ).map { list ->\n                list.map { activityPubTagAdapter.adapt(it) }\n            }\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/usecase/UpdateActivityPubUserListUseCase.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.usecase\n\nimport com.zhangke.fread.activitypub.app.internal.content.ActivityPubContent\nimport com.zhangke.fread.activitypub.app.internal.content.ActivityPubContent.ContentTab\nimport com.zhangke.fread.activitypub.app.internal.content.ActivityPubContent.ContentTab.ListTimeline\nimport com.zhangke.fread.common.content.FreadContentRepo\n\nclass UpdateActivityPubUserListUseCase (\n    private val contentRepo: FreadContentRepo,\n) {\n\n    suspend operator fun invoke(\n        content: ActivityPubContent,\n        allUserCreatedList: List<ListTimeline>\n    ) {\n        val allListIdSet = allUserCreatedList.map { it.listId }.toSet()\n        val localListIdSet = content.tabList\n            .filterIsInstance<ListTimeline>()\n            .map { it.listId }\n            .toSet()\n        val newTabList = content.tabList.dropNotExistListTab(allListIdSet).toMutableList()\n        var maxOrder = content.tabList.maxByOrNull { it.order }?.order ?: 0\n        allUserCreatedList.filter { it.listId !in localListIdSet }\n            .map { ListTimeline(it.listId, it.name, maxOrder++) }\n            .let { newTabList.addAll(it) }\n        if (content.tabList.sortedBy { it.order } == newTabList.sortedBy { it.order }) {\n            return\n        }\n        content.copy(tabList = newTabList).let { contentRepo.insertContent(it) }\n    }\n\n    private fun List<ContentTab>.dropNotExistListTab(\n        allListId: Set<String>\n    ): List<ContentTab> {\n        return this.filter {\n            if (it is ListTimeline) {\n                it.listId in allListId\n            } else {\n                true\n            }\n        }\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/usecase/content/GetUserCreatedListUseCase.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.usecase.content\n\nimport com.zhangke.activitypub.entities.ActivityPubListEntity\nimport com.zhangke.fread.activitypub.app.internal.auth.ActivityPubClientManager\nimport com.zhangke.fread.status.model.PlatformLocator\n\nclass GetUserCreatedListUseCase (\n    private val clientManager: ActivityPubClientManager\n) {\n\n    suspend operator fun invoke(locator: PlatformLocator): Result<List<ActivityPubListEntity>> {\n        return clientManager.getClient(locator)\n            .listsRepo\n            .getAccountLists()\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/usecase/content/ReorderActivityPubTabUseCase.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.usecase.content\n\nimport com.zhangke.fread.activitypub.app.internal.content.ActivityPubContent\nimport com.zhangke.fread.common.content.FreadContentRepo\n\nclass ReorderActivityPubTabUseCase (\n    private val contentRepo: FreadContentRepo,\n) {\n\n    suspend operator fun invoke(\n        content: ActivityPubContent,\n        fromTab: ActivityPubContent.ContentTab,\n        toTab: ActivityPubContent.ContentTab,\n    ) {\n        if (fromTab == toTab) return\n        val newShowingList = if (fromTab.order > toTab.order) {\n            // move up\n            content.tabList.map { item ->\n                if (item.order in toTab.order until fromTab.order) {\n                    item.newOrder(item.order + 1)\n                } else if (item == fromTab) {\n                    fromTab.newOrder(order = toTab.order)\n                } else {\n                    item\n                }\n            }.sortedBy { it.order }\n        } else {\n            // move down\n            content.tabList.map { item ->\n                if (item.order > fromTab.order && item.order <= toTab.order) {\n                    item.newOrder(order = item.order - 1)\n                } else if (item == fromTab) {\n                    fromTab.newOrder(order = toTab.order)\n                } else {\n                    item\n                }\n            }.sortedBy { it.order }\n        }\n        contentRepo.insertContent(content.copy(tabList = newShowingList))\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/usecase/emoji/GetCustomEmojiUseCase.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.usecase.emoji\n\nimport com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubCustomEmojiEntityAdapter\nimport com.zhangke.fread.activitypub.app.internal.auth.ActivityPubClientManager\nimport com.zhangke.fread.activitypub.app.internal.screen.status.post.adapter.CustomEmojiAdapter\nimport com.zhangke.fread.activitypub.app.internal.screen.status.post.composable.GroupedCustomEmojiCell\nimport com.zhangke.fread.status.model.PlatformLocator\n\nclass GetCustomEmojiUseCase (\n    private val clientManager: ActivityPubClientManager,\n    private val emojiEntityAdapter: ActivityPubCustomEmojiEntityAdapter,\n    private val emojiAdapter: CustomEmojiAdapter,\n) {\n\n    suspend operator fun invoke(locator: PlatformLocator): Result<List<GroupedCustomEmojiCell>> {\n        return clientManager.getClient(locator)\n            .emojiRepo\n            .getCustomEmojis()\n            .map { list -> list.map(emojiEntityAdapter::toCustomEmoji) }\n            .map { emojiAdapter.toEmojiCell(it) }\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/usecase/emoji/MapCustomEmojiUseCase.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.usecase.emoji\n\nimport com.zhangke.fread.status.model.Emoji\n\nclass MapCustomEmojiUseCase () {\n\n    operator fun invoke(content: String, emojis: List<Emoji>): String {\n        return content\n//        var mappedContent = content\n//        emojis.forEach {\n//            mappedContent =\n//                content.replace(\n//                    \":${it.shortcode}:\",\n//                    \"<img src=\\\"${it.url}\\\" alt=\\\"${it.shortcode}\\\" />\",\n//                )\n//        }\n//        return mappedContent\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/usecase/media/UploadMediaAttachmentUseCase.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.usecase.media\n\nimport com.zhangke.activitypub.api.MediaRepo\nimport com.zhangke.activitypub.entities.ActivityPubMediaAttachmentEntity\nimport com.zhangke.activitypub.entities.ActivityPubResponse\nimport com.zhangke.framework.utils.ContentProviderFile\nimport com.zhangke.framework.utils.exceptionOrThrow\nimport com.zhangke.fread.activitypub.app.internal.auth.ActivityPubClientManager\nimport com.zhangke.fread.status.model.PlatformLocator\nimport io.ktor.http.HttpStatusCode\nimport kotlinx.coroutines.delay\nimport kotlin.time.Duration.Companion.seconds\n\nclass UploadMediaAttachmentUseCase (\n    private val clientManager: ActivityPubClientManager,\n) {\n\n    suspend operator fun invoke(\n        locator: PlatformLocator,\n        file: ContentProviderFile,\n        onProgress: (Float) -> Unit = {},\n    ): Result<String> {\n        val mediaRepo = clientManager.getClient(locator).mediaRepo\n        val response = mediaRepo.uploadFile(file, onProgress)\n        if (response.isFailure) return Result.failure(response.exceptionOrThrow())\n        val (code, attachment) = response.getOrThrow()\n        if (code != HttpStatusCode.Accepted.value) return Result.success(attachment.id)\n        var repeatCount = 0\n        val interval = 3.seconds\n        while (repeatCount < 10) {\n            val mediaResponse = mediaRepo.getMedia(attachment.id)\n            if (mediaResponse.isFailure || mediaResponse.getOrNull()?.code == HttpStatusCode.PartialContent.value) {\n                delay(interval)\n            } else {\n                return mediaResponse.map { it.response.id }\n            }\n            repeatCount++\n        }\n        return Result.failure(RuntimeException(\"File upload failed, processing timeout!\"))\n    }\n\n    private suspend fun MediaRepo.uploadFile(\n        file: ContentProviderFile,\n        onProgress: (Float) -> Unit = {},\n    ): Result<ActivityPubResponse<ActivityPubMediaAttachmentEntity>> {\n        return try {\n            val bytes = file.readBytes()\n                ?: return Result.failure(RuntimeException(\"File invalid!\"))\n            this.postFile(\n                fileName = file.fileName,\n                fileSize = file.size.bytes,\n                byteArray = bytes,\n                fileMediaType = file.mimeType,\n                onProgress = onProgress,\n            )\n        } catch (t: Throwable) {\n            Result.failure(RuntimeException(\"File invalid!\"))\n        }\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/usecase/platform/GetInstancePostStatusRulesUseCase.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.usecase.platform\n\nimport com.zhangke.activitypub.entities.ActivityPubInstanceEntity\nimport com.zhangke.fread.activitypub.app.internal.auth.ActivityPubClientManager\nimport com.zhangke.fread.activitypub.app.internal.screen.status.post.PostBlogRules\nimport com.zhangke.fread.status.model.PlatformLocator\n\nclass GetInstancePostStatusRulesUseCase (\n    private val clientManager: ActivityPubClientManager,\n) {\n\n    suspend operator fun invoke(locator: PlatformLocator): Result<PostBlogRules> {\n        return clientManager.getClient(locator)\n            .instanceRepo\n            .getInstanceInformation()\n            .map { it.toRule() }\n    }\n\n    private fun ActivityPubInstanceEntity.toRule(): PostBlogRules {\n        val config = configuration\n        return PostBlogRules.default(\n            maxCharacters = config.statuses?.maxCharacters ?: 0,\n            maxMediaCount = config.statuses?.maxMediaAttachments ?: 0,\n            maxPollOptions = config.polls?.maxOptions ?: 0,\n            supportsQuotePost = this.supportsQuotePost,\n        )\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/usecase/source/user/SearchUserSourceNoTokenUseCase.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.usecase.source.user\n\nimport com.zhangke.framework.network.FormalBaseUrl\nimport com.zhangke.framework.network.HttpScheme\nimport com.zhangke.framework.utils.WebFinger\nimport com.zhangke.framework.utils.toPlatformUri\nimport com.zhangke.fread.activitypub.app.internal.auth.ActivityPubClientManager\nimport com.zhangke.fread.activitypub.app.internal.repo.user.UserRepo\nimport com.zhangke.fread.activitypub.app.internal.source.UserSourceTransformer\nimport com.zhangke.fread.activitypub.app.internal.uri.UserUriTransformer\nimport com.zhangke.fread.activitypub.app.internal.usecase.GetDefaultBaseUrlUseCase\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.source.StatusSource\nimport com.zhangke.fread.status.uri.FormalUri\n\nclass SearchUserSourceNoTokenUseCase (\n    private val clientManager: ActivityPubClientManager,\n    private val userRepo: UserRepo,\n    private val userUriTransformer: UserUriTransformer,\n    private val userSourceTransformer: UserSourceTransformer,\n    private val getDefaultBaseUrl: GetDefaultBaseUrlUseCase,\n) {\n\n    suspend operator fun invoke(\n        query: String,\n    ): Result<List<StatusSource>> {\n        val locator = PlatformLocator(baseUrl = getDefaultBaseUrl())\n        searchAsUserUri(locator, query).getOrNull()?.let { return Result.success(listOf(it)) }\n        searchAsWebFinger(locator, query).getOrNull()?.let { return Result.success(listOf(it)) }\n        searchAsUrl(query).getOrNull()?.let { return Result.success(listOf(it)) }\n        return searchUser(locator, query)\n    }\n\n    private suspend fun searchAsUserUri(\n        locator: PlatformLocator,\n        query: String\n    ): Result<StatusSource?> {\n        FormalUri.from(query)\n            ?.let(userUriTransformer::parse)\n            ?.let {\n                userRepo.getUserSource(\n                    locator = locator,\n                    userUriInsights = it,\n                ).getOrNull()\n            }?.let {\n                return Result.success(it)\n            }\n        return Result.success(null)\n    }\n\n    private suspend fun searchAsWebFinger(\n        locator: PlatformLocator,\n        query: String,\n    ): Result<StatusSource?> {\n        val webFinger = WebFinger.create(query) ?: return Result.success(null)\n        return userRepo.lookupUserSource(\n            locator = locator,\n            acct = webFinger.toString(),\n        )\n    }\n\n    /**\n     * https://m.cmx.im/@webb@androiddev.social\n     * https://androiddev.social/@webb\n     */\n    private suspend fun searchAsUrl(query: String): Result<StatusSource?> {\n        WebFinger.create(query) ?: return Result.success(null)\n        // FIXME: fix no scheme query\n        val uri = query.toPlatformUri()\n        val baseUrl = FormalBaseUrl.parse(query) ?: return Result.success(null)\n        val scheme = if (uri.scheme.isNullOrEmpty()) HttpScheme.HTTPS else \"${uri.scheme}://\"\n        if (!HttpScheme.validate(scheme)) return Result.success(null)\n        val host = uri.host\n        if (host.isNullOrEmpty()) return Result.success(null)\n        val acct = uri.path\n            ?.removePrefix(\"/\")\n            ?.removeSuffix(\"/\")\n        if (acct.isNullOrEmpty()) return Result.success(null)\n        val locator = PlatformLocator(baseUrl = baseUrl)\n        return userRepo.lookupUserSource(\n            locator = locator,\n            acct = acct,\n        )\n    }\n\n    private suspend fun searchUser(\n        locator: PlatformLocator,\n        query: String\n    ): Result<List<StatusSource>> {\n        return clientManager.getClient(locator)\n            .searchRepo.queryAccount(query = query)\n            .map { list ->\n                list.map { userSourceTransformer.createByUserEntity(it) }\n            }\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/usecase/status/GetStatusContextUseCase.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.usecase.status\n\nimport com.zhangke.activitypub.entities.ActivityPubStatusContextEntity\nimport com.zhangke.activitypub.entities.ActivityPubStatusEntity\nimport com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubStatusAdapter\nimport com.zhangke.fread.activitypub.app.internal.auth.ActivityPubClientManager\nimport com.zhangke.fread.activitypub.app.internal.auth.LoggedAccountProvider\nimport com.zhangke.fread.activitypub.app.internal.model.ActivityPubLoggedAccount\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.model.StatusUiState\nimport com.zhangke.fread.status.platform.BlogPlatform\nimport com.zhangke.fread.status.status.model.DescendantStatus\nimport com.zhangke.fread.status.status.model.Status\nimport com.zhangke.fread.status.status.model.StatusContext\n\nclass GetStatusContextUseCase (\n    private val clientManager: ActivityPubClientManager,\n    private val statusAdapter: ActivityPubStatusAdapter,\n    private val loggedAccountProvider: LoggedAccountProvider,\n) {\n\n    suspend operator fun invoke(\n        locator: PlatformLocator,\n        status: Status,\n    ): Result<StatusContext> {\n        val loggedAccount = loggedAccountProvider.getAccount(locator)\n        val statusId = status.id\n        return clientManager.getClient(locator)\n            .statusRepo\n            .getStatusContext(statusId)\n            .map {\n                convert(\n                    parentId = statusId,\n                    entity = it,\n                    platform = status.platform,\n                    locator = locator,\n                    loggedAccount = loggedAccount,\n                )\n            }\n    }\n\n    private suspend fun convert(\n        parentId: String,\n        entity: ActivityPubStatusContextEntity,\n        platform: BlogPlatform,\n        locator: PlatformLocator,\n        loggedAccount: ActivityPubLoggedAccount?,\n    ): StatusContext {\n        return StatusContext(\n            ancestors = entity.ancestors.toStatusList(\n                locator = locator,\n                platform = platform,\n                loggedAccount = loggedAccount,\n            ),\n            status = null,\n            descendants = convertDescendantsStatus(\n                parentId = parentId,\n                descendants = entity.descendants,\n                platform = platform,\n                locator = locator,\n                loggedAccount = loggedAccount,\n            ),\n        )\n    }\n\n    private suspend fun convertDescendantsStatus(\n        parentId: String,\n        descendants: List<ActivityPubStatusEntity>,\n        platform: BlogPlatform,\n        locator: PlatformLocator,\n        loggedAccount: ActivityPubLoggedAccount?,\n    ): List<DescendantStatus> {\n        if (descendants.isEmpty()) return emptyList()\n        val firstLevelDescendants = descendants.filter { it.inReplyToId == parentId }\n        return firstLevelDescendants.map { entity ->\n            DescendantStatus(\n                entity.toUiState(locator, platform, loggedAccount),\n                buildDescendantsStatus(entity.id, descendants, platform, locator, loggedAccount)\n            )\n        }\n    }\n\n    private suspend fun buildDescendantsStatus(\n        parentId: String,\n        descendants: List<ActivityPubStatusEntity>,\n        platform: BlogPlatform,\n        locator: PlatformLocator,\n        loggedAccount: ActivityPubLoggedAccount?,\n    ): DescendantStatus? {\n        if (descendants.isEmpty()) return null\n        val descentEntity = descendants.firstOrNull { it.inReplyToId == parentId } ?: return null\n        val descendantDescendant =\n            buildDescendantsStatus(descentEntity.id, descendants, platform, locator, loggedAccount)\n        return DescendantStatus(\n            status = descentEntity.toUiState(locator, platform, loggedAccount),\n            descendantStatus = descendantDescendant,\n        )\n    }\n\n    private suspend fun List<ActivityPubStatusEntity>.toStatusList(\n        locator: PlatformLocator,\n        platform: BlogPlatform,\n        loggedAccount: ActivityPubLoggedAccount?,\n    ): List<StatusUiState> {\n        return this.map { statusEntity ->\n            statusEntity.toUiState(\n                platform = platform,\n                loggedAccount = loggedAccount,\n                locator = locator,\n            )\n        }\n    }\n\n    private suspend fun ActivityPubStatusEntity.toUiState(\n        locator: PlatformLocator,\n        platform: BlogPlatform,\n        loggedAccount: ActivityPubLoggedAccount?,\n    ): StatusUiState {\n        return statusAdapter.toStatusUiState(\n            entity = this,\n            platform = platform,\n            loggedAccount = loggedAccount,\n            locator = locator,\n        )\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/usecase/status/GetTimelineStatusUseCase.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.usecase.status\n\nimport com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubStatusAdapter\nimport com.zhangke.fread.activitypub.app.internal.auth.ActivityPubClientManager\nimport com.zhangke.fread.activitypub.app.internal.model.ActivityPubStatusSourceType\nimport com.zhangke.fread.activitypub.app.internal.repo.platform.ActivityPubPlatformRepo\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.status.model.Status\n\nclass GetTimelineStatusUseCase (\n    private val clientManager: ActivityPubClientManager,\n    private val platformRepo: ActivityPubPlatformRepo,\n    private val statusAdapter: ActivityPubStatusAdapter,\n) {\n\n    suspend operator fun invoke(\n        locator: PlatformLocator,\n        type: ActivityPubStatusSourceType,\n        maxId: String?,\n        minId: String?,\n        limit: Int,\n        listId: String? = null,\n    ): Result<List<Status>> {\n        val timelineRepo = clientManager.getClient(locator).timelinesRepo\n        val platformResult = platformRepo.getPlatform(locator)\n        if (platformResult.isFailure) {\n            return Result.failure(platformResult.exceptionOrNull()!!)\n        }\n        val platform = platformResult.getOrThrow()\n        val entitiesResult = when (type) {\n            ActivityPubStatusSourceType.TIMELINE_HOME -> timelineRepo.homeTimeline(\n                limit = limit,\n                minId = minId,\n                maxId = maxId,\n            )\n\n            ActivityPubStatusSourceType.TIMELINE_LOCAL -> timelineRepo.localTimelines(\n                limit = limit,\n                minId = minId,\n                maxId = maxId,\n            )\n\n            ActivityPubStatusSourceType.TIMELINE_PUBLIC -> timelineRepo.publicTimelines(\n                limit = limit,\n                minId = minId,\n                maxId = maxId,\n            )\n\n            ActivityPubStatusSourceType.LIST -> timelineRepo.getTimelineList(\n                listId = listId!!,\n                limit = limit,\n                minId = minId,\n                maxId = maxId,\n            )\n        }\n        return entitiesResult.map { list ->\n            list.filter { it.id != minId && it.id != maxId }\n                .map { statusAdapter.toStatus(it, platform) }\n        }\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/usecase/status/GetUserStatusUseCase.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.usecase.status\n\nimport com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubStatusAdapter\nimport com.zhangke.fread.activitypub.app.internal.auth.ActivityPubClientManager\nimport com.zhangke.fread.activitypub.app.internal.auth.LoggedAccountProvider\nimport com.zhangke.fread.activitypub.app.internal.model.UserUriInsights\nimport com.zhangke.fread.activitypub.app.internal.repo.WebFingerBaseUrlToUserIdRepo\nimport com.zhangke.fread.activitypub.app.internal.repo.platform.ActivityPubPlatformRepo\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.model.StatusUiState\n\nclass GetUserStatusUseCase (\n    private val clientManager: ActivityPubClientManager,\n    private val webFingerBaseUrlToUserIdRepo: WebFingerBaseUrlToUserIdRepo,\n    private val activityPubStatusAdapter: ActivityPubStatusAdapter,\n    private val platformRepo: ActivityPubPlatformRepo,\n    private val loggedAccountProvider: LoggedAccountProvider,\n) {\n\n    suspend operator fun invoke(\n        locator: PlatformLocator,\n        userInsights: UserUriInsights,\n        limit: Int,\n        maxId: String?,\n    ): Result<List<StatusUiState>> {\n        val userIdResult = webFingerBaseUrlToUserIdRepo.getUserId(userInsights.webFinger, locator)\n        if (userIdResult.isFailure) return Result.failure(userIdResult.exceptionOrNull()!!)\n        val userId = userIdResult.getOrThrow()\n        val platformResult = platformRepo.getPlatform(locator)\n        if (platformResult.isFailure) return Result.failure(platformResult.exceptionOrNull()!!)\n        val platform = platformResult.getOrThrow()\n        val account = loggedAccountProvider.getAccount(locator)\n        return clientManager.getClient(locator)\n            .accountRepo.getStatuses(\n                id = userId,\n                limit = limit,\n                maxId = maxId,\n            ).map { list ->\n                list.map {\n                    activityPubStatusAdapter.toStatusUiState(\n                        entity = it,\n                        platform = platform,\n                        locator = locator,\n                        loggedAccount = account,\n                    )\n                }\n            }\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/usecase/status/StatusInteractiveUseCase.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.usecase.status\n\nimport com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubStatusAdapter\nimport com.zhangke.fread.activitypub.app.internal.auth.ActivityPubClientManager\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.model.StatusActionType\nimport com.zhangke.fread.status.status.model.Status\n\nclass StatusInteractiveUseCase (\n    private val clientManager: ActivityPubClientManager,\n    private val activityPubStatusAdapter: ActivityPubStatusAdapter,\n) {\n\n    suspend operator fun invoke(\n        locator: PlatformLocator,\n        status: Status,\n        type: StatusActionType,\n    ): Result<Status?> {\n        val statusId = if (status is Status.Reblog) {\n            status.reblog.id\n        } else {\n            status.id\n        }\n        val statusRepo = clientManager.getClient(locator).statusRepo\n        val blog = status.intrinsicBlog\n        val interactionResult = when (type) {\n            StatusActionType.LIKE -> {\n                if (blog.like.liked == true) {\n                    statusRepo.unfavourite(statusId)\n                } else {\n                    statusRepo.favourite(statusId)\n                }\n            }\n\n            StatusActionType.FORWARD -> {\n                if (blog.forward.forward == true) {\n                    statusRepo.unreblog(statusId)\n                } else {\n                    statusRepo.reblog(statusId)\n                }\n            }\n\n            StatusActionType.BOOKMARK -> {\n                if (blog.bookmark.bookmarked == true) {\n                    statusRepo.unbookmark(statusId)\n                } else {\n                    statusRepo.bookmark(statusId)\n                }\n            }\n\n            StatusActionType.DELETE -> statusRepo.delete(statusId)\n\n            StatusActionType.PIN -> {\n                if (blog.pinned) {\n                    statusRepo.unpin(statusId)\n                } else {\n                    statusRepo.pin(statusId)\n                }\n            }\n\n            else -> {\n                Result.failure(IllegalArgumentException(\"Unknown interaction: $type\"))\n            }\n        }\n        if (interactionResult.isFailure) {\n            return Result.failure(interactionResult.exceptionOrNull()!!)\n        }\n        if (type == StatusActionType.DELETE) {\n            return Result.success(null)\n        }\n        val resultNewStatusEntity = interactionResult.getOrThrow()\n        val newStatus = activityPubStatusAdapter.toStatus(\n            resultNewStatusEntity,\n            status.platform,\n        )\n        val resultStatus = if (status is Status.Reblog) {\n            status.copy(reblog = newStatus.intrinsicBlog)\n        } else {\n            newStatus\n        }\n        return Result.success(resultStatus)\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/usecase/status/VotePollUseCase.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.usecase.status\n\nimport com.zhangke.fread.activitypub.app.internal.adapter.ActivityPubPollAdapter\nimport com.zhangke.fread.activitypub.app.internal.auth.ActivityPubClientManager\nimport com.zhangke.fread.status.blog.Blog\nimport com.zhangke.fread.status.blog.BlogPoll\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.status.model.Status\n\nclass VotePollUseCase (\n    private val clientManager: ActivityPubClientManager,\n    private val pollAdapter: ActivityPubPollAdapter,\n) {\n\n    suspend operator fun invoke(\n        locator: PlatformLocator,\n        blog: Blog,\n        votedOption: List<BlogPoll.Option>,\n    ): Result<Status> {\n        return clientManager.getClient(locator)\n            .statusRepo\n            .votes(\n                id = blog.poll!!.id,\n                choices = votedOption.map { it.index },\n            )\n            .map { pollAdapter.adapt(it) }\n            .map { poll ->\n                Status.NewBlog(blog.copy(poll = poll))\n            }\n    }\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/utils/Base64Utils.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.utils\n\nimport io.ktor.utils.io.core.toByteArray\nimport kotlin.io.encoding.Base64\nimport kotlin.io.encoding.ExperimentalEncodingApi\n\n// FIXME: Support Base64.NO_WRAP\n@OptIn(ExperimentalEncodingApi::class)\ninternal fun String.encodeToBase64(): String {\n    return Base64.UrlSafe.encode(this.toByteArray())\n}\n\n// FIXME: Support Base64.NO_WRAP\n@OptIn(ExperimentalEncodingApi::class)\ninternal fun String.decodeFromBase64(): String {\n    return Base64.UrlSafe.decode(this).decodeToString()\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/utils/DeleteTextUtil.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.utils\n\nimport androidx.compose.ui.text.TextRange\nimport androidx.compose.ui.text.input.TextFieldValue\n\nobject DeleteTextUtil {\n\n    fun deleteText(text: TextFieldValue): TextFieldValue {\n        if (text.text.isEmpty()) return text\n        val startIndex = text.selection.start\n        if (text.selection.length > 0) {\n            val newText = text.text.remove(text.selection)\n            return TextFieldValue(newText, TextRange(startIndex))\n        }\n        val range = findBeforeEmoji(text.text, text.selection.start)?.toTextRange()\n        if (range != null) {\n            val newText = text.text.remove(range)\n            return TextFieldValue(newText, TextRange(range.start))\n        }\n        if (startIndex > 0) {\n            val removedRange = TextRange(startIndex - 1, startIndex)\n            return TextFieldValue(text.text.remove(removedRange), TextRange(startIndex - 1))\n        }\n        return text\n    }\n\n    private fun IntRange.toTextRange(): TextRange {\n        return TextRange(start, endInclusive)\n    }\n\n    private fun String.remove(range: TextRange): String {\n        if (this.isEmpty()) return this\n        if (range.length < 0) return this\n        if (range.start < 0 || range.end > this.length) return this\n        val prefix = this.substring(0, range.start)\n        val suffix = this.substring(range.end)\n        return prefix + suffix\n    }\n\n    // must be space\n    private const val STATE_INIT = 0\n\n    // must be colon\n    private const val STATE_SCANNING_START_COLON = 1\n\n    // can be anything but blank\n    private const val STATE_SCANNING_CODE = 2\n\n    // must be colon\n    private const val STATE_SCANNING_END_COLON = 3\n\n    // must be space\n    private const val STATE_SCANNING_END_SPACE = 4\n\n    // 判断 index 之前是否是 emoji\n    internal fun findBeforeEmoji(text: String, index: Int): IntRange? {\n        //“ :1234_iu01: ”\n        if (index <= 0 || index > text.length) return null\n        var currentIndex = index - 1\n        var state = STATE_INIT\n        val charArray = text.toCharArray()\n        while (currentIndex >= 0) {\n            val char = charArray[currentIndex]\n            when (state) {\n                STATE_INIT -> {\n                    if (char.isSpace) {\n                        state = STATE_SCANNING_START_COLON\n                        currentIndex--\n                    } else {\n                        return null\n                    }\n                }\n\n                STATE_SCANNING_START_COLON -> {\n                    if (char.isColon) {\n                        state = STATE_SCANNING_CODE\n                        currentIndex--\n                    } else {\n                        return null\n                    }\n                }\n\n                STATE_SCANNING_CODE -> {\n                    if (char.validateCode) {\n                        currentIndex--\n                    } else if (char.isColon) {\n                        state = STATE_SCANNING_END_COLON\n                    } else {\n                        return null\n                    }\n                }\n\n                STATE_SCANNING_END_COLON -> {\n                    if (char.isColon) {\n                        state = STATE_SCANNING_END_SPACE\n                        currentIndex--\n                    } else {\n                        return null\n                    }\n                }\n\n                STATE_SCANNING_END_SPACE -> {\n                    return if (char.isSpace) {\n                        IntRange(currentIndex, index)\n                    } else {\n                        null\n                    }\n                }\n            }\n        }\n        return null\n    }\n\n    private val Char.isSpace: Boolean get() = this == ' '\n    private val Char.isColon: Boolean get() = this == ':'\n    private val Char.validateCode: Boolean get() = this.isLetterOrDigit() || this == '_'\n\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/utils/MastodonHelper.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.utils\n\nimport com.zhangke.fread.activitypub.app.Res\nimport com.zhangke.fread.common.utils.StorageHelper\nimport okio.FileSystem\nimport okio.Path.Companion.toPath\nimport okio.SYSTEM\nimport okio.openZip\nimport org.jetbrains.compose.resources.ExperimentalResourceApi\n\nclass MastodonHelper (\n    private val storageHelper: StorageHelper,\n) {\n\n    private val fileSystem = FileSystem.SYSTEM\n\n    @OptIn(ExperimentalResourceApi::class)\n    suspend fun getLocalMastodonJson(): String? = runCatching {\n        val cacheMastodonServersZipPath = storageHelper.cacheDir.resolve(\"mastodon-servers.zip\")\n\n        // Res.getUri is not available, so copy to cache dir\n        if (!fileSystem.exists(cacheMastodonServersZipPath)) {\n            fileSystem.write(cacheMastodonServersZipPath) {\n                write(Res.readBytes(\"files/mastodon-servers.zip\"))\n            }\n        }\n\n        val zip = FileSystem.SYSTEM.openZip(cacheMastodonServersZipPath)\n        return zip.read(\"servers.json\".toPath()) {\n            readUtf8()\n        }\n    }.getOrNull()\n}"
  },
  {
    "path": "plugins/activitypub-app/src/commonMain/kotlin/com/zhangke/fread/activitypub/app/internal/utils/PlatformLocatorUtils.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.utils\n\nimport com.zhangke.fread.activitypub.app.internal.content.ActivityPubContent\nimport com.zhangke.fread.status.model.PlatformLocator\n\nfun createPlatformLocator(content: ActivityPubContent): PlatformLocator {\n    return PlatformLocator(baseUrl = content.baseUrl, accountUri = content.accountUri)\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/iosMain/kotlin/com/zhangke/fread/activitypub/app/ActivityPubIosModule.kt",
    "content": "package com.zhangke.fread.activitypub.app\n\nimport androidx.room.Room\nimport androidx.sqlite.driver.bundled.BundledSQLiteDriver\nimport com.zhangke.fread.activitypub.app.internal.db.ActivityPubDatabases\nimport com.zhangke.fread.activitypub.app.internal.db.ActivityPubLoggedAccountDatabase\nimport com.zhangke.fread.activitypub.app.internal.db.status.ActivityPubStatusDatabases\nimport com.zhangke.fread.activitypub.app.internal.db.status.ActivityPubStatusReadStateDatabases\nimport com.zhangke.fread.activitypub.app.internal.push.ActivityPubPushManager\nimport com.zhangke.fread.common.documentDirectory\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.IO\nimport org.koin.core.module.Module\nimport org.koin.core.module.dsl.factoryOf\n\nactual fun Module.createPlatformModule() {\n    factory<ActivityPubDatabases> {\n        val dbFilePath = getDBFilePath(ActivityPubDatabases.DB_NAME)\n        Room.databaseBuilder<ActivityPubDatabases>(\n            name = dbFilePath,\n        ).setDriver(BundledSQLiteDriver())\n            .setQueryCoroutineContext(Dispatchers.IO)\n            .build()\n    }\n    single<ActivityPubLoggedAccountDatabase> {\n        val dbFilePath = getDBFilePath(ActivityPubLoggedAccountDatabase.DB_NAME)\n        Room.databaseBuilder<ActivityPubLoggedAccountDatabase>(\n            name = dbFilePath,\n        ).setDriver(BundledSQLiteDriver())\n            .setQueryCoroutineContext(Dispatchers.IO)\n            .build()\n    }\n    factory<ActivityPubStatusDatabases> {\n        val dbFilePath = getDBFilePath(ActivityPubStatusDatabases.DB_NAME)\n        Room.databaseBuilder<ActivityPubStatusDatabases>(\n            name = dbFilePath,\n        ).setDriver(BundledSQLiteDriver())\n            .setQueryCoroutineContext(Dispatchers.IO)\n            .addMigrations(ActivityPubStatusDatabases.MIGRATION_1_2)\n            .build()\n    }\n    factory<ActivityPubStatusReadStateDatabases> {\n        val dbFilePath = getDBFilePath(ActivityPubStatusReadStateDatabases.DB_NAME)\n        Room.databaseBuilder<ActivityPubStatusReadStateDatabases>(\n            name = dbFilePath,\n        ).setDriver(BundledSQLiteDriver())\n            .setQueryCoroutineContext(Dispatchers.IO)\n            .build()\n    }\n\n    factoryOf(::ActivityPubPushManager)\n}\n\nprivate fun getDBFilePath(dbName: String): String {\n    return documentDirectory() + \"/$dbName\"\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/iosMain/kotlin/com/zhangke/fread/activitypub/app/internal/push/ActivityPubPushManager.ios.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.push\n\nimport com.zhangke.fread.status.model.PlatformLocator\n\nactual class ActivityPubPushManager() {\n\n    actual suspend fun subscribe(locator: PlatformLocator, accountId: String) {\n        throw NotImplementedError(\"Not implemented for iOS\")\n    }\n\n    actual suspend fun unsubscribe(locator: PlatformLocator, accountId: String) {\n        throw NotImplementedError(\"Not implemented for iOS\")\n    }\n}\n"
  },
  {
    "path": "plugins/activitypub-app/src/iosMain/kotlin/com/zhangke/fread/activitypub/app/internal/push/ActivityPubPushMessageReceiverHelper.ios.kt",
    "content": "package com.zhangke.fread.activitypub.app.internal.push\n\nimport com.zhangke.fread.common.push.PushMessage\n\nactual class ActivityPubPushMessageReceiverHelper {\n\n    actual fun onReceiveNewMessage(message: PushMessage) {\n        error(\"Not implemented\")\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/.gitignore",
    "content": "/build"
  },
  {
    "path": "plugins/bluesky/build.gradle.kts",
    "content": "plugins {\n    id(\"fread.project.feature.kmp\")\n    id(\"com.google.devtools.ksp\")\n    id(\"kotlin-parcelize\")\n    alias(libs.plugins.room)\n}\n\nandroid {\n    namespace = \"com.zhangke.fread.bluesky\"\n    sourceSets {\n        getByName(\"main\") {\n            assets.srcDirs(\"src/androidMain/assets\")\n            res.srcDirs(\"src/androidMain/res\")\n            manifest.srcFile(\"src/androidMain/AndroidManifest.xml\")\n        }\n    }\n}\n\nkotlin {\n    sourceSets {\n        commonMain {\n            dependencies {\n                implementation(project(path = \":framework\"))\n                implementation(project(path = \":commonbiz:common\"))\n                implementation(project(path = \":bizframework:status-provider\"))\n                implementation(project(path = \":commonbiz:status-ui\"))\n                implementation(project(path = \":commonbiz:sharedscreen\"))\n                implementation(project(\":commonbiz:analytics\"))\n\n                implementation(compose.components.resources)\n\n                implementation(libs.arrow.core)\n\n                implementation(libs.kotlinx.serialization.core)\n                implementation(libs.kotlinx.serialization.json)\n\n                implementation(libs.ktor.client.content.negotiation)\n                implementation(libs.ktor.client.serialization.kotlinx.json)\n                implementation(libs.ktor.client.logging)\n                implementation(libs.jetbrains.lifecycle.viewmodel)\n                implementation(libs.androidx.constraintlayout.compose.kmp)\n                implementation(libs.imageLoader)\n                implementation(libs.androidx.room)\n                implementation(libs.auto.service.annotations)\n                implementation(libs.androidx.paging.common)\n                implementation(libs.leftright)\n\n                implementation(libs.haze)\n\n                implementation(libs.krouter.runtime)\n                api(libs.bluesky)\n            }\n        }\n        commonTest {\n            dependencies {\n                implementation(kotlin(\"test\"))\n            }\n        }\n        androidMain {\n            dependencies {\n                implementation(libs.androidx.core.ktx)\n                implementation(libs.bundles.androidx.activity)\n                implementation(libs.androidx.browser)\n            }\n        }\n    }\n    configureCommonMainKsp()\n}\n\ndependencies {\n    kspAll(libs.androidx.room.compiler)\n    kspAll(libs.auto.service.ksp)\n    kspAll(libs.krouter.collecting.compiler)\n}\n\ncompose {\n    resources {\n        publicResClass = false\n        packageOfResClass = \"com.zhangke.fread.bluesky\"\n        generateResClass = always\n    }\n}\n\nroom {\n    schemaDirectory(\"$projectDir/schemas\")\n}\n"
  },
  {
    "path": "plugins/bluesky/consumer-rules.pro",
    "content": ""
  },
  {
    "path": "plugins/bluesky/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguardFiles setting in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n\n# Uncomment this to preserve the line number information for\n# debugging stack traces.\n#-keepattributes SourceFile,LineNumberTable\n\n# If you keep the line number information, uncomment this to\n# hide the original source file name.\n#-renamesourcefileattribute SourceFile"
  },
  {
    "path": "plugins/bluesky/src/androidMain/kotlin/com/zhangke/fread/bluesky/BlueskyAndroidModule.kt",
    "content": "package com.zhangke.fread.bluesky\n\nimport androidx.room.Room\nimport com.zhangke.fread.bluesky.internal.db.BlueskyLoggedAccountDatabase\nimport org.koin.android.ext.koin.androidContext\nimport org.koin.core.module.Module\n\nactual fun Module.createPlatformModule() {\n    single<BlueskyLoggedAccountDatabase> {\n        Room.databaseBuilder(\n            androidContext(),\n            BlueskyLoggedAccountDatabase::class.java,\n            BlueskyLoggedAccountDatabase.DB_NAME,\n        ).build()\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/androidTest/java/com/zhangke/fread/bluesky/ExampleInstrumentedTest.kt",
    "content": "package com.zhangke.fread.bluesky\n\nimport androidx.test.platform.app.InstrumentationRegistry\nimport androidx.test.ext.junit.runners.AndroidJUnit4\n\nimport org.junit.Test\nimport org.junit.runner.RunWith\n\nimport org.junit.Assert.*\n\n/**\n * Instrumented test, which will execute on an Android device.\n *\n * See [testing documentation](http://d.android.com/tools/testing).\n */\n@RunWith(AndroidJUnit4::class)\nclass ExampleInstrumentedTest {\n    @Test\n    fun useAppContext() {\n        // Context of the app under test.\n        val appContext = InstrumentationRegistry.getInstrumentation().targetContext\n        assertEquals(\"com.zhangke.fread.bluesky.test\", appContext.packageName)\n    }\n}"
  },
  {
    "path": "plugins/bluesky/src/commonMain/composeResources/drawable/bluesky_logo.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"600dp\"\n    android:height=\"530dp\"\n    android:viewportWidth=\"600\"\n    android:viewportHeight=\"530\">\n  <path\n      android:pathData=\"m135.72,44.03c66.5,49.92 138.02,151.14 164.28,205.46 26.26,-54.32 97.78,-155.54 164.28,-205.46 47.98,-36.02 125.72,-63.89 125.72,24.8 0,17.71 -10.15,148.79 -16.11,170.07 -20.7,73.98 -96.14,92.85 -163.25,81.43 117.3,19.96 147.14,86.09 82.7,152.22 -122.39,125.59 -175.91,-31.51 -189.63,-71.77 -2.51,-7.38 -3.69,-10.83 -3.71,-7.9 -0.02,-2.94 -1.19,0.52 -3.71,7.9 -13.71,40.26 -67.23,197.36 -189.63,71.77 -64.44,-66.13 -34.6,-132.26 82.7,-152.22 -67.11,11.42 -142.55,-7.45 -163.25,-81.43 -5.96,-21.28 -16.11,-152.36 -16.11,-170.07 0,-88.69 77.74,-60.82 125.72,-24.8z\"\n      android:fillColor=\"#1185fe\"/>\n</vector>\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/BlueskyAccountManager.kt",
    "content": "package com.zhangke.fread.bluesky\n\nimport com.zhangke.fread.bluesky.internal.account.BlueskyLoggedAccount\nimport com.zhangke.fread.bluesky.internal.account.BlueskyLoggedAccountManager\nimport com.zhangke.fread.bluesky.internal.content.BlueskyContent\nimport com.zhangke.fread.bluesky.internal.screen.add.AddBlueskyContentScreenNavKey\nimport com.zhangke.fread.bluesky.internal.usecase.UnblockUserWithoutUriUseCase\nimport com.zhangke.fread.common.utils.GlobalScreenNavigation\nimport com.zhangke.fread.status.account.AccountRefreshResult\nimport com.zhangke.fread.status.account.IAccountManager\nimport com.zhangke.fread.status.account.LoggedAccount\nimport com.zhangke.fread.status.author.BlogAuthor\nimport com.zhangke.fread.status.model.FreadContent\nimport com.zhangke.fread.status.model.LoggedAccountDetail\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.model.Relationships\nimport com.zhangke.fread.status.model.notBluesky\nimport com.zhangke.fread.status.platform.BlogPlatform\nimport com.zhangke.fread.status.uri.FormalUri\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.map\n\nclass BlueskyAccountManager(\n    private val accountManager: BlueskyLoggedAccountManager,\n    private val unblockUserWithoutUri: UnblockUserWithoutUriUseCase,\n) : IAccountManager {\n\n    override suspend fun getAllLoggedAccount(): List<LoggedAccount> {\n        return accountManager.getAllAccount()\n    }\n\n    override fun getAllAccountFlow(): Flow<List<LoggedAccount>> {\n        return accountManager.getAllAccountFlow()\n    }\n\n    override fun getAllAccountDetailFlow(): Flow<List<LoggedAccountDetail>>? {\n        return accountManager.getAllAccountFlow()\n            .map { list ->\n                list.map {\n                    LoggedAccountDetail(\n                        account = it,\n                        author = it.user,\n                    )\n                }\n            }\n    }\n\n    override suspend fun triggerLaunchAuth(platform: BlogPlatform, account: LoggedAccount?) {\n        if (platform.protocol.notBluesky) return\n        GlobalScreenNavigation.navigate(\n            AddBlueskyContentScreenNavKey(\n                baseUrl = platform.baseUrl,\n                loginMode = true,\n                avatar = account?.avatar,\n                displayName = account?.userName,\n                handle = account?.prettyHandle,\n            )\n        )\n    }\n\n    override suspend fun refreshAllAccountInfo(): List<AccountRefreshResult> {\n        return accountManager.refreshAccountProfile()\n    }\n\n    override suspend fun logout(account: LoggedAccount): Boolean {\n        if (account !is BlueskyLoggedAccount) return false\n        accountManager.logout(account.uri)\n        return true\n    }\n\n    override fun subscribeNotification() {\n\n    }\n\n    override suspend fun getRelationships(\n        account: LoggedAccount,\n        accounts: List<BlogAuthor>,\n    ): Result<Map<FormalUri, Relationships>> {\n        // not implemented for Bluesky currently\n        return Result.success(emptyMap())\n    }\n\n    override suspend fun cancelFollowRequest(\n        account: LoggedAccount,\n        user: BlogAuthor\n    ): Result<Unit>? {\n        return null\n    }\n\n    override suspend fun unblockAccount(\n        account: LoggedAccount,\n        user: BlogAuthor\n    ): Result<Unit>? {\n        if (account.platform.protocol.notBluesky) return null\n        val locator = PlatformLocator(\n            baseUrl = account.platform.baseUrl,\n            accountUri = account.uri,\n        )\n        return unblockUserWithoutUri(locator, user)\n    }\n\n    override suspend fun selectContentWithAccount(\n        contentList: List<FreadContent>,\n        account: LoggedAccount\n    ): List<FreadContent> {\n        return contentList.filterIsInstance<BlueskyContent>()\n            .filter { it.baseUrl == account.platform.baseUrl }\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/BlueskyModule.kt",
    "content": "package com.zhangke.fread.bluesky\n\nimport com.zhangke.framework.nav.NavEntryProvider\nimport com.zhangke.framework.network.FormalBaseUrl\nimport com.zhangke.fread.bluesky.internal.account.BlueskyLoggedAccountManager\nimport com.zhangke.fread.bluesky.internal.adapter.BlueskyAccountAdapter\nimport com.zhangke.fread.bluesky.internal.adapter.BlueskyFeedsAdapter\nimport com.zhangke.fread.bluesky.internal.adapter.BlueskyNotificationAdapter\nimport com.zhangke.fread.bluesky.internal.adapter.BlueskyProfileAdapter\nimport com.zhangke.fread.bluesky.internal.adapter.BlueskyStatusAdapter\nimport com.zhangke.fread.bluesky.internal.client.BlueskyClientManager\nimport com.zhangke.fread.bluesky.internal.content.BlueskyContentManager\nimport com.zhangke.fread.bluesky.internal.migrate.BlueskyContentMigrator\nimport com.zhangke.fread.bluesky.internal.repo.BlueskyLoggedAccountRepo\nimport com.zhangke.fread.bluesky.internal.repo.BlueskyPlatformRepo\nimport com.zhangke.fread.bluesky.internal.screen.add.AddBlueskyContentViewModel\nimport com.zhangke.fread.bluesky.internal.screen.content.BlueskyContentContainerViewModel\nimport com.zhangke.fread.bluesky.internal.screen.feeds.detail.FeedsDetailViewModel\nimport com.zhangke.fread.bluesky.internal.screen.feeds.explorer.ExplorerFeedsViewModel\nimport com.zhangke.fread.bluesky.internal.screen.feeds.following.BskyFollowingFeedsViewModel\nimport com.zhangke.fread.bluesky.internal.screen.feeds.home.HomeFeedsContainerViewModel\nimport com.zhangke.fread.bluesky.internal.screen.publish.PublishPostViewModel\nimport com.zhangke.fread.bluesky.internal.screen.search.SearchStatusViewModel\nimport com.zhangke.fread.bluesky.internal.screen.user.detail.BskyUserDetailViewModel\nimport com.zhangke.fread.bluesky.internal.screen.user.edit.EditProfileViewModel\nimport com.zhangke.fread.bluesky.internal.screen.user.list.UserListViewModel\nimport com.zhangke.fread.bluesky.internal.uri.platform.PlatformUriTransformer\nimport com.zhangke.fread.bluesky.internal.uri.user.UserUriTransformer\nimport com.zhangke.fread.bluesky.internal.usecase.BskyStatusInteractiveUseCase\nimport com.zhangke.fread.bluesky.internal.usecase.CreateRecordUseCase\nimport com.zhangke.fread.bluesky.internal.usecase.DeleteRecordUseCase\nimport com.zhangke.fread.bluesky.internal.usecase.GetAllListsUseCase\nimport com.zhangke.fread.bluesky.internal.usecase.GetAtIdentifierUseCase\nimport com.zhangke.fread.bluesky.internal.usecase.GetCompletedNotificationUseCase\nimport com.zhangke.fread.bluesky.internal.usecase.GetFeedsStatusUseCase\nimport com.zhangke.fread.bluesky.internal.usecase.GetFollowingFeedsUseCase\nimport com.zhangke.fread.bluesky.internal.usecase.GetStatusContextUseCase\nimport com.zhangke.fread.bluesky.internal.usecase.LoginToBskyUseCase\nimport com.zhangke.fread.bluesky.internal.usecase.PinFeedsUseCase\nimport com.zhangke.fread.bluesky.internal.usecase.PublishingPostUseCase\nimport com.zhangke.fread.bluesky.internal.usecase.RefreshSessionUseCase\nimport com.zhangke.fread.bluesky.internal.usecase.UnblockUserWithoutUriUseCase\nimport com.zhangke.fread.bluesky.internal.usecase.UnpinFeedsUseCase\nimport com.zhangke.fread.bluesky.internal.usecase.UpdateBlockUseCase\nimport com.zhangke.fread.bluesky.internal.usecase.UpdateHomeTabUseCase\nimport com.zhangke.fread.bluesky.internal.usecase.UpdatePinnedFeedsOrderUseCase\nimport com.zhangke.fread.bluesky.internal.usecase.UpdatePreferencesUseCase\nimport com.zhangke.fread.bluesky.internal.usecase.UpdateProfileRecordUseCase\nimport com.zhangke.fread.bluesky.internal.usecase.UpdateRelationshipUseCase\nimport com.zhangke.fread.bluesky.internal.usecase.UploadBlobUseCase\nimport com.zhangke.fread.bluesky.internal.usecase.UploadImageByImageUrlUseCase\nimport com.zhangke.fread.common.browser.BrowserInterceptor\nimport com.zhangke.fread.status.IStatusProvider\nimport com.zhangke.fread.status.model.PlatformLocator\nimport org.koin.core.module.Module\nimport org.koin.core.module.dsl.factoryOf\nimport org.koin.core.module.dsl.singleOf\nimport org.koin.core.module.dsl.viewModel\nimport org.koin.core.module.dsl.viewModelOf\nimport org.koin.dsl.bind\nimport org.koin.dsl.module\n\nval blueskyModule = module {\n\n    createPlatformModule()\n\n    factoryOf(::BlueskyNavEntryProvider) bind NavEntryProvider::class\n\n    singleOf(::BlueskyContentManager)\n    singleOf(::BlueskyScreenProvider)\n    singleOf(::BlueskyLoggedAccountRepo)\n    singleOf(::BlueskyClientManager)\n    singleOf(::BlueskyPlatformRepo)\n\n    factoryOf(::BlueskyAccountManager)\n    factoryOf(::BlueskyPublishManager)\n    factoryOf(::BlueskySearchEngine)\n    factoryOf(::BlueskyNotificationResolver)\n    factoryOf(::BlueskyStatusResolver)\n    factoryOf(::BlueskyStatusSourceResolver)\n    factoryOf(::BskyStartup)\n    factoryOf(::BlueskyContentMigrator)\n    factoryOf(::BlueskyLoggedAccountManager)\n\n    factoryOf(::BlueskyStatusAdapter)\n    factoryOf(::BlueskyAccountAdapter)\n    factoryOf(::BlueskyNotificationAdapter)\n    factoryOf(::BlueskyFeedsAdapter)\n    factoryOf(::BlueskyProfileAdapter)\n    factoryOf(::UserUriTransformer)\n    factoryOf(::PlatformUriTransformer)\n    factoryOf(::LoginToBskyUseCase)\n    factoryOf(::UpdateBlockUseCase)\n    factoryOf(::UpdateHomeTabUseCase)\n    factoryOf(::UpdatePinnedFeedsOrderUseCase)\n    factoryOf(::UnpinFeedsUseCase)\n    factoryOf(::UpdateRelationshipUseCase)\n    factoryOf(::UpdatePreferencesUseCase)\n    factoryOf(::GetAllListsUseCase)\n    factoryOf(::GetFollowingFeedsUseCase)\n    factoryOf(::GetFeedsStatusUseCase)\n    factoryOf(::GetCompletedNotificationUseCase)\n    factoryOf(::GetStatusContextUseCase)\n    factoryOf(::GetAtIdentifierUseCase)\n    factoryOf(::CreateRecordUseCase)\n    factoryOf(::DeleteRecordUseCase)\n    factoryOf(::PublishingPostUseCase)\n    factoryOf(::UploadBlobUseCase)\n    factoryOf(::UnblockUserWithoutUriUseCase)\n    factoryOf(::BskyStatusInteractiveUseCase)\n    factoryOf(::UpdateProfileRecordUseCase)\n    factoryOf(::PinFeedsUseCase)\n    factoryOf(::RefreshSessionUseCase)\n    factoryOf(::UploadImageByImageUrlUseCase)\n\n    viewModel { params ->\n        AddBlueskyContentViewModel(\n            loginToBluesky = get(),\n            contentRepo = get(),\n            platformRepo = get(),\n            onboardingComponent = get(),\n            baseUrl = params.values.getOrNull(0) as FormalBaseUrl?,\n            loginMode = params.values[1] as Boolean,\n            avatar = params.values.getOrNull(2) as String?,\n            displayName = params.values.getOrNull(3) as String?,\n            handle = params.values.getOrNull(4) as String?,\n        )\n    }\n    viewModelOf(::BlueskyContentContainerViewModel)\n    viewModelOf(::HomeFeedsContainerViewModel)\n    viewModel { params ->\n        BskyFollowingFeedsViewModel(\n            getFollowingFeeds = get(),\n            contentRepo = get(),\n            updatePinnedFeedsOrder = get(),\n            accountManager = get(),\n            contentId = params.getOrNull<String>(),\n            locator = params.getOrNull<PlatformLocator>(),\n        )\n    }\n    viewModelOf(::ExplorerFeedsViewModel)\n    viewModelOf(::FeedsDetailViewModel)\n    viewModelOf(::SearchStatusViewModel)\n    viewModelOf(::BskyUserDetailViewModel)\n    viewModelOf(::EditProfileViewModel)\n    viewModel { params ->\n        UserListViewModel(\n            clientManager = get(),\n            accountAdapter = get(),\n            updateRelationship = get(),\n            updateBlock = get(),\n            locator = params[0],\n            type = params[1],\n            postUri = params.values.getOrNull(2) as? String?,\n            userDid = params.values.getOrNull(3) as? String?,\n        )\n    }\n    viewModel { params ->\n        PublishPostViewModel(\n            clientManager = get(),\n            getAllLists = get(),\n            platformUriHelper = get(),\n            configManager = get(),\n            publishingPost = get(),\n            linkPreviewCardRepo = get(),\n            locator = params[0],\n            defaultText = params.values.getOrNull(1) as String?,\n            replyBlogJsonString = params.values.getOrNull(2) as String?,\n            quoteBlogJsonString = params.values.getOrNull(3) as String?,\n        )\n    }\n\n    factoryOf(::BlueskyProvider) bind IStatusProvider::class\n    factoryOf(::BskyUrlInterceptor) bind BrowserInterceptor::class\n\n}\n\nexpect fun Module.createPlatformModule()\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/BlueskyNavEntryProvider.kt",
    "content": "package com.zhangke.fread.bluesky\n\nimport androidx.navigation3.runtime.EntryProviderScope\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.nav.NavEntryProvider\nimport com.zhangke.fread.bluesky.internal.screen.add.AddBlueskyContentScreen\nimport com.zhangke.fread.bluesky.internal.screen.add.AddBlueskyContentScreenNavKey\nimport com.zhangke.fread.bluesky.internal.screen.feeds.explorer.ExplorerFeedsScreen\nimport com.zhangke.fread.bluesky.internal.screen.feeds.explorer.ExplorerFeedsScreenNavKey\nimport com.zhangke.fread.bluesky.internal.screen.feeds.following.BskyFollowingFeedsPage\nimport com.zhangke.fread.bluesky.internal.screen.feeds.following.BskyFollowingFeedsPageNavKey\nimport com.zhangke.fread.bluesky.internal.screen.feeds.home.HomeFeedsScreen\nimport com.zhangke.fread.bluesky.internal.screen.feeds.home.HomeFeedsScreenNavKey\nimport com.zhangke.fread.bluesky.internal.screen.publish.PublishPostScreen\nimport com.zhangke.fread.bluesky.internal.screen.publish.PublishPostScreenNavKey\nimport com.zhangke.fread.bluesky.internal.screen.search.SearchStatusScreen\nimport com.zhangke.fread.bluesky.internal.screen.search.SearchStatusScreenNavKey\nimport com.zhangke.fread.bluesky.internal.screen.user.detail.BskyUserDetailScreen\nimport com.zhangke.fread.bluesky.internal.screen.user.detail.BskyUserDetailScreenNavKey\nimport com.zhangke.fread.bluesky.internal.screen.user.edit.EditProfileScreen\nimport com.zhangke.fread.bluesky.internal.screen.user.edit.EditProfileScreenNavKey\nimport com.zhangke.fread.bluesky.internal.screen.user.list.UserListScreen\nimport com.zhangke.fread.bluesky.internal.screen.user.list.UserListScreenNavKey\nimport kotlinx.serialization.modules.PolymorphicModuleBuilder\nimport kotlinx.serialization.modules.subclass\nimport org.koin.compose.viewmodel.koinViewModel\nimport org.koin.core.parameter.parameterArrayOf\nimport org.koin.core.parameter.parametersOf\n\nclass BlueskyNavEntryProvider : NavEntryProvider {\n\n    override fun EntryProviderScope<NavKey>.build() {\n        entry<BskyFollowingFeedsPageNavKey> { key ->\n            BskyFollowingFeedsPage(\n                koinViewModel { parametersOf(key.contentId, key.locator) }\n            )\n        }\n        entry<HomeFeedsScreenNavKey> { key ->\n            HomeFeedsScreen(\n                feedsJson = key.feedsJson,\n                locator = key.locator,\n            )\n        }\n        entry<SearchStatusScreenNavKey> {\n            SearchStatusScreen(koinViewModel())\n        }\n        entry<AddBlueskyContentScreenNavKey> { key ->\n            AddBlueskyContentScreen(\n                koinViewModel {\n                    parametersOf(\n                        key.baseUrl,\n                        key.loginMode,\n                        key.avatar,\n                        key.displayName,\n                        key.handle,\n                    )\n                }\n            )\n        }\n        entry<PublishPostScreenNavKey> { key ->\n            PublishPostScreen(\n                koinViewModel {\n                    parametersOf(\n                        key.locator,\n                        key.defaultText,\n                        key.replyToJsonString,\n                        key.quoteJsonString,\n                    )\n                }\n            )\n        }\n        entry<UserListScreenNavKey> { key ->\n            UserListScreen(\n                locator = key.locator,\n                type = key.type,\n                viewModel = koinViewModel {\n                    parameterArrayOf(\n                        key.locator,\n                        key.type,\n                        key.postUri,\n                        key.did,\n                    )\n                },\n            )\n        }\n        entry<BskyUserDetailScreenNavKey> { key ->\n            BskyUserDetailScreen(\n                locator = key.locator,\n                did = key.did,\n                viewModel = koinViewModel { parametersOf(key.locator, key.did) },\n            )\n        }\n        entry<EditProfileScreenNavKey> { key ->\n            EditProfileScreen(\n                koinViewModel { parametersOf(key.locator) }\n            )\n        }\n        entry<ExplorerFeedsScreenNavKey> { key ->\n            ExplorerFeedsScreen(\n                locator = key.locator,\n                inlineMode = false,\n            )\n        }\n    }\n\n    override fun PolymorphicModuleBuilder<NavKey>.polymorph() {\n        subclass(BskyFollowingFeedsPageNavKey::class)\n        subclass(HomeFeedsScreenNavKey::class)\n        subclass(SearchStatusScreenNavKey::class)\n        subclass(AddBlueskyContentScreenNavKey::class)\n        subclass(PublishPostScreenNavKey::class)\n        subclass(UserListScreenNavKey::class)\n        subclass(BskyUserDetailScreenNavKey::class)\n        subclass(EditProfileScreenNavKey::class)\n        subclass(ExplorerFeedsScreenNavKey::class)\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/BlueskyNotificationResolver.kt",
    "content": "package com.zhangke.fread.bluesky\n\nimport app.bsky.actor.GetProfilesQueryParams\nimport app.bsky.notification.ListNotificationsQueryParams\nimport app.bsky.notification.UpdateSeenRequest\nimport com.zhangke.fread.bluesky.internal.account.BlueskyLoggedAccount\nimport com.zhangke.fread.bluesky.internal.adapter.BlueskyAccountAdapter\nimport com.zhangke.fread.bluesky.internal.adapter.BlueskyNotificationAdapter\nimport com.zhangke.fread.bluesky.internal.client.BlueskyClientManager\nimport com.zhangke.fread.bluesky.internal.usecase.GetCompletedNotificationUseCase\nimport com.zhangke.fread.status.account.LoggedAccount\nimport com.zhangke.fread.status.author.BlogAuthor\nimport com.zhangke.fread.status.model.notBluesky\nimport com.zhangke.fread.status.notification.INotificationResolver\nimport com.zhangke.fread.status.notification.PagedStatusNotification\nimport sh.christian.ozone.api.Did\nimport kotlinx.datetime.Clock\nimport kotlin.time.ExperimentalTime\n\nclass BlueskyNotificationResolver(\n    private val clientManager: BlueskyClientManager,\n    private val getCompletedNotification: GetCompletedNotificationUseCase,\n    private val notificationAdapter: BlueskyNotificationAdapter,\n    private val accountAdapter: BlueskyAccountAdapter,\n) : INotificationResolver {\n\n    override suspend fun getNotifications(\n        account: LoggedAccount,\n        type: INotificationResolver.NotificationRequestType,\n        cursor: String?,\n    ): Result<PagedStatusNotification>? {\n        if (account !is BlueskyLoggedAccount) return null\n        return getCompletedNotification(\n            locator = account.locator,\n            params = ListNotificationsQueryParams(\n                reasons = if (type == INotificationResolver.NotificationRequestType.MENTION) {\n                    listOf(\"mention\", \"reply\", \"quote\")\n                } else {\n                    emptyList()\n                },\n                cursor = cursor,\n                limit = 25,\n            ),\n        ).map { paged ->\n            PagedStatusNotification(\n                cursor = paged.cursor,\n                reachEnd = paged.cursor == null,\n                notifications = paged.notifications.map {\n                    notificationAdapter.convert(it, account.locator, account.platform)\n                },\n            )\n        }\n    }\n\n    override suspend fun getNotificationUserDetail(\n        account: LoggedAccount,\n        users: List<BlogAuthor>,\n    ): Result<List<BlogAuthor>>? {\n        if (account !is BlueskyLoggedAccount) return null\n        val didList = users.filter { it.banner.isNullOrEmpty() }\n            .mapNotNull { it.webFinger.did }\n            .map { Did(it) }\n        if (didList.isEmpty()) return Result.success(emptyList())\n        return clientManager.getClient(account.locator)\n            .getProfilesCatching(GetProfilesQueryParams(actors = didList.take(25)))\n            .map { result ->\n                result.profiles.map { profile ->\n                    accountAdapter.convertToBlogAuthor(\n                        did = profile.did.did,\n                        handle = profile.handle.handle,\n                        profileViewDetailed = profile,\n                    )\n                }\n            }\n    }\n\n    override suspend fun rejectFollowRequest(\n        account: LoggedAccount,\n        requestAuthor: BlogAuthor\n    ): Result<Unit>? {\n        return null\n    }\n\n    override suspend fun acceptFollowRequest(\n        account: LoggedAccount,\n        requestAuthor: BlogAuthor\n    ): Result<Unit>? {\n        return null\n    }\n\n    @OptIn(ExperimentalTime::class)\n    override suspend fun updateUnreadNotification(\n        account: LoggedAccount,\n        notificationLastReadId: String\n    ): Result<Unit>? {\n        if (account.platform.protocol.notBluesky) return null\n        if (account !is BlueskyLoggedAccount) return null\n        val client = clientManager.getClient(account.locator)\n        return client.updateSeenCatching(UpdateSeenRequest(Clock.System.now()))\n            .map { }\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/BlueskyProvider.kt",
    "content": "package com.zhangke.fread.bluesky\n\nimport com.zhangke.fread.bluesky.internal.content.BlueskyContentManager\nimport com.zhangke.fread.status.IStatusProvider\nimport com.zhangke.fread.status.account.IAccountManager\nimport com.zhangke.fread.status.content.IContentManager\nimport com.zhangke.fread.status.notification.INotificationResolver\nimport com.zhangke.fread.status.platform.IPlatformResolver\nimport com.zhangke.fread.status.publish.IPublishBlogManager\nimport com.zhangke.fread.status.screen.IStatusScreenProvider\nimport com.zhangke.fread.status.search.ISearchEngine\nimport com.zhangke.fread.status.source.IStatusSourceResolver\nimport com.zhangke.fread.status.status.IStatusResolver\n\nclass BlueskyProvider(\n    blueskyContentManager: BlueskyContentManager,\n    screenProvider: BlueskyScreenProvider,\n    searchEngine: BlueskySearchEngine,\n    statusResolver: BlueskyStatusResolver,\n    statusSourceResolver: BlueskyStatusSourceResolver,\n    accountManager: BlueskyAccountManager,\n    notificationResolver: BlueskyNotificationResolver,\n    blueskyPublishManager: BlueskyPublishManager,\n) : IStatusProvider {\n\n    override val contentManager: IContentManager = blueskyContentManager\n\n    override val screenProvider: IStatusScreenProvider = screenProvider\n\n    override val searchEngine: ISearchEngine = searchEngine\n\n    override val statusResolver: IStatusResolver = statusResolver\n\n    override val statusSourceResolver: IStatusSourceResolver = statusSourceResolver\n\n    override val accountManager: IAccountManager = accountManager\n\n    override val notificationResolver: INotificationResolver = notificationResolver\n\n    override val publishManager: IPublishBlogManager = blueskyPublishManager\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/BlueskyPublishManager.kt",
    "content": "package com.zhangke.fread.bluesky\n\nimport com.zhangke.fread.bluesky.internal.account.BlueskyLoggedAccount\nimport com.zhangke.fread.bluesky.internal.screen.publish.PublishPostMediaAttachment\nimport com.zhangke.fread.bluesky.internal.screen.publish.PublishPostMediaAttachmentFile\nimport com.zhangke.fread.bluesky.internal.usecase.PublishingPostUseCase\nimport com.zhangke.fread.status.account.LoggedAccount\nimport com.zhangke.fread.status.model.PublishBlogRules\nimport com.zhangke.fread.status.model.notBluesky\nimport com.zhangke.fread.status.publish.IPublishBlogManager\nimport com.zhangke.fread.status.publish.PublishingMedia\nimport com.zhangke.fread.status.publish.PublishingPost\n\nclass BlueskyPublishManager (\n    private val publishingPost: PublishingPostUseCase,\n) : IPublishBlogManager {\n\n    override suspend fun getPublishBlogRules(account: LoggedAccount): Result<PublishBlogRules>? {\n        if (account.platform.protocol.notBluesky) return null\n        return Result.success(\n            PublishBlogRules(\n                maxCharacters = 300,\n                maxMediaCount = 4,\n                maxPollOptions = 0,\n                supportSpoiler = false,\n                supportPoll = false,\n                maxLanguageCount = 2,\n                mediaAltMaxCharacters = 2000,\n            )\n        )\n    }\n\n    override suspend fun publish(\n        account: LoggedAccount,\n        post: PublishingPost\n    ): Result<Unit>? {\n        if (account.platform.protocol.notBluesky) return null\n        val bskyAccount = account as BlueskyLoggedAccount\n        return publishingPost(\n            account = bskyAccount,\n            content = post.content,\n            interactionSetting = post.interactionSetting,\n            selectedLanguages = listOf(post.languageCode),\n            attachment = post.medias.convert(),\n        )\n    }\n\n    private fun List<PublishingMedia>.convert(): PublishPostMediaAttachment? {\n        if (this.isEmpty()) return null\n        if (this.first().isVideo) {\n            return PublishPostMediaAttachment.Video(this.first().convert())\n        }\n        return PublishPostMediaAttachment.Image(this.map { it.convert() })\n    }\n\n    private fun PublishingMedia.convert(): PublishPostMediaAttachmentFile {\n        return PublishPostMediaAttachmentFile(\n            file = this.file,\n            alt = this.alt,\n            isVideo = this.isVideo,\n        )\n    }\n}"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/BlueskyScreenProvider.kt",
    "content": "package com.zhangke.fread.bluesky\n\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.architect.json.globalJson\nimport com.zhangke.framework.nav.Tab\nimport com.zhangke.framework.utils.WebFinger\nimport com.zhangke.fread.bluesky.internal.account.BlueskyLoggedAccount\nimport com.zhangke.fread.bluesky.internal.content.BlueskyContent\nimport com.zhangke.fread.bluesky.internal.model.BlueskyFeeds\nimport com.zhangke.fread.bluesky.internal.screen.add.AddBlueskyContentScreenNavKey\nimport com.zhangke.fread.bluesky.internal.screen.explorer.BlueskyExplorerTab\nimport com.zhangke.fread.bluesky.internal.screen.feeds.following.BskyFollowingFeedsPageNavKey\nimport com.zhangke.fread.bluesky.internal.screen.feeds.home.HomeFeedsScreenNavKey\nimport com.zhangke.fread.bluesky.internal.screen.content.BlueskyContentTab\nimport com.zhangke.fread.bluesky.internal.screen.publish.PublishPostScreenNavKey\nimport com.zhangke.fread.bluesky.internal.screen.user.detail.BskyUserDetailScreenNavKey\nimport com.zhangke.fread.bluesky.internal.screen.user.list.UserListScreenNavKey\nimport com.zhangke.fread.bluesky.internal.screen.user.list.UserListType\nimport com.zhangke.fread.bluesky.internal.uri.user.UserUriTransformer\nimport com.zhangke.fread.status.account.LoggedAccount\nimport com.zhangke.fread.status.blog.Blog\nimport com.zhangke.fread.status.model.FreadContent\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.model.StatusProviderProtocol\nimport com.zhangke.fread.status.model.notBluesky\nimport com.zhangke.fread.status.platform.BlogPlatform\nimport com.zhangke.fread.status.screen.IStatusScreenProvider\nimport com.zhangke.fread.status.uri.FormalUri\n\nclass BlueskyScreenProvider(\n    private val userUriTransformer: UserUriTransformer,\n) : IStatusScreenProvider {\n\n    override fun getReplyBlogScreen(\n        locator: PlatformLocator,\n        blog: Blog,\n    ): NavKey? {\n        if (blog.platform.protocol.notBluesky) return null\n        return PublishPostScreenNavKey(\n            locator = locator,\n            replyToJsonString = globalJson.encodeToString(blog)\n        )\n    }\n\n    override fun getEditBlogScreen(\n        locator: PlatformLocator,\n        blog: Blog\n    ): NavKey? {\n        return null\n    }\n\n    override fun getQuoteBlogScreen(locator: PlatformLocator, blog: Blog): NavKey? {\n        if (blog.platform.protocol.notBluesky) return null\n        return PublishPostScreenNavKey(\n            locator = locator,\n            quoteJsonString = globalJson.encodeToString(blog)\n        )\n    }\n\n    override fun getContentScreen(\n        content: FreadContent,\n        isLatestTab: Boolean\n    ): Tab {\n        return BlueskyContentTab(content.id, isLatestTab)\n    }\n\n    override fun getEditContentConfigScreenScreen(content: FreadContent): NavKey? {\n        if (content !is BlueskyContent) return null\n        return BskyFollowingFeedsPageNavKey(contentId = content.id, locator = null)\n    }\n\n    override fun getUserDetailScreen(\n        locator: PlatformLocator,\n        uri: FormalUri,\n        userId: String?,\n    ): NavKey? {\n        val did = userUriTransformer.parse(uri)?.did ?: return null\n        return BskyUserDetailScreenNavKey(locator = locator, did = did)\n    }\n\n    override fun getUserDetailScreen(\n        locator: PlatformLocator,\n        did: String,\n        protocol: StatusProviderProtocol\n    ): NavKey? {\n        if (protocol.notBluesky) return null\n        return BskyUserDetailScreenNavKey(locator = locator, did = did)\n    }\n\n    override fun getUserDetailScreen(\n        locator: PlatformLocator,\n        webFinger: WebFinger,\n        protocol: StatusProviderProtocol\n    ): NavKey? {\n        if (protocol.notBluesky) return null\n        return BskyUserDetailScreenNavKey(locator, webFinger.did ?: return null)\n    }\n\n    override fun getTagTimelineScreen(\n        locator: PlatformLocator,\n        tag: String,\n        protocol: StatusProviderProtocol\n    ): NavKey? {\n        if (protocol.notBluesky) return null\n        return HomeFeedsScreenNavKey.create(\n            feeds = BlueskyFeeds.Hashtags(tag),\n            locator = locator,\n        )\n    }\n\n    override fun getBlogFavouritedScreen(\n        locator: PlatformLocator,\n        blog: Blog,\n        protocol: StatusProviderProtocol\n    ): NavKey? {\n        if (protocol.notBluesky) return null\n        return UserListScreenNavKey(\n            locator = locator,\n            type = UserListType.LIKE,\n            postUri = blog.url,\n        )\n    }\n\n    override fun getBlogBoostedScreen(\n        locator: PlatformLocator,\n        blog: Blog,\n        protocol: StatusProviderProtocol\n    ): NavKey? {\n        if (protocol.notBluesky) return null\n        return UserListScreenNavKey(\n            locator = locator,\n            type = UserListType.REBLOG,\n            postUri = blog.url,\n        )\n    }\n\n    override fun getExplorerTab(locator: PlatformLocator, platform: BlogPlatform): Tab? {\n        if (platform.protocol.notBluesky) return null\n        return BlueskyExplorerTab(locator)\n    }\n\n    override fun getAddContentScreen(protocol: StatusProviderProtocol): NavKey? {\n        if (protocol.notBluesky) return null\n        return AddBlueskyContentScreenNavKey()\n    }\n\n    override fun getPublishScreen(account: LoggedAccount, text: String): NavKey? {\n        if (account !is BlueskyLoggedAccount) return null\n        return PublishPostScreenNavKey(\n            locator = account.locator,\n            defaultText = text,\n        )\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/BlueskySearchEngine.kt",
    "content": "package com.zhangke.fread.bluesky\n\nimport app.bsky.actor.GetProfileQueryParams\nimport app.bsky.actor.SearchActorsQueryParams\nimport app.bsky.feed.SearchPostsQueryParams\nimport com.zhangke.fread.bluesky.internal.account.BlueskyLoggedAccountManager\nimport com.zhangke.fread.bluesky.internal.adapter.BlueskyAccountAdapter\nimport com.zhangke.fread.bluesky.internal.adapter.BlueskyStatusAdapter\nimport com.zhangke.fread.bluesky.internal.client.BlueskyClientManager\nimport com.zhangke.fread.bluesky.internal.repo.BlueskyPlatformRepo\nimport com.zhangke.fread.bluesky.internal.usecase.GetAtIdentifierUseCase\nimport com.zhangke.fread.status.author.BlogAuthor\nimport com.zhangke.fread.status.model.Hashtag\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.model.StatusUiState\nimport com.zhangke.fread.status.platform.BlogPlatform\nimport com.zhangke.fread.status.search.ISearchEngine\nimport com.zhangke.fread.status.search.SearchContentResult\nimport com.zhangke.fread.status.search.SearchResult\nimport com.zhangke.fread.status.source.StatusSource\nimport kotlinx.coroutines.async\nimport kotlinx.coroutines.supervisorScope\n\nclass BlueskySearchEngine(\n    private val clientManager: BlueskyClientManager,\n    private val accountAdapter: BlueskyAccountAdapter,\n    private val getAtIdentifier: GetAtIdentifierUseCase,\n    private val blueskyPlatformRepo: BlueskyPlatformRepo,\n    private val statusAdapter: BlueskyStatusAdapter,\n    private val platformRepo: BlueskyPlatformRepo,\n    private val accountManager: BlueskyLoggedAccountManager,\n) : ISearchEngine {\n\n    override suspend fun search(\n        locator: PlatformLocator,\n        query: String,\n    ): Result<List<SearchResult>> {\n        return supervisorScope {\n            val postsDeferred = async { searchStatus(locator, query, null) }\n            val actorsDeferred = async { searchAuthor(locator, query, null) }\n            val postsResult = postsDeferred.await()\n            val actorResult = actorsDeferred.await()\n            if (postsResult.isFailure && actorResult.isFailure) {\n                Result.failure(\n                    postsResult.exceptionOrNull() ?: actorResult.exceptionOrNull()!!\n                )\n            } else {\n                val status: List<SearchResult> = postsResult.getOrNull()\n                    ?.map { SearchResult.SearchedStatus(it) } ?: emptyList()\n                val actors: List<SearchResult> = actorResult.getOrNull()\n                    ?.map { actor -> SearchResult.Author(actor) } ?: emptyList()\n                Result.success(actors + status)\n            }\n        }\n    }\n\n    override suspend fun searchStatus(\n        locator: PlatformLocator,\n        query: String,\n        maxId: String?,\n    ): Result<List<StatusUiState>> {\n        val client = clientManager.getClient(locator)\n        val account = client.loggedAccountProvider()\n        val platform = platformRepo.getPlatform(client.baseUrl)\n        return client.searchPostsCatching(SearchPostsQueryParams(q = query))\n            .map { result ->\n                result.posts.map {\n                    statusAdapter.convertToUiState(\n                        locator = locator,\n                        postView = it,\n                        platform = platform,\n                        loggedAccount = account,\n                    )\n                }\n            }\n    }\n\n    override suspend fun searchHashtag(\n        locator: PlatformLocator,\n        query: String,\n        offset: Int?,\n    ): Result<List<Hashtag>> {\n        return Result.success(emptyList())\n    }\n\n    override suspend fun searchAuthor(\n        locator: PlatformLocator,\n        query: String,\n        offset: Int?,\n    ): Result<List<BlogAuthor>> {\n        val client = clientManager.getClient(locator)\n        return client.searchActorsCatching(SearchActorsQueryParams(q = query))\n            .map { result ->\n                result.actors.map { accountAdapter.convertToBlogAuthor(it) }\n            }\n    }\n\n    override suspend fun searchSourceNoToken(query: String): Result<List<StatusSource>> {\n        val client = clientManager.getClient(getDefaultPlatformLocator())\n        val identifier = getAtIdentifier(query)\n            ?: return client.searchActorsCatching(SearchActorsQueryParams(q = query, limit = 10))\n                .map { result ->\n                    result.actors.map { accountAdapter.createSource(it) }\n                }\n        return client.getProfileCatching(GetProfileQueryParams(identifier))\n            .map { profile -> listOf(accountAdapter.createSource(profile)) }\n    }\n\n    private suspend fun getDefaultPlatformLocator(): PlatformLocator {\n        val baseUrl = accountManager.getAllAccount().firstOrNull()?.platform?.baseUrl\n            ?: blueskyPlatformRepo.getAllPlatform().first().baseUrl\n        return PlatformLocator(baseUrl = baseUrl)\n    }\n\n    private fun BlogPlatform.compareWithQuery(query: String): Boolean {\n        if (this.name.contains(query)) return true\n        if (this.baseUrl.toString().contains(query)) return true\n        return uri.contains(query)\n    }\n\n    private fun BlogPlatform.toContentResult(): SearchContentResult {\n        return SearchContentResult.Platform(this)\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/BlueskyStatusResolver.kt",
    "content": "package com.zhangke.fread.bluesky\n\nimport app.bsky.actor.GetProfileQueryParams\nimport app.bsky.feed.GetAuthorFeedFilter\nimport app.bsky.feed.GetAuthorFeedQueryParams\nimport app.bsky.feed.GetPostsQueryParams\nimport com.zhangke.fread.bluesky.internal.adapter.BlueskyStatusAdapter\nimport com.zhangke.fread.bluesky.internal.client.BlueskyClientManager\nimport com.zhangke.fread.bluesky.internal.repo.BlueskyPlatformRepo\nimport com.zhangke.fread.bluesky.internal.uri.user.UserUriTransformer\nimport com.zhangke.fread.bluesky.internal.usecase.BskyStatusInteractiveUseCase\nimport com.zhangke.fread.bluesky.internal.usecase.GetStatusContextUseCase\nimport com.zhangke.fread.bluesky.internal.usecase.UpdateRelationshipType\nimport com.zhangke.fread.bluesky.internal.usecase.UpdateRelationshipUseCase\nimport com.zhangke.fread.status.author.BlogAuthor\nimport com.zhangke.fread.status.blog.Blog\nimport com.zhangke.fread.status.blog.BlogPoll\nimport com.zhangke.fread.status.blog.BlogTranslation\nimport com.zhangke.fread.status.model.PagedData\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.model.StatusActionType\nimport com.zhangke.fread.status.model.StatusUiState\nimport com.zhangke.fread.status.model.notBluesky\nimport com.zhangke.fread.status.platform.BlogPlatform\nimport com.zhangke.fread.status.status.IStatusResolver\nimport com.zhangke.fread.status.status.model.Status\nimport com.zhangke.fread.status.status.model.StatusContext\nimport com.zhangke.fread.status.uri.FormalUri\nimport sh.christian.ozone.api.AtUri\nimport sh.christian.ozone.api.Did\n\nclass BlueskyStatusResolver(\n    private val clientManager: BlueskyClientManager,\n    private val statusAdapter: BlueskyStatusAdapter,\n    private val platformRepo: BlueskyPlatformRepo,\n    private val uriTransformer: UserUriTransformer,\n    private val updateRelationship: UpdateRelationshipUseCase,\n    private val statusInteractive: BskyStatusInteractiveUseCase,\n    private val getStatusContextFunction: GetStatusContextUseCase,\n    private val userUriTransformer: UserUriTransformer,\n) : IStatusResolver {\n\n    override suspend fun getStatus(\n        locator: PlatformLocator,\n        blogId: String?,\n        blogUri: String?,\n        platform: BlogPlatform\n    ): Result<StatusUiState>? {\n        if (platform.protocol.notBluesky) return null\n        val client = clientManager.getClient(locator)\n        val account = client.loggedAccountProvider()\n        if (blogUri.isNullOrEmpty()) return Result.failure(IllegalArgumentException(\"blogUri is null!\"))\n        return client.getPostsCatching(GetPostsQueryParams(listOf(AtUri(blogUri))))\n            .map {\n                statusAdapter.convertToUiState(\n                    locator = locator,\n                    postView = it.posts.first(),\n                    platform = platform,\n                    loggedAccount = account,\n                )\n            }\n    }\n\n    override suspend fun getStatusList(\n        uri: FormalUri,\n        limit: Int,\n        maxId: String?,\n    ): Result<PagedData<StatusUiState>>? {\n        val uriInsight = uriTransformer.parse(uri) ?: return null\n        val platform = platformRepo.getAllPlatform().first()\n        val locator = PlatformLocator(baseUrl = platform.baseUrl)\n        val client = clientManager.getClient(locator)\n        val account = client.loggedAccountProvider()\n        return client.getAuthorFeedCatching(\n            GetAuthorFeedQueryParams(\n                actor = Did(uriInsight.did),\n                filter = GetAuthorFeedFilter.PostsAndAuthorThreads,\n                includePins = true,\n                limit = 80,\n            )\n        ).map { result ->\n            PagedData(\n                list = result.feed.map {\n                    statusAdapter.convertToUiState(\n                        locator = locator,\n                        feedViewPost = it,\n                        platform = platform,\n                        loggedAccount = account,\n                    )\n                },\n                cursor = result.cursor,\n            )\n        }\n    }\n\n    override suspend fun interactive(\n        locator: PlatformLocator,\n        status: Status,\n        type: StatusActionType,\n    ): Result<Status?>? {\n        if (status.platform.protocol.notBluesky) return null\n        return statusInteractive(locator, status, type)\n    }\n\n    override suspend fun votePoll(\n        locator: PlatformLocator,\n        blog: Blog,\n        votedOption: List<BlogPoll.Option>\n    ): Result<Status>? {\n        return null\n    }\n\n    override suspend fun getStatusContext(\n        locator: PlatformLocator,\n        status: Status\n    ): Result<StatusContext>? {\n        if (status.platform.protocol.notBluesky) return null\n        return getStatusContextFunction(locator, status)\n    }\n\n    override suspend fun follow(\n        locator: PlatformLocator,\n        target: BlogAuthor\n    ): Result<Unit>? {\n        val userUriInsights = userUriTransformer.parse(target.uri) ?: return null\n        return updateRelationship(\n            locator = locator,\n            targetDid = userUriInsights.did,\n            type = UpdateRelationshipType.FOLLOW,\n        ).map { }\n    }\n\n    override suspend fun unfollow(\n        locator: PlatformLocator,\n        target: BlogAuthor\n    ): Result<Unit>? {\n        val userUriInsights = userUriTransformer.parse(target.uri) ?: return null\n        return updateRelationship(\n            locator = locator,\n            targetDid = userUriInsights.did,\n            type = UpdateRelationshipType.UNFOLLOW,\n        ).map { }\n    }\n\n    override suspend fun isFollowing(\n        locator: PlatformLocator,\n        target: BlogAuthor\n    ): Result<Boolean>? {\n        val did = userUriTransformer.parse(target.uri)?.did ?: return null\n        val client = clientManager.getClient(locator)\n        val profileResult = client.getProfileCatching(GetProfileQueryParams(Did(did)))\n        if (profileResult.isFailure) return Result.failure(profileResult.exceptionOrNull()!!)\n        val profile = profileResult.getOrThrow()\n        return Result.success(profile.viewer?.following?.atUri.isNullOrEmpty().not())\n    }\n\n    override suspend fun translate(\n        locator: PlatformLocator,\n        status: Status,\n        lan: String\n    ): Result<BlogTranslation>? {\n        if (status.platform.protocol.notBluesky) return null\n        return Result.failure(RuntimeException(\"Not implemented\"))\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/BlueskyStatusSourceResolver.kt",
    "content": "package com.zhangke.fread.bluesky\n\nimport app.bsky.actor.GetProfileQueryParams\nimport com.zhangke.fread.bluesky.internal.adapter.BlueskyAccountAdapter\nimport com.zhangke.fread.bluesky.internal.client.BlueskyClientManager\nimport com.zhangke.fread.bluesky.internal.repo.BlueskyPlatformRepo\nimport com.zhangke.fread.bluesky.internal.uri.user.UserUriTransformer\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.source.IStatusSourceResolver\nimport com.zhangke.fread.status.source.StatusSource\nimport com.zhangke.fread.status.uri.FormalUri\nimport sh.christian.ozone.api.Did\n\nclass BlueskyStatusSourceResolver(\n    private val clientManager: BlueskyClientManager,\n    private val platformRepo: BlueskyPlatformRepo,\n    private val accountAdapter: BlueskyAccountAdapter,\n    private val userUriTransformer: UserUriTransformer,\n) : IStatusSourceResolver {\n\n    override suspend fun resolveSourceByUri(uri: FormalUri): Result<StatusSource?> {\n        val uriInsight = userUriTransformer.parse(uri) ?: return Result.success(null)\n        val locator = PlatformLocator(baseUrl = platformRepo.getAllPlatform().first().baseUrl)\n        val client = clientManager.getClient(locator)\n        return client.getProfileCatching(GetProfileQueryParams(Did(uriInsight.did)))\n            .map { profile -> accountAdapter.createSource(profile) }\n    }\n\n    override suspend fun resolveRssSource(rssUrl: String): Result<StatusSource>? {\n        return null\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/BskyStartup.kt",
    "content": "package com.zhangke.fread.bluesky\n\nimport com.zhangke.framework.architect.coroutines.ApplicationScope\nimport com.zhangke.framework.module.ModuleStartup\nimport com.zhangke.fread.bluesky.internal.migrate.BlueskyContentMigrator\nimport com.zhangke.fread.bluesky.internal.usecase.RefreshSessionUseCase\nimport kotlinx.coroutines.launch\n\nclass BskyStartup(\n    private val refreshSession: RefreshSessionUseCase,\n    private val contentMigrator: BlueskyContentMigrator,\n) : ModuleStartup {\n\n    override fun onAppCreate() {\n        ApplicationScope.launch {\n            contentMigrator.migrate()\n            refreshSession()\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/BskyUrlInterceptor.kt",
    "content": "package com.zhangke.fread.bluesky\n\nimport androidx.navigation3.runtime.NavKey\nimport app.bsky.actor.GetProfileQueryParams\nimport app.bsky.feed.GetPostThreadQueryParams\nimport app.bsky.feed.GetPostThreadResponseThreadUnion\nimport com.atproto.repo.GetRecordQueryParams\nimport com.zhangke.framework.network.FormalBaseUrl\nimport com.zhangke.framework.network.HttpScheme\nimport com.zhangke.framework.network.SimpleUri\nimport com.zhangke.framework.network.addProtocolSuffixIfNecessary\nimport com.zhangke.fread.bluesky.internal.account.BlueskyLoggedAccount\nimport com.zhangke.fread.bluesky.internal.account.BlueskyLoggedAccountManager\nimport com.zhangke.fread.bluesky.internal.adapter.BlueskyStatusAdapter\nimport com.zhangke.fread.bluesky.internal.client.BlueskyClientManager\nimport com.zhangke.fread.bluesky.internal.client.BskyCollections\nimport com.zhangke.fread.bluesky.internal.content.BlueskyContent\nimport com.zhangke.fread.bluesky.internal.repo.BlueskyPlatformRepo\nimport com.zhangke.fread.bluesky.internal.screen.user.detail.BskyUserDetailScreenNavKey\nimport com.zhangke.fread.common.browser.BrowserInterceptor\nimport com.zhangke.fread.common.browser.InterceptorResult\nimport com.zhangke.fread.common.content.FreadContentRepo\nimport com.zhangke.fread.commonbiz.shared.screen.status.context.StatusContextScreenNavKey\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.model.createBlueskyProtocol\nimport sh.christian.ozone.api.Handle\nimport sh.christian.ozone.api.RKey\n\nclass BskyUrlInterceptor(\n    private val accountManager: BlueskyLoggedAccountManager,\n    private val clientManager: BlueskyClientManager,\n    private val statusAdapter: BlueskyStatusAdapter,\n    private val platformRepo: BlueskyPlatformRepo,\n    private val contentRepo: FreadContentRepo,\n) : BrowserInterceptor {\n\n    override suspend fun intercept(\n        locator: PlatformLocator?,\n        url: String,\n        isFromExternal: Boolean,\n    ): InterceptorResult {\n        var uri = SimpleUri.parse(url) ?: return InterceptorResult.CanNotIntercept\n        if (!HttpScheme.validate(uri.scheme.orEmpty().addProtocolSuffixIfNecessary())) {\n            return InterceptorResult.CanNotIntercept\n        }\n        if (uri.host.isNullOrEmpty()) return InterceptorResult.CanNotIntercept\n        val isProfileUrl = isProfileUrl(uri)\n        val isPostUrl = isPostUrl(uri)\n        if (!isProfileUrl && !isPostUrl) {\n            if (isFromExternal) {\n                if (platformRepo.appViewDomains.any { uri.host == it }) {\n                    val content = contentRepo.getAllContent()\n                        .firstNotNullOfOrNull { it as? BlueskyContent }\n                    if (content != null) {\n                        return InterceptorResult.SwitchHomeContent(content)\n                    }\n                }\n            }\n            return InterceptorResult.CanNotIntercept\n        }\n        uri = uri.copy(host = platformRepo.mapAppToBackendDomain(uri.host!!))\n        val baseUrl = locator?.baseUrl ?: FormalBaseUrl.parse(uri.host!!)\n        ?: return InterceptorResult.CanNotIntercept\n        var account: BlueskyLoggedAccount? = null\n        val fixedLocator = if (locator?.accountUri != null) {\n            locator\n        } else {\n            val accounts = accountManager.getAllAccount()\n                .filter { it.fromPlatform.baseUrl == baseUrl }\n            if (accounts.isEmpty()) {\n                locator ?: PlatformLocator(baseUrl)\n            } else if (accounts.size == 1) {\n                account = accounts.first()\n                PlatformLocator(account.platform.baseUrl, account.uri)\n            } else {\n                return InterceptorResult.RequireSelectAccount(createBlueskyProtocol())\n            }\n        }\n        parseProfile(fixedLocator, uri)?.let {\n            return InterceptorResult.SuccessWithOpenNewScreen(it)\n        }\n        parsePost(fixedLocator, uri, account)?.let {\n            return InterceptorResult.SuccessWithOpenNewScreen(it)\n        }\n        return InterceptorResult.CanNotIntercept\n    }\n\n    private fun isProfileUrl(uri: SimpleUri): Boolean {\n        val path = uri.path\n        if (path.isNullOrEmpty()) return false\n        if (!path.startsWith(\"/profile/\")) return false\n        val handle = path.split('/').lastOrNull()\n        if (handle.isNullOrEmpty()) return false\n        if (!handle.contains('.')) return false\n        return true\n    }\n\n    private suspend fun parseProfile(locator: PlatformLocator, uri: SimpleUri): NavKey? {\n        if (!isProfileUrl(uri)) return null\n        val handle = uri.path?.split('/')?.lastOrNull()\n        if (handle.isNullOrEmpty()) return null\n        val profile = clientManager.getClient(locator)\n            .getProfileCatching(GetProfileQueryParams(Handle(handle)))\n            .getOrNull()\n        if (profile == null) return null\n        return BskyUserDetailScreenNavKey(locator = locator, did = profile.did.did)\n    }\n\n    private fun isPostUrl(uri: SimpleUri): Boolean {\n        val path = uri.path\n        if (path.isNullOrEmpty()) return false\n        val groupedPath = path.removePrefix(\"/\").removeSuffix(\"/\").split('/')\n        if (groupedPath.size != 4) return false\n        if (groupedPath[0] != \"profile\") return false\n        if (groupedPath[2] != \"post\") return false\n        return true\n    }\n\n    private suspend fun parsePost(\n        locator: PlatformLocator,\n        uri: SimpleUri,\n        account: BlueskyLoggedAccount?\n    ): NavKey? {\n        if (!isPostUrl(uri)) return null\n        val groupedPath = uri.path\n            ?.removePrefix(\"/\")\n            ?.removeSuffix(\"/\")\n            ?.split('/')\n            ?: return null\n        val client = clientManager.getClient(locator)\n        val statusUiState = client.getRecordCatching(\n            GetRecordQueryParams(\n                repo = Handle(groupedPath[1]),\n                collection = BskyCollections.feedPost,\n                rkey = RKey(groupedPath[3]),\n            )\n        ).mapCatching { value ->\n            client.getPostThreadCatching(GetPostThreadQueryParams(uri = value.uri))\n                .map { (it.thread as? GetPostThreadResponseThreadUnion.ThreadViewPost)?.value?.post }\n                .getOrNull()\n        }.mapCatching { postView ->\n            if (postView != null) {\n                statusAdapter.convertToUiState(\n                    postView = postView,\n                    locator = locator,\n                    platform = platformRepo.getPlatform(locator.baseUrl),\n                    loggedAccount = account,\n                    pinned = false,\n                )\n            } else {\n                null\n            }\n        }.getOrNull() ?: return null\n        return StatusContextScreenNavKey.create(statusUiState)\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/account/BlueskyLoggedAccount.kt",
    "content": "package com.zhangke.fread.bluesky.internal.account\n\nimport com.zhangke.framework.datetime.Instant\nimport com.zhangke.fread.status.account.LoggedAccount\nimport com.zhangke.fread.status.author.BlogAuthor\nimport com.zhangke.fread.status.platform.BlogPlatform\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.json.JsonObject\n\n@Serializable\ndata class BlueskyLoggedAccount(\n    val user: BlogAuthor,\n    val fromPlatform: BlogPlatform,\n    val did: String,\n    val didDoc: JsonObject,\n    val handle: String,\n    val email: String?,\n    val emailConfirmed: Boolean?,\n    val emailAuthFactor: Boolean?,\n    val accessJwt: String,\n    val refreshJwt: String,\n    val active: Boolean?,\n    val createAt: Instant? = null,\n) : LoggedAccount {\n\n    override val uri = user.uri\n\n    override val webFinger = user.webFinger\n\n    override val platform = fromPlatform\n\n    override val id: String = did\n\n    override val userName = user.name\n\n    override val description = user.description\n\n    override val avatar = user.avatar\n\n    override val emojis = user.emojis\n\n    override val prettyHandle: String = user.prettyHandle\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/account/BlueskyLoggedAccountManager.kt",
    "content": "package com.zhangke.fread.bluesky.internal.account\n\nimport app.bsky.actor.GetProfileQueryParams\nimport app.bsky.actor.ProfileViewDetailed\nimport com.atproto.server.CreateSessionRequest\nimport com.atproto.server.CreateSessionResponse\nimport com.zhangke.framework.network.FormalBaseUrl\nimport com.zhangke.framework.utils.exceptionOrThrow\nimport com.zhangke.fread.bluesky.internal.adapter.BlueskyAccountAdapter\nimport com.zhangke.fread.bluesky.internal.client.BlueskyClient\nimport com.zhangke.fread.bluesky.internal.client.BlueskyClientManager\nimport com.zhangke.fread.bluesky.internal.repo.BlueskyLoggedAccountRepo\nimport com.zhangke.fread.bluesky.internal.repo.BlueskyPlatformRepo\nimport com.zhangke.fread.status.account.AccountRefreshResult\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.platform.BlogPlatform\nimport com.zhangke.fread.status.uri.FormalUri\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.mapNotNull\nimport sh.christian.ozone.api.Did\n\nclass BlueskyLoggedAccountManager(\n    private val clientManager: BlueskyClientManager,\n    private val accountAdapter: BlueskyAccountAdapter,\n    private val platformRepo: BlueskyPlatformRepo,\n    private val accountRepo: BlueskyLoggedAccountRepo,\n) {\n\n    suspend fun login(\n        baseUrl: FormalBaseUrl,\n        identifier: String,\n        password: String,\n        factorToken: String? = null,\n    ): Result<BlueskyLoggedAccount> {\n        val noAccountClient = clientManager.getClientNoAccount(baseUrl)\n        val sessionResult =\n            noAccountClient.createSessionCatching(\n                CreateSessionRequest(\n                    identifier = identifier,\n                    password = password,\n                    authFactorToken = factorToken,\n                )\n            )\n        if (sessionResult.isFailure) {\n            return Result.failure(sessionResult.exceptionOrThrow())\n        }\n        val session = sessionResult.getOrThrow()\n        val platform = platformRepo.getPlatform(baseUrl)\n        val account = saveAccountToLocal(session, null, platform)\n        val locator = PlatformLocator(baseUrl = baseUrl, accountUri = account.uri)\n        val profileResult = clientManager.getClient(locator).getProfile(session.did.did)\n        if (profileResult.isFailure) {\n            return Result.failure(profileResult.exceptionOrThrow())\n        }\n        return Result.success(saveAccountToLocal(session, profileResult.getOrThrow(), platform))\n    }\n\n    private suspend fun saveAccountToLocal(\n        session: CreateSessionResponse,\n        profile: ProfileViewDetailed?,\n        platform: BlogPlatform,\n    ): BlueskyLoggedAccount {\n        val loggedAccount = accountAdapter.createBlueskyAccount(\n            profileViewDetailed = profile,\n            createSessionResponse = session,\n            platform = platform,\n        )\n        accountRepo.insert(loggedAccount)\n        return loggedAccount\n    }\n\n    suspend fun logout(uri: FormalUri) {\n        accountRepo.deleteByUri(uri.toString())\n    }\n\n    suspend fun getAllAccount(): List<BlueskyLoggedAccount> {\n        return accountRepo.queryAll()\n    }\n\n    fun getAllAccountFlow(): Flow<List<BlueskyLoggedAccount>> {\n        return accountRepo.queryAllFlow()\n    }\n\n    fun getAccountFlow(uri: FormalUri): Flow<BlueskyLoggedAccount> {\n        return getAllAccountFlow().mapNotNull { it.firstOrNull { it.uri == uri } }\n    }\n\n    suspend fun getAccount(locator: PlatformLocator): BlueskyLoggedAccount? {\n        val allAccount = getAllAccount()\n        if (locator.accountUri != null) {\n            allAccount.firstOrNull { it.uri == locator.accountUri }?.let { return it }\n        }\n        return allAccount.firstOrNull()\n    }\n\n    suspend fun updateAccountProfile(\n        locator: PlatformLocator,\n        profile: ProfileViewDetailed,\n    ) {\n        val account = getAccount(locator) ?: return\n        if (account.did != profile.did.did) return\n        val newAccount = accountAdapter.updateProfile(account, profile)\n        accountRepo.updateAccount(account, newAccount)\n    }\n\n    suspend fun refreshAccountProfile(): List<AccountRefreshResult> {\n        return accountRepo.queryAll().map { account ->\n            val locator =\n                PlatformLocator(accountUri = account.uri, baseUrl = account.platform.baseUrl)\n            val result = clientManager.getClient(locator = locator).getProfile(account.did)\n            if (result.isFailure) {\n                AccountRefreshResult.Failure(account, result.exceptionOrThrow())\n            } else {\n                val newAccount = accountAdapter.updateProfile(account, result.getOrThrow())\n                accountRepo.updateAccount(account, newAccount)\n                AccountRefreshResult.Success(newAccount)\n            }\n        }\n    }\n\n    private suspend fun BlueskyClient.getProfile(did: String): Result<ProfileViewDetailed> {\n        return this.getProfileCatching(GetProfileQueryParams(Did(did)))\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/adapter/BlueskyAccountAdapter.kt",
    "content": "package com.zhangke.fread.bluesky.internal.adapter\n\nimport app.bsky.actor.ProfileView\nimport app.bsky.actor.ProfileViewBasic\nimport app.bsky.actor.ProfileViewDetailed\nimport app.bsky.actor.ViewerState\nimport com.atproto.server.CreateSessionResponse\nimport com.atproto.server.RefreshSessionResponse\nimport com.zhangke.framework.architect.json.Empty\nimport com.zhangke.framework.datetime.Instant\nimport com.zhangke.framework.utils.WebFinger\nimport com.zhangke.framework.utils.prettyHandle\nimport com.zhangke.fread.status.model.createBlueskyProtocol\nimport com.zhangke.fread.bluesky.internal.account.BlueskyLoggedAccount\nimport com.zhangke.fread.bluesky.internal.uri.user.UserUriTransformer\nimport com.zhangke.fread.bluesky.internal.utils.bskyJson\nimport com.zhangke.fread.status.author.BlogAuthor\nimport com.zhangke.fread.status.model.Relationships\nimport com.zhangke.fread.status.platform.BlogPlatform\nimport com.zhangke.fread.status.source.StatusSource\nimport kotlinx.serialization.json.JsonObject\nimport kotlinx.serialization.json.jsonObject\nimport sh.christian.ozone.api.model.JsonContent\nimport kotlin.time.ExperimentalTime\n\nclass BlueskyAccountAdapter(\n    private val userUriTransformer: UserUriTransformer,\n) {\n\n    @OptIn(ExperimentalTime::class)\n    fun createBlueskyAccount(\n        profileViewDetailed: ProfileViewDetailed?,\n        createSessionResponse: CreateSessionResponse,\n        platform: BlogPlatform,\n    ): BlueskyLoggedAccount {\n        val did = createSessionResponse.did.did\n        val author =\n            convertToBlogAuthor(did, createSessionResponse.handle.handle, profileViewDetailed)\n        return BlueskyLoggedAccount(\n            user = author,\n            fromPlatform = platform,\n            did = did,\n            didDoc = createSessionResponse.didDoc?.tryToJsonObject()\n                ?: JsonObject.Empty,\n            handle = createSessionResponse.handle.handle,\n            email = createSessionResponse.email,\n            emailConfirmed = createSessionResponse.emailConfirmed,\n            emailAuthFactor = createSessionResponse.emailAuthFactor,\n            accessJwt = createSessionResponse.accessJwt,\n            refreshJwt = createSessionResponse.refreshJwt,\n            active = createSessionResponse.active,\n            createAt = profileViewDetailed?.createdAt?.let { Instant(it) },\n        )\n    }\n\n    fun convertToBlogAuthor(\n        did: String,\n        handle: String,\n        profileViewDetailed: ProfileViewDetailed?,\n    ): BlogAuthor {\n        return BlogAuthor(\n            uri = userUriTransformer.createUserUri(did),\n            webFinger = WebFinger.createFromDid(did),\n            handle = handle,\n            name = profileViewDetailed?.displayName.orEmpty(),\n            avatar = profileViewDetailed?.avatar?.uri,\n            banner = profileViewDetailed?.banner?.uri,\n            description = profileViewDetailed?.description.orEmpty(),\n            emojis = emptyList(),\n            followersCount = profileViewDetailed?.followersCount,\n            followingCount = profileViewDetailed?.followsCount,\n            statusesCount = profileViewDetailed?.postsCount,\n            relationships = profileViewDetailed?.viewer?.let(::convertRelationship)\n        )\n    }\n\n    fun convertToBlogAuthor(profile: ProfileViewBasic): BlogAuthor {\n        val did = profile.did.did\n        return BlogAuthor(\n            uri = userUriTransformer.createUserUri(did),\n            webFinger = WebFinger.createFromDid(did),\n            handle = profile.handle.handle,\n            name = profile.displayName.orEmpty(),\n            description = \"\",\n            avatar = profile.avatar?.uri,\n            banner = null,\n            emojis = emptyList(),\n            followersCount = null,\n            followingCount = null,\n            statusesCount = null,\n            relationships = profile.viewer?.let(::convertRelationship)\n        )\n    }\n\n    fun convertToBlogAuthor(\n        profile: ProfileView\n    ): BlogAuthor {\n        return BlogAuthor(\n            uri = userUriTransformer.createUserUri(profile.did.did),\n            webFinger = WebFinger.createFromDid(profile.did.did),\n            handle = profile.handle.handle,\n            name = profile.displayName.orEmpty(),\n            avatar = profile.avatar?.uri,\n            banner = null,\n            description = profile.description.orEmpty(),\n            emojis = emptyList(),\n            followersCount = null,\n            followingCount = null,\n            statusesCount = null,\n            relationships = profile.viewer?.let(::convertRelationship)\n        )\n    }\n\n    fun createSource(\n        profile: ProfileViewDetailed,\n    ): StatusSource {\n        return StatusSource(\n            uri = userUriTransformer.createUserUri(profile.did.did),\n            name = profile.displayName.orEmpty(),\n            handle = profile.handle.handle.prettyHandle(),\n            description = profile.description.orEmpty(),\n            protocol = createBlueskyProtocol(),\n            thumbnail = profile.avatar?.uri,\n        )\n    }\n\n    fun createSource(\n        profile: ProfileView,\n    ): StatusSource {\n        return StatusSource(\n            uri = userUriTransformer.createUserUri(profile.did.did),\n            name = profile.displayName.orEmpty(),\n            handle = profile.handle.handle.prettyHandle(),\n            description = profile.description.orEmpty(),\n            protocol = createBlueskyProtocol(),\n            thumbnail = profile.avatar?.uri,\n        )\n    }\n\n    fun updateNewSession(\n        account: BlueskyLoggedAccount,\n        session: RefreshSessionResponse,\n    ): BlueskyLoggedAccount {\n        val newDid = session.did.did\n        return account.copy(\n            user = account.user.copy(\n                uri = userUriTransformer.createUserUri(newDid),\n                webFinger = WebFinger.createFromDid(newDid),\n            ),\n            accessJwt = session.accessJwt,\n            refreshJwt = session.refreshJwt,\n            handle = session.handle.handle,\n            did = session.did.did,\n            didDoc = session.didDoc?.tryToJsonObject() ?: JsonObject.Empty,\n            active = session.active,\n        )\n    }\n\n    @OptIn(ExperimentalTime::class)\n    fun updateProfile(\n        account: BlueskyLoggedAccount,\n        profile: ProfileViewDetailed,\n    ): BlueskyLoggedAccount {\n        val newDid = profile.did.did\n        return account.copy(\n            user = convertToBlogAuthor(newDid, profile.handle.handle, profile),\n            handle = profile.handle.handle,\n            did = newDid,\n            createAt = profile.createdAt?.let { Instant(it) },\n        )\n    }\n\n    private fun JsonContent.tryToJsonObject(): JsonObject? {\n        return bskyJson.encodeToJsonElement(JsonContent.serializer(), this)\n            .takeIf { it is JsonObject }?.jsonObject\n    }\n\n    fun convertRelationship(viewerState: ViewerState): Relationships {\n        return Relationships(\n            following = viewerState.following?.atUri != null,\n            followedBy = viewerState.followedBy?.atUri != null,\n            blocking = viewerState.blocking?.atUri != null,\n            blockedBy = viewerState.blockedBy == true,\n            muting = viewerState.muted == true,\n            requested = null,\n            requestedBy = null,\n        )\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/adapter/BlueskyFeedsAdapter.kt",
    "content": "package com.zhangke.fread.bluesky.internal.adapter\n\nimport app.bsky.actor.SavedFeed\nimport app.bsky.feed.GeneratorView\nimport app.bsky.graph.ListView\nimport com.zhangke.fread.bluesky.internal.model.BlueskyFeeds\n\nclass BlueskyFeedsAdapter(\n    private val profileAdapter: BlueskyProfileAdapter,\n) {\n\n    fun convertToFeeds(\n        savedFeed: SavedFeed,\n        generator: GeneratorView,\n    ): BlueskyFeeds.Feeds {\n        return BlueskyFeeds.Feeds(\n            uri = generator.uri.atUri,\n            cid = generator.cid.cid,\n            did = generator.did.did,\n            pinned = savedFeed.pinned,\n            displayName = generator.displayName,\n            description = generator.description,\n            avatar = generator.avatar?.uri,\n            likeCount = generator.likeCount,\n            likedRecord = generator.viewer?.like?.atUri,\n            creator = profileAdapter.convertToProfile(generator.creator),\n        )\n    }\n\n    fun convertToFeeds(\n        generator: GeneratorView,\n        pinned: Boolean,\n    ): BlueskyFeeds.Feeds {\n        return BlueskyFeeds.Feeds(\n            uri = generator.uri.atUri,\n            cid = generator.cid.cid,\n            did = generator.did.did,\n            pinned = pinned,\n            displayName = generator.displayName,\n            description = generator.description,\n            avatar = generator.avatar?.uri,\n            likeCount = generator.likeCount,\n            likedRecord = generator.viewer?.like?.atUri,\n            creator = profileAdapter.convertToProfile(generator.creator),\n        )\n    }\n\n    fun convertToList(\n        feed: SavedFeed,\n        listView: ListView,\n    ): BlueskyFeeds.List {\n        return BlueskyFeeds.List(\n            id = feed.id,\n            uri = feed.value,\n            name = listView.name,\n            description = listView.description,\n            avatar = listView.avatar?.uri,\n            pinned = feed.pinned,\n        )\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/adapter/BlueskyNotificationAdapter.kt",
    "content": "package com.zhangke.fread.bluesky.internal.adapter\n\nimport com.zhangke.fread.bluesky.internal.model.CompletedBskyNotification\nimport com.zhangke.fread.bluesky.internal.utils.bskyJson\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.notification.StatusNotification\nimport com.zhangke.fread.status.platform.BlogPlatform\nimport com.zhangke.fread.status.status.model.Status\n\nclass BlueskyNotificationAdapter(\n    private val accountAdapter: BlueskyAccountAdapter,\n    private val statusAdapter: BlueskyStatusAdapter,\n) {\n\n    fun convert(\n        notification: CompletedBskyNotification,\n        locator: PlatformLocator,\n        platform: BlogPlatform,\n    ): StatusNotification {\n        return notification.convertNotification(locator, platform)\n    }\n\n    private fun CompletedBskyNotification.convertNotification(\n        locator: PlatformLocator,\n        blogPlatform: BlogPlatform,\n    ): StatusNotification {\n        val author = accountAdapter.convertToBlogAuthor(this.author)\n        return when (this.record) {\n            is CompletedBskyNotification.Record.Like -> {\n                StatusNotification.Like(\n                    id = this.cid,\n                    locator = locator,\n                    author = author,\n                    unread = !this.isRead,\n                    blog = statusAdapter.convertToBlog(\n                        post = this.record.post.record.bskyJson(),\n                        id = this.record.post.cid.cid,\n                        url = this.record.post.uri.atUri,\n                        platform = blogPlatform,\n                        author = accountAdapter.convertToBlogAuthor(this.record.post.author),\n                    ),\n                    createAt = this.record.createAt,\n                )\n            }\n\n            is CompletedBskyNotification.Record.Follow -> {\n                StatusNotification.Follow(\n                    id = this.cid,\n                    author = author,\n                    locator = locator,\n                    unread = !this.isRead,\n                    createAt = this.record.createAt,\n                )\n            }\n\n            is CompletedBskyNotification.Record.Mention -> {\n                StatusNotification.Mention(\n                    author = author,\n                    id = this.cid,\n                    unread = !this.isRead,\n                    status = statusAdapter.convertToUiState(\n                        locator = locator,\n                        status = Status.NewBlog(\n                            statusAdapter.convertToBlog(\n                                post = this.record.post,\n                                id = this.record.cid,\n                                url = this.record.uri,\n                                platform = blogPlatform,\n                                author = author,\n                            )\n                        ),\n                        logged = true,\n                        isOwner = this.record.isOwner,\n                    ),\n                )\n            }\n\n            is CompletedBskyNotification.Record.Reply -> {\n                StatusNotification.Reply(\n                    id = this.cid,\n                    author = author,\n                    unread = !this.isRead,\n                    reply = statusAdapter.convertToUiState(\n                        locator = locator,\n                        status = Status.NewBlog(\n                            statusAdapter.convertToBlog(\n                                post = this.record.reply,\n                                id = this.record.cid,\n                                url = this.record.uri,\n                                platform = blogPlatform,\n                                author = author,\n                            )\n                        ),\n                        logged = true,\n                        isOwner = this.record.isOwner,\n                    ),\n                )\n            }\n\n            is CompletedBskyNotification.Record.Quote -> {\n                StatusNotification.Quote(\n                    id = this.cid,\n                    unread = !this.isRead,\n                    author = author,\n                    quote = statusAdapter.convertToUiState(\n                        locator = locator,\n                        status = Status.NewBlog(\n                            statusAdapter.convertToBlog(\n                                post = this.record.quote,\n                                id = this.record.cid,\n                                url = this.record.uri,\n                                platform = blogPlatform,\n                                author = author,\n                            )\n                        ),\n                        logged = true,\n                        isOwner = this.record.isOwner,\n                    ),\n                )\n            }\n\n            is CompletedBskyNotification.Record.Repost -> {\n                StatusNotification.Repost(\n                    id = this.cid,\n                    author = author,\n                    locator = locator,\n                    unread = !this.isRead,\n                    blog = statusAdapter.convertToBlog(\n                        post = this.record.post.record.bskyJson(),\n                        id = this.record.post.cid.cid,\n                        url = this.record.post.uri.atUri,\n                        platform = blogPlatform,\n                        author = author,\n                    ),\n                    createAt = this.record.createAt,\n                )\n            }\n\n            is CompletedBskyNotification.Record.OnlyMessage -> {\n                StatusNotification.Unknown(\n                    id = this.cid,\n                    locator = locator,\n                    unread = !this.isRead,\n                    message = this.record.message,\n                    createAt = this.record.createAt,\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/adapter/BlueskyProfileAdapter.kt",
    "content": "package com.zhangke.fread.bluesky.internal.adapter\n\nimport app.bsky.actor.ProfileView\nimport com.zhangke.fread.bluesky.internal.model.BlueskyProfile\n\nclass BlueskyProfileAdapter() {\n\n    fun convertToProfile(profile: ProfileView): BlueskyProfile {\n        return BlueskyProfile(\n            did = profile.did.did,\n            handle = profile.handle.handle,\n            displayName = profile.displayName,\n            description = profile.description,\n            avatar = profile.avatar?.uri,\n        )\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/adapter/BlueskyStatusAdapter.kt",
    "content": "package com.zhangke.fread.bluesky.internal.adapter\n\nimport app.bsky.embed.AspectRatio\nimport app.bsky.embed.ExternalViewExternal\nimport app.bsky.embed.ImagesViewImage\nimport app.bsky.embed.RecordViewRecord\nimport app.bsky.embed.RecordViewRecordUnion\nimport app.bsky.embed.RecordWithMediaViewMediaUnion\nimport app.bsky.embed.VideoView\nimport app.bsky.feed.FeedViewPost\nimport app.bsky.feed.FeedViewPostReasonUnion\nimport app.bsky.feed.Post\nimport app.bsky.feed.PostView\nimport app.bsky.feed.PostViewEmbedUnion\nimport app.bsky.richtext.Facet\nimport app.bsky.richtext.FacetFeatureUnion\nimport com.zhangke.framework.datetime.Instant\nimport com.zhangke.fread.bluesky.internal.account.BlueskyLoggedAccount\nimport com.zhangke.fread.bluesky.internal.utils.bskyJson\nimport com.zhangke.fread.common.utils.formatDefault\nimport com.zhangke.fread.status.author.BlogAuthor\nimport com.zhangke.fread.status.blog.Blog\nimport com.zhangke.fread.status.blog.BlogEmbed\nimport com.zhangke.fread.status.blog.BlogMedia\nimport com.zhangke.fread.status.blog.BlogMediaMeta\nimport com.zhangke.fread.status.blog.BlogMediaType\nimport com.zhangke.fread.status.model.BlogTranslationUiState\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.model.StatusUiState\nimport com.zhangke.fread.status.model.StatusVisibility\nimport com.zhangke.fread.status.platform.BlogPlatform\nimport com.zhangke.fread.status.status.model.Status\nimport kotlinx.serialization.json.decodeFromJsonElement\nimport kotlinx.serialization.json.encodeToJsonElement\n\nclass BlueskyStatusAdapter(\n    private val accountAdapter: BlueskyAccountAdapter,\n) {\n\n    fun convert(\n        postView: PostView,\n        platform: BlogPlatform,\n        pinned: Boolean = false,\n    ): Status {\n        return Status.NewBlog(\n            blog = convertToBlog(\n                postView = postView,\n                platform = platform,\n                pinned = pinned,\n            ),\n        )\n    }\n\n    fun convert(\n        feedViewPost: FeedViewPost,\n        platform: BlogPlatform,\n    ): Status {\n        val feedsReason = feedViewPost.reason\n        val pinned = feedsReason is FeedViewPostReasonUnion.ReasonPin\n        val blog = convertToBlog(\n            postView = feedViewPost.post,\n            platform = platform,\n            pinned = pinned,\n        )\n        if (feedsReason is FeedViewPostReasonUnion.ReasonRepost) {\n            val author = accountAdapter.convertToBlogAuthor(feedsReason.value.by)\n            return Status.Reblog(\n                id = blog.id,\n                createAt = Instant(blog.createAt.instant),\n                author = author,\n                reblog = blog,\n            )\n        }\n        return Status.NewBlog(blog)\n    }\n\n    fun convertToUiState(\n        locator: PlatformLocator,\n        status: Status,\n        logged: Boolean,\n        isOwner: Boolean,\n    ): StatusUiState {\n        return StatusUiState(\n            status = status,\n            logged = logged,\n            isOwner = isOwner,\n            blogTranslationState = BlogTranslationUiState(support = false),\n            locator = locator,\n        )\n    }\n\n    fun convertToUiState(\n        locator: PlatformLocator,\n        postView: PostView,\n        platform: BlogPlatform,\n        loggedAccount: BlueskyLoggedAccount?,\n        pinned: Boolean = false,\n    ): StatusUiState {\n        val status = convert(postView, platform, pinned)\n        return convertToUiState(\n            locator = locator,\n            status = status,\n            logged = loggedAccount != null,\n            isOwner = loggedAccount != null && postView.author.did.did == loggedAccount.did,\n        )\n    }\n\n    fun convertToUiState(\n        locator: PlatformLocator,\n        feedViewPost: FeedViewPost,\n        platform: BlogPlatform,\n        loggedAccount: BlueskyLoggedAccount?,\n    ): StatusUiState {\n        val status = convert(\n            feedViewPost = feedViewPost,\n            platform = platform,\n        )\n        return convertToUiState(\n            locator = locator,\n            status = status,\n            logged = loggedAccount != null,\n            isOwner = loggedAccount != null && feedViewPost.post.author.did.did == loggedAccount.did,\n        )\n    }\n\n    private fun convertToBlog(\n        postView: PostView,\n        platform: BlogPlatform,\n        pinned: Boolean,\n    ): Blog {\n        val post: Post = postView.record.bskyJson()\n        return convertToBlog(\n            post = post,\n            id = postView.cid.cid,\n            author = accountAdapter.convertToBlogAuthor(postView.author),\n            url = postView.uri.atUri,\n            replyCount = postView.replyCount,\n            likeCount = postView.likeCount,\n            repostCount = postView.repostCount,\n            liked = postView.viewer?.like?.atUri.isNullOrEmpty().not(),\n            forward = postView.viewer?.repost?.atUri.isNullOrEmpty().not(),\n            mediaList = postView.embed?.let(::convertToMedia) ?: emptyList(),\n            embedList = convertEmbed(postView.embed, platform),\n            platform = platform,\n            pinned = pinned,\n        )\n    }\n\n    private fun convertToBlog(\n        recordView: RecordViewRecord,\n        platform: BlogPlatform,\n    ): Blog {\n        val post: Post =\n            bskyJson.decodeFromJsonElement(bskyJson.encodeToJsonElement(recordView.value))\n        return convertToBlog(\n            post = post,\n            id = recordView.cid.cid,\n            author = accountAdapter.convertToBlogAuthor(recordView.author),\n            url = recordView.uri.atUri,\n            repostCount = recordView.repostCount,\n            likeCount = recordView.likeCount,\n            replyCount = recordView.replyCount,\n            platform = platform,\n        )\n    }\n\n    fun convertToBlog(\n        post: Post,\n        id: String,\n        author: BlogAuthor,\n        url: String,\n        platform: BlogPlatform,\n        liked: Boolean = false,\n        forward: Boolean = false,\n        pinned: Boolean = false,\n        repostCount: Long? = null,\n        likeCount: Long? = null,\n        replyCount: Long? = null,\n        embedList: List<BlogEmbed> = emptyList(),\n        mediaList: List<BlogMedia> = emptyList(),\n    ): Blog {\n        val createAt = Instant(post.createdAt)\n        return Blog(\n            id = id,\n            author = author,\n            title = null,\n            description = null,\n            content = post.text,\n            url = url,\n            link = buildLink(\n                url = url,\n                handle = author.handle,\n            ),\n            createAt = createAt,\n            formattedCreateAt = createAt.formatDefault(),\n            like = Blog.Like(\n                support = true,\n                liked = liked,\n                likedCount = likeCount ?: 0L,\n            ),\n            forward = Blog.Forward(\n                support = true,\n                forward = forward,\n                forwardCount = repostCount ?: 0L,\n            ),\n            bookmark = Blog.Bookmark(\n                support = false,\n            ),\n            reply = Blog.Reply(\n                support = true,\n                repliesCount = replyCount ?: 0L,\n            ),\n            quote = Blog.Quote(support = true, enabled = true),\n            supportEdit = false,\n            sensitive = false,\n            spoilerText = \"\",\n            isReply = post.reply != null,\n            language = post.langs.firstOrNull()?.tag,\n            platform = platform,\n            mediaList = mediaList,\n            emojis = emptyList(),\n            mentions = emptyList(),\n            tags = emptyList(),\n            pinned = pinned,\n            poll = null,\n            facets = post.facets.map { it.convert() },\n            visibility = StatusVisibility.PUBLIC,\n            embeds = embedList,\n            supportTranslate = false,\n        )\n    }\n\n    private fun buildLink(\n        url: String,\n        handle: String,\n    ): String {\n        //https://bsky.app/profile/lotuscat.bsky.social/post/3llqkin45722p\n        return buildString {\n            append(\"https://bsky.app/profile/\")\n            append(handle.removePrefix(\"@\"))\n            append(\"/post/\")\n            append(url.substringAfterLast(\"/\"))\n        }\n    }\n\n    private fun convertToMedia(embedUnion: PostViewEmbedUnion): List<BlogMedia> {\n        return when (embedUnion) {\n            is PostViewEmbedUnion.ImagesView -> {\n                embedUnion.value.images.map { it.toMedia() }\n            }\n\n            is PostViewEmbedUnion.VideoView -> {\n                listOf(embedUnion.value.toMedia())\n            }\n\n            is PostViewEmbedUnion.RecordWithMediaView -> {\n                when (val media = embedUnion.value.media) {\n                    // ExternalView will be convert to link embed\n                    is RecordWithMediaViewMediaUnion.ExternalView -> emptyList()\n                    is RecordWithMediaViewMediaUnion.ImagesView -> {\n                        media.value.images.map { it.toMedia() }\n                    }\n\n                    is RecordWithMediaViewMediaUnion.VideoView -> {\n                        listOf(media.value.toMedia())\n                    }\n\n                    is RecordWithMediaViewMediaUnion.Unknown -> emptyList()\n                }\n            }\n\n            else -> emptyList()\n        }\n    }\n\n    private fun ImagesViewImage.toMedia(): BlogMedia {\n        return BlogMedia(\n            id = this.fullsize.uri,\n            url = this.fullsize.uri,\n            type = BlogMediaType.IMAGE,\n            previewUrl = this.thumb.uri,\n            remoteUrl = null,\n            description = this.alt,\n            blurhash = null,\n            meta = aspectRatio?.let(::buildImageMediaMeta),\n        )\n    }\n\n    private fun buildImageMediaMeta(aspectRatio: AspectRatio): BlogMediaMeta.ImageMeta {\n        return BlogMediaMeta.ImageMeta(\n            original = BlogMediaMeta.ImageMeta.LayoutMeta(\n                width = aspectRatio.width,\n                height = aspectRatio.height,\n                size = null,\n                aspect = aspectRatio.aspect,\n            ),\n            small = null,\n            focus = null,\n        )\n    }\n\n    private fun VideoView.toMedia(): BlogMedia {\n        return BlogMedia(\n            id = this.playlist.uri,\n            url = this.playlist.uri,\n            type = BlogMediaType.VIDEO,\n            previewUrl = this.thumbnail?.uri,\n            remoteUrl = null,\n            description = this.alt,\n            blurhash = null,\n            meta = aspectRatio?.let(::buildImageVideoMeta),\n        )\n    }\n\n    private fun buildImageVideoMeta(aspectRatio: AspectRatio): BlogMediaMeta.VideoMeta {\n        return BlogMediaMeta.VideoMeta(\n            length = null,\n            duration = null,\n            fps = null,\n            size = null,\n            width = aspectRatio.width,\n            height = aspectRatio.height,\n            aspect = aspectRatio.aspect,\n            audioEncode = null,\n            audioBitrate = null,\n            audioChannels = null,\n            original = BlogMediaMeta.VideoMeta.LayoutMeta(\n                width = aspectRatio.width,\n                height = aspectRatio.height,\n                size = null,\n                aspect = aspectRatio.aspect,\n                frameRate = null,\n                duration = null,\n                bitrate = null,\n            ),\n            small = null,\n        )\n    }\n\n    private val AspectRatio.aspect: Float\n        get() = (width.toDouble() / height.toDouble()).toFloat()\n\n    private fun convertEmbed(\n        embedUnion: PostViewEmbedUnion?,\n        platform: BlogPlatform,\n    ): List<BlogEmbed> {\n        if (embedUnion is PostViewEmbedUnion.ExternalView) {\n            return listOf(embedUnion.value.external.toLinkEmbed())\n        }\n        if (embedUnion is PostViewEmbedUnion.RecordWithMediaView) {\n            val embeds = mutableListOf<BlogEmbed>()\n            val media = embedUnion.value.media\n            if (media is RecordWithMediaViewMediaUnion.ExternalView) {\n                // another type will be convert to media list in Blog\n                embeds += media.value.external.toLinkEmbed()\n            }\n            val record = embedUnion.value.record.record\n            if (record is RecordViewRecordUnion.ViewRecord) {\n                embeds += record.toBlogEmbed(platform)\n            }\n            return embeds\n        }\n        if (embedUnion is PostViewEmbedUnion.RecordView) {\n            val embedRecord = embedUnion.value.record\n            if (embedRecord is RecordViewRecordUnion.ViewRecord) {\n                return listOf(embedRecord.toBlogEmbed(platform))\n            }\n        }\n        return emptyList()\n    }\n\n    private fun RecordViewRecordUnion.ViewRecord.toBlogEmbed(\n        platform: BlogPlatform,\n    ): BlogEmbed {\n        return BlogEmbed.Blog(convertToBlog(this.value, platform))\n    }\n\n    private fun ExternalViewExternal.toLinkEmbed(): BlogEmbed.Link {\n        return BlogEmbed.Link(\n            url = this.uri.uri,\n            title = this.title,\n            description = this.description,\n            image = this.thumb?.uri,\n            video = false,\n        )\n    }\n\n    private fun Facet.convert(): com.zhangke.fread.status.model.Facet {\n        return com.zhangke.fread.status.model.Facet(\n            byteStart = this.index.byteStart,\n            byteEnd = this.index.byteEnd,\n            features = this.features.mapNotNull { feature ->\n                when (feature) {\n                    is FacetFeatureUnion.Mention -> com.zhangke.fread.status.model.FacetFeatureUnion.Mention(\n                        did = feature.value.did.did,\n                    )\n\n                    is FacetFeatureUnion.Link -> com.zhangke.fread.status.model.FacetFeatureUnion.Link(\n                        uri = feature.value.uri.uri,\n                    )\n\n                    is FacetFeatureUnion.Tag -> com.zhangke.fread.status.model.FacetFeatureUnion.Tag(\n                        tag = feature.value.tag,\n                    )\n\n                    is FacetFeatureUnion.Unknown -> null\n                }\n            }\n        )\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/client/BlueskyClient.kt",
    "content": "package com.zhangke.fread.bluesky.internal.client\n\nimport app.bsky.actor.GetPreferencesResponse\nimport app.bsky.actor.GetProfileQueryParams\nimport app.bsky.actor.GetProfilesQueryParams\nimport app.bsky.actor.GetProfilesResponse\nimport app.bsky.actor.ProfileView\nimport app.bsky.actor.ProfileViewDetailed\nimport app.bsky.actor.PutPreferencesRequest\nimport app.bsky.actor.SearchActorsQueryParams\nimport app.bsky.actor.SearchActorsResponse\nimport app.bsky.feed.GetActorFeedsQueryParams\nimport app.bsky.feed.GetActorFeedsResponse\nimport app.bsky.feed.GetActorLikesQueryParams\nimport app.bsky.feed.GetActorLikesResponse\nimport app.bsky.feed.GetAuthorFeedQueryParams\nimport app.bsky.feed.GetAuthorFeedResponse\nimport app.bsky.feed.GetFeedGeneratorsQueryParams\nimport app.bsky.feed.GetFeedGeneratorsResponse\nimport app.bsky.feed.GetFeedQueryParams\nimport app.bsky.feed.GetFeedResponse\nimport app.bsky.feed.GetLikesQueryParams\nimport app.bsky.feed.GetListFeedQueryParams\nimport app.bsky.feed.GetListFeedResponse\nimport app.bsky.feed.GetPostThreadQueryParams\nimport app.bsky.feed.GetPostThreadResponse\nimport app.bsky.feed.GetPostsQueryParams\nimport app.bsky.feed.GetPostsResponse\nimport app.bsky.feed.GetRepostedByQueryParams\nimport app.bsky.feed.GetSuggestedFeedsQueryParams\nimport app.bsky.feed.GetSuggestedFeedsResponse\nimport app.bsky.feed.GetTimelineQueryParams\nimport app.bsky.feed.GetTimelineResponse\nimport app.bsky.feed.SearchPostsQueryParams\nimport app.bsky.feed.SearchPostsResponse\nimport app.bsky.graph.GetBlocksQueryParams\nimport app.bsky.graph.GetFollowersQueryParams\nimport app.bsky.graph.GetFollowsQueryParams\nimport app.bsky.graph.GetListQueryParams\nimport app.bsky.graph.GetListResponse\nimport app.bsky.graph.GetListsQueryParams\nimport app.bsky.graph.GetListsResponse\nimport app.bsky.graph.GetMutesQueryParams\nimport app.bsky.graph.MuteActorRequest\nimport app.bsky.graph.UnmuteActorRequest\nimport app.bsky.notification.ListNotificationsQueryParams\nimport app.bsky.notification.ListNotificationsResponse\nimport app.bsky.notification.UpdateSeenRequest\nimport app.bsky.unspecced.GetPopularFeedGeneratorsQueryParams\nimport app.bsky.unspecced.GetPopularFeedGeneratorsResponse\nimport com.atproto.repo.ApplyWritesRequest\nimport com.atproto.repo.ApplyWritesResponse\nimport com.atproto.repo.CreateRecordRequest\nimport com.atproto.repo.CreateRecordResponse\nimport com.atproto.repo.DeleteRecordRequest\nimport com.atproto.repo.DeleteRecordResponse\nimport com.atproto.repo.GetRecordQueryParams\nimport com.atproto.repo.GetRecordResponse\nimport com.atproto.repo.PutRecordRequest\nimport com.atproto.repo.PutRecordResponse\nimport com.atproto.repo.UploadBlobResponse\nimport com.atproto.server.CreateSessionRequest\nimport com.atproto.server.CreateSessionResponse\nimport com.atproto.server.RefreshSessionResponse\nimport com.zhangke.framework.network.FormalBaseUrl\nimport com.zhangke.framework.utils.exceptionOrThrow\nimport com.zhangke.fread.bluesky.internal.account.BlueskyLoggedAccount\nimport com.zhangke.fread.bluesky.internal.utils.toResult\nimport com.zhangke.fread.status.model.PagedData\nimport io.ktor.client.HttpClient\nimport io.ktor.client.engine.HttpClientEngine\nimport io.ktor.client.plugins.DefaultRequest\nimport io.ktor.client.plugins.logging.LogLevel\nimport io.ktor.client.plugins.logging.Logging\nimport io.ktor.http.Url\nimport kotlinx.serialization.json.Json\nimport sh.christian.ozone.BlueskyApi\nimport sh.christian.ozone.XrpcBlueskyApi\nimport sh.christian.ozone.api.AtIdentifier\nimport sh.christian.ozone.api.Did\nimport sh.christian.ozone.api.Uri\nimport sh.christian.ozone.api.response.AtpResponse\n\nclass BlueskyClient(\n    val baseUrl: FormalBaseUrl,\n    private val engine: HttpClientEngine,\n    val json: Json,\n    val loggedAccountProvider: suspend () -> BlueskyLoggedAccount?,\n    val newSessionUpdater: suspend (RefreshSessionResponse) -> Unit,\n    val onLoginRequest: suspend () -> Unit,\n) : BlueskyApi by XrpcBlueskyApi(\n    createBlueskyHttpClient(\n        engine,\n        json,\n        baseUrl.toString(),\n        loggedAccountProvider,\n        newSessionUpdater,\n        onLoginRequest,\n    )\n) {\n\n    suspend fun createSessionCatching(request: CreateSessionRequest): Result<CreateSessionResponse> {\n        return runCatching { createSession(request) }.toResult()\n    }\n\n    suspend fun refreshSessionCatching(): Result<RefreshSessionResponse> {\n        return kotlin.runCatching { refreshSession() }.toResult()\n    }\n\n    suspend fun getProfileCatching(request: GetProfileQueryParams): Result<ProfileViewDetailed> {\n        return runCatching { getProfile(request) }.toResult()\n    }\n\n    suspend fun getProfileCatching(did: String): Result<ProfileViewDetailed> {\n        return runCatching { getProfile(GetProfileQueryParams(Did(did))) }.toResult()\n    }\n\n    suspend fun getProfilesCatching(request: GetProfilesQueryParams): Result<GetProfilesResponse> {\n        return runCatching { getProfiles(request) }.toResult()\n    }\n\n    suspend fun getTimelineCatching(request: GetTimelineQueryParams): Result<GetTimelineResponse> {\n        return runCatching { getTimeline(request) }.toResult()\n    }\n\n    suspend fun getPreferencesCatching(): Result<GetPreferencesResponse> {\n        return runCatching { getPreferences() }.toResult()\n    }\n\n    suspend fun putPreferencesCatching(request: PutPreferencesRequest): Result<Unit> {\n        return runCatching { putPreferences(request) }.toResult()\n    }\n\n    suspend fun getFeedGeneratorsCatching(params: GetFeedGeneratorsQueryParams): Result<GetFeedGeneratorsResponse> {\n        return runCatching { getFeedGenerators(params) }.toResult()\n    }\n\n    suspend fun getFeedCatching(params: GetFeedQueryParams): Result<GetFeedResponse> {\n        return runCatching { getFeed(params) }.toResult()\n    }\n\n    suspend fun getPopularFeedGeneratorsUnspeccedCatching(params: GetPopularFeedGeneratorsQueryParams): Result<GetPopularFeedGeneratorsResponse> {\n        return runCatching { getPopularFeedGeneratorsUnspecced(params) }.toResult()\n    }\n\n    suspend fun getActorFeedsCatching(request: GetActorFeedsQueryParams): Result<GetActorFeedsResponse> {\n        return runCatching { getActorFeeds(request) }.toResult()\n    }\n\n    suspend fun getAuthorFeedCatching(request: GetAuthorFeedQueryParams): Result<GetAuthorFeedResponse> {\n        return runCatching { getAuthorFeed(request) }.toResult()\n    }\n\n    suspend fun getSuggestedFeedsCatching(params: GetSuggestedFeedsQueryParams): Result<GetSuggestedFeedsResponse> {\n        return runCatching { getSuggestedFeeds(params) }.toResult()\n    }\n\n    suspend fun getListsCatching(request: GetListsQueryParams): Result<GetListsResponse> {\n        return runCatching { getLists(request) }.toResult()\n    }\n\n    suspend fun getListCatching(params: GetListQueryParams): Result<GetListResponse> {\n        return runCatching { getList(params) }.toResult()\n    }\n\n    suspend fun getListFeedCatching(params: GetListFeedQueryParams): Result<GetListFeedResponse> {\n        return runCatching { getListFeed(params) }.toResult()\n    }\n\n    suspend fun getRecordCatching(params: GetRecordQueryParams): Result<GetRecordResponse> {\n        return runCatching { getRecord(params) }.toResult()\n    }\n\n    suspend fun createRecordCatching(params: CreateRecordRequest): Result<CreateRecordResponse> {\n        return runCatching { createRecord(params) }.toResult()\n    }\n\n    suspend fun putRecordCatching(params: PutRecordRequest): Result<PutRecordResponse> {\n        return runCatching { putRecord(params) }.toResult()\n    }\n\n    suspend fun deleteRecordCatching(params: DeleteRecordRequest): Result<DeleteRecordResponse> {\n        return runCatching { deleteRecord(params) }.toResult()\n    }\n\n    suspend fun getPostThreadCatching(params: GetPostThreadQueryParams): Result<GetPostThreadResponse> {\n        return runCatching { getPostThread(params) }.toResult()\n    }\n\n    suspend fun getPostsCatching(params: GetPostsQueryParams): Result<GetPostsResponse> {\n        return runCatching { getPosts(params) }.toResult()\n    }\n\n    suspend fun searchPostsCatching(params: SearchPostsQueryParams): Result<SearchPostsResponse> {\n        return runCatching { searchPosts(params) }.toResult()\n    }\n\n    suspend fun searchActorsCatching(params: SearchActorsQueryParams): Result<SearchActorsResponse> {\n        return runCatching { searchActors(params) }.toResult()\n    }\n\n    suspend fun getActorLikesCatching(params: GetActorLikesQueryParams): Result<GetActorLikesResponse> {\n        return runCatching { getActorLikes(params) }.toResult()\n    }\n\n    suspend fun listNotificationsCatching(params: ListNotificationsQueryParams): Result<ListNotificationsResponse> {\n        return runCatching { listNotifications(params) }.toResult()\n    }\n\n    suspend fun updateSeenCatching(request: UpdateSeenRequest): Result<Unit> {\n        return runCatching { updateSeen(request) }.toResult()\n    }\n\n    suspend fun muteActorCatching(actor: AtIdentifier): Result<Unit> {\n        return runCatching { muteActor(MuteActorRequest(actor)) }.toResult()\n    }\n\n    suspend fun unmuteActorCatching(actor: AtIdentifier): Result<Unit> {\n        return runCatching { unmuteActor(UnmuteActorRequest(actor)) }.toResult()\n    }\n\n    suspend fun getFollowsCatching(params: GetFollowsQueryParams): Result<PagedData<ProfileView>> {\n        return runCatching { getFollows(params) }.toResult()\n            .map { PagedData(list = it.follows, cursor = it.cursor) }\n    }\n\n    suspend fun getFollowersCatching(params: GetFollowersQueryParams): Result<PagedData<ProfileView>> {\n        return runCatching { getFollowers(params) }.toResult()\n            .map { PagedData(list = it.followers, cursor = it.cursor) }\n    }\n\n    suspend fun getMutesCatching(params: GetMutesQueryParams): Result<PagedData<ProfileView>> {\n        return runCatching { getMutes(params) }.toResult()\n            .map { PagedData(list = it.mutes, cursor = it.cursor) }\n    }\n\n    suspend fun getBlocksCatching(params: GetBlocksQueryParams): Result<PagedData<ProfileView>> {\n        return runCatching { getBlocks(params) }.toResult()\n            .map { PagedData(list = it.blocks, cursor = it.cursor) }\n    }\n\n    suspend fun getLikesCatching(params: GetLikesQueryParams): Result<PagedData<ProfileView>> {\n        return runCatching { getLikes(params) }.toResult()\n            .map { data -> PagedData(list = data.likes.map { it.actor }, cursor = data.cursor) }\n    }\n\n    suspend fun getRepostedCatching(params: GetRepostedByQueryParams): Result<PagedData<ProfileView>> {\n        return runCatching { getRepostedBy(params) }.toResult()\n            .map { data -> PagedData(list = data.repostedBy, cursor = data.cursor) }\n    }\n\n    suspend fun uploadBlobCatching(data: ByteArray): Result<UploadBlobResponse> {\n        return runCatching { uploadBlob(data) }.toResult()\n    }\n\n    suspend fun applyWritesCatching(request: ApplyWritesRequest): Result<ApplyWritesResponse> {\n        return runCatching { applyWrites(request) }.toResult()\n    }\n\n    suspend fun searchPostsByUri(uri: String): Result<SearchPostsResponse> {\n        return runCatching {\n            searchPosts(SearchPostsQueryParams(q = \"\", url = Uri(uri)))\n        }.toResult()\n    }\n\n    private fun <T : Any> Result<AtpResponse<T>>.toResult(): Result<T> {\n        if (this.isFailure) return Result.failure(this.exceptionOrThrow())\n        return this.getOrThrow().toResult()\n    }\n}\n\nprivate fun createBlueskyHttpClient(\n    engine: HttpClientEngine,\n    json: Json,\n    baseUrl: String,\n    accountProvider: suspend () -> BlueskyLoggedAccount?,\n    newSessionUpdater: suspend (RefreshSessionResponse) -> Unit,\n    onLoginRequest: suspend () -> Unit,\n): HttpClient {\n    return HttpClient(engine) {\n        install(Logging) {\n            level = LogLevel.ALL\n        }\n        install(DefaultRequest) {\n            val hostUrl = Url(baseUrl)\n            url.protocol = hostUrl.protocol\n            url.host = hostUrl.host\n            url.port = hostUrl.port\n        }\n        install(XrpcAuthPlugin) {\n            this.json = json\n            this.accountProvider = accountProvider\n            this.newSessionUpdater = newSessionUpdater\n            this.onLoginRequest = onLoginRequest\n        }\n        install(AtProtoProxyPlugin)\n        expectSuccess = false\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/client/BlueskyClientManager.kt",
    "content": "package com.zhangke.fread.bluesky.internal.client\n\nimport com.atproto.server.RefreshSessionResponse\nimport com.zhangke.framework.architect.coroutines.ApplicationScope\nimport com.zhangke.framework.architect.http.createHttpClientEngine\nimport com.zhangke.framework.architect.json.globalJson\nimport com.zhangke.framework.network.FormalBaseUrl\nimport com.zhangke.framework.utils.throwInDebug\nimport com.zhangke.fread.bluesky.internal.account.BlueskyLoggedAccount\nimport com.zhangke.fread.bluesky.internal.adapter.BlueskyAccountAdapter\nimport com.zhangke.fread.bluesky.internal.repo.BlueskyLoggedAccountRepo\nimport com.zhangke.fread.status.model.PlatformLocator\nimport kotlinx.coroutines.launch\n\nclass BlueskyClientManager(\n    private val loggedAccountRepo: BlueskyLoggedAccountRepo,\n    private val accountAdapter: BlueskyAccountAdapter,\n) {\n\n    private val cachedClient = mutableMapOf<PlatformLocator, BlueskyClient>()\n    private val cachedAccount = mutableMapOf<PlatformLocator, BlueskyLoggedAccount>()\n\n    private val httpClientEngine by lazy {\n        createHttpClientEngine()\n    }\n\n    init {\n        ApplicationScope.launch {\n            loggedAccountRepo.queryAllFlow().collect {\n                clearCache()\n            }\n        }\n    }\n\n    fun clearCache() {\n        cachedClient.clear()\n        cachedAccount.clear()\n    }\n\n    fun getClient(locator: PlatformLocator): BlueskyClient {\n        cachedClient[locator]?.let { return it }\n        val loggedAccountProvider = suspend { getLoggedAccount(locator) }\n        return createClient(locator, loggedAccountProvider).also { cachedClient[locator] = it }\n    }\n\n    fun getClientNoAccount(baseUrl: FormalBaseUrl): BlueskyClient {\n        return BlueskyClient(\n            baseUrl = baseUrl,\n            engine = httpClientEngine,\n            json = globalJson,\n            loggedAccountProvider = { null },\n            newSessionUpdater = { },\n            onLoginRequest = {},\n        )\n    }\n\n    private fun createClient(\n        locator: PlatformLocator,\n        loggedAccountProvider: suspend () -> BlueskyLoggedAccount?,\n    ): BlueskyClient {\n        return BlueskyClient(\n            baseUrl = locator.baseUrl,\n            engine = httpClientEngine,\n            json = globalJson,\n            loggedAccountProvider = loggedAccountProvider,\n            newSessionUpdater = { updateNewSession(locator, it) },\n            onLoginRequest = {\n//                GlobalScreenNavigation.navigate(AddBlueskyContentScreen(role.baseUrl!!, true))\n            },\n        )\n    }\n\n    suspend fun updateNewSession(locator: PlatformLocator, session: RefreshSessionResponse) {\n        cachedAccount.clear()\n        val account = getLoggedAccount(locator) ?: return\n        val newAccount = accountAdapter.updateNewSession(account, session)\n        loggedAccountRepo.updateAccount(account, newAccount)\n    }\n\n    private suspend fun getLoggedAccount(locator: PlatformLocator): BlueskyLoggedAccount? {\n        cachedAccount[locator]?.let { return it }\n        if (locator.accountUri != null) {\n            return loggedAccountRepo.queryByUri(locator.accountUri.toString())\n                ?.also { cachedAccount[locator] = it }\n        }\n        val thisPlatformAccounts = loggedAccountRepo.queryAll()\n            .filter { it.platform.baseUrl.equalsDomain(locator.baseUrl) }\n        if (thisPlatformAccounts.size > 1) {\n            throwInDebug(\"Multiple accounts found for base URL: ${locator.baseUrl}\")\n            return null\n        }\n        return thisPlatformAccounts.firstOrNull()\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/client/BlueskyResponseUtils.kt",
    "content": "package com.zhangke.fread.bluesky.internal.client\n\nimport sh.christian.ozone.api.response.AtpErrorDescription\nimport sh.christian.ozone.api.response.AtpResponse\n\nprivate fun <T : Any> AtpResponse<T>.toResult(): Result<T> {\n    return when (this) {\n        is AtpResponse.Success -> Result.success(this.response)\n        is AtpResponse.Failure -> Result.failure(BlueskyApiException.fromResponse(this))\n    }\n}\n\ndata class BlueskyApiException(\n    val response: Any?,\n    val error: AtpErrorDescription?,\n    val headers: Map<String, String>,\n) : RuntimeException() {\n\n    companion object {\n\n        fun <T : Any> fromResponse(failure: AtpResponse.Failure<T>): BlueskyApiException {\n            return BlueskyApiException(\n                response = failure.response,\n                error = failure.error,\n                headers = failure.headers,\n            )\n        }\n    }\n}\n\nval AtpErrorDescription.expired: Boolean get() = error == \"ExpiredToken\"\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/client/BskyCollections.kt",
    "content": "package com.zhangke.fread.bluesky.internal.client\n\nimport sh.christian.ozone.api.Nsid\n\nobject BskyCollections {\n\n    val feedLike = Nsid(\"app.bsky.feed.like\")\n\n    val feedRepost = Nsid(\"app.bsky.feed.repost\")\n\n    val feedPost = Nsid(\"app.bsky.feed.post\")\n\n    val profile = Nsid(\"app.bsky.actor.profile\")\n\n    val follow = Nsid(\"app.bsky.graph.follow\")\n\n    val block = Nsid(\"app.bsky.graph.block\")\n\n    val postGate = Nsid(\"app.bsky.feed.postgate\")\n\n    val threadGate = Nsid(\"app.bsky.feed.threadgate\")\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/client/BskyHttpPlugin.kt",
    "content": "package com.zhangke.fread.bluesky.internal.client\n\nimport com.atproto.server.RefreshSessionResponse\nimport com.zhangke.fread.bluesky.internal.account.BlueskyLoggedAccount\nimport io.ktor.client.HttpClient\nimport io.ktor.client.call.HttpClientCall\nimport io.ktor.client.call.body\nimport io.ktor.client.call.save\nimport io.ktor.client.plugins.HttpClientPlugin\nimport io.ktor.client.plugins.HttpSend\nimport io.ktor.client.plugins.plugin\nimport io.ktor.client.request.HttpRequestBuilder\nimport io.ktor.client.request.HttpRequestPipeline\nimport io.ktor.client.request.bearerAuth\nimport io.ktor.client.request.post\nimport io.ktor.client.statement.bodyAsText\nimport io.ktor.http.HttpHeaders.Authorization\nimport io.ktor.http.HttpStatusCode.Companion.BadRequest\nimport io.ktor.util.AttributeKey\nimport kotlinx.serialization.json.Json\nimport sh.christian.ozone.api.response.AtpErrorDescription\nimport sh.christian.ozone.api.response.StatusCode\n\ninternal class AtProtoProxyPlugin {\n    companion object : HttpClientPlugin<Unit, AtProtoProxyPlugin> {\n        override val key = AttributeKey<AtProtoProxyPlugin>(\"AtprotoProxyPlugin\")\n\n        override fun prepare(block: Unit.() -> Unit): AtProtoProxyPlugin = AtProtoProxyPlugin()\n\n        override fun install(\n            plugin: AtProtoProxyPlugin,\n            scope: HttpClient,\n        ) {\n            scope.requestPipeline.intercept(HttpRequestPipeline.State) {\n                if (context.url.pathSegments\n                        .lastOrNull()\n                        ?.startsWith(\"chat.bsky.convo.\") == true\n                ) {\n                    context.headers[\"Atproto-Proxy\"] = \"did:web:api.bsky.chat#bsky_chat\"\n                }\n            }\n        }\n    }\n}\n\ninternal class XrpcAuthPlugin(\n    private val json: Json,\n    private val accountProvider: suspend () -> BlueskyLoggedAccount?,\n    private val newSessionUpdater: suspend (RefreshSessionResponse) -> Unit,\n    private val onLoginRequest: suspend () -> Unit,\n) {\n\n    class Config(\n        var json: Json? = null,\n        var accountProvider: suspend () -> BlueskyLoggedAccount? = { null },\n        var newSessionUpdater: suspend (RefreshSessionResponse) -> Unit = {},\n        var onLoginRequest: suspend () -> Unit = {},\n    )\n\n    companion object : HttpClientPlugin<Config, XrpcAuthPlugin> {\n\n        private const val REFRESH_TOKEN_METHOD = \"com.atproto.server.refreshSession\"\n        private const val REFRESH_TOKEN_PATH = \"/xrpc/$REFRESH_TOKEN_METHOD\"\n\n        override val key = AttributeKey<XrpcAuthPlugin>(\"XrpcAuthPlugin\")\n\n        override fun prepare(block: Config.() -> Unit): XrpcAuthPlugin {\n            val config = Config().apply(block)\n            return XrpcAuthPlugin(\n                config.json!!,\n                config.accountProvider,\n                config.newSessionUpdater,\n                config.onLoginRequest,\n            )\n        }\n\n        override fun install(plugin: XrpcAuthPlugin, scope: HttpClient) {\n            scope.plugin(HttpSend).intercept { context ->\n                if (!context.headers.contains(Authorization)) {\n                    val account = plugin.accountProvider()\n                    if (account != null) {\n                        if (context.isRefreshTokenRequest) {\n                            context.bearerAuth(account.refreshJwt)\n                        } else {\n                            context.bearerAuth(account.accessJwt)\n                        }\n                    }\n                }\n\n                var result: HttpClientCall = execute(context)\n                if (result.response.status != BadRequest) {\n                    return@intercept result\n                }\n\n                result = result.save()\n\n                val response = runCatching<AtpErrorDescription> {\n                    plugin.json.decodeFromString(result.response.bodyAsText())\n                }\n                if (response.getOrNull()?.expired == true) {\n                    if (context.isRefreshTokenRequest) {\n                        plugin.onLoginRequest()\n                    } else {\n                        val account = plugin.accountProvider()\n                        if (account != null) {\n                            refreshToken(scope, account.refreshJwt)?.let { refreshedResponse ->\n                                plugin.newSessionUpdater(refreshedResponse)\n                                context.headers.remove(Authorization)\n                                context.bearerAuth(refreshedResponse.accessJwt)\n                                result = execute(context)\n                            }\n                        }\n                    }\n                }\n                result\n            }\n        }\n\n        private val HttpRequestBuilder.isRefreshTokenRequest: Boolean\n            get() = url.pathSegments.contains(REFRESH_TOKEN_METHOD)\n\n        private suspend fun refreshToken(\n            scope: HttpClient,\n            refreshToken: String,\n        ): RefreshSessionResponse? {\n            return runCatching {\n                val response = scope.post(REFRESH_TOKEN_PATH) {\n                    bearerAuth(refreshToken)\n                }\n                if (StatusCode.fromCode(response.status.value) == StatusCode.Okay) {\n                    response.body<RefreshSessionResponse>()\n                } else {\n                    null\n                }\n            }.getOrNull()\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/client/records.kt",
    "content": "\npackage com.zhangke.fread.bluesky.internal.client\n\nimport app.bsky.feed.Like\nimport app.bsky.feed.Repost\nimport app.bsky.graph.Block\nimport app.bsky.graph.Follow\nimport com.atproto.repo.StrongRef\nimport com.zhangke.fread.bluesky.internal.utils.bskyJson\nimport kotlinx.datetime.Instant\nimport sh.christian.ozone.api.Did\nimport sh.christian.ozone.api.model.JsonContent\nimport kotlinx.datetime.Clock\n\ninternal fun likeRecord(\n    subject: StrongRef,\n    createdAt: Instant = Clock.System.now(),\n): JsonContent {\n    return Like(\n        subject = subject,\n        createdAt = createdAt,\n    ).bskyJson()\n}\n\ninternal fun repostRecord(\n    subject: StrongRef,\n    createdAt: Instant = Clock.System.now(),\n): JsonContent {\n    return Repost(\n        subject = subject,\n        createdAt = createdAt,\n    ).bskyJson()\n}\n\ninternal fun followRecord(\n    did: String,\n    createdAt: Instant = Clock.System.now(),\n): JsonContent {\n    return Follow(\n        subject = Did(did),\n        createdAt = createdAt,\n    ).bskyJson()\n}\n\ninternal fun blockRecord(\n    did: String,\n    createdAt: Instant = Clock.System.now(),\n): JsonContent {\n    return Block(\n        subject = Did(did),\n        createdAt = createdAt,\n    ).bskyJson()\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/client/rkeys.kt",
    "content": "package com.zhangke.fread.bluesky.internal.client\n\nimport com.zhangke.fread.status.status.model.Status\nimport sh.christian.ozone.api.RKey\n\ninternal val Status.rkey: RKey\n    get() = intrinsicBlog.url.adjustToRkey()\n\ninternal fun String.adjustToRkey(): RKey = RKey(this.substringAfterLast(\"/\"))\n\ninternal val selfRkey: RKey = RKey(\"self\")\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/composable/BlueskyFeedsUi.kt",
    "content": "package com.zhangke.fread.bluesky.internal.composable\n\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.filled.ListAlt\nimport androidx.compose.material.icons.filled.Add\nimport androidx.compose.material3.CircularProgressIndicator\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.alpha\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport com.seiko.imageloader.model.ImageAction\nimport com.seiko.imageloader.model.ImageRequest\nimport com.seiko.imageloader.rememberImageActionPainter\nimport com.seiko.imageloader.ui.AutoSizeBox\nimport com.zhangke.framework.composable.freadPlaceholder\nimport com.zhangke.framework.utils.formatToHumanReadable\nimport com.zhangke.fread.bluesky.internal.model.BlueskyFeeds\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.statusui.ic_drag_indicator\nimport org.jetbrains.compose.resources.painterResource\nimport org.jetbrains.compose.resources.stringResource\n\n@Composable\nfun BlueskyFollowingFeeds(\n    modifier: Modifier,\n    feeds: BlueskyFeeds,\n    onFeedsClick: (BlueskyFeeds) -> Unit,\n) {\n    Row(\n        modifier = modifier.clickable { onFeedsClick(feeds) }\n            .padding(start = 16.dp, end = 8.dp, top = 16.dp, bottom = 16.dp),\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        FeedsAvatar(feeds.avatar, Modifier)\n        Column(\n            modifier = Modifier.weight(1F).padding(horizontal = 16.dp),\n        ) {\n            Text(\n                text = feeds.displayName(),\n                style = MaterialTheme.typography.titleMedium,\n                maxLines = 1,\n                textAlign = TextAlign.Start,\n                overflow = TextOverflow.Ellipsis,\n            )\n            val subtitle = buildFeedsSubtitle(feeds)\n            if (!subtitle.isNullOrEmpty()) {\n                Text(\n                    modifier = Modifier.padding(top = 1.dp),\n                    text = subtitle,\n                    style = MaterialTheme.typography.labelMedium,\n                    maxLines = 1,\n                    overflow = TextOverflow.Ellipsis,\n                    color = MaterialTheme.colorScheme.onSurfaceVariant,\n                )\n            }\n        }\n        Icon(\n            modifier = Modifier\n                .align(Alignment.CenterVertically)\n                .size(24.dp)\n                .alpha(0.7F)\n                .padding(2.dp),\n            painter = painterResource(com.zhangke.fread.statusui.Res.drawable.ic_drag_indicator),\n            contentDescription = \"Drag for reorder Content Config\",\n        )\n    }\n}\n\n@Composable\nprivate fun buildFeedsSubtitle(feeds: BlueskyFeeds): String? {\n    if (feeds !is BlueskyFeeds.Feeds) return null\n    return stringResource(\n        LocalizedString.bsky_feeds_item_subtitle,\n        feeds.creator.prettyHandle,\n        (feeds.likeCount ?: 0).formatToHumanReadable(),\n    )\n}\n\n@Composable\nfun BlueskyExploringFeeds(\n    modifier: Modifier,\n    feeds: BlueskyFeeds,\n    loading: Boolean = false,\n    onFeedsClick: (BlueskyFeeds) -> Unit,\n    onAddClick: ((BlueskyFeeds) -> Unit)? = null,\n) {\n    Column(\n        modifier = modifier.clickable { onFeedsClick(feeds) }.padding(horizontal = 16.dp),\n    ) {\n        Spacer(modifier = Modifier.size(8.dp))\n        Row(\n            modifier = Modifier.fillMaxWidth(),\n            verticalAlignment = Alignment.CenterVertically,\n        ) {\n            FeedsAvatar(feeds.avatar, Modifier)\n            Column(\n                modifier = Modifier.weight(1F).padding(horizontal = 16.dp),\n                verticalArrangement = Arrangement.Center,\n            ) {\n                Text(\n                    modifier = Modifier.fillMaxWidth(),\n                    text = feeds.displayName(),\n                    style = MaterialTheme.typography.titleMedium,\n                    maxLines = 1,\n                    textAlign = TextAlign.Start,\n                    overflow = TextOverflow.Ellipsis,\n                )\n\n                if (!feeds.creatorName.isNullOrEmpty()) {\n                    Text(\n                        modifier = Modifier.fillMaxWidth().padding(top = 2.dp),\n                        text = stringResource(\n                            LocalizedString.bsky_feeds_explorer_creator_label,\n                            feeds.creatorName!!,\n                        ),\n                        style = MaterialTheme.typography.labelMedium,\n                        maxLines = 1,\n                        color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6F),\n                        textAlign = TextAlign.Start,\n                        overflow = TextOverflow.Ellipsis,\n                    )\n                }\n            }\n            if (!feeds.pinned && onAddClick != null && !loading) {\n                IconButton(onClick = { onAddClick(feeds) }) {\n                    Icon(\n                        imageVector = Icons.Default.Add,\n                        contentDescription = \"Add Feeds\",\n                    )\n                }\n            } else if (loading) {\n                CircularProgressIndicator(modifier = Modifier.size(18.dp))\n            }\n        }\n        if (feeds.description != null) {\n            Text(\n                modifier = Modifier.fillMaxWidth().padding(top = 8.dp),\n                text = feeds.description!!,\n                style = MaterialTheme.typography.bodyMedium,\n                maxLines = 8,\n                textAlign = TextAlign.Start,\n                overflow = TextOverflow.Ellipsis,\n            )\n        }\n\n        Text(\n            modifier = Modifier.fillMaxWidth().padding(top = 8.dp),\n            text = stringResource(\n                LocalizedString.bsky_feeds_explorer_liked_by,\n                (feeds.likeCount ?: 0L).formatToHumanReadable(),\n            ),\n            style = MaterialTheme.typography.labelMedium,\n            maxLines = 1,\n            color = MaterialTheme.colorScheme.onSurfaceVariant,\n            textAlign = TextAlign.Start,\n            overflow = TextOverflow.Ellipsis,\n        )\n        Spacer(modifier = Modifier.size(8.dp))\n    }\n}\n\n@Composable\ninternal fun FeedsAvatar(\n    url: String?,\n    modifier: Modifier,\n) {\n    if (url.isNullOrEmpty()) {\n        Icon(\n            modifier = modifier\n                .size(42.dp)\n                .clip(RoundedCornerShape(6.dp))\n                .background(MaterialTheme.colorScheme.primary),\n            imageVector = Icons.AutoMirrored.Filled.ListAlt,\n            tint = Color.White,\n            contentDescription = \"Avatar\",\n        )\n    } else {\n        AutoSizeBox(\n            modifier = modifier,\n            request = remember(url) {\n                ImageRequest(url)\n            },\n        ) { action ->\n            Image(\n                painter = rememberImageActionPainter(action),\n                contentDescription = \"Avatar\",\n                modifier = Modifier\n                    .size(42.dp)\n                    .clip(RoundedCornerShape(6.dp))\n                    .freadPlaceholder(action !is ImageAction.Success),\n            )\n        }\n    }\n}\n\nprivate val BlueskyFeeds.avatar: String?\n    get() {\n        return when (this) {\n            is BlueskyFeeds.Feeds -> avatar\n            is BlueskyFeeds.List -> avatar\n            else -> null\n        }\n    }\n\nprivate val BlueskyFeeds.description: String?\n    get() {\n        return when (this) {\n            is BlueskyFeeds.Feeds -> description\n            is BlueskyFeeds.List -> description\n            else -> null\n        }\n    }\n\nprivate val BlueskyFeeds.likeCount: Long?\n    get() {\n        return when (this) {\n            is BlueskyFeeds.Feeds -> likeCount\n            else -> null\n        }\n    }\n\nprivate val BlueskyFeeds.creatorName: String?\n    get() {\n        return when (this) {\n            is BlueskyFeeds.Feeds -> creator.displayName\n            else -> null\n        }\n    }\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/composable/DetailTopBar.kt",
    "content": "package com.zhangke.fread.bluesky.internal.composable\n\nimport androidx.compose.foundation.layout.RowScope\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.statusBars\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.material3.TopAppBarDefaults\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.sp\nimport com.zhangke.framework.composable.Toolbar\nimport com.zhangke.framework.utils.BlendColorUtils\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun DetailTopBar(\n    progress: Float,\n    title: String,\n    onBackClick: () -> Unit,\n    actions: @Composable RowScope.() -> Unit,\n) {\n    val topBarContainerColor = MaterialTheme.colorScheme.surface.copy(progress)\n    val onTopBarColor = BlendColorUtils.blend(\n        fraction = progress,\n        startColor = MaterialTheme.colorScheme.inverseOnSurface,\n        endColor = MaterialTheme.colorScheme.onSurface,\n    )\n    TopAppBar(\n        title = {\n            if (progress >= 1F) {\n                Text(\n                    modifier = Modifier,\n                    text = title,\n                    fontSize = 22.sp,\n                    maxLines = 1,\n                )\n            }\n        },\n        navigationIcon = {\n            Toolbar.BackButton(onBackClick = onBackClick)\n        },\n        windowInsets = WindowInsets.statusBars,\n        colors = TopAppBarDefaults.topAppBarColors(\n            containerColor = topBarContainerColor,\n            navigationIconContentColor = onTopBarColor,\n            actionIconContentColor = onTopBarColor,\n        ),\n        actions = actions,\n    )\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/content/BlueskyContent.kt",
    "content": "package com.zhangke.fread.bluesky.internal.content\n\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport com.zhangke.framework.architect.json.JsonModuleBuilder\nimport com.zhangke.framework.network.FormalBaseUrl\nimport com.zhangke.framework.security.Md5\nimport com.zhangke.fread.bluesky.internal.model.BlueskyFeeds\nimport com.zhangke.fread.commonbiz.bluesky_logo\nimport com.zhangke.fread.status.account.LoggedAccount\nimport com.zhangke.fread.status.model.FreadContent\nimport com.zhangke.fread.status.uri.FormalUri\nimport com.zhangke.krouter.annotation.Service\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.modules.SerializersModuleBuilder\nimport kotlinx.serialization.serializer\nimport org.jetbrains.compose.resources.painterResource\n\n@Service\nclass BlueskyContentJsonModuleBuilder : JsonModuleBuilder {\n\n    override fun SerializersModuleBuilder.buildSerializersModule() {\n        polymorphic(\n            baseClass = FreadContent::class,\n            actualClass = BlueskyContent::class,\n            actualSerializer = serializer(),\n        )\n    }\n}\n\n@Serializable\ndata class BlueskyContent(\n    override val name: String,\n    override val order: Int,\n    val baseUrl: FormalBaseUrl,\n    val feedsList: List<BlueskyFeeds>,\n    override val accountUri: FormalUri? = null,\n) : FreadContent {\n\n    private val _id: String by lazy {\n        if (accountUri == null) {\n            Md5.md5(baseUrl.toString())\n        } else {\n            Md5.md5(baseUrl.toString() + accountUri.toString())\n        }\n    }\n\n    override val id: String get() = _id\n\n    override fun newOrder(newOrder: Int): FreadContent {\n        return copy(order = newOrder)\n    }\n\n    @Composable\n    override fun Subtitle(account: LoggedAccount?) {\n        Row(verticalAlignment = Alignment.CenterVertically) {\n            Image(\n                modifier = Modifier.size(14.dp),\n                painter = painterResource(com.zhangke.fread.commonbiz.Res.drawable.bluesky_logo),\n                contentDescription = null,\n            )\n            Text(\n                modifier = Modifier\n                    .padding(start = 4.dp)\n                    .align(Alignment.Bottom),\n                text = account?.prettyHandle ?: baseUrl.host,\n                style = MaterialTheme.typography.labelMedium,\n                maxLines = 1,\n                overflow = TextOverflow.Ellipsis,\n                color = MaterialTheme.colorScheme.onSurfaceVariant,\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/content/BlueskyContentManager.kt",
    "content": "package com.zhangke.fread.bluesky.internal.content\n\nimport com.zhangke.fread.bluesky.internal.screen.add.AddBlueskyContentScreenNavKey\nimport com.zhangke.fread.status.content.AddContentAction\nimport com.zhangke.fread.status.content.IContentManager\nimport com.zhangke.fread.status.model.ContentConfig\nimport com.zhangke.fread.status.model.FreadContent\nimport com.zhangke.fread.status.model.notBluesky\nimport com.zhangke.fread.status.platform.BlogPlatform\n\nclass BlueskyContentManager() : IContentManager {\n\n    override suspend fun addContent(\n        platform: BlogPlatform,\n        action: AddContentAction\n    ) {\n        if (platform.protocol.notBluesky) return\n        action.onFinishPage()\n        action.onOpenNewPage(AddBlueskyContentScreenNavKey(baseUrl = platform.baseUrl))\n    }\n\n    override fun restoreContent(config: ContentConfig): FreadContent? {\n        // Bluesky does not container any old content data\n        return null\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/db/BlueskyLoggedAccountDatabase.kt",
    "content": "package com.zhangke.fread.bluesky.internal.db\n\nimport androidx.room.Dao\nimport androidx.room.Database\nimport androidx.room.Entity\nimport androidx.room.Insert\nimport androidx.room.OnConflictStrategy\nimport androidx.room.PrimaryKey\nimport androidx.room.Query\nimport androidx.room.RoomDatabase\nimport androidx.room.TypeConverters\nimport com.zhangke.fread.bluesky.internal.account.BlueskyLoggedAccount\nimport com.zhangke.fread.bluesky.internal.db.converter.BlueskyLoggedAccountConverter\nimport kotlinx.coroutines.flow.Flow\n\nprivate const val TABLE_NAME = \"logged_accounts\"\nprivate const val DB_VERSION = 1\n\n@Entity(tableName = TABLE_NAME)\ndata class BlueskyLoggedAccountEntity(\n    @PrimaryKey val uri: String,\n    val account: BlueskyLoggedAccount,\n    val addedTimestamp: Long,\n)\n\n@Dao\ninterface BlueskyLoggedAccountDao {\n\n    @Query(\"SELECT * FROM $TABLE_NAME ORDER BY addedTimestamp\")\n    fun queryAllFlow(): Flow<List<BlueskyLoggedAccountEntity>>\n\n    @Query(\"SELECT * FROM $TABLE_NAME WHERE uri=:uri\")\n    suspend fun queryByUri(uri: String): BlueskyLoggedAccountEntity?\n\n    @Query(\"SELECT * FROM $TABLE_NAME ORDER BY addedTimestamp\")\n    suspend fun queryAll(): List<BlueskyLoggedAccountEntity>\n\n    @Insert(onConflict = OnConflictStrategy.REPLACE)\n    suspend fun insert(account: BlueskyLoggedAccountEntity)\n\n    @Query(\"DELETE FROM $TABLE_NAME WHERE uri=:uri\")\n    suspend fun deleteByUri(uri: String)\n}\n\n@TypeConverters(BlueskyLoggedAccountConverter::class)\n@Database(\n    entities = [\n        BlueskyLoggedAccountEntity::class,\n    ],\n    version = DB_VERSION,\n    exportSchema = false,\n)\nabstract class BlueskyLoggedAccountDatabase : RoomDatabase() {\n\n    abstract fun getDao(): BlueskyLoggedAccountDao\n\n    companion object {\n\n        internal const val DB_NAME = \"bluesky_logged_accounts.db\"\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/db/converter/BlueskyLoggedAccountConverter.kt",
    "content": "package com.zhangke.fread.bluesky.internal.db.converter\n\nimport androidx.room.TypeConverter\nimport com.zhangke.framework.architect.json.fromJson\nimport com.zhangke.fread.bluesky.internal.account.BlueskyLoggedAccount\nimport com.zhangke.fread.bluesky.internal.utils.bskyJson\nimport kotlinx.serialization.encodeToString\n\nclass BlueskyLoggedAccountConverter {\n\n    @TypeConverter\n    fun fromAccount(account: BlueskyLoggedAccount): String {\n        return bskyJson.encodeToString(account)\n    }\n\n    @TypeConverter\n    fun toAccount(text: String): BlueskyLoggedAccount {\n        return bskyJson.fromJson(text)\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/db/converter/GeneratorViewConverter.kt",
    "content": "package com.zhangke.fread.bluesky.internal.db.converter\n\nimport androidx.room.TypeConverter\nimport app.bsky.feed.GeneratorView\nimport com.zhangke.framework.architect.json.fromJson\nimport com.zhangke.fread.bluesky.internal.utils.bskyJson\nimport kotlinx.serialization.encodeToString\n\nclass GeneratorViewConverter {\n\n    @TypeConverter\n    fun fromGeneratorView(generator: GeneratorView): String {\n        return bskyJson.encodeToString(generator)\n    }\n\n    @TypeConverter\n    fun toGeneratorView(text: String): GeneratorView {\n        return bskyJson.fromJson(text)\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/migrate/BlueskyContentMigrator.kt",
    "content": "package com.zhangke.fread.bluesky.internal.migrate\n\nimport com.zhangke.fread.bluesky.internal.account.BlueskyLoggedAccountManager\nimport com.zhangke.fread.bluesky.internal.content.BlueskyContent\nimport com.zhangke.fread.common.content.FreadContentRepo\n\nclass BlueskyContentMigrator(\n    private val freadContentRepo: FreadContentRepo,\n    private val accountManager: BlueskyLoggedAccountManager,\n) {\n\n    suspend fun migrate() {\n        val allContent = freadContentRepo.getAllOldContents().filter { it.second is BlueskyContent }\n        if (allContent.isEmpty()) return\n        val allAccounts = accountManager.getAllAccount()\n        allContent.map { it.second as BlueskyContent }\n            .map { content ->\n                if (content.accountUri != null) {\n                    content\n                } else {\n                    val account = allAccounts.firstOrNull { it.platform.baseUrl == content.baseUrl }\n                    content.copy(accountUri = account?.uri)\n                }\n            }.let { freadContentRepo.insertAll(it) }\n        for (content in allContent) {\n            freadContentRepo.deleteOldContents(content.first)\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/model/BlueskyFeeds.kt",
    "content": "package com.zhangke.fread.bluesky.internal.model\n\nimport androidx.compose.runtime.Composable\nimport com.zhangke.fread.localization.LocalizedString\nimport kotlinx.serialization.Serializable\nimport org.jetbrains.compose.resources.stringResource\n\n/**\n * Feeds 一词包含两个含义，一是广义上的 Feeds 信息流。\n * 二是 Bluesky 中的 Feeds，Bluesky 中的 Feeds 是由别人创建的，由 FeedsGenerator 生成的 Feeds。\n * BlueskyFeeds 类中的 Feeds 是广义上的 Feeds，包含着 FeedsGenerator 生成的 Feeds，\n * 也包含 Following Timeline 和 用户自己创建的 List。\n */\n@Serializable\nsealed class BlueskyFeeds {\n\n    /**\n     * pinned to Home Screen\n     */\n    abstract val pinned: Boolean\n\n    abstract val id: String\n\n    @Composable\n    abstract fun displayName(): String\n\n    @Serializable\n    data class FollowingTimeline(\n        override val pinned: Boolean,\n    ) : BlueskyFeeds() {\n\n        override val id: String get() = \"FollowingTimeline\"\n\n        @Composable\n        override fun displayName(): String {\n            return stringResource(LocalizedString.bsky_feeds_following_name)\n        }\n    }\n\n    @Serializable\n    data class Feeds(\n        val uri: String,\n        val cid: String,\n        val did: String,\n        override val pinned: Boolean,\n        val displayName: String,\n        val description: String? = null,\n        val avatar: String? = null,\n        val likeCount: Long? = null,\n        val likedRecord: String? = null,\n        val creator: BlueskyProfile,\n    ) : BlueskyFeeds() {\n\n        val liked: Boolean get() = !likedRecord.isNullOrEmpty()\n\n        override val id: String get() = cid\n\n        @Composable\n        override fun displayName(): String {\n            return displayName\n        }\n    }\n\n    @Serializable\n    data class List(\n        override val id: String,\n        val uri: String,\n        val name: String,\n        val description: String? = null,\n        val avatar: String? = null,\n        override val pinned: Boolean,\n    ) : BlueskyFeeds() {\n\n        @Composable\n        override fun displayName(): String {\n            return name\n        }\n    }\n\n    @Serializable\n    data class Hashtags(\n        val hashtag: String,\n        override val pinned: Boolean = false,\n    ) : BlueskyFeeds() {\n\n        override val id: String get() = hashtag\n\n        @Composable\n        override fun displayName() = hashtag\n    }\n\n    @Serializable\n    data class UserPosts(\n        val did: String,\n        override val pinned: Boolean = false,\n    ) : BlueskyFeeds() {\n\n        override val id: String get() = \"$did/posts\"\n\n        @Composable\n        override fun displayName(): String {\n            return stringResource(LocalizedString.bsky_feeds_user_posts)\n        }\n    }\n\n    @Serializable\n    data class UserReplies(\n        val did: String,\n        override val pinned: Boolean = false,\n    ) : BlueskyFeeds() {\n\n        override val id: String get() = \"$did/replies\"\n\n        @Composable\n        override fun displayName(): String {\n            return stringResource(LocalizedString.bsky_feeds_user_replies)\n        }\n    }\n\n    @Serializable\n    data class UserMedias(\n        val did: String?,\n        override val pinned: Boolean = false,\n    ) : BlueskyFeeds() {\n\n        override val id: String get() = \"$did/medias\"\n\n        @Composable\n        override fun displayName(): String {\n            return stringResource(LocalizedString.bsky_feeds_user_medias)\n        }\n    }\n\n    @Serializable\n    data class UserLikes(\n        val did: String? = null,\n        override val pinned: Boolean = false,\n    ) : BlueskyFeeds() {\n\n        override val id: String get() = \"$did/likes\"\n\n        @Composable\n        override fun displayName(): String {\n            return stringResource(LocalizedString.bsky_feeds_user_likes)\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/model/BlueskyProfile.kt",
    "content": "package com.zhangke.fread.bluesky.internal.model\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class BlueskyProfile(\n    val did: String,\n    val handle: String,\n    val displayName: String?,\n    val description: String?,\n    val avatar: String?,\n){\n\n    val prettyHandle: String = if (handle.startsWith('@')) handle else \"@$handle\"\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/model/BskyPagingFeeds.kt",
    "content": "package com.zhangke.fread.bluesky.internal.model\n\nimport com.zhangke.fread.status.model.StatusUiState\n\ndata class BskyPagingFeeds(\n    val cursor: String?,\n    val feeds: List<StatusUiState>,\n)\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/model/CompletedBskyNotification.kt",
    "content": "package com.zhangke.fread.bluesky.internal.model\n\nimport app.bsky.actor.ProfileView\nimport app.bsky.feed.Post\nimport app.bsky.feed.PostView\nimport com.atproto.label.Label\nimport com.zhangke.framework.datetime.Instant\nimport sh.christian.ozone.api.AtUri\n\ndata class PagedCompletedBskyNotifications(\n    val cursor: String? = null,\n    val notifications: List<CompletedBskyNotification>,\n    val priority: Boolean? = null,\n    val seenAt: Instant? = null,\n)\n\ndata class CompletedBskyNotification(\n    val uri: String,\n    val cid: String,\n    val author: ProfileView,\n    val reasonSubject: AtUri? = null,\n    val record: Record,\n    val isRead: Boolean,\n    val indexedAt: Instant,\n    val labels: List<Label> = emptyList(),\n) {\n\n    sealed interface Record {\n\n        data class Like(\n            val post: PostView,\n            val createAt: Instant,\n        ) : Record\n\n        data class Repost(\n            val post: PostView,\n            val createAt: Instant,\n        ) : Record\n\n        data class Follow(val createAt: Instant) : Record\n\n        data class Mention(\n            val post: Post,\n            val cid: String,\n            val uri: String,\n            val isOwner: Boolean,\n        ) : Record\n\n        data class Quote(\n            val quote: Post,\n            val cid: String,\n            val uri: String,\n            val post: PostView,\n            val isOwner: Boolean,\n        ) : Record\n\n        data class Reply(\n            val reply: Post,\n            val cid: String,\n            val uri: String,\n            val isOwner: Boolean,\n        ) : Record\n\n        data class OnlyMessage(\n            val message: String,\n            val createAt: Instant,\n        ) : Record\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/repo/BlueskyLoggedAccountRepo.kt",
    "content": "package com.zhangke.fread.bluesky.internal.repo\n\nimport com.zhangke.fread.bluesky.internal.account.BlueskyLoggedAccount\nimport com.zhangke.fread.bluesky.internal.db.BlueskyLoggedAccountDao\nimport com.zhangke.fread.bluesky.internal.db.BlueskyLoggedAccountDatabase\nimport com.zhangke.fread.bluesky.internal.db.BlueskyLoggedAccountEntity\nimport com.zhangke.fread.common.utils.getCurrentTimeMillis\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.map\n\nclass BlueskyLoggedAccountRepo(\n    database: BlueskyLoggedAccountDatabase,\n) {\n\n    private val dao: BlueskyLoggedAccountDao = database.getDao()\n\n    fun queryAllFlow(): Flow<List<BlueskyLoggedAccount>> {\n        return dao.queryAllFlow().map { list -> list.map { it.account } }\n    }\n\n    suspend fun queryByUri(uri: String): BlueskyLoggedAccount? {\n        return dao.queryByUri(uri)?.account\n    }\n\n    suspend fun queryAll(): List<BlueskyLoggedAccount> {\n        return dao.queryAll().map { it.account }\n    }\n\n    suspend fun insert(account: BlueskyLoggedAccount) {\n        val addedTimestamp =\n            dao.queryByUri(account.uri.toString())?.addedTimestamp ?: getCurrentTimeMillis()\n        val entity = BlueskyLoggedAccountEntity(\n            uri = account.uri.toString(),\n            account = account,\n            addedTimestamp = addedTimestamp,\n        )\n        dao.insert(entity)\n    }\n\n    suspend fun updateAccount(account: BlueskyLoggedAccount, newAccount: BlueskyLoggedAccount) {\n        if (account.did != newAccount.did) {\n            // maybe did will change?\n            deleteByUri(account.uri.toString())\n        }\n        insert(newAccount)\n    }\n\n    suspend fun deleteByUri(uri: String) {\n        dao.deleteByUri(uri)\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/repo/BlueskyPlatformRepo.kt",
    "content": "package com.zhangke.fread.bluesky.internal.repo\n\nimport com.zhangke.framework.network.FormalBaseUrl\nimport com.zhangke.fread.bluesky.internal.uri.platform.PlatformUriTransformer\nimport com.zhangke.fread.status.model.createBlueskyProtocol\nimport com.zhangke.fread.status.platform.BlogPlatform\n\nclass BlueskyPlatformRepo(\n    private val platformUriTransformer: PlatformUriTransformer,\n) {\n\n    private val appToBackendDomainMap = mapOf(\n        \"bsky.app\" to \"bsky.social\"\n    )\n\n    val appViewDomains: Set<String> = appToBackendDomainMap.keys.toSet()\n\n    fun getAllPlatform(): List<BlogPlatform> {\n        val baseUrl = FormalBaseUrl.parse(\"https://bsky.social\")!!\n        return listOf(createBlueskyPlatform(baseUrl))\n    }\n\n    suspend fun getPlatform(baseUrl: FormalBaseUrl): BlogPlatform {\n        return createBlueskyPlatform(baseUrl)\n    }\n\n    private fun createBlueskyPlatform(baseUrl: FormalBaseUrl): BlogPlatform {\n        return BlogPlatform(\n            uri = platformUriTransformer.build(baseUrl).toString(),\n            name = \"Bluesky\",\n            description = \"Bluesky is social media as it should be. Find your community among millions of users, unleash your creativity, and have some fun again.\",\n            thumbnail = \"https://web-cdn.bsky.app/static/apple-touch-icon.png\",\n            protocol = createBlueskyProtocol(),\n            baseUrl = baseUrl,\n            supportsQuotePost = true,\n        )\n    }\n\n    fun mapAppToBackendDomain(domain: String): String {\n        return appToBackendDomainMap[domain]?.takeIf { it.isNotEmpty() } ?: domain\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/screen/BlueskyRoutes.kt",
    "content": "package com.zhangke.fread.bluesky.internal.screen\n\nimport com.zhangke.framework.network.GlobalRoutes\n\nobject BlueskyRoutes {\n\n    const val ROOT = \"${GlobalRoutes.ROOT_PREFIX}bluesky\"\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/screen/add/AddBlueskyContentScreen.kt",
    "content": "package com.zhangke.fread.bluesky.internal.screen.add\n\nimport androidx.compose.foundation.Canvas\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.imePadding\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.text.KeyboardOptions\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.AlternateEmail\nimport androidx.compose.material.icons.filled.Language\nimport androidx.compose.material.icons.filled.Lock\nimport androidx.compose.material3.Button\nimport androidx.compose.material3.ButtonDefaults\nimport androidx.compose.material3.Card\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.OutlinedTextField\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.SnackbarHost\nimport androidx.compose.material3.SnackbarHostState\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.graphics.StrokeCap\nimport androidx.compose.ui.text.input.KeyboardType\nimport androidx.compose.ui.text.input.PasswordVisualTransformation\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.composable.currentOrThrow\nimport com.zhangke.framework.composable.ConsumeFlow\nimport com.zhangke.framework.composable.ConsumeSnackbarFlow\nimport com.zhangke.framework.composable.LoadingDialog\nimport com.zhangke.framework.composable.Toolbar\nimport com.zhangke.framework.composable.rememberSnackbarHostState\nimport com.zhangke.framework.nav.LocalNavBackStack\nimport com.zhangke.framework.network.FormalBaseUrl\nimport com.zhangke.fread.common.utils.LocalToastHelper\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.ui.BlogAuthorAvatar\nimport kotlinx.serialization.Serializable\nimport org.jetbrains.compose.resources.getString\nimport org.jetbrains.compose.resources.stringResource\n\n@Serializable\ndata class AddBlueskyContentScreenNavKey(\n    val baseUrl: FormalBaseUrl? = null,\n    val loginMode: Boolean = false,\n    val avatar: String? = null,\n    val displayName: String? = null,\n    val handle: String? = null,\n) : NavKey\n\n@Composable\nfun AddBlueskyContentScreen(viewModel: AddBlueskyContentViewModel) {\n    val toastHelper = LocalToastHelper.current\n    val backStack = LocalNavBackStack.currentOrThrow\n    val snackBarHostState = rememberSnackbarHostState()\n    val uiState by viewModel.uiState.collectAsState()\n    AddBlueskyContentContent(\n        uiState = uiState,\n        snackBarHostState = snackBarHostState,\n        onHostingChange = viewModel::onHostingChange,\n        onUserNameChange = viewModel::onUserNameChange,\n        onPasswordChange = viewModel::onPasswordChange,\n        onFactorTokenChange = viewModel::onFactorTokenChange,\n        onBackClick = backStack::removeLastOrNull,\n        onLoginClick = viewModel::onLoginClick,\n    )\n    LoadingDialog(loading = uiState.logging, onDismissRequest = viewModel::onCancelLogin)\n    ConsumeSnackbarFlow(snackBarHostState, viewModel.snackBarMessage)\n    ConsumeFlow(viewModel.loginSuccessFlow) {\n        toastHelper.showToast(getString(LocalizedString.addContentSuccessSnackbar))\n        backStack.removeLastOrNull()\n    }\n}\n\n@Composable\nprivate fun AddBlueskyContentContent(\n    uiState: AddBlueskyContentUiState,\n    snackBarHostState: SnackbarHostState,\n    onHostingChange: (String) -> Unit,\n    onUserNameChange: (String) -> Unit,\n    onPasswordChange: (String) -> Unit,\n    onFactorTokenChange: (String) -> Unit,\n    onBackClick: () -> Unit,\n    onLoginClick: () -> Unit,\n) {\n    val avatarSize = 48.dp\n    Scaffold(\n        modifier = Modifier.fillMaxSize().imePadding(),\n        topBar = {\n            Toolbar(\n                title = stringResource(LocalizedString.bsky_add_content_title),\n                onBackClick = onBackClick,\n            )\n        },\n        snackbarHost = { SnackbarHost(snackBarHostState) },\n    ) { innerPadding ->\n        Column(\n            modifier = Modifier.fillMaxSize()\n                .padding(innerPadding)\n                .padding(start = 16.dp, end = 16.dp),\n        ) {\n            if (uiState.loginToSpecAccount) {\n                AccountInfoCard(\n                    modifier = Modifier.padding(top = 18.dp),\n                    avatar = uiState.avatar.orEmpty(),\n                    avatarSize = avatarSize,\n                    displayName = uiState.displayName.orEmpty(),\n                    handle = uiState.handle!!,\n                )\n\n                val lineColor = MaterialTheme.colorScheme.outlineVariant\n\n                Canvas(\n                    Modifier.height(48.dp)\n                        .padding(\n                            start = 16.dp + avatarSize / 2,\n                            top = 8.dp,\n                        ).width(1.dp),\n                ) {\n                    drawLine(\n                        color = lineColor,\n                        strokeWidth = 1.dp.toPx(),\n                        cap = StrokeCap.Round,\n                        start = Offset(0F, 0f),\n                        end = Offset(0F, size.height),\n                    )\n                }\n            } else {\n                OutlinedTextField(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .padding(top = 18.dp),\n                    value = uiState.hosting,\n                    leadingIcon = {\n                        Icon(\n                            imageVector = Icons.Default.Language,\n                            contentDescription = null,\n                        )\n                    },\n                    onValueChange = onHostingChange,\n                    label = {\n                        Text(stringResource(LocalizedString.bsky_add_content_hosting_provider))\n                    },\n                    singleLine = true,\n                )\n                OutlinedTextField(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .padding(top = 16.dp),\n                    value = uiState.username,\n                    leadingIcon = {\n                        Icon(\n                            imageVector = Icons.Default.AlternateEmail,\n                            contentDescription = null,\n                        )\n                    },\n                    onValueChange = onUserNameChange,\n                    label = {\n                        Text(stringResource(LocalizedString.bsky_add_content_user_name))\n                    },\n                    singleLine = true,\n                )\n                Spacer(modifier = Modifier.height(16.dp))\n            }\n            OutlinedTextField(\n                modifier = Modifier.fillMaxWidth(),\n                value = uiState.password,\n                visualTransformation = PasswordVisualTransformation(),\n                keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),\n                onValueChange = onPasswordChange,\n                leadingIcon = {\n                    Icon(\n                        imageVector = Icons.Default.Lock,\n                        contentDescription = null,\n                    )\n                },\n                label = {\n                    Text(stringResource(LocalizedString.bsky_add_content_password))\n                },\n                singleLine = true,\n            )\n            if (uiState.authFactorRequired) {\n                OutlinedTextField(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .padding(top = 16.dp),\n                    value = uiState.factorToken,\n                    onValueChange = onFactorTokenChange,\n                    label = {\n                        Text(stringResource(LocalizedString.bsky_add_content_factor_token))\n                    },\n                    singleLine = true,\n                )\n            }\n            Box(modifier = Modifier.fillMaxWidth().padding(top = 16.dp)) {\n                Button(\n                    modifier = Modifier.align(Alignment.CenterEnd),\n                    onClick = onLoginClick,\n                    colors = ButtonDefaults.buttonColors(\n                        containerColor = MaterialTheme.colorScheme.primaryContainer,\n                        contentColor = MaterialTheme.colorScheme.onPrimaryContainer,\n                    ),\n                ) {\n                    Text(stringResource(LocalizedString.login))\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun AccountInfoCard(\n    modifier: Modifier,\n    avatarSize: Dp,\n    avatar: String,\n    displayName: String,\n    handle: String,\n) {\n    Box(modifier = modifier) {\n        Card(modifier = Modifier.fillMaxWidth()) {\n            Row(\n                modifier = Modifier.fillMaxWidth().padding(16.dp),\n                verticalAlignment = Alignment.CenterVertically,\n            ) {\n                BlogAuthorAvatar(\n                    modifier = Modifier.size(avatarSize),\n                    imageUrl = avatar,\n                )\n                Column(\n                    modifier = Modifier.padding(start = 8.dp).weight(1F),\n                ) {\n                    Text(\n                        text = displayName,\n                        style = MaterialTheme.typography.titleMedium,\n                        maxLines = 1,\n                        overflow = TextOverflow.Ellipsis,\n                    )\n                    Text(\n                        text = handle,\n                        style = MaterialTheme.typography.bodyMedium,\n                        color = MaterialTheme.colorScheme.onSurfaceVariant,\n                    )\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/screen/add/AddBlueskyContentUiState.kt",
    "content": "package com.zhangke.fread.bluesky.internal.screen.add\n\ndata class AddBlueskyContentUiState(\n    val loginMode: Boolean,\n    val hosting: String,\n    val username: String,\n    val password: String,\n    val factorToken: String,\n    val logging: Boolean,\n    val avatar: String?,\n    val displayName: String?,\n    val handle: String?,\n    val authFactorRequired: Boolean,\n) {\n\n    val loginToSpecAccount: Boolean\n        get() = loginMode && !handle.isNullOrEmpty()\n\n    companion object {\n\n        fun default(\n            loginMode: Boolean,\n            hosting: String = \"\",\n            username: String = \"\",\n            password: String = \"\",\n            avatar: String? = null,\n            displayName: String? = null,\n            handle: String? = null,\n            logging: Boolean = false,\n        ): AddBlueskyContentUiState {\n            return AddBlueskyContentUiState(\n                loginMode = loginMode,\n                hosting = hosting,\n                username = username,\n                password = password,\n                logging = logging,\n                factorToken = \"\",\n                authFactorRequired = false,\n                avatar = avatar,\n                displayName = displayName,\n                handle = handle,\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/screen/add/AddBlueskyContentViewModel.kt",
    "content": "package com.zhangke.fread.bluesky.internal.screen.add\n\nimport androidx.lifecycle.ViewModel\nimport com.zhangke.framework.composable.TextString\nimport com.zhangke.framework.composable.textOf\nimport com.zhangke.framework.ktx.launchInViewModel\nimport com.zhangke.framework.network.FormalBaseUrl\nimport com.zhangke.fread.bluesky.internal.account.BlueskyLoggedAccount\nimport com.zhangke.fread.bluesky.internal.content.BlueskyContent\nimport com.zhangke.fread.bluesky.internal.repo.BlueskyPlatformRepo\nimport com.zhangke.fread.bluesky.internal.usecase.LoginToBskyUseCase\nimport com.zhangke.fread.bluesky.internal.utils.AtRequestException\nimport com.zhangke.fread.common.content.FreadContentRepo\nimport com.zhangke.fread.common.onboarding.OnboardingComponent\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.SharedFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.asSharedFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.update\n\nclass AddBlueskyContentViewModel(\n    private val loginToBluesky: LoginToBskyUseCase,\n    private val contentRepo: FreadContentRepo,\n    private val platformRepo: BlueskyPlatformRepo,\n    private val onboardingComponent: OnboardingComponent,\n    private val baseUrl: FormalBaseUrl?,\n    private val loginMode: Boolean,\n    private val avatar: String?,\n    private val displayName: String?,\n    private val handle: String?,\n) : ViewModel() {\n\n    private val _uiState = MutableStateFlow(\n        AddBlueskyContentUiState.default(\n            hosting = baseUrl.toString(),\n            loginMode = loginMode,\n            avatar = avatar,\n            displayName = displayName,\n            handle = handle,\n        )\n    )\n    val uiState: StateFlow<AddBlueskyContentUiState> = _uiState.asStateFlow()\n\n    private val _snackBarMessage = MutableSharedFlow<TextString>()\n    val snackBarMessage: SharedFlow<TextString> = _snackBarMessage\n\n    private val _loginSuccessFlow = MutableSharedFlow<Unit>()\n    val loginSuccessFlow: SharedFlow<Unit> = _loginSuccessFlow.asSharedFlow()\n\n    private var loggingJob: Job? = null\n\n    init {\n        onboardingComponent.clearState()\n        launchInViewModel {\n            platformRepo.getAllPlatform()\n                .first()\n                .baseUrl\n                .let { baseUrl ->\n                    _uiState.update { it.copy(hosting = baseUrl.toString()) }\n                }\n        }\n    }\n\n    fun onHostingChange(hosting: String) {\n        _uiState.update { it.copy(hosting = hosting) }\n    }\n\n    fun onUserNameChange(username: String) {\n        _uiState.update { it.copy(username = username) }\n    }\n\n    fun onPasswordChange(password: String) {\n        _uiState.update { it.copy(password = password) }\n    }\n\n    fun onFactorTokenChange(factorToken: String) {\n        _uiState.update { it.copy(factorToken = factorToken) }\n    }\n\n    fun onLoginClick() {\n        if (_uiState.value.logging) return\n        if (loggingJob?.isActive == true) return\n        val hosting = uiState.value.hosting.trim()\n        val identifier =\n            if (uiState.value.loginToSpecAccount) {\n                uiState.value.handle!!.trim().removePrefix(\"@\")\n            } else {\n                uiState.value.username.trim()\n            }\n        val password = uiState.value.password.trim()\n        val factorToken = uiState.value.factorToken.trim()\n        loggingJob = launchInViewModel {\n            val baseUrl = FormalBaseUrl.parse(hosting)\n            if (baseUrl == null) {\n                _snackBarMessage.emit(textOf(\"Invalid host!\"))\n                return@launchInViewModel\n            }\n            _uiState.update { it.copy(logging = true) }\n            loginToBluesky(baseUrl, identifier, password, factorToken)\n                .onSuccess { account ->\n                    _uiState.update { it.copy(logging = false) }\n                    if (!loginMode) {\n                        saveBlueskyContent(account)\n                    }\n                    _loginSuccessFlow.emit(Unit)\n                    onboardingComponent.onboardingSuccess()\n                }\n                .onFailure { t ->\n                    _uiState.update { it.copy(logging = false) }\n                    if (t is AtRequestException) {\n                        if (t.needAuthFactorTokenRequired) {\n                            _uiState.update { it.copy(authFactorRequired = true) }\n                        }\n                        if (!t.errorMessage.isNullOrEmpty()) {\n                            _snackBarMessage.emit(textOf(t.errorMessage))\n                        } else {\n                            _snackBarMessage.emit(\n                                textOf(t.error ?: t.message ?: \"Login Failed! by Bsky Api.\")\n                            )\n                        }\n                    } else {\n                        _snackBarMessage.emit(\n                            textOf(t.message ?: \"Login Failed! ${t::class.simpleName}\")\n                        )\n                    }\n                }\n        }\n    }\n\n    fun onCancelLogin() {\n        loggingJob?.cancel()\n        _uiState.update { it.copy(logging = false) }\n    }\n\n    private suspend fun saveBlueskyContent(account: BlueskyLoggedAccount) {\n        val content = BlueskyContent(\n            order = contentRepo.getMaxOrder() + 1,\n            name = account.platform.name,\n            baseUrl = account.platform.baseUrl,\n            feedsList = emptyList(),\n            accountUri = account.uri,\n        )\n        contentRepo.insertContent(content)\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/screen/content/BlueskyContentContainerViewModel.kt",
    "content": "package com.zhangke.fread.bluesky.internal.screen.content\n\nimport com.zhangke.framework.lifecycle.ContainerViewModel\nimport com.zhangke.fread.bluesky.internal.account.BlueskyLoggedAccountManager\nimport com.zhangke.fread.bluesky.internal.client.BlueskyClientManager\nimport com.zhangke.fread.bluesky.internal.usecase.UpdateHomeTabUseCase\nimport com.zhangke.fread.common.config.FreadConfigManager\nimport com.zhangke.fread.common.content.FreadContentRepo\n\nclass BlueskyContentContainerViewModel(\n    private val contentRepo: FreadContentRepo,\n    private val freadConfigManager: FreadConfigManager,\n    private val accountManager: BlueskyLoggedAccountManager,\n    private val updateHomeTab: UpdateHomeTabUseCase,\n    private val clientManager: BlueskyClientManager,\n) : ContainerViewModel<BlueskyContentViewModel, BlueskyContentContainerViewModel.Params>() {\n\n    override fun createSubViewModel(params: Params): BlueskyContentViewModel {\n        return BlueskyContentViewModel(\n            contentId = params.contentId,\n            contentRepo = contentRepo,\n            updateHomeTab = updateHomeTab,\n            freadConfigManager = freadConfigManager,\n            accountManager = accountManager,\n            clientManager = clientManager,\n        )\n    }\n\n    fun getSubViewModel(contentId: String): BlueskyContentViewModel {\n        return obtainSubViewModel(Params(contentId))\n    }\n\n    class Params(val contentId: String) : SubViewModelParams() {\n\n        override val key: String get() = contentId.toString()\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/screen/content/BlueskyContentTab.kt",
    "content": "package com.zhangke.fread.bluesky.internal.screen.content\n\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.navigationBarsPadding\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.pager.HorizontalPager\nimport androidx.compose.foundation.pager.rememberPagerState\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.SnackbarHost\nimport androidx.compose.material3.SnackbarHostState\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBarDefaults\nimport androidx.compose.material3.rememberTopAppBarState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.input.nestedscroll.nestedScroll\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.unit.dp\nimport com.zhangke.framework.composable.LocalContentPadding\nimport com.zhangke.framework.composable.LocalSnackbarHostState\nimport com.zhangke.framework.composable.contentBottomPadding\nimport com.zhangke.framework.composable.currentOrThrow\nimport com.zhangke.framework.composable.plusContentPadding\nimport com.zhangke.framework.composable.rememberSnackbarHostState\nimport com.zhangke.framework.nav.BaseTab\nimport com.zhangke.framework.nav.LocalNavBackStack\nimport com.zhangke.framework.nav.TabOptions\nimport com.zhangke.fread.bluesky.internal.account.BlueskyLoggedAccount\nimport com.zhangke.fread.bluesky.internal.content.BlueskyContent\nimport com.zhangke.fread.bluesky.internal.screen.add.AddBlueskyContentScreenNavKey\nimport com.zhangke.fread.bluesky.internal.screen.feeds.home.HomeFeedsTab\nimport com.zhangke.fread.bluesky.internal.screen.publish.PublishPostScreenNavKey\nimport com.zhangke.fread.commonbiz.shared.composable.NotLoginPageError\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.ui.common.HomeContentTabsTopBar\nimport com.zhangke.fread.status.ui.common.LocalNestedTabConnection\nimport com.zhangke.fread.status.ui.common.PublishingFab\nimport com.zhangke.fread.status.ui.style.LocalStatusUiConfig\nimport kotlinx.coroutines.launch\nimport org.koin.compose.viewmodel.koinViewModel\n\nclass BlueskyContentTab(\n    private val contentId: String,\n    private val isLatestContent: Boolean,\n) : BaseTab() {\n\n    override val options: TabOptions?\n        @Composable get() = null\n\n    @Composable\n    override fun Content() {\n        super.Content()\n        val backStack = LocalNavBackStack.currentOrThrow\n        val viewModel = koinViewModel<BlueskyContentContainerViewModel>().getSubViewModel(contentId)\n        val uiState by viewModel.uiState.collectAsState()\n        val snackBarHostState = rememberSnackbarHostState()\n        BlueskyHomeContent(\n            uiState = uiState,\n            snackBarHostState = snackBarHostState,\n            onPostBlogClick = {\n                uiState.locator?.let { backStack.add(PublishPostScreenNavKey(locator = it)) }\n            },\n            onTitleClick = {},\n            onLoginClick = {\n                uiState.content?.baseUrl?.let { baseUrl ->\n                    backStack.add(\n                        AddBlueskyContentScreenNavKey(\n                            baseUrl = baseUrl,\n                            loginMode = true,\n                        )\n                    )\n                }\n            },\n        )\n    }\n\n    @OptIn(ExperimentalMaterial3Api::class)\n    @Composable\n    private fun BlueskyHomeContent(\n        uiState: BlueskyContentUiState,\n        snackBarHostState: SnackbarHostState,\n        onPostBlogClick: (BlueskyLoggedAccount) -> Unit,\n        onTitleClick: (BlueskyContent) -> Unit,\n        onLoginClick: () -> Unit,\n    ) {\n        val coroutineScope = rememberCoroutineScope()\n        val mainTabConnection = LocalNestedTabConnection.current\n        val showFb = uiState.account != null\n        val tabList = remember(uiState.locator, uiState.content) {\n            if (uiState.content != null) {\n                createTabList(uiState.content, uiState.locator)\n            } else {\n                emptyList()\n            }\n        }\n        val tabTitles = tabList.map { it.options.title }\n        val pagerState = rememberPagerState(0) { tabList.size }\n        val topBarState = rememberTopAppBarState()\n        val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(topBarState)\n        LaunchedEffect(mainTabConnection, topBarState) {\n            mainTabConnection.scrollToTopFlow.collect {\n                topBarState.contentOffset = 0F\n                topBarState.heightOffset = 0F\n            }\n        }\n        Scaffold(\n            modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),\n            topBar = {\n                if (uiState.content != null && tabList.isNotEmpty()) {\n                    HomeContentTabsTopBar(\n                        title = uiState.content.name,\n                        account = uiState.account,\n                        showAccountInfo = uiState.showAccountInTopBar,\n                        selectedTabIndex = pagerState.currentPage,\n                        tabTitles = tabTitles,\n                        scrollBehavior = scrollBehavior,\n                        showNextIcon = !isLatestContent && uiState.showNextButton,\n                        showRefreshButton = uiState.showRefreshButton,\n                        onMenuClick = {\n                            coroutineScope.launch {\n                                mainTabConnection.openDrawer()\n                            }\n                        },\n                        onRefreshClick = {\n                            coroutineScope.launch {\n                                mainTabConnection.scrollToTop()\n                                mainTabConnection.refresh()\n                            }\n                        },\n                        onNextClick = {\n                            coroutineScope.launch {\n                                mainTabConnection.switchToNextTab()\n                            }\n                        },\n                        onTitleClick = {\n                            onTitleClick(uiState.content)\n                        },\n                        onDoubleClick = {\n                            coroutineScope.launch {\n                                mainTabConnection.scrollToTop()\n                            }\n                        },\n                        onTabClick = {\n                            coroutineScope.launch {\n                                pagerState.scrollToPage(it)\n                            }\n                        },\n                    )\n                }\n            },\n            floatingActionButton = {\n                if (showFb) {\n                    val inImmersiveMode by mainTabConnection.inImmersiveFlow.collectAsState()\n                    val immersiveNavBar = LocalStatusUiConfig.current.immersiveNavBar\n                    PublishingFab(\n                        visible = !inImmersiveMode || !immersiveNavBar,\n                        onPublishClick = { onPostBlogClick(uiState.account) },\n                    )\n                }\n            },\n            contentWindowInsets = WindowInsets(0, 0, 0, 0),\n            snackbarHost = {\n                SnackbarHost(\n                    modifier = Modifier\n                        .navigationBarsPadding()\n                        .contentBottomPadding(),\n                    hostState = snackBarHostState,\n                )\n            },\n        ) { paddings ->\n            CompositionLocalProvider(\n                LocalSnackbarHostState provides snackBarHostState,\n                LocalContentPadding provides plusContentPadding(paddings),\n            ) {\n                if (uiState.content != null) {\n                    if (uiState.authFailed) {\n                        NotLoginPageError(\n                            modifier = Modifier\n                                .padding(LocalContentPadding.current)\n                                .padding(top = 64.dp),\n                            message = null,\n                            onLoginClick = onLoginClick,\n                        )\n                    } else if (tabList.isNotEmpty()) {\n                        val contentScrollInProgress by mainTabConnection.contentScrollInpProgress.collectAsState()\n                        HorizontalPager(\n                            modifier = Modifier.fillMaxSize(),\n                            state = pagerState,\n                            userScrollEnabled = !contentScrollInProgress,\n                        ) { pageIndex ->\n                            tabList[pageIndex].Content()\n                        }\n                    }\n                } else if (uiState.errorMessage != null) {\n                    Box(\n                        modifier = Modifier.fillMaxSize()\n                            .padding(LocalContentPadding.current),\n                    ) {\n                        Text(\n                            modifier = Modifier\n                                .padding(start = 16.dp, top = 64.dp, end = 16.dp)\n                                .fillMaxWidth()\n                                .align(Alignment.TopCenter),\n                            text = uiState.errorMessage,\n                            textAlign = TextAlign.Center,\n                        )\n                    }\n                } else if (!uiState.initializing && uiState.account == null) {\n                    // not login\n                    NotLoginPageError(\n                        modifier = Modifier\n                            .padding(LocalContentPadding.current)\n                            .padding(top = 64.dp),\n                        message = null,\n                        onLoginClick = onLoginClick,\n                    )\n                }\n            }\n        }\n    }\n\n    private fun createTabList(\n        content: BlueskyContent,\n        locator: PlatformLocator?,\n    ): List<HomeFeedsTab> {\n        if (locator == null) return emptyList()\n        return content.feedsList.filter { it.pinned }\n            .map { HomeFeedsTab(feeds = it, locator = locator) }\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/screen/content/BlueskyContentUiState.kt",
    "content": "package com.zhangke.fread.bluesky.internal.screen.content\n\nimport com.zhangke.fread.bluesky.internal.account.BlueskyLoggedAccount\nimport com.zhangke.fread.bluesky.internal.content.BlueskyContent\nimport com.zhangke.fread.status.model.PlatformLocator\n\ndata class BlueskyContentUiState(\n    val initializing: Boolean,\n    val locator: PlatformLocator?,\n    val content: BlueskyContent?,\n    val account: BlueskyLoggedAccount?,\n    val authExpired: Boolean?,\n    val showAccountInTopBar: Boolean,\n    val showRefreshButton: Boolean,\n    val showNextButton: Boolean,\n    val errorMessage: String? = null,\n) {\n\n    val authFailed: Boolean\n        get() {\n            if (initializing) return false\n            if (content?.feedsList.isNullOrEmpty() && account == null) return true\n            return authExpired == true\n        }\n\n    companion object {\n\n        fun default(\n            initializing: Boolean,\n            locator: PlatformLocator? = null,\n            config: BlueskyContent? = null,\n            account: BlueskyLoggedAccount? = null,\n            errorMessage: String? = null\n        ): BlueskyContentUiState {\n            return BlueskyContentUiState(\n                initializing = initializing,\n                locator = locator,\n                content = config,\n                account = account,\n                authExpired = null,\n                showAccountInTopBar = false,\n                errorMessage = errorMessage,\n                showRefreshButton = false,\n                showNextButton = false,\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/screen/content/BlueskyContentViewModel.kt",
    "content": "package com.zhangke.fread.bluesky.internal.screen.content\n\nimport com.zhangke.framework.ktx.launchInViewModel\nimport com.zhangke.framework.lifecycle.SubViewModel\nimport com.zhangke.fread.bluesky.internal.account.BlueskyLoggedAccountManager\nimport com.zhangke.fread.bluesky.internal.client.BlueskyClientManager\nimport com.zhangke.fread.bluesky.internal.content.BlueskyContent\nimport com.zhangke.fread.bluesky.internal.usecase.UpdateHomeTabUseCase\nimport com.zhangke.fread.bluesky.internal.utils.createPlatformLocator\nimport com.zhangke.fread.common.config.FreadConfigManager\nimport com.zhangke.fread.common.content.FreadContentRepo\nimport com.zhangke.fread.status.account.isAuthenticationFailure\nimport com.zhangke.fread.status.model.PlatformLocator\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.distinctUntilChanged\nimport kotlinx.coroutines.flow.map\nimport kotlinx.coroutines.flow.update\n\nclass BlueskyContentViewModel(\n    private val contentId: String,\n    private val contentRepo: FreadContentRepo,\n    private val freadConfigManager: FreadConfigManager,\n    private val updateHomeTab: UpdateHomeTabUseCase,\n    private val accountManager: BlueskyLoggedAccountManager,\n    private val clientManager: BlueskyClientManager,\n) : SubViewModel() {\n\n    private val _uiState = MutableStateFlow(BlueskyContentUiState.default(initializing = true))\n    val uiState: StateFlow<BlueskyContentUiState> = _uiState\n\n    private var observeAccountJob: Job? = null\n\n    init {\n        launchInViewModel {\n            contentRepo.getContentFlow(contentId)\n                .distinctUntilChanged()\n                .map { it as? BlueskyContent }\n                .collect { config ->\n                    if (config != null) {\n                        val locator = createPlatformLocator(config)\n                        _uiState.update { state ->\n                            state.copy(\n                                locator = locator,\n                                content = config,\n                            )\n                        }\n                        startObserveAccount(locator)\n                    } else {\n                        _uiState.update { state ->\n                            state.copy(errorMessage = \"Cant find validate config by id: $contentId\")\n                        }\n                    }\n                }\n        }\n        launchInViewModel {\n            delay(200)\n            _uiState.update { it.copy(initializing = false) }\n        }\n        launchInViewModel {\n            freadConfigManager.homeTabRefreshButtonVisibleFlow\n                .collect { visible ->\n                    _uiState.update { it.copy(showRefreshButton = visible) }\n                }\n        }\n        launchInViewModel {\n            freadConfigManager.homeTabNextButtonVisibleFlow\n                .collect { visible ->\n                    _uiState.update { it.copy(showNextButton = visible) }\n                }\n        }\n    }\n\n    private fun startObserveAccount(locator: PlatformLocator) {\n        observeAccountJob?.cancel()\n        if (locator.accountUri == null) {\n            _uiState.update { it.copy(account = null) }\n            return\n        }\n        observeAccountJob = launchInViewModel {\n            accountManager.getAccountFlow(locator.accountUri!!)\n                .distinctUntilChanged()\n                .collect { account ->\n                    val accountSize = accountManager.getAllAccount().size\n                    _uiState.update {\n                        it.copy(account = account, showAccountInTopBar = accountSize > 1)\n                    }\n                    updateHomeTab(contentId, locator)\n                    clientManager.getClient(locator)\n                        .getProfileCatching(account.did)\n                        .onFailure { t ->\n                            _uiState.update { it.copy(authExpired = t.isAuthenticationFailure) }\n                        }.onSuccess {\n                            _uiState.update { it.copy(authExpired = false) }\n                        }\n                }\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/screen/explorer/BlueskyExplorerTab.kt",
    "content": "package com.zhangke.fread.bluesky.internal.screen.explorer\n\nimport androidx.compose.animation.ExperimentalSharedTransitionApi\nimport androidx.compose.runtime.Composable\nimport com.zhangke.framework.nav.BaseTab\nimport com.zhangke.framework.nav.TabOptions\nimport com.zhangke.fread.bluesky.internal.screen.feeds.explorer.ExplorerFeedsScreen\nimport com.zhangke.fread.status.model.PlatformLocator\n\nclass BlueskyExplorerTab(\n    private val locator: PlatformLocator,\n) : BaseTab() {\n\n    override val options: TabOptions?\n        @Composable get() = null\n\n    @OptIn(ExperimentalSharedTransitionApi::class)\n    @Composable\n    override fun Content() {\n        super.Content()\n        ExplorerFeedsScreen(locator, true)\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/screen/feeds/detail/FeedsDetailScreen.kt",
    "content": "package com.zhangke.fread.bluesky.internal.screen.feeds.detail\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Share\nimport androidx.compose.material3.ButtonDefaults\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.ModalBottomSheet\nimport androidx.compose.material3.SheetState\nimport androidx.compose.material3.SnackbarHost\nimport androidx.compose.material3.SnackbarHostState\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.DisposableEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.text.SpanStyle\nimport androidx.compose.ui.text.buildAnnotatedString\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.text.style.TextDecoration\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport com.zhangke.framework.composable.ConsumeFlow\nimport com.zhangke.framework.composable.ConsumeSnackbarFlow\nimport com.zhangke.framework.composable.currentOrThrow\nimport com.zhangke.framework.composable.noRippleClick\nimport com.zhangke.framework.composable.rememberSnackbarHostState\nimport com.zhangke.framework.nav.LocalNavBackStack\nimport com.zhangke.framework.utils.formatToHumanReadable\nimport com.zhangke.fread.bluesky.internal.composable.FeedsAvatar\nimport com.zhangke.fread.bluesky.internal.model.BlueskyFeeds\nimport com.zhangke.fread.bluesky.internal.model.BlueskyProfile\nimport com.zhangke.fread.bluesky.internal.screen.user.detail.BskyUserDetailScreenNavKey\nimport com.zhangke.fread.common.handler.LocalTextHandler\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.ui.action.likeAlt\nimport com.zhangke.fread.status.ui.action.likeIcon\nimport com.zhangke.fread.status.ui.action.pinAlt\nimport org.jetbrains.compose.resources.stringResource\nimport org.koin.compose.viewmodel.koinViewModel\n\n@Composable\nfun rememberFeedsDetailBottomSheetState(): FeedsDetailBottomSheetState {\n    return remember { FeedsDetailBottomSheetState() }\n}\n\nclass FeedsDetailBottomSheetState {\n\n    internal var feeds: BlueskyFeeds? = null\n\n    internal var locator: PlatformLocator? = null\n\n    internal var visible by mutableStateOf(false)\n\n    fun show(locator: PlatformLocator, feeds: BlueskyFeeds) {\n        this.locator = locator\n        this.feeds = feeds\n        visible = true\n    }\n\n    fun hide() {\n        locator = null\n        feeds = null\n        visible = false\n    }\n}\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun FeedsDetailBottomSheet(\n    state: FeedsDetailBottomSheetState,\n    sheetState: SheetState,\n    onFeedsUpdate: (BlueskyFeeds.Feeds) -> Unit,\n) {\n    val viewModel = koinViewModel<FeedsDetailViewModel>()\n    if (!state.visible) {\n        viewModel.clearState()\n        return\n    }\n    val feeds = state.feeds ?: run {\n        viewModel.clearState()\n        return\n    }\n    val locator = state.locator ?: run {\n        viewModel.clearState()\n        return\n    }\n    DisposableEffect(feeds, locator) {\n        viewModel.initialize(locator, feeds)\n        onDispose {\n            viewModel.clearState()\n        }\n    }\n    ModalBottomSheet(\n        sheetState = sheetState,\n        onDismissRequest = {\n            state.hide()\n            viewModel.clearState()\n        },\n    ) {\n        FeedsDetailContentHost(\n            viewModel = viewModel,\n            locator = locator,\n            onFeedsUpdate = onFeedsUpdate,\n        )\n    }\n}\n\n@Composable\nprivate fun FeedsDetailContentHost(\n    viewModel: FeedsDetailViewModel,\n    locator: PlatformLocator,\n    onFeedsUpdate: (BlueskyFeeds.Feeds) -> Unit,\n) {\n    val backStack = LocalNavBackStack.currentOrThrow\n    val uiState by viewModel.uiState.collectAsState()\n    val textHandler = LocalTextHandler.current\n    val snackbarHostState = rememberSnackbarHostState()\n    val currentState = uiState ?: return\n    FeedsDetailContent(\n        uiState = currentState,\n        snackbarHostState = snackbarHostState,\n        onCreatorClick = {\n            backStack.add(BskyUserDetailScreenNavKey(locator = locator, did = it.did))\n        },\n        onShareFeedsClick = { textHandler.shareUrl(url = it.uri, text = it.displayName) },\n        onShareListClick = { textHandler.shareUrl(url = it.uri, text = it.name) },\n        onLikeClick = viewModel::onLikeClick,\n        onPinClick = viewModel::onPinClick,\n    )\n    ConsumeSnackbarFlow(snackbarHostState, viewModel.snackBarMessageFlow)\n    ConsumeFlow(viewModel.feedsUpdateFlow) { onFeedsUpdate(it) }\n}\n\n@Composable\nprivate fun FeedsDetailContent(\n    uiState: FeedsDetailUiState,\n    snackbarHostState: SnackbarHostState,\n    onCreatorClick: (BlueskyProfile) -> Unit,\n    onShareFeedsClick: (BlueskyFeeds.Feeds) -> Unit,\n    onShareListClick: (BlueskyFeeds.List) -> Unit,\n    onLikeClick: () -> Unit,\n    onPinClick: () -> Unit,\n) {\n    Column(modifier = Modifier.fillMaxWidth().padding(top = 8.dp, bottom = 16.dp)) {\n        val feeds = uiState.feeds\n        if (feeds is BlueskyFeeds.Feeds) {\n            FeedsDetail(\n                feeds = feeds,\n                onCreatorClick = onCreatorClick,\n                onShareClick = onShareFeedsClick,\n                onLikeClick = onLikeClick,\n                onPinClick = onPinClick,\n            )\n        } else if (feeds is BlueskyFeeds.List) {\n            ListDetail(\n                feeds = feeds,\n                onShareClick = onShareListClick,\n            )\n        }\n    }\n    SnackbarHost(\n        modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp),\n        hostState = snackbarHostState,\n    )\n}\n\n@Composable\nprivate fun FeedsDetail(\n    feeds: BlueskyFeeds.Feeds,\n    onCreatorClick: (BlueskyProfile) -> Unit,\n    onShareClick: (BlueskyFeeds.Feeds) -> Unit,\n    onLikeClick: () -> Unit,\n    onPinClick: () -> Unit,\n) {\n    FeedsBasicInfo(\n        name = feeds.displayName(),\n        authorHandle = feeds.creator.prettyHandle,\n        avatar = feeds.avatar,\n        description = feeds.description,\n        onShareClick = { onShareClick(feeds) },\n        onCreatorClick = { onCreatorClick(feeds.creator) },\n    )\n    Text(\n        modifier = Modifier.fillMaxWidth()\n            .padding(top = 8.dp, start = 16.dp, end = 16.dp),\n        text = stringResource(\n            LocalizedString.bsky_feeds_explorer_liked_by,\n            (feeds.likeCount ?: 0L).formatToHumanReadable(),\n        ),\n        style = MaterialTheme.typography.labelMedium,\n        maxLines = 1,\n        color = MaterialTheme.colorScheme.onSurfaceVariant,\n        textAlign = TextAlign.Start,\n        overflow = TextOverflow.Ellipsis,\n    )\n    Row(\n        modifier = Modifier.fillMaxWidth()\n            .padding(start = 16.dp, top = 8.dp, end = 16.dp),\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        IconButton(\n            onClick = onLikeClick,\n        ) {\n            Icon(\n                modifier = Modifier.size(24.dp),\n                imageVector = likeIcon(liked = feeds.liked),\n                contentDescription = likeAlt(),\n                tint = if (feeds.liked) {\n                    MaterialTheme.colorScheme.tertiary\n                } else {\n                    MaterialTheme.colorScheme.onSurfaceVariant\n                }\n            )\n        }\n        Spacer(modifier = Modifier.width(8.dp))\n        val pinBtnColors = if (feeds.pinned) {\n            ButtonDefaults.textButtonColors(\n                containerColor = MaterialTheme.colorScheme.secondaryContainer,\n                contentColor = Color.White,\n            )\n        } else {\n            ButtonDefaults.textButtonColors(\n                containerColor = MaterialTheme.colorScheme.primaryContainer,\n                contentColor = Color.White,\n            )\n        }\n        TextButton(\n            modifier = Modifier.weight(1F),\n            onClick = onPinClick,\n            colors = pinBtnColors,\n        ) {\n            Text(pinAlt(feeds.pinned))\n        }\n    }\n}\n\n\n@Composable\nprivate fun ListDetail(\n    feeds: BlueskyFeeds.List,\n    onShareClick: (BlueskyFeeds.List) -> Unit,\n) {\n    FeedsBasicInfo(\n        name = feeds.name,\n        avatar = feeds.avatar,\n        authorHandle = null,\n        description = feeds.description,\n        onShareClick = { onShareClick(feeds) },\n        onCreatorClick = {},\n    )\n}\n\n@Composable\nprivate fun FeedsBasicInfo(\n    avatar: String?,\n    name: String,\n    authorHandle: String?,\n    description: String?,\n    onShareClick: () -> Unit,\n    onCreatorClick: () -> Unit,\n) {\n    Row(\n        modifier = Modifier.fillMaxWidth().padding(start = 16.dp, end = 8.dp),\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        FeedsAvatar(\n            url = avatar,\n            modifier = Modifier,\n        )\n        Column(\n            modifier = Modifier.weight(1F).padding(start = 16.dp),\n        ) {\n            Text(\n                text = name,\n                maxLines = 1,\n                style = MaterialTheme.typography.titleMedium\n                    .copy(fontWeight = FontWeight.SemiBold),\n            )\n            if (!authorHandle.isNullOrEmpty()) {\n                val authorPrefix =\n                    stringResource(LocalizedString.bsky_feeds_detail_creator_prefix)\n                val creator = remember(authorHandle) {\n                    buildAnnotatedString {\n                        append(authorPrefix)\n                        append(\" \")\n                        append(authorHandle)\n                        addStyle(\n                            style = SpanStyle(textDecoration = TextDecoration.Underline),\n                            start = authorPrefix.length,\n                            end = length,\n                        )\n                    }\n                }\n                Text(\n                    modifier = Modifier.noRippleClick { onCreatorClick() }\n                        .padding(top = 1.dp),\n                    text = creator,\n                    maxLines = 1,\n                    color = MaterialTheme.colorScheme.onSurfaceVariant,\n                    style = MaterialTheme.typography.labelMedium,\n                )\n            }\n        }\n        IconButton(\n            onClick = { onShareClick() },\n        ) {\n            Icon(\n                imageVector = Icons.Default.Share,\n                contentDescription = \"Share\",\n            )\n        }\n    }\n    if (!description.isNullOrEmpty()) {\n        Text(\n            modifier = Modifier.padding(top = 8.dp, start = 16.dp, end = 16.dp),\n            text = description,\n            textAlign = TextAlign.Start,\n            style = MaterialTheme.typography.bodyMedium,\n        )\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/screen/feeds/detail/FeedsDetailUiState.kt",
    "content": "package com.zhangke.fread.bluesky.internal.screen.feeds.detail\n\nimport com.zhangke.fread.bluesky.internal.model.BlueskyFeeds\n\ndata class FeedsDetailUiState(\n    val feeds: BlueskyFeeds,\n) {\n\n    companion object {\n\n        fun default(feeds: BlueskyFeeds): FeedsDetailUiState {\n            return FeedsDetailUiState(\n                feeds = feeds,\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/screen/feeds/detail/FeedsDetailViewModel.kt",
    "content": "package com.zhangke.fread.bluesky.internal.screen.feeds.detail\n\nimport androidx.lifecycle.ViewModel\nimport app.bsky.feed.GetFeedGeneratorsQueryParams\nimport app.bsky.feed.Like\nimport com.atproto.repo.StrongRef\nimport com.zhangke.framework.composable.TextString\nimport com.zhangke.framework.composable.emitTextMessageFromThrowable\nimport com.zhangke.framework.ktx.launchInViewModel\nimport com.zhangke.fread.bluesky.internal.adapter.BlueskyFeedsAdapter\nimport com.zhangke.fread.bluesky.internal.client.BlueskyClientManager\nimport com.zhangke.fread.bluesky.internal.client.BskyCollections\nimport com.zhangke.fread.bluesky.internal.client.adjustToRkey\nimport com.zhangke.fread.bluesky.internal.model.BlueskyFeeds\nimport com.zhangke.fread.bluesky.internal.usecase.CreateRecordUseCase\nimport com.zhangke.fread.bluesky.internal.usecase.DeleteRecordUseCase\nimport com.zhangke.fread.bluesky.internal.usecase.PinFeedsUseCase\nimport com.zhangke.fread.bluesky.internal.usecase.UnpinFeedsUseCase\nimport com.zhangke.fread.bluesky.internal.utils.bskyJson\nimport com.zhangke.fread.status.model.PlatformLocator\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asSharedFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.update\nimport kotlinx.datetime.Clock\nimport sh.christian.ozone.api.AtUri\nimport sh.christian.ozone.api.Cid\nimport kotlin.time.ExperimentalTime\n\nclass FeedsDetailViewModel(\n    private val clientManager: BlueskyClientManager,\n    private val feedsAdapter: BlueskyFeedsAdapter,\n    private val createRecord: CreateRecordUseCase,\n    private val deleteRecord: DeleteRecordUseCase,\n    private val followFeeds: PinFeedsUseCase,\n    private val unfollowFeeds: UnpinFeedsUseCase,\n) : ViewModel() {\n\n    private val _uiState = MutableStateFlow<FeedsDetailUiState?>(null)\n    val uiState = _uiState.asStateFlow()\n\n    private val _snackBarMessageFlow = MutableSharedFlow<TextString>()\n    val snackBarMessageFlow = _snackBarMessageFlow.asSharedFlow()\n\n    private val _feedsUpdateFlow = MutableSharedFlow<BlueskyFeeds.Feeds>()\n    val feedsUpdateFlow = _feedsUpdateFlow.asSharedFlow()\n\n    private var likeJob: Job? = null\n    private var pinJob: Job? = null\n\n    private var locator: PlatformLocator? = null\n\n    fun initialize(locator: PlatformLocator, feeds: BlueskyFeeds) {\n        this.locator = locator\n        _uiState.value = FeedsDetailUiState.default(feeds)\n        getFeedsDetail()\n    }\n\n    fun clearState() {\n        likeJob?.cancel()\n        pinJob?.cancel()\n        locator = null\n        _uiState.value = null\n    }\n\n    @OptIn(ExperimentalTime::class)\n    fun onLikeClick() {\n        if (likeJob?.isActive == true) return\n        val feeds = _uiState.value?.feeds\n        if (feeds !is BlueskyFeeds.Feeds) return\n        val locator = locator ?: return\n        likeJob?.cancel()\n        likeJob = launchInViewModel {\n            if (feeds.liked) {\n                deleteRecord(\n                    locator = locator,\n                    collection = BskyCollections.feedLike,\n                    rkey = feeds.likedRecord!!.adjustToRkey(),\n                )\n            } else {\n                createRecord(\n                    locator = locator,\n                    collection = BskyCollections.feedLike,\n                    record = Like(\n                        subject = StrongRef(\n                            uri = AtUri(feeds.uri),\n                            cid = Cid(feeds.cid),\n                        ),\n                        createdAt = Clock.System.now(),\n                    ).bskyJson()\n                )\n            }.onSuccess {\n                getFeedsDetail()\n            }.onFailure {\n                _snackBarMessageFlow.emitTextMessageFromThrowable(it)\n            }\n        }\n    }\n\n    fun onPinClick() {\n        val feeds = _uiState.value?.feeds\n        if (feeds !is BlueskyFeeds.Feeds) return\n        if (pinJob?.isActive == true) return\n        val locator = locator ?: return\n        pinJob?.cancel()\n        pinJob = launchInViewModel {\n            if (feeds.pinned) {\n                unfollowFeeds(locator = locator, feeds = feeds)\n            } else {\n                followFeeds(locator = locator, feeds = feeds)\n            }.onSuccess {\n                val newFeeds = feeds.copy(pinned = !feeds.pinned)\n                _uiState.update { state ->\n                    state?.copy(feeds = newFeeds) ?: FeedsDetailUiState.default(newFeeds)\n                }\n                _feedsUpdateFlow.emit(newFeeds)\n            }.onFailure {\n                _snackBarMessageFlow.emitTextMessageFromThrowable(it)\n            }\n        }\n    }\n\n    private fun getFeedsDetail() {\n        launchInViewModel {\n            val locator = locator ?: return@launchInViewModel\n            val feeds = _uiState.value?.feeds\n            if (feeds !is BlueskyFeeds.Feeds) return@launchInViewModel\n            clientManager.getClient(locator)\n                .getFeedGeneratorsCatching(GetFeedGeneratorsQueryParams(listOf(AtUri(feeds.uri))))\n                .onSuccess { response ->\n                    response.feeds.firstOrNull { it.uri.atUri == feeds.uri }\n                        ?.let { generatorView ->\n                            feedsAdapter.convertToFeeds(\n                                generator = generatorView,\n                                pinned = feeds.pinned,\n                            )\n                        }?.let { feed ->\n                            _uiState.update {\n                                it?.copy(feeds = feed) ?: FeedsDetailUiState.default(feeds)\n                            }\n                            _feedsUpdateFlow.emit(feed)\n                        }\n                }\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/screen/feeds/explorer/ExplorerFeedsScreen.kt",
    "content": "package com.zhangke.fread.bluesky.internal.screen.feeds.explorer\n\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.HorizontalDivider\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.ScaffoldDefaults\nimport androidx.compose.material3.SnackbarHost\nimport androidx.compose.material3.SnackbarHostState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.blur.applyBlurSource\nimport com.zhangke.framework.composable.ConsumeSnackbarFlow\nimport com.zhangke.framework.composable.LocalContentPadding\nimport com.zhangke.framework.composable.Toolbar\nimport com.zhangke.framework.composable.currentOrThrow\nimport com.zhangke.framework.composable.rememberSnackbarHostState\nimport com.zhangke.framework.composable.rememberTransientModalBottomSheetState\nimport com.zhangke.framework.loadable.lazycolumn.LoadableLazyColumn\nimport com.zhangke.framework.loadable.lazycolumn.rememberLoadableLazyColumnState\nimport com.zhangke.framework.nav.LocalNavBackStack\nimport com.zhangke.fread.bluesky.internal.composable.BlueskyExploringFeeds\nimport com.zhangke.fread.bluesky.internal.screen.feeds.detail.FeedsDetailBottomSheet\nimport com.zhangke.fread.bluesky.internal.screen.feeds.detail.rememberFeedsDetailBottomSheetState\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.ui.placeholder.TitleWithAvatarItemPlaceholder\nimport kotlinx.serialization.Serializable\nimport org.jetbrains.compose.resources.stringResource\nimport org.koin.compose.viewmodel.koinViewModel\nimport org.koin.core.parameter.parametersOf\n\n@Serializable\ndata class ExplorerFeedsScreenNavKey(\n    val locator: PlatformLocator,\n) : NavKey\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun ExplorerFeedsScreen(\n    locator: PlatformLocator,\n    inlineMode: Boolean = false,\n) {\n    val backStack = LocalNavBackStack.currentOrThrow\n    val viewModel: ExplorerFeedsViewModel = koinViewModel { parametersOf(locator) }\n    val uiState by viewModel.uiState.collectAsState()\n    val snackBarState = rememberSnackbarHostState()\n\n    val feedsDetailBottomSheetState = rememberFeedsDetailBottomSheetState()\n    val feedsDetailSheetState = rememberTransientModalBottomSheetState()\n    FeedsDetailBottomSheet(\n        state = feedsDetailBottomSheetState,\n        sheetState = feedsDetailSheetState,\n        onFeedsUpdate = viewModel::onFeedsUpdate,\n    )\n    ExplorerFeedsContent(\n        uiState = uiState,\n        snackBarState = snackBarState,\n        inlineMode = inlineMode,\n        onBackClick = backStack::removeLastOrNull,\n        onRefresh = viewModel::onRefresh,\n        onLoadMore = viewModel::onLoadMore,\n        onFeedsClick = { feeds ->\n            feedsDetailBottomSheetState.show(locator = locator, feeds = feeds.feeds)\n        },\n        onFollowClick = viewModel::onFollowClick,\n    )\n    ConsumeSnackbarFlow(snackBarState, viewModel.snackBarMessage)\n}\n\n@Composable\nprivate fun ExplorerFeedsContent(\n    uiState: ExplorerFeedsUiState,\n    snackBarState: SnackbarHostState,\n    inlineMode: Boolean,\n    onBackClick: () -> Unit,\n    onRefresh: () -> Unit,\n    onLoadMore: () -> Unit,\n    onFollowClick: (BlueskyFeedsUiState) -> Unit,\n    onFeedsClick: (BlueskyFeedsUiState) -> Unit,\n) {\n    Scaffold(\n        modifier = Modifier.fillMaxSize(),\n        topBar = {\n            if (!inlineMode) {\n                Toolbar(\n                    title = stringResource(LocalizedString.bsky_feeds_explorer_more),\n                    onBackClick = onBackClick,\n                )\n            }\n        },\n        contentWindowInsets = if (inlineMode) {\n            WindowInsets()\n        } else {\n            ScaffoldDefaults.contentWindowInsets\n        },\n        snackbarHost = {\n            SnackbarHost(\n                modifier = Modifier.padding(LocalContentPadding.current),\n                hostState = snackBarState,\n            )\n        },\n    ) { innerPadding ->\n        if (uiState.initializing) {\n            InitializingPlaceholder(\n                modifier = Modifier.padding(innerPadding)\n                    .padding(LocalContentPadding.current),\n            )\n        } else {\n            val loadableState = rememberLoadableLazyColumnState(\n                onRefresh = onRefresh,\n                onLoadMore = onLoadMore,\n            )\n            LoadableLazyColumn(\n                modifier = Modifier\n                    .fillMaxSize()\n                    .padding(innerPadding),\n                state = loadableState,\n                lazyColumnModifier = Modifier.applyBlurSource(),\n                contentPadding = LocalContentPadding.current,\n                loadState = uiState.loadMoreState,\n                refreshing = uiState.refreshing,\n            ) {\n                items(uiState.feeds) { item ->\n                    BlueskyExploringFeeds(\n                        modifier = Modifier.fillMaxSize(),\n                        feeds = item.feeds,\n                        loading = item.followRequesting,\n                        onAddClick = { onFollowClick(item) },\n                        onFeedsClick = { onFeedsClick(item) },\n                    )\n                    HorizontalDivider(\n                        modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp)\n                    )\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun InitializingPlaceholder(modifier: Modifier) {\n    Column(\n        modifier = modifier.fillMaxSize(),\n    ) {\n        repeat(30) {\n            TitleWithAvatarItemPlaceholder(Modifier.fillMaxWidth())\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/screen/feeds/explorer/ExplorerFeedsUiState.kt",
    "content": "package com.zhangke.fread.bluesky.internal.screen.feeds.explorer\n\nimport com.zhangke.framework.utils.LoadState\nimport com.zhangke.fread.bluesky.internal.model.BlueskyFeeds\n\ndata class ExplorerFeedsUiState(\n    val initializing: Boolean,\n    val pageError: Throwable?,\n    val refreshing: Boolean,\n    val loadMoreState: LoadState,\n    val feeds: List<BlueskyFeedsUiState>,\n) {\n\n    companion object {\n\n        fun default(): ExplorerFeedsUiState {\n            return ExplorerFeedsUiState(\n                initializing = false,\n                pageError = null,\n                refreshing = false,\n                loadMoreState = LoadState.Idle,\n                feeds = emptyList(),\n            )\n        }\n    }\n}\n\ndata class BlueskyFeedsUiState(\n    val followRequesting: Boolean,\n    val feeds: BlueskyFeeds.Feeds,\n)\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/screen/feeds/explorer/ExplorerFeedsViewModel.kt",
    "content": "package com.zhangke.fread.bluesky.internal.screen.feeds.explorer\n\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport app.bsky.actor.PreferencesUnion.SavedFeedsPrefV2\nimport app.bsky.feed.GetSuggestedFeedsQueryParams\nimport com.zhangke.framework.collections.updateItem\nimport com.zhangke.framework.composable.TextString\nimport com.zhangke.framework.composable.emitTextMessageFromThrowable\nimport com.zhangke.framework.composable.toTextStringOrNull\nimport com.zhangke.framework.ktx.launchInViewModel\nimport com.zhangke.framework.utils.LoadState\nimport com.zhangke.framework.utils.exceptionOrThrow\nimport com.zhangke.fread.bluesky.internal.adapter.BlueskyFeedsAdapter\nimport com.zhangke.fread.bluesky.internal.client.BlueskyClientManager\nimport com.zhangke.fread.bluesky.internal.model.BlueskyFeeds\nimport com.zhangke.fread.bluesky.internal.usecase.PinFeedsUseCase\nimport com.zhangke.fread.status.model.PlatformLocator\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.async\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asSharedFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.update\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.supervisorScope\nclass ExplorerFeedsViewModel(\n    private val clientManager: BlueskyClientManager,\n    private val feedsAdapter: BlueskyFeedsAdapter,\n    private val followFeeds: PinFeedsUseCase,\n    private val locator: PlatformLocator,\n) : ViewModel() {\n\n    companion object {\n\n        private const val FLAG_CURSOR_ENDING = \"flag_cursor_ending_for_suggested_feeds\"\n    }\n\n    private val _uiState = MutableStateFlow(ExplorerFeedsUiState.default())\n    val uiState = _uiState.asStateFlow()\n\n    private val _snackBarMessage = MutableSharedFlow<TextString>()\n    val snackBarMessage = _snackBarMessage.asSharedFlow()\n\n    private var refreshMoreJob: Job? = null\n    private var loadMoreJob: Job? = null\n    private var cursor: String? = null\n\n    private val pinnedFeedsUris = mutableListOf<String>()\n\n    init {\n        launchInViewModel {\n            _uiState.update { it.copy(initializing = true) }\n            getSuggestedFeeds(null)\n                .onSuccess { list ->\n                    _uiState.update {\n                        it.copy(\n                            initializing = false,\n                            feeds = list,\n                        )\n                    }\n                }.onFailure { t ->\n                    _uiState.update { it.copy(initializing = false, pageError = t) }\n                }\n        }\n    }\n\n    fun onRefresh() {\n        if (refreshMoreJob?.isActive == true) return\n        refreshMoreJob?.cancel()\n        refreshMoreJob = viewModelScope.launch {\n            _uiState.update { it.copy(refreshing = true) }\n            getSuggestedFeeds(null)\n                .onSuccess { list ->\n                    _uiState.update {\n                        it.copy(\n                            refreshing = false,\n                            feeds = list,\n                        )\n                    }\n                }.onFailure { t ->\n                    _uiState.update { it.copy(refreshing = false) }\n                    _snackBarMessage.emitTextMessageFromThrowable(t)\n                }\n        }\n    }\n\n    fun onLoadMore() {\n        if (cursor == FLAG_CURSOR_ENDING) return\n        if (loadMoreJob?.isActive == true) return\n        loadMoreJob?.cancel()\n        loadMoreJob = viewModelScope.launch {\n            _uiState.update { it.copy(loadMoreState = LoadState.Loading) }\n            getSuggestedFeeds()\n                .onSuccess { list ->\n                    _uiState.update {\n                        it.copy(\n                            loadMoreState = LoadState.Idle,\n                            feeds = it.feeds + list,\n                        )\n                    }\n                }.onFailure { t ->\n                    _uiState.update {\n                        it.copy(loadMoreState = LoadState.Failed(t.toTextStringOrNull()))\n                    }\n                }\n        }\n    }\n\n    private suspend fun getSuggestedFeeds(cursor: String? = this.cursor): Result<List<BlueskyFeedsUiState>> {\n        val client = clientManager.getClient(locator)\n        return supervisorScope {\n            val pinnedFeedsDeferred = async { getPinnedFeeds() }\n            val feedsListDeferred = async {\n                client.getSuggestedFeedsCatching(GetSuggestedFeedsQueryParams(cursor = cursor))\n                    .onSuccess {\n                        this@ExplorerFeedsViewModel.cursor =\n                            if (it.cursor.isNullOrBlank()) FLAG_CURSOR_ENDING else it.cursor\n                    }\n            }\n            val pinnedFeeds = pinnedFeedsDeferred.await().getOrNull() ?: emptyList()\n            feedsListDeferred.await().map {\n                it.feeds.map { item ->\n                    BlueskyFeedsUiState(\n                        followRequesting = false,\n                        feeds = feedsAdapter.convertToFeeds(\n                            generator = item,\n                            pinned = pinnedFeeds.any { uri -> uri == item.uri.atUri },\n                        ),\n                    )\n                }\n            }\n        }\n    }\n\n    fun onFollowClick(feedsUiState: BlueskyFeedsUiState) {\n        if (feedsUiState.followRequesting) return\n        viewModelScope.launch {\n            _uiState.update { state ->\n                state.copy(\n                    feeds = state.feeds.updateItem(feedsUiState) {\n                        it.copy(followRequesting = true)\n                    },\n                )\n            }\n            followFeeds(locator, feedsUiState.feeds)\n                .onSuccess {\n                    _uiState.update { state ->\n                        state.copy(\n                            feeds = state.feeds.map { item ->\n                                if (item.feeds.uri == feedsUiState.feeds.uri) {\n                                    item.copy(\n                                        feeds = item.feeds.copy(pinned = true),\n                                        followRequesting = false,\n                                    )\n                                } else {\n                                    item\n                                }\n                            },\n                        )\n                    }\n                    pinnedFeedsUris += feedsUiState.feeds.uri\n                }.onFailure { t ->\n                    _uiState.update { state ->\n                        state.copy(\n                            feeds = state.feeds.map { item ->\n                                if (item.feeds.uri == feedsUiState.feeds.uri) {\n                                    item.copy(followRequesting = false)\n                                } else {\n                                    item\n                                }\n                            },\n                        )\n                    }\n                    _snackBarMessage.emitTextMessageFromThrowable(t)\n                }\n        }\n    }\n\n    fun onFeedsUpdate(feeds: BlueskyFeeds.Feeds) {\n        _uiState.update { state ->\n            state.copy(\n                feeds = state.feeds.map {\n                    if (it.feeds.cid == feeds.cid) {\n                        it.copy(feeds = feeds)\n                    } else {\n                        it\n                    }\n                }\n            )\n        }\n        if (feeds.pinned) {\n            pinnedFeedsUris += feeds.uri\n        } else {\n            pinnedFeedsUris -= feeds.uri\n        }\n    }\n\n    private suspend fun getPinnedFeeds(): Result<List<String>> {\n        if (pinnedFeedsUris.isNotEmpty()) return Result.success(pinnedFeedsUris)\n        val preferenceResult = clientManager.getClient(locator).getPreferencesCatching()\n        if (preferenceResult.isFailure) return Result.failure(preferenceResult.exceptionOrThrow())\n        return preferenceResult.map {\n            it.preferences.filterIsInstance<SavedFeedsPrefV2>()\n                .firstOrNull()\n                ?.value\n                ?.items\n                ?: emptyList()\n        }.map { feeds -> feeds.map { it.value } }\n            .onSuccess { pinnedFeedsUris += it }\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/screen/feeds/following/BskyFollowingFeedsPage.kt",
    "content": "package com.zhangke.fread.bluesky.internal.screen.feeds.following\n\nimport androidx.compose.animation.core.animateDpAsState\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.material.ExperimentalMaterialApi\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Delete\nimport androidx.compose.material.icons.filled.Explore\nimport androidx.compose.material.pullrefresh.pullRefresh\nimport androidx.compose.material.pullrefresh.rememberPullRefreshState\nimport androidx.compose.material3.CircularProgressIndicator\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.SnackbarHost\nimport androidx.compose.material3.SnackbarHostState\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.key\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.composable.ConsumeFlow\nimport com.zhangke.framework.composable.ConsumeSnackbarFlow\nimport com.zhangke.framework.composable.DefaultFailed\nimport com.zhangke.framework.composable.FreadDialog\nimport com.zhangke.framework.composable.SimpleIconButton\nimport com.zhangke.framework.composable.Toolbar\nimport com.zhangke.framework.composable.currentOrThrow\nimport com.zhangke.framework.composable.noRippleClick\nimport com.zhangke.framework.composable.rememberSnackbarHostState\nimport com.zhangke.framework.composable.rememberTransientModalBottomSheetState\nimport com.zhangke.framework.nav.LocalNavBackStack\nimport com.zhangke.fread.bluesky.internal.composable.BlueskyFollowingFeeds\nimport com.zhangke.fread.bluesky.internal.model.BlueskyFeeds\nimport com.zhangke.fread.bluesky.internal.screen.feeds.detail.FeedsDetailBottomSheet\nimport com.zhangke.fread.bluesky.internal.screen.feeds.detail.rememberFeedsDetailBottomSheetState\nimport com.zhangke.fread.bluesky.internal.screen.feeds.explorer.ExplorerFeedsScreenNavKey\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.ui.placeholder.TitleWithAvatarItemPlaceholder\nimport kotlinx.serialization.Serializable\nimport org.burnoutcrew.reorderable.ReorderableItem\nimport org.burnoutcrew.reorderable.detectReorderAfterLongPress\nimport org.burnoutcrew.reorderable.rememberReorderableLazyListState\nimport org.burnoutcrew.reorderable.reorderable\nimport org.jetbrains.compose.resources.stringResource\n\n@Serializable\ndata class BskyFollowingFeedsPageNavKey(\n    val contentId: String?,\n    val locator: PlatformLocator?,\n) : NavKey\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun BskyFollowingFeedsPage(viewModel: BskyFollowingFeedsViewModel) {\n    val backStack = LocalNavBackStack.currentOrThrow\n    val uiState by viewModel.uiState.collectAsState()\n    val snackBarState = rememberSnackbarHostState()\n\n    val feedsDetailBottomSheetState = rememberFeedsDetailBottomSheetState()\n    val feedsDetailSheetState = rememberTransientModalBottomSheetState()\n    FeedsDetailBottomSheet(\n        state = feedsDetailBottomSheetState,\n        sheetState = feedsDetailSheetState,\n        onFeedsUpdate = viewModel::onFeedsUpdate,\n    )\n    BskyFeedsExplorerContent(\n        uiState = uiState,\n        snackBarState = snackBarState,\n        onBackClick = backStack::removeLastOrNull,\n        onRefresh = viewModel::onRefresh,\n        onFeedsClick = { feed ->\n            uiState.locator?.let { locator ->\n                feedsDetailBottomSheetState.show(locator = locator, feeds = feed)\n            }\n        },\n        onExplorerClick = {\n            uiState.locator?.let { backStack.add(ExplorerFeedsScreenNavKey(it)) }\n        },\n        onFeedsReorder = viewModel::onFeedsOrderChanged,\n        onDeleteClick = viewModel::onDeleteClick,\n    )\n\n    ConsumeSnackbarFlow(snackBarState, viewModel.snackBarMessage)\n\n    ConsumeFlow(viewModel.finishPageFlow) {\n        backStack.removeLastOrNull()\n    }\n\n    LaunchedEffect(Unit) {\n        viewModel.onPageResume()\n    }\n}\n\n@OptIn(ExperimentalMaterialApi::class)\n@Composable\nprivate fun BskyFeedsExplorerContent(\n    uiState: BskyFeedsExplorerUiState,\n    snackBarState: SnackbarHostState,\n    onBackClick: () -> Unit,\n    onRefresh: () -> Unit,\n    onFeedsClick: (BlueskyFeeds) -> Unit,\n    onExplorerClick: () -> Unit,\n    onFeedsReorder: (Int, Int) -> Unit,\n    onDeleteClick: () -> Unit,\n) {\n    var showDeleteConfirmDialog by remember {\n        mutableStateOf(false)\n    }\n    Scaffold(\n        modifier = Modifier.fillMaxSize(),\n        topBar = {\n            Toolbar(\n                title = stringResource(LocalizedString.feeds),\n                onBackClick = onBackClick,\n                actions = {\n                    SimpleIconButton(\n                        onClick = onExplorerClick,\n                        imageVector = Icons.Default.Explore,\n                        contentDescription = stringResource(LocalizedString.bsky_feeds_explorer_more),\n                    )\n\n                    SimpleIconButton(\n                        onClick = { showDeleteConfirmDialog = true },\n                        imageVector = Icons.Default.Delete,\n                        contentDescription = \"Delete content\",\n                    )\n                }\n            )\n        },\n        snackbarHost = {\n            SnackbarHost(hostState = snackBarState)\n        },\n    ) { innerPadding ->\n        if (uiState.initializing && uiState.followingFeeds.isEmpty()) {\n            InitializingPlaceholder(modifier = Modifier.padding(innerPadding))\n        } else {\n            val pullRefreshState = rememberPullRefreshState(\n                refreshing = uiState.refreshing,\n                onRefresh = onRefresh,\n            )\n            var feedsInUi by remember(uiState.followingFeeds) {\n                mutableStateOf(uiState.followingFeeds)\n            }\n            key(uiState.followingFeeds) {\n                val state = rememberReorderableLazyListState(\n                    onMove = { from, to ->\n                        if (feedsInUi.isEmpty()) return@rememberReorderableLazyListState\n                        feedsInUi = feedsInUi.toMutableList().apply {\n                            if (from.index <= feedsInUi.lastIndex) {\n                                if (to.index > feedsInUi.lastIndex) {\n                                    add(removeAt(from.index))\n                                } else {\n                                    add(to.index, removeAt(from.index))\n                                }\n                            }\n                        }\n                    },\n                    onDragEnd = { startIndex, endIndex ->\n                        onFeedsReorder(startIndex, endIndex)\n                    },\n                )\n                LazyColumn(\n                    modifier = Modifier.fillMaxSize()\n                        .padding(innerPadding)\n                        .pullRefresh(pullRefreshState)\n                        .reorderable(state)\n                        .detectReorderAfterLongPress(state),\n                    state = state.listState,\n                ) {\n                    if (uiState.pageError != null) {\n                        item {\n                            Box(modifier = Modifier.fillMaxSize()) {\n                                DefaultFailed(\n                                    modifier = Modifier.fillMaxSize(),\n                                    exception = uiState.pageError,\n                                )\n                            }\n                        }\n                    } else {\n                        if (feedsInUi.isNotEmpty()) {\n                            items(\n                                items = feedsInUi,\n                                key = { it.uiKey },\n                            ) { feed ->\n                                ReorderableItem(\n                                    state = state,\n                                    key = feed.uiKey,\n                                ) { dragging ->\n                                    val elevation by animateDpAsState(\n                                        targetValue = if (dragging) 16.dp else 0.dp,\n                                        label = \"BskyPinnedFeedsItemElevation\",\n                                    )\n                                    Surface(\n                                        modifier = Modifier\n                                            .fillMaxWidth(),\n                                        shadowElevation = elevation,\n                                    ) {\n                                        BlueskyFollowingFeeds(\n                                            modifier = Modifier.fillMaxSize(),\n                                            feeds = feed,\n                                            onFeedsClick = onFeedsClick,\n                                        )\n                                    }\n                                }\n                            }\n                            item {\n                                Box(modifier = Modifier.fillMaxWidth()) {\n                                    TextButton(\n                                        modifier = Modifier.padding(top = 16.dp, bottom = 32.dp)\n                                            .align(Alignment.Center),\n                                        onClick = onExplorerClick,\n                                    ) {\n                                        Text(\n                                            text = stringResource(LocalizedString.bsky_feeds_explorer_more)\n                                        )\n                                    }\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n        }\n\n        if (uiState.reordering) {\n            Box(\n                modifier = Modifier.fillMaxSize()\n                    .noRippleClick { }\n                    .background(color = MaterialTheme.colorScheme.surface.copy(alpha = 0.6F)),\n                contentAlignment = Alignment.Center,\n            ) {\n                CircularProgressIndicator(\n                    modifier = Modifier.size(64.dp),\n                )\n            }\n        }\n    }\n    if (showDeleteConfirmDialog) {\n        FreadDialog(\n            onDismissRequest = { showDeleteConfirmDialog = false },\n            contentText = stringResource(LocalizedString.statusUiEditContentDeleteDialogContent),\n            onNegativeClick = {\n                showDeleteConfirmDialog = false\n            },\n            onPositiveClick = {\n                showDeleteConfirmDialog = false\n                onDeleteClick()\n            },\n        )\n    }\n}\n\nprivate val BlueskyFeeds.uiKey: String get() = \"${this::class.simpleName}@${this.id}\"\n\n@Composable\nprivate fun InitializingPlaceholder(modifier: Modifier) {\n    Column(\n        modifier = modifier.fillMaxSize(),\n    ) {\n        repeat(30) {\n            TitleWithAvatarItemPlaceholder(Modifier.fillMaxWidth())\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/screen/feeds/following/BskyFollowingFeedsUiState.kt",
    "content": "package com.zhangke.fread.bluesky.internal.screen.feeds.following\n\nimport com.zhangke.fread.bluesky.internal.model.BlueskyFeeds\nimport com.zhangke.fread.status.model.PlatformLocator\n\ndata class BskyFeedsExplorerUiState(\n    val initializing: Boolean,\n    val locator: PlatformLocator?,\n    val pageError: Throwable?,\n    val refreshing: Boolean,\n    val followingFeeds: List<BlueskyFeeds>,\n    val reordering: Boolean,\n) {\n\n    companion object {\n\n        fun default(): BskyFeedsExplorerUiState {\n            return BskyFeedsExplorerUiState(\n                initializing = false,\n                refreshing = false,\n                locator = null,\n                pageError = null,\n                followingFeeds = emptyList(),\n                reordering = false,\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/screen/feeds/following/BskyFollowingFeedsViewModel.kt",
    "content": "package com.zhangke.fread.bluesky.internal.screen.feeds.following\n\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.zhangke.framework.composable.TextString\nimport com.zhangke.framework.composable.emitTextMessageFromThrowable\nimport com.zhangke.framework.ktx.launchInViewModel\nimport com.zhangke.framework.utils.exceptionOrThrow\nimport com.zhangke.fread.bluesky.internal.account.BlueskyLoggedAccountManager\nimport com.zhangke.fread.bluesky.internal.content.BlueskyContent\nimport com.zhangke.fread.bluesky.internal.model.BlueskyFeeds\nimport com.zhangke.fread.bluesky.internal.usecase.GetFollowingFeedsUseCase\nimport com.zhangke.fread.bluesky.internal.usecase.UpdatePinnedFeedsOrderUseCase\nimport com.zhangke.fread.common.content.FreadContentRepo\nimport com.zhangke.fread.status.model.PlatformLocator\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asSharedFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.update\nimport kotlinx.coroutines.launch\nclass BskyFollowingFeedsViewModel(\n    private val getFollowingFeeds: GetFollowingFeedsUseCase,\n    private val contentRepo: FreadContentRepo,\n    private val updatePinnedFeedsOrder: UpdatePinnedFeedsOrderUseCase,\n    private val accountManager: BlueskyLoggedAccountManager,\n    private val contentId: String?,\n    private val locator: PlatformLocator?,\n) : ViewModel() {\n\n    private val _uiState = MutableStateFlow(BskyFeedsExplorerUiState.default())\n    val uiState = _uiState.asStateFlow()\n\n    private val _snackBarMessage = MutableSharedFlow<TextString>()\n    val snackBarMessage = _snackBarMessage.asSharedFlow()\n\n    private val _finishPageFlow = MutableSharedFlow<Unit>()\n    val finishPageFlow = _finishPageFlow.asSharedFlow()\n\n    private var initJob: Job? = null\n\n    private var cachedLocator: PlatformLocator? = locator\n\n    init {\n        loadFeedsList(false)\n    }\n\n    fun onRefresh() {\n        loadFeedsList(true)\n    }\n\n    fun onPageResume() {\n        loadFeedsList(false)\n    }\n\n    fun onFeedsUpdate(feeds: BlueskyFeeds) {\n        val feedsList = _uiState.value.followingFeeds\n        val exists = feedsList.any { it.id == feeds.id }\n        val newFeedsList = if (feeds.pinned) {\n            if (exists) {\n                feedsList.map {\n                    if (it.id == feeds.id) {\n                        feeds\n                    } else {\n                        it\n                    }\n                }\n            } else {\n                feedsList + feeds\n            }\n        } else {\n            if (exists) {\n                feedsList.filter { it.id != feeds.id }\n            } else {\n                feedsList\n            }\n        }\n        _uiState.update { it.copy(followingFeeds = newFeedsList) }\n    }\n\n    private fun loadFeedsList(refreshing: Boolean) {\n        if (initJob?.isActive == true) return\n        initJob?.cancel()\n        initJob = viewModelScope.launch {\n            _uiState.update {\n                it.copy(\n                    initializing = !refreshing,\n                    refreshing = refreshing,\n                    pageError = null,\n                )\n            }\n            val locatorResult = getLocator()\n            if (locatorResult.isFailure) {\n                _uiState.update {\n                    it.copy(\n                        initializing = false,\n                        refreshing = false,\n                        pageError = locatorResult.exceptionOrNull(),\n                    )\n                }\n                return@launch\n            }\n            val locator = locatorResult.getOrThrow()\n            _uiState.update { it.copy(locator = locator) }\n            getFollowingFeeds(locator)\n                .onSuccess { list ->\n                    _uiState.update {\n                        it.copy(\n                            initializing = false,\n                            refreshing = false,\n                            followingFeeds = list,\n                        )\n                    }\n                }.onFailure { t ->\n                    _uiState.update {\n                        it.copy(\n                            initializing = false,\n                            refreshing = false,\n                            pageError = t,\n                        )\n                    }\n                }\n        }\n    }\n\n    fun onFeedsOrderChanged(startIndex: Int, endIndex: Int) {\n        val followingFeeds = _uiState.value.followingFeeds.toMutableList()\n        if (startIndex > followingFeeds.lastIndex || endIndex > followingFeeds.lastIndex) {\n            return\n        }\n        launchInViewModel {\n            _uiState.update { it.copy(reordering = true) }\n            val locatorResult = getLocator()\n            if (locatorResult.isFailure) {\n                _uiState.update { it.copy(reordering = false) }\n                _snackBarMessage.emitTextMessageFromThrowable(locatorResult.exceptionOrThrow())\n                return@launchInViewModel\n            }\n            val locator = locatorResult.getOrThrow()\n            if (endIndex > followingFeeds.lastIndex) {\n                followingFeeds.add(followingFeeds.removeAt(startIndex))\n            } else {\n                followingFeeds.add(endIndex, followingFeeds.removeAt(startIndex))\n            }\n            updatePinnedFeedsOrder(\n                locator = locator,\n                feeds = followingFeeds,\n            ).onSuccess {\n                _uiState.update { it.copy(reordering = false) }\n                loadFeedsList(false)\n            }.onFailure {\n                _uiState.update { it.copy(reordering = false) }\n                _snackBarMessage.emitTextMessageFromThrowable(it)\n            }\n        }\n    }\n\n    fun onDeleteClick() {\n        launchInViewModel {\n            if (!contentId.isNullOrEmpty()) {\n                contentRepo.delete(contentId)\n            } else {\n                contentRepo.getAllContent().filterIsInstance<BlueskyContent>()\n                    .firstOrNull { it.baseUrl == locator?.baseUrl }\n                    ?.let { contentRepo.delete(it.id) }\n            }\n            getLocator().onSuccess { role ->\n                accountManager.getAllAccount().firstOrNull {\n                    it.fromPlatform.baseUrl == role.baseUrl\n                }?.let {\n                    accountManager.logout(it.uri)\n                }\n            }\n            _finishPageFlow.emit(Unit)\n        }\n    }\n\n    private suspend fun getLocator(): Result<PlatformLocator> {\n        if (cachedLocator != null) return Result.success(cachedLocator!!)\n        val content = contentId?.let { contentRepo.getContent(it) }?.let { it as? BlueskyContent }\n        if (content == null) return Result.failure(IllegalArgumentException(\"Content not found $contentId\"))\n        return Result.success(\n            PlatformLocator(\n                accountUri = content.accountUri,\n                baseUrl = content.baseUrl,\n            )\n        ).onSuccess { this.cachedLocator = it }\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/screen/feeds/home/HomeFeedsContainerViewModel.kt",
    "content": "package com.zhangke.fread.bluesky.internal.screen.feeds.home\n\nimport com.zhangke.framework.lifecycle.ContainerViewModel\nimport com.zhangke.fread.bluesky.internal.model.BlueskyFeeds\nimport com.zhangke.fread.bluesky.internal.usecase.GetFeedsStatusUseCase\nimport com.zhangke.fread.common.adapter.StatusUiStateAdapter\nimport com.zhangke.fread.common.status.StatusUpdater\nimport com.zhangke.fread.commonbiz.shared.usecase.RefactorToNewStatusUseCase\nimport com.zhangke.fread.status.StatusProvider\nimport com.zhangke.fread.status.model.PlatformLocator\n\nclass HomeFeedsContainerViewModel(\n    private val statusUiStateAdapter: StatusUiStateAdapter,\n    private val statusUpdater: StatusUpdater,\n    private val statusProvider: StatusProvider,\n    private val refactorToNewStatus: RefactorToNewStatusUseCase,\n    private val getFeedsStatus: GetFeedsStatusUseCase,\n) : ContainerViewModel<HomeFeedsViewModel, HomeFeedsContainerViewModel.Params>() {\n\n    fun getViewModel(\n        feeds: BlueskyFeeds,\n        locator: PlatformLocator,\n    ): HomeFeedsViewModel {\n        return obtainSubViewModel(\n            Params(feeds = feeds, locator = locator)\n        )\n    }\n\n    override fun createSubViewModel(params: Params): HomeFeedsViewModel {\n        return HomeFeedsViewModel(\n            statusUiStateAdapter = statusUiStateAdapter,\n            statusProvider = statusProvider,\n            getFeedsStatus = getFeedsStatus,\n            statusUpdater = statusUpdater,\n            refactorToNewStatus = refactorToNewStatus,\n            feeds = params.feeds,\n            locator = params.locator,\n        )\n    }\n\n    class Params(\n        val feeds: BlueskyFeeds,\n        val locator: PlatformLocator,\n    ) : SubViewModelParams() {\n\n        override val key: String\n            get() = feeds.toString() + locator\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/screen/feeds/home/HomeFeedsScreen.kt",
    "content": "package com.zhangke.fread.bluesky.internal.screen.feeds.home\n\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.SnackbarHost\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.architect.json.globalJson\nimport com.zhangke.framework.composable.LocalSnackbarHostState\nimport com.zhangke.framework.composable.Toolbar\nimport com.zhangke.framework.composable.currentOrThrow\nimport com.zhangke.framework.composable.rememberSnackbarHostState\nimport com.zhangke.framework.nav.LocalNavBackStack\nimport com.zhangke.fread.bluesky.internal.model.BlueskyFeeds\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.model.PlatformLocator\nimport kotlinx.serialization.Serializable\nimport org.jetbrains.compose.resources.stringResource\n\n@Serializable\ndata class HomeFeedsScreenNavKey(\n    val feedsJson: String,\n    val locator: PlatformLocator,\n) : NavKey {\n\n    companion object {\n\n        fun create(feeds: BlueskyFeeds, locator: PlatformLocator): HomeFeedsScreenNavKey {\n            return HomeFeedsScreenNavKey(\n                locator = locator,\n                feedsJson = globalJson.encodeToString(\n                    serializer = BlueskyFeeds.serializer(),\n                    value = feeds,\n                ),\n            )\n        }\n    }\n}\n\n@Composable\nfun HomeFeedsScreen(\n    feedsJson: String,\n    locator: PlatformLocator,\n) {\n    val backStack = LocalNavBackStack.currentOrThrow\n    val snackbarHostState = rememberSnackbarHostState()\n\n    val tab = remember {\n        HomeFeedsTab(\n            feeds = globalJson.decodeFromString(feedsJson),\n            locator = locator,\n        )\n    }\n    Scaffold(\n        modifier = Modifier.fillMaxSize(),\n        topBar = {\n            Toolbar(\n                title = stringResource(LocalizedString.feeds),\n                onBackClick = backStack::removeLastOrNull,\n            )\n        },\n        snackbarHost = {\n            SnackbarHost(hostState = snackbarHostState)\n        },\n    ) { innerPadding ->\n        Box(modifier = Modifier.padding(innerPadding).fillMaxSize()) {\n            CompositionLocalProvider(\n                LocalSnackbarHostState provides snackbarHostState,\n            ) {\n                tab.Content()\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/screen/feeds/home/HomeFeedsTab.kt",
    "content": "package com.zhangke.fread.bluesky.internal.screen.feeds.home\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.MutableState\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.rememberCoroutineScope\nimport com.zhangke.framework.composable.currentOrThrow\nimport com.zhangke.framework.composable.ConsumeSnackbarFlow\nimport com.zhangke.framework.composable.LocalSnackbarHostState\nimport com.zhangke.framework.nav.BaseTab\nimport com.zhangke.framework.nav.LocalNavBackStack\nimport com.zhangke.framework.nav.TabOptions\nimport com.zhangke.fread.bluesky.internal.model.BlueskyFeeds\nimport com.zhangke.fread.bluesky.internal.screen.add.AddBlueskyContentScreenNavKey\nimport com.zhangke.fread.commonbiz.shared.composable.FeedsContent\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.ui.common.LocalNestedTabConnection\nimport org.koin.compose.viewmodel.koinViewModel\n\nclass HomeFeedsTab(\n    private val feeds: BlueskyFeeds,\n    private val locator: PlatformLocator,\n    private val contentCanScrollBackward: MutableState<Boolean>? = null,\n) : BaseTab() {\n\n    override val options: TabOptions\n        @Composable get() = TabOptions(title = feeds.displayName())\n\n    @Composable\n    override fun Content() {\n        val backStack = LocalNavBackStack.currentOrThrow\n        val viewModel = koinViewModel<HomeFeedsContainerViewModel>().getViewModel(feeds, locator)\n        val uiState by viewModel.uiState.collectAsState()\n        val coroutineScope = rememberCoroutineScope()\n        val mainTabConnection = LocalNestedTabConnection.current\n        FeedsContent(\n            uiState = uiState,\n            openScreenFlow = viewModel.openScreenFlow,\n            newStatusNotifyFlow = viewModel.newStatusNotifyFlow,\n            onRefresh = viewModel::onRefresh,\n            onLoadMore = viewModel::onLoadMore,\n            composedStatusInteraction = viewModel.composedStatusInteraction,\n            observeScrollToTopEvent = true,\n            contentCanScrollBackward = contentCanScrollBackward,\n            nestedScrollConnection = null,\n            onImmersiveEvent = {\n                if (it) {\n                    mainTabConnection.openImmersiveMode(coroutineScope)\n                } else {\n                    mainTabConnection.closeImmersiveMode(coroutineScope)\n                }\n            },\n            onScrollInProgress = {},\n            onLoginClick = {\n                backStack.add(\n                    AddBlueskyContentScreenNavKey(\n                        baseUrl = locator.baseUrl,\n                        loginMode = true,\n                    )\n                )\n            },\n        )\n\n        val snackbarHostState = LocalSnackbarHostState.current\n        ConsumeSnackbarFlow(snackbarHostState, viewModel.errorMessageFlow)\n\n        LaunchedEffect(Unit) { viewModel.onPageResume() }\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/screen/feeds/home/HomeFeedsViewModel.kt",
    "content": "package com.zhangke.fread.bluesky.internal.screen.feeds.home\n\nimport com.zhangke.framework.lifecycle.SubViewModel\nimport com.zhangke.fread.bluesky.internal.model.BlueskyFeeds\nimport com.zhangke.fread.bluesky.internal.usecase.GetFeedsStatusUseCase\nimport com.zhangke.fread.common.adapter.StatusUiStateAdapter\nimport com.zhangke.fread.common.feeds.model.RefreshResult\nimport com.zhangke.fread.common.status.StatusUpdater\nimport com.zhangke.fread.commonbiz.shared.feeds.FeedsViewModelController\nimport com.zhangke.fread.commonbiz.shared.feeds.IFeedsViewModelController\nimport com.zhangke.fread.commonbiz.shared.usecase.RefactorToNewStatusUseCase\nimport com.zhangke.fread.status.StatusProvider\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.model.StatusUiState\nimport com.zhangke.fread.status.richtext.preParseStatusUiState\n\nclass HomeFeedsViewModel(\n    private val statusUiStateAdapter: StatusUiStateAdapter,\n    private val statusProvider: StatusProvider,\n    private val getFeedsStatus: GetFeedsStatusUseCase,\n    refactorToNewStatus: RefactorToNewStatusUseCase,\n    statusUpdater: StatusUpdater,\n    private val feeds: BlueskyFeeds,\n    private val locator: PlatformLocator,\n) : SubViewModel(), IFeedsViewModelController by FeedsViewModelController(\n    statusProvider = statusProvider,\n    statusUpdater = statusUpdater,\n    statusUiStateAdapter = statusUiStateAdapter,\n    refactorToNewStatus = refactorToNewStatus,\n) {\n\n    companion object {\n\n        private const val FLAG_CURSOR_ENDING = \"flag_cursor_ending_for_suggested_feeds\"\n    }\n\n    private var cursor: String? = null\n\n    init {\n        initController(\n            coroutineScope = viewModelScope,\n            locatorResolver = { locator },\n            loadFirstPageLocalFeeds = {\n                Result.success(emptyList())\n            },\n            loadNewFromServerFunction = ::loadNewDataFromServer,\n            loadMoreFunction = { loadMoreDataFromServer() },\n            onStatusUpdate = {},\n        )\n        initFeeds(false)\n    }\n\n    private suspend fun loadNewDataFromServer(): Result<RefreshResult> {\n        return loadFeeds(null).map {\n            RefreshResult(\n                newStatus = it,\n                deletedStatus = emptyList(),\n            )\n        }\n    }\n\n    private suspend fun loadMoreDataFromServer(): Result<List<StatusUiState>> {\n        if (cursor == FLAG_CURSOR_ENDING) return Result.success(emptyList())\n        return loadFeeds()\n    }\n\n    private suspend fun loadFeeds(cursor: String? = this.cursor): Result<List<StatusUiState>> {\n        return getFeedsStatus(locator = locator, feeds = feeds, cursor = cursor).map {\n            this.cursor = if (it.cursor.isNullOrBlank()) FLAG_CURSOR_ENDING else it.cursor\n            it.feeds.onEach { status -> status.preParseStatusUiState() }\n        }\n    }\n\n    fun onPageResume() {\n        if (uiState.value.pageErrorContent == null) return\n        initFeeds(true)\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/screen/publish/MentionCandidateBar.kt",
    "content": "package com.zhangke.fread.bluesky.internal.screen.publish\n\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.layout.widthIn\nimport androidx.compose.foundation.lazy.LazyRow\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport app.bsky.actor.ProfileView\nimport com.zhangke.framework.composable.freadPlaceholder\nimport com.zhangke.framework.composable.requireSuccessData\nimport com.zhangke.fread.status.ui.BlogAuthorAvatar\n\n@Composable\nfun MentionCandidateBar(\n    uiState: PublishPostUiState,\n    onMentionClick: (ProfileView) -> Unit,\n) {\n    val mentionState = uiState.mentionState\n    if (mentionState.isIdle || mentionState.isFailed) return\n    Spacer(modifier = Modifier.height(8.dp))\n    if (mentionState.isLoading) {\n        LazyRow(\n            modifier = Modifier.fillMaxWidth(),\n            contentPadding = PaddingValues(horizontal = 6.dp)\n        ) {\n            items(10) {\n                MentionedItem(null, onMentionClick)\n                Spacer(modifier = Modifier.width(8.dp))\n            }\n        }\n    } else if (mentionState.isSuccess) {\n        LazyRow(\n            modifier = Modifier.fillMaxWidth(),\n            contentPadding = PaddingValues(horizontal = 6.dp)\n        ) {\n            items(mentionState.requireSuccessData()) {\n                MentionedItem(it, onMentionClick)\n                Spacer(modifier = Modifier.width(8.dp))\n            }\n        }\n    }\n    Spacer(modifier = Modifier.height(8.dp))\n}\n\n@Composable\nprivate fun MentionedItem(\n    account: ProfileView?,\n    onMentionClick: (ProfileView) -> Unit,\n) {\n    Row(\n        modifier = Modifier\n            .border(\n                width = 1.dp,\n                shape = RoundedCornerShape(12),\n                color = MaterialTheme.colorScheme.outline,\n            )\n            .clickable(account != null) { account?.let(onMentionClick) }\n            .padding(vertical = 6.dp),\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        Spacer(modifier = Modifier.width(6.dp))\n        BlogAuthorAvatar(\n            modifier = Modifier.size(18.dp),\n            imageUrl = account?.avatar?.uri,\n        )\n        Text(\n            modifier = Modifier\n                .padding(start = 4.dp)\n                .widthIn(min = 80.dp)\n                .freadPlaceholder(account?.handle.isNullOrEmpty()),\n            text = account?.handle?.handle.orEmpty(),\n            style = MaterialTheme.typography.labelMedium,\n        )\n        Spacer(modifier = Modifier.width(6.dp))\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/screen/publish/PublishPostEmbed.kt",
    "content": "package com.zhangke.fread.bluesky.internal.screen.publish\n\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/screen/publish/PublishPostMediaAttachment.kt",
    "content": "package com.zhangke.fread.bluesky.internal.screen.publish\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport com.zhangke.fread.commonbiz.shared.screen.publish.PublishPostMedia\n\n@Composable\nfun PublishPostMediaAttachment(\n    modifier: Modifier,\n    media: PublishPostMediaAttachment,\n    mediaAltMaxCharacters: Int,\n    onAltChanged: (PublishPostMedia, String) -> Unit,\n    onDeleteClick: (PublishPostMedia) -> Unit,\n) {\n    com.zhangke.fread.commonbiz.shared.screen.publish.PublishPostMediaAttachment(\n        modifier = modifier,\n        medias = media.medias,\n        mediaAltMaxCharacters = mediaAltMaxCharacters,\n        onAltChanged = onAltChanged,\n        onDeleteClick = onDeleteClick,\n    )\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/screen/publish/PublishPostScreen.kt",
    "content": "package com.zhangke.fread.bluesky.internal.screen.publish\n\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material3.SnackbarHostState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.text.input.TextFieldValue\nimport androidx.compose.ui.unit.dp\nimport androidx.navigation3.runtime.NavKey\nimport app.bsky.actor.ProfileView\nimport com.zhangke.framework.composable.currentOrThrow\nimport com.zhangke.framework.composable.ConsumeFlow\nimport com.zhangke.framework.composable.ConsumeSnackbarFlow\nimport com.zhangke.framework.composable.rememberSnackbarHostState\nimport com.zhangke.framework.nav.LocalNavBackStack\nimport com.zhangke.framework.nav.popIfNotRoot\nimport com.zhangke.framework.nav.replaceTopOrAdd\nimport com.zhangke.framework.utils.PlatformUri\nimport com.zhangke.fread.commonbiz.shared.screen.publish.PublishPostFeaturesPanel\nimport com.zhangke.fread.commonbiz.shared.screen.publish.PublishPostMedia\nimport com.zhangke.fread.commonbiz.shared.screen.publish.PublishPostScaffold\nimport com.zhangke.fread.commonbiz.shared.screen.publish.bottomPaddingAsBottomBar\nimport com.zhangke.fread.commonbiz.shared.screen.publish.composable.PostInteractionSettingLabel\nimport com.zhangke.fread.commonbiz.shared.screen.publish.multi.MultiAccountPublishingScreenKey\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.model.ReplySetting\nimport com.zhangke.fread.status.ui.common.DetectedLinkCard\nimport com.zhangke.fread.status.ui.common.LinkPreviewCard\nimport com.zhangke.fread.status.ui.publish.BlogInQuoting\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class PublishPostScreenNavKey(\n    val locator: PlatformLocator,\n    val defaultText: String? = null,\n    val replyToJsonString: String? = null,\n    val quoteJsonString: String? = null,\n) : NavKey\n\n@Composable\nfun PublishPostScreen(viewModel: PublishPostViewModel) {\n    val backStack = LocalNavBackStack.currentOrThrow\n    val uiState by viewModel.uiState.collectAsState()\n    val snackBarHostState = rememberSnackbarHostState()\n    PublishPostContent(\n        uiState = uiState,\n        snackBarHostState = snackBarHostState,\n        onBackClick = backStack::popIfNotRoot,\n        onContentChanged = viewModel::onContentChanged,\n        onQuoteChange = viewModel::onQuoteChange,\n        onSettingSelected = viewModel::onReplySettingChange,\n        onSettingOptionsSelected = viewModel::onSettingOptionsSelected,\n        onMediaSelected = viewModel::onMediaSelected,\n        onLanguageSelected = viewModel::onLanguageSelected,\n        onMediaAltChanged = viewModel::onMediaAltChanged,\n        onMediaDeleteClick = viewModel::onMediaDeleteClick,\n        onPublishClick = viewModel::onPublishClick,\n        onAddAccountClick = {\n            val key = MultiAccountPublishingScreenKey.create(\n                uiState.account?.let { listOf(it) }.orEmpty(),\n            )\n            if (uiState.hasInputtedData) {\n                backStack.add(key)\n            } else {\n                backStack.replaceTopOrAdd(key)\n            }\n        },\n        onMentionCandidateClick = viewModel::onMentionCandidateClick,\n        onLinkPreviewCardRemoveClicked = viewModel::onLinkPreviewCardRemoveClicked,\n    )\n    ConsumeSnackbarFlow(snackBarHostState, viewModel.snackBarMessageFlow)\n    ConsumeFlow(viewModel.finishPageFlow) {\n        backStack.popIfNotRoot()\n    }\n}\n\n@Composable\nprivate fun PublishPostContent(\n    uiState: PublishPostUiState,\n    snackBarHostState: SnackbarHostState,\n    onBackClick: () -> Unit,\n    onContentChanged: (TextFieldValue) -> Unit,\n    onQuoteChange: (Boolean) -> Unit,\n    onSettingSelected: (ReplySetting) -> Unit,\n    onSettingOptionsSelected: (ReplySetting.CombineOption) -> Unit,\n    onMediaSelected: (List<PlatformUri>) -> Unit,\n    onLanguageSelected: (List<String>) -> Unit,\n    onMediaAltChanged: (PublishPostMedia, String) -> Unit,\n    onMediaDeleteClick: (PublishPostMedia) -> Unit,\n    onPublishClick: () -> Unit,\n    onAddAccountClick: () -> Unit,\n    onMentionCandidateClick: (ProfileView) -> Unit,\n    onLinkPreviewCardRemoveClicked: () -> Unit,\n) {\n    PublishPostScaffold(\n        account = uiState.account,\n        snackBarHostState = snackBarHostState,\n        content = uiState.content,\n        showSwitchAccountIcon = false,\n        publishEnabled = uiState.publishEnabled,\n        showAddAccountIcon = uiState.showAddAccountIcon,\n        publishing = uiState.publishing,\n        replyingBlog = uiState.replyBlog,\n        onContentChanged = onContentChanged,\n        onPublishClick = onPublishClick,\n        onBackClick = onBackClick,\n        onAddAccountClick = onAddAccountClick,\n        postSettingLabel = {\n            PostInteractionSettingLabel(\n                modifier = Modifier.padding(top = 1.dp),\n                setting = uiState.interactionSetting,\n                lists = uiState.list,\n                onQuoteChange = onQuoteChange,\n                onSettingSelected = onSettingSelected,\n                onSettingOptionsSelected = onSettingOptionsSelected,\n            )\n        },\n        bottomPanel = {\n            PublishPostFeaturesPanel(\n                modifier = Modifier.fillMaxWidth().bottomPaddingAsBottomBar(),\n                contentLength = uiState.content.text.length,\n                maxContentLimit = uiState.maxCharacters,\n                mediaAvailableCount = uiState.remainingImageCount,\n                selectedLanguages = uiState.selectedLanguages,\n                maxLanguageCount = uiState.maxLanguageCount,\n                onMediaSelected = onMediaSelected,\n                onLanguageSelected = onLanguageSelected,\n                floatingBar = {\n                    MentionCandidateBar(\n                        uiState = uiState,\n                        onMentionClick = onMentionCandidateClick,\n                    )\n                },\n            )\n        },\n        attachment = { style ->\n            if (uiState.attachment != null) {\n                PublishPostMediaAttachment(\n                    modifier = Modifier.padding(start = 16.dp, top = 16.dp, end = 16.dp)\n                        .fillMaxWidth(),\n                    media = uiState.attachment,\n                    mediaAltMaxCharacters = uiState.mediaAltMaxCharacters,\n                    onAltChanged = onMediaAltChanged,\n                    onDeleteClick = onMediaDeleteClick,\n                )\n            }\n            if (uiState.quoteBlog != null) {\n                BlogInQuoting(\n                    modifier = Modifier.fillMaxWidth()\n                        .padding(16.dp),\n                    blog = uiState.quoteBlog,\n                    style = style.statusStyle,\n                )\n            }\n            if (uiState.detectedLinkCard != null && uiState.detectedLinkCard !is DetectedLinkCard.Deleted) {\n                LinkPreviewCard(\n                    modifier = Modifier.fillMaxWidth().padding(16.dp),\n                    card = uiState.detectedLinkCard,\n                    onRemoveClick = onLinkPreviewCardRemoveClicked,\n                )\n            }\n        },\n        allowHashtagInHashtag = true,\n    )\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/screen/publish/PublishPostUiState.kt",
    "content": "package com.zhangke.fread.bluesky.internal.screen.publish\n\nimport androidx.compose.ui.text.TextRange\nimport androidx.compose.ui.text.input.TextFieldValue\nimport app.bsky.actor.ProfileView\nimport com.zhangke.framework.composable.LoadableState\nimport com.zhangke.framework.utils.ContentProviderFile\nimport com.zhangke.framework.utils.LinkPreviewInfo\nimport com.zhangke.fread.bluesky.internal.account.BlueskyLoggedAccount\nimport com.zhangke.fread.commonbiz.shared.screen.publish.PublishPostMedia\nimport com.zhangke.fread.status.blog.Blog\nimport com.zhangke.fread.status.model.PostInteractionSetting\nimport com.zhangke.fread.status.model.ReplySetting\nimport com.zhangke.fread.status.model.StatusList\nimport com.zhangke.fread.status.ui.common.DetectedLinkCard\n\ndata class PublishPostUiState(\n    val content: TextFieldValue,\n    val maxCharacters: Int,\n    val maxMediaCount: Int,\n    val mediaAltMaxCharacters: Int,\n    val account: BlueskyLoggedAccount?,\n    val attachment: PublishPostMediaAttachment?,\n    val interactionSetting: PostInteractionSetting,\n    val selectedLanguages: List<String>,\n    val maxLanguageCount: Int,\n    val publishing: Boolean,\n    val replyBlog: Blog?,\n    val quoteBlog: Blog?,\n    val list: List<StatusList>,\n    val mentionState: LoadableState<List<ProfileView>>,\n    val detectedLinkCard: DetectedLinkCard?,\n) {\n\n    val publishEnabled: Boolean\n        get() {\n            if (publishing) return false\n            if (content.text.isEmpty() && attachment == null) return false\n            if (content.text.length > maxCharacters) return false\n            if (detectedLinkCard != null && detectedLinkCard is DetectedLinkCard.Loading) {\n                return false\n            }\n            return true\n        }\n\n    val remainingImageCount: Int\n        get() {\n            return if (attachment is PublishPostMediaAttachment.Image) {\n                maxMediaCount - attachment.files.size\n            } else {\n                maxMediaCount\n            }\n        }\n\n    val showAddAccountIcon: Boolean\n        get() = replyBlog == null\n\n    val hasInputtedData: Boolean\n        get() {\n            if (content.text.isNotEmpty()) return true\n            if (attachment != null) return true\n            return false\n        }\n\n    companion object {\n\n        fun default(\n            defaultContent: String? = null,\n        ): PublishPostUiState {\n            return PublishPostUiState(\n                content = TextFieldValue(\n                    text = defaultContent.orEmpty(),\n                    selection = TextRange(defaultContent?.length ?: 0),\n                ),\n                maxCharacters = 300,\n                mediaAltMaxCharacters = 2000,\n                maxMediaCount = 4,\n                interactionSetting = PostInteractionSetting(\n                    allowQuote = true,\n                    replySetting = ReplySetting.Everybody,\n                ),\n                account = null,\n                attachment = null,\n                replyBlog = null,\n                quoteBlog = null,\n                selectedLanguages = emptyList(),\n                maxLanguageCount = 3,\n                publishing = false,\n                list = emptyList(),\n                mentionState = LoadableState.idle(),\n                detectedLinkCard = null,\n            )\n        }\n    }\n}\n\nsealed interface PublishPostMediaAttachment {\n\n    val medias: List<PublishPostMedia>\n        get() = when (this) {\n            is Image -> files\n            is Video -> listOf(file)\n        }\n\n    val isEmpty: Boolean get() = medias.isEmpty()\n\n    data class Image(val files: List<PublishPostMediaAttachmentFile>) : PublishPostMediaAttachment\n\n    data class Video(val file: PublishPostMediaAttachmentFile) : PublishPostMediaAttachment\n}\n\nfun PublishPostMediaAttachment?.isNullOrEmpty(): Boolean {\n    return this == null || this.isEmpty\n}\n\ndata class PublishPostMediaAttachmentFile(\n    val file: ContentProviderFile,\n    override val isVideo: Boolean,\n    override val alt: String?,\n) : PublishPostMedia {\n\n    override val uri: String\n        get() = file.uri.toString()\n\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/screen/publish/PublishPostViewModel.kt",
    "content": "package com.zhangke.fread.bluesky.internal.screen.publish\n\nimport androidx.compose.ui.text.input.TextFieldValue\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport app.bsky.actor.ProfileView\nimport app.bsky.actor.SearchActorsQueryParams\nimport app.bsky.graph.ListView\nimport com.zhangke.framework.architect.json.fromJson\nimport com.zhangke.framework.architect.json.globalJson\nimport com.zhangke.framework.composable.LoadableState\nimport com.zhangke.framework.composable.TextString\nimport com.zhangke.framework.composable.emitTextMessageFromThrowable\nimport com.zhangke.framework.ktx.launchInViewModel\nimport com.zhangke.framework.utils.ContentProviderFile\nimport com.zhangke.framework.utils.ExtractUrlFromTextUtils\nimport com.zhangke.framework.utils.PlatformUri\nimport com.zhangke.fread.bluesky.internal.client.BlueskyClientManager\nimport com.zhangke.fread.bluesky.internal.usecase.GetAllListsUseCase\nimport com.zhangke.fread.bluesky.internal.usecase.PublishingPostUseCase\nimport com.zhangke.fread.common.config.FreadConfigManager\nimport com.zhangke.fread.common.repo.LinkPreviewCardRepo\nimport com.zhangke.fread.common.utils.MentionTextUtil\nimport com.zhangke.fread.common.utils.PlatformUriHelper\nimport com.zhangke.fread.commonbiz.shared.screen.publish.PublishPostMedia\nimport com.zhangke.fread.status.blog.Blog\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.model.ReplySetting\nimport com.zhangke.fread.status.model.StatusList\nimport com.zhangke.fread.status.ui.common.DetectedLinkCard\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.IO\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.async\nimport kotlinx.coroutines.awaitAll\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asSharedFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.update\nimport kotlinx.coroutines.launch\nimport sh.christian.ozone.api.Did\n\nclass PublishPostViewModel(\n    private val clientManager: BlueskyClientManager,\n    private val getAllLists: GetAllListsUseCase,\n    private val platformUriHelper: PlatformUriHelper,\n    private val configManager: FreadConfigManager,\n    private val publishingPost: PublishingPostUseCase,\n    private val locator: PlatformLocator,\n    private val linkPreviewCardRepo: LinkPreviewCardRepo,\n    defaultText: String?,\n    replyBlogJsonString: String?,\n    quoteBlogJsonString: String?,\n) : ViewModel() {\n\n    private val _uiState = MutableStateFlow(PublishPostUiState.default(defaultText))\n    val uiState = _uiState.asStateFlow()\n\n    private val _snackBarMessageFlow = MutableSharedFlow<TextString>()\n    val snackBarMessageFlow = _snackBarMessageFlow.asSharedFlow()\n\n    private val _finishPageFlow = MutableSharedFlow<Unit>()\n    val finishPageFlow = _finishPageFlow.asSharedFlow()\n\n    private var searchMentionUserJob: Job? = null\n\n    private var publishJob: Job? = null\n\n    private val mentionedUsers = mutableSetOf<ProfileView>()\n\n    private var extractLinkPreviewCardJob: Job? = null\n\n    init {\n        launchInViewModel(Dispatchers.IO) {\n            val reply: Blog? = replyBlogJsonString?.let {\n                globalJson.fromJson<Blog>(it)\n            }\n            val quote: Blog? = quoteBlogJsonString?.let {\n                globalJson.fromJson<Blog>(it)\n            }\n            _uiState.update { it.copy(replyBlog = reply, quoteBlog = quote) }\n        }\n        launchInViewModel {\n            val client = clientManager.getClient(locator)\n            val account = client.loggedAccountProvider() ?: return@launchInViewModel\n            _uiState.update { it.copy(account = account) }\n        }\n        launchInViewModel {\n            configManager.getBskyPublishLanguage().let {\n                _uiState.update { state ->\n                    val selectedLanguages = it.ifEmpty { listOf(\"en\") }\n                    state.copy(selectedLanguages = selectedLanguages)\n                }\n            }\n        }\n        loadUserList()\n    }\n\n    fun onContentChanged(text: TextFieldValue) {\n        _uiState.update { it.copy(content = text) }\n        maybeNeedSearchAccount(text)\n        extractLinkPreviewCard(text.text)\n    }\n\n    private fun maybeNeedSearchAccount(content: TextFieldValue) {\n        searchMentionUserJob?.cancel()\n        val mentionText = MentionTextUtil.findTypingMentionName(content)?.removePrefix(\"@\")\n        if (mentionText == null || mentionText.length < 2) {\n            _uiState.update {\n                it.copy(mentionState = LoadableState.idle())\n            }\n            return\n        }\n        searchMentionUserJob = launchInViewModel {\n            _uiState.update { it.copy(mentionState = LoadableState.loading()) }\n            clientManager.getClient(locator)\n                .searchActorsCatching(SearchActorsQueryParams(q = mentionText, limit = 10))\n                .map { it.actors }\n                .onSuccess { list ->\n                    _uiState.update { it.copy(mentionState = LoadableState.success(list)) }\n                }\n                .onFailure {\n                    _uiState.update { it.copy(mentionState = LoadableState.idle()) }\n                }\n        }\n    }\n\n    private fun extractLinkPreviewCard(content: String) {\n        if (uiState.value.quoteBlog != null || !uiState.value.attachment.isNullOrEmpty()) {\n            _uiState.update { it.copy(detectedLinkCard = null) }\n            return\n        }\n        extractLinkPreviewCardJob?.cancel()\n        extractLinkPreviewCardJob = launchInViewModel {\n            val link = ExtractUrlFromTextUtils.extract(content).firstOrNull()\n            if (link == null) {\n                _uiState.update { it.copy(detectedLinkCard = null) }\n                return@launchInViewModel\n            } else if (link == _uiState.value.detectedLinkCard?.link) {\n                return@launchInViewModel\n            }\n            _uiState.update { it.copy(detectedLinkCard = DetectedLinkCard.Loading(link)) }\n            linkPreviewCardRepo.fetchPreviewInfo(link)\n                .onSuccess { info ->\n                    _uiState.update {\n                        it.copy(detectedLinkCard = DetectedLinkCard.Loaded(link, info))\n                    }\n                }.onFailure { t ->\n                    _uiState.update {\n                        it.copy(detectedLinkCard = DetectedLinkCard.Failure(link, t))\n                    }\n                }\n        }\n    }\n\n    fun onLinkPreviewCardRemoveClicked() {\n        extractLinkPreviewCardJob?.cancel()\n        _uiState.update { state ->\n            state.copy(\n                detectedLinkCard = state.detectedLinkCard?.link?.let { DetectedLinkCard.Deleted(it) }\n            )\n        }\n    }\n\n    fun onMentionCandidateClick(profile: ProfileView) {\n        _uiState.update { state ->\n            state.copy(\n                content = MentionTextUtil.insertMention(\n                    text = state.content,\n                    insertText = profile.handle.handle,\n                )\n            )\n        }\n        mentionedUsers += profile\n    }\n\n    fun onQuoteChange(allowQuote: Boolean) {\n        _uiState.update {\n            it.copy(interactionSetting = it.interactionSetting.copy(allowQuote = allowQuote))\n        }\n    }\n\n    fun onReplySettingChange(replySetting: ReplySetting) {\n        _uiState.update {\n            it.copy(\n                interactionSetting = it.interactionSetting.copy(replySetting = replySetting)\n            )\n        }\n    }\n\n    fun onSettingOptionsSelected(option: ReplySetting.CombineOption) {\n        _uiState.update { state ->\n            val options = state.interactionSetting.replySetting.let { it as? ReplySetting.Combined }\n                ?.options?.toMutableList() ?: mutableListOf()\n            if (option in options) {\n                options.remove(option)\n            } else {\n                options.add(option)\n            }\n            state.copy(\n                interactionSetting = state.interactionSetting.copy(\n                    replySetting = ReplySetting.Combined(options),\n                )\n            )\n        }\n    }\n\n    fun onMediaSelected(medias: List<PlatformUri>) {\n        if (medias.isEmpty()) return\n        viewModelScope.launch {\n            val currentAttachment = uiState.value.attachment\n            val fileList = medias.map {\n                async { platformUriHelper.read(it) }\n            }.awaitAll().filterNotNull()\n            val attachment = if (fileList.first().isVideo) {\n                PublishPostMediaAttachment.Video(fileList.first().toUiFile(true))\n            } else {\n                PublishPostMediaAttachment.Image(fileList.map { it.toUiFile(false) })\n            }\n            _uiState.update {\n                it.copy(attachment = currentAttachment.merge(attachment), detectedLinkCard = null)\n            }\n        }\n    }\n\n    private fun PublishPostMediaAttachment?.merge(\n        attachment: PublishPostMediaAttachment\n    ): PublishPostMediaAttachment {\n        if (this == null) return attachment\n        if (this is PublishPostMediaAttachment.Video || attachment is PublishPostMediaAttachment.Video) {\n            return attachment\n        }\n        val files = (this as PublishPostMediaAttachment.Image).files\n        return PublishPostMediaAttachment.Image(files + (attachment as PublishPostMediaAttachment.Image).files)\n    }\n\n    private fun ContentProviderFile.toUiFile(isVideo: Boolean): PublishPostMediaAttachmentFile {\n        return PublishPostMediaAttachmentFile(\n            file = this,\n            alt = null,\n            isVideo = isVideo,\n        )\n    }\n\n    fun onMediaAltChanged(file: PublishPostMedia, alt: String) {\n        file as PublishPostMediaAttachmentFile\n        val attachment = uiState.value.attachment\n        if (attachment is PublishPostMediaAttachment.Video) {\n            _uiState.update {\n                it.copy(attachment = PublishPostMediaAttachment.Video(file.copy(alt = alt)))\n            }\n        } else {\n            val files = (attachment as PublishPostMediaAttachment.Image).files.map {\n                if (it == file) {\n                    it.copy(alt = alt)\n                } else {\n                    it\n                }\n            }\n            _uiState.update { it.copy(attachment = PublishPostMediaAttachment.Image(files)) }\n        }\n    }\n\n    fun onMediaDeleteClick(file: PublishPostMedia) {\n        val attachment = uiState.value.attachment\n        if (attachment is PublishPostMediaAttachment.Video) {\n            _uiState.update { it.copy(attachment = null) }\n        } else {\n            val files = (attachment as PublishPostMediaAttachment.Image).files.filter { it != file }\n            _uiState.update { it.copy(attachment = PublishPostMediaAttachment.Image(files)) }\n        }\n    }\n\n    fun onLanguageSelected(selectedLanguages: List<String>) {\n        launchInViewModel {\n            configManager.updateBskyPublishLanguage(selectedLanguages)\n        }\n        _uiState.update { it.copy(selectedLanguages = selectedLanguages) }\n    }\n\n    fun onPublishClick() {\n        if (publishJob?.isActive == true) return\n        publishJob?.cancel()\n        val account = uiState.value.account ?: return\n        if (uiState.value.detectedLinkCard is DetectedLinkCard.Loading) {\n            return\n        }\n        publishJob = launchInViewModel {\n            _uiState.update { it.copy(publishing = true) }\n            publishingPost(\n                account = account,\n                content = uiState.value.content.text,\n                selectedLanguages = uiState.value.selectedLanguages,\n                interactionSetting = uiState.value.interactionSetting,\n                replyBlog = uiState.value.replyBlog,\n                quoteBlog = uiState.value.quoteBlog,\n                attachment = uiState.value.attachment,\n                mentionedUsers = mentionedUsers,\n                linkPreviewInfo = (uiState.value.detectedLinkCard as? DetectedLinkCard.Loaded)?.info,\n            ).onFailure { t ->\n                _uiState.update { it.copy(publishing = false) }\n                _snackBarMessageFlow.emitTextMessageFromThrowable(t)\n            }.onSuccess {\n                _uiState.update { it.copy(publishing = false) }\n                _finishPageFlow.emit(Unit)\n            }\n        }\n    }\n\n    private fun loadUserList() {\n        launchInViewModel {\n            val client = clientManager.getClient(locator)\n            val account = client.loggedAccountProvider() ?: return@launchInViewModel\n            getAllLists(locator, Did(account.did))\n                .map { lists -> lists.map { listView -> listView.toStatusList() } }\n                .onSuccess { lists -> _uiState.update { it.copy(list = lists) } }\n        }\n    }\n\n    private fun ListView.toStatusList(): StatusList {\n        return StatusList(\n            name = this.name,\n            uri = this.uri.atUri,\n            cid = this.cid.cid,\n        )\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/screen/search/SearchStatusScreen.kt",
    "content": "package com.zhangke.fread.bluesky.internal.screen.search\n\nimport androidx.compose.runtime.Composable\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.fread.commonbiz.shared.screen.search.AbstractSearchStatusScreen\nimport com.zhangke.fread.status.model.PlatformLocator\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class SearchStatusScreenNavKey(\n    val locator: PlatformLocator,\n    val did: String,\n) : NavKey\n\n@Composable\nfun SearchStatusScreen(viewModel: SearchStatusViewModel) {\n    AbstractSearchStatusScreen(viewModel)\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/screen/search/SearchStatusViewModel.kt",
    "content": "package com.zhangke.fread.bluesky.internal.screen.search\n\nimport app.bsky.feed.SearchPostsQueryParams\nimport com.zhangke.fread.bluesky.internal.adapter.BlueskyStatusAdapter\nimport com.zhangke.fread.bluesky.internal.client.BlueskyClientManager\nimport com.zhangke.fread.bluesky.internal.repo.BlueskyPlatformRepo\nimport com.zhangke.fread.common.adapter.StatusUiStateAdapter\nimport com.zhangke.fread.common.status.StatusUpdater\nimport com.zhangke.fread.commonbiz.shared.screen.search.AbstractSearchStatusViewModel\nimport com.zhangke.fread.commonbiz.shared.usecase.RefactorToNewStatusUseCase\nimport com.zhangke.fread.status.StatusProvider\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.model.StatusUiState\nimport sh.christian.ozone.api.Did\n\nclass SearchStatusViewModel(\n    private val clientManager: BlueskyClientManager,\n    statusProvider: StatusProvider,\n    statusUiStateAdapter: StatusUiStateAdapter,\n    statusUpdater: StatusUpdater,\n    refactorToNewStatus: RefactorToNewStatusUseCase,\n    private val platformRepo: BlueskyPlatformRepo,\n    private val statusAdapter: BlueskyStatusAdapter,\n    private val locator: PlatformLocator,\n    private val did: String,\n) : AbstractSearchStatusViewModel(\n    statusProvider = statusProvider,\n    statusUiStateAdapter = statusUiStateAdapter,\n    statusUpdater = statusUpdater,\n    refactorToNewStatus = refactorToNewStatus,\n) {\n\n    private var cursor: String? = null\n\n    override suspend fun performSearch(\n        query: String,\n        loadMore: Boolean\n    ): Result<List<StatusUiState>> {\n        if (!loadMore) {\n            cursor = null\n        }\n        if (loadMore && cursor == null) {\n            return Result.success(emptyList())\n        }\n        val client = clientManager.getClient(locator)\n        val account = client.loggedAccountProvider()\n        val platform = platformRepo.getPlatform(client.baseUrl)\n        val params = SearchPostsQueryParams(\n            q = query,\n            author = Did(did),\n            cursor = cursor,\n        )\n        return client.searchPostsCatching(params)\n            .onSuccess { cursor = it.cursor }\n            .map { result ->\n                result.posts.map {\n                    statusAdapter.convertToUiState(\n                        locator = locator,\n                        postView = it,\n                        platform = platform,\n                        loggedAccount = account,\n                    )\n                }\n            }\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/screen/user/detail/BskyUserDetailScreen.kt",
    "content": "package com.zhangke.fread.bluesky.internal.screen.user.detail\n\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.filled.Logout\nimport androidx.compose.material.icons.automirrored.filled.VolumeOff\nimport androidx.compose.material.icons.automirrored.outlined.ListAlt\nimport androidx.compose.material.icons.filled.Block\nimport androidx.compose.material.icons.filled.MoreVert\nimport androidx.compose.material.icons.filled.Search\nimport androidx.compose.material3.FilledTonalButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.MenuDefaults\nimport androidx.compose.material3.SnackbarHostState\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.navigation3.runtime.NavBackStack\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.composable.AlertConfirmDialog\nimport com.zhangke.framework.composable.ConsumeFlow\nimport com.zhangke.framework.composable.ConsumeSnackbarFlow\nimport com.zhangke.framework.composable.FreadDialog\nimport com.zhangke.framework.composable.PopupMenu\nimport com.zhangke.framework.composable.SimpleIconButton\nimport com.zhangke.framework.composable.currentOrThrow\nimport com.zhangke.framework.composable.rememberSnackbarHostState\nimport com.zhangke.framework.nav.ContentPaddingsHorizontalPagerWithTab\nimport com.zhangke.framework.nav.LocalNavBackStack\nimport com.zhangke.fread.bluesky.internal.model.BlueskyFeeds\nimport com.zhangke.fread.bluesky.internal.screen.feeds.following.BskyFollowingFeedsPageNavKey\nimport com.zhangke.fread.bluesky.internal.screen.feeds.home.HomeFeedsScreenNavKey\nimport com.zhangke.fread.bluesky.internal.screen.feeds.home.HomeFeedsTab\nimport com.zhangke.fread.bluesky.internal.screen.search.SearchStatusScreenNavKey\nimport com.zhangke.fread.bluesky.internal.screen.user.edit.EditProfileScreenNavKey\nimport com.zhangke.fread.bluesky.internal.screen.user.list.UserListScreenNavKey\nimport com.zhangke.fread.bluesky.internal.screen.user.list.UserListType\nimport com.zhangke.fread.common.browser.LocalActivityBrowserLauncher\nimport com.zhangke.fread.common.browser.launchWebTabInApp\nimport com.zhangke.fread.common.handler.LocalTextHandler\nimport com.zhangke.fread.commonbiz.shared.screen.ImageViewerImage\nimport com.zhangke.fread.commonbiz.shared.screen.ImageViewerScreenNavKey\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.richtext.buildRichText\nimport com.zhangke.fread.status.ui.action.DropDownCopyLinkItem\nimport com.zhangke.fread.status.ui.action.DropDownOpenInBrowserItem\nimport com.zhangke.fread.status.ui.action.ModalDropdownMenuItem\nimport com.zhangke.fread.status.ui.common.DetailPageScaffold\nimport com.zhangke.fread.status.ui.common.LocalNestedTabConnection\nimport com.zhangke.fread.status.ui.common.LocalStatusSharedElementConfig\nimport com.zhangke.fread.status.ui.common.NestedTabConnection\nimport com.zhangke.fread.status.ui.common.RelationshipStateButton\nimport com.zhangke.fread.status.ui.common.UserFollowLine\nimport com.zhangke.fread.status.ui.user.UserHandleLine\nimport kotlinx.serialization.Serializable\nimport org.jetbrains.compose.resources.stringResource\n\n@Serializable\ndata class BskyUserDetailScreenNavKey(\n    val locator: PlatformLocator,\n    val did: String,\n) : NavKey\n\n@Composable\nfun BskyUserDetailScreen(\n    locator: PlatformLocator,\n    did: String,\n    viewModel: BskyUserDetailViewModel,\n) {\n    val backStack = LocalNavBackStack.currentOrThrow\n    val browserLauncher = LocalActivityBrowserLauncher.current\n    val activityTextHandler = LocalTextHandler.current\n    val uiState by viewModel.uiState.collectAsState()\n    val snackBarState = rememberSnackbarHostState()\n    val coroutineScope = rememberCoroutineScope()\n    UserDetailContent(\n        uiState = uiState,\n        snackbarHostState = snackBarState,\n        locator = locator,\n        onBackClick = backStack::removeLastOrNull,\n        onSearchClick = {\n            backStack.add(SearchStatusScreenNavKey(locator = locator, did = did))\n        },\n        onBannerClick = { openFullImageScreen(backStack, uiState.banner) },\n        onAvatarClick = { openFullImageScreen(backStack, uiState.avatar) },\n        onFollowClick = viewModel::onFollowClick,\n        onBlockClick = viewModel::onBlockClick,\n        onUnblockClick = viewModel::onUnblockClick,\n        onUnfollowClick = viewModel::onUnfollowClick,\n        onFollowerClick = {\n            backStack.add(\n                UserListScreenNavKey(\n                    locator = locator,\n                    type = UserListType.FOLLOWERS,\n                    did = did\n                )\n            )\n        },\n        onFollowingClick = {\n            backStack.add(\n                UserListScreenNavKey(\n                    locator = locator,\n                    type = UserListType.FOLLOWING,\n                    did = did\n                )\n            )\n        },\n        onOpenInBrowserClick = {\n            uiState.userHomePageUrl?.let {\n                browserLauncher.launchWebTabInApp(\n                    scope = coroutineScope,\n                    url = it,\n                    checkAppSupportPage = false,\n                )\n            }\n        },\n        onCopyLinkClick = {\n            uiState.userHomePageUrl?.let { activityTextHandler.copyText(it) }\n        },\n        onEditProfileClick = { backStack.add(EditProfileScreenNavKey(locator = locator)) },\n        onMuteClick = { viewModel.onMuteClick(true) },\n        onUnmuteClick = { viewModel.onMuteClick(false) },\n        onBlockedUserListClick = {\n            backStack.add(\n                UserListScreenNavKey(\n                    locator = locator,\n                    type = UserListType.BLOCKED,\n                    did = did\n                )\n            )\n        },\n        onMuteUserListClick = {\n            backStack.add(\n                UserListScreenNavKey(\n                    locator = locator,\n                    type = UserListType.MUTED,\n                    did = did\n                )\n            )\n        },\n        onHashtagClick = { tag ->\n            backStack.add(\n                HomeFeedsScreenNavKey.create(\n                    feeds = BlueskyFeeds.Hashtags(tag),\n                    locator = locator,\n                )\n            )\n        },\n        onFollowingFeedsClick = {\n            backStack.add(\n                BskyFollowingFeedsPageNavKey(\n                    contentId = null,\n                    locator = locator,\n                )\n            )\n        },\n        onLogoutClick = viewModel::onLogoutClick,\n    )\n    ConsumeSnackbarFlow(snackBarState, viewModel.snackBarMessage)\n    LaunchedEffect(Unit) { viewModel.onPageResume() }\n    ConsumeFlow(viewModel.finishPageFlow) { backStack.removeLastOrNull() }\n}\n\n@Composable\nprivate fun UserDetailContent(\n    uiState: BskyUserDetailUiState,\n    snackbarHostState: SnackbarHostState,\n    locator: PlatformLocator,\n    onBackClick: () -> Unit,\n    onSearchClick: () -> Unit,\n    onBannerClick: () -> Unit,\n    onAvatarClick: () -> Unit,\n    onFollowClick: () -> Unit,\n    onUnfollowClick: () -> Unit,\n    onUnblockClick: () -> Unit,\n    onFollowerClick: () -> Unit,\n    onFollowingClick: () -> Unit,\n    onBlockClick: () -> Unit,\n    onMuteClick: () -> Unit,\n    onUnmuteClick: () -> Unit,\n    onOpenInBrowserClick: () -> Unit,\n    onCopyLinkClick: () -> Unit,\n    onEditProfileClick: () -> Unit,\n    onBlockedUserListClick: () -> Unit,\n    onMuteUserListClick: () -> Unit,\n    onHashtagClick: (String) -> Unit,\n    onFollowingFeedsClick: () -> Unit,\n    onLogoutClick: () -> Unit,\n) {\n    val contentCanScrollBackward = remember { mutableStateOf(false) }\n    DetailPageScaffold(\n        modifier = Modifier.fillMaxSize(),\n        snackbarHostState = snackbarHostState,\n        title = remember(uiState.displayName) {\n            buildRichText(uiState.displayName.orEmpty())\n        },\n        avatar = uiState.avatar.orEmpty(),\n        banner = uiState.banner,\n        description = remember(uiState.description) {\n            buildRichText(uiState.description.orEmpty())\n        },\n        privateNote = null,\n        loading = uiState.loading,\n        contentCanScrollBackward = contentCanScrollBackward,\n        onBannerClick = onBannerClick,\n        onAvatarClick = onAvatarClick,\n        onUrlClick = {},\n        onMaybeHashtagClick = onHashtagClick,\n        onBackClick = onBackClick,\n        topBarActions = {\n            TopBarActions(\n                uiState = uiState,\n                onBlockClick = onBlockClick,\n                onMuteClick = onMuteClick,\n                onSearchClick = onSearchClick,\n                onUnmuteClick = onUnmuteClick,\n                onOpenInBrowserClick = onOpenInBrowserClick,\n                onCopyLinkClick = onCopyLinkClick,\n                onFollowingFeedsClick = onFollowingFeedsClick,\n                onBlockedUserListClick = onBlockedUserListClick,\n                onMuteUserListClick = onMuteUserListClick,\n                onLogoutClick = onLogoutClick,\n            )\n        },\n        handleLine = {\n            UserHandleLine(\n                modifier = Modifier,\n                handle = uiState.prettyHandle,\n                bot = false,\n                followedBy = uiState.relationship?.followedBy == true,\n            )\n        },\n        followInfoLine = {\n            UserFollowLine(\n                modifier = Modifier,\n                followersCount = uiState.followersCount,\n                followingCount = uiState.followsCount,\n                statusesCount = uiState.postsCount,\n                onFollowerClick = onFollowerClick,\n                onFollowingClick = onFollowingClick,\n            )\n        },\n        topDetailContentAction = {\n            if (uiState.isOwner) {\n                FilledTonalButton(\n                    onClick = onEditProfileClick,\n                ) {\n                    Text(\n                        text = stringResource(LocalizedString.statusUiEditProfile)\n                    )\n                }\n            } else if (uiState.relationship != null) {\n                RelationshipStateButton(\n                    modifier = Modifier,\n                    relationship = uiState.relationship,\n                    onFollowClick = onFollowClick,\n                    onUnfollowClick = onUnfollowClick,\n                    onUnblockClick = onUnblockClick,\n                    onCancelFollowRequestClick = {},\n                )\n            }\n        },\n    ) { progress ->\n        val tabs = remember(uiState.tabs) {\n            uiState.tabs.map {\n                HomeFeedsTab(\n                    contentCanScrollBackward = contentCanScrollBackward,\n                    locator = locator,\n                    feeds = it,\n                )\n            }\n        }\n        val preSharedElementConfig = LocalStatusSharedElementConfig.current\n        val sharedElementConfig = remember(preSharedElementConfig) {\n            preSharedElementConfig.copy(label = \"user-timeline\")\n        }\n        val nestedTabConnection = remember { NestedTabConnection() }\n        CompositionLocalProvider(\n            LocalNestedTabConnection provides nestedTabConnection,\n            LocalStatusSharedElementConfig provides sharedElementConfig,\n        ) {\n            val contentScrollInProgress by nestedTabConnection.contentScrollInpProgress.collectAsState()\n            ContentPaddingsHorizontalPagerWithTab(\n                tabList = tabs,\n                blurEnabled = progress >= 1F,\n                pagerUserScrollEnabled = !contentScrollInProgress,\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun TopBarActions(\n    uiState: BskyUserDetailUiState,\n    onSearchClick: () -> Unit,\n    onBlockClick: () -> Unit,\n    onMuteClick: () -> Unit,\n    onUnmuteClick: () -> Unit,\n    onOpenInBrowserClick: () -> Unit,\n    onCopyLinkClick: () -> Unit,\n    onBlockedUserListClick: () -> Unit,\n    onMuteUserListClick: () -> Unit,\n    onFollowingFeedsClick: () -> Unit,\n    onLogoutClick: () -> Unit,\n) {\n    SimpleIconButton(\n        onClick = onSearchClick,\n        imageVector = Icons.Default.Search,\n        contentDescription = stringResource(LocalizedString.search),\n    )\n    if (uiState.isOwner) {\n        SimpleIconButton(\n            onClick = onFollowingFeedsClick,\n            imageVector = Icons.AutoMirrored.Outlined.ListAlt,\n            contentDescription = stringResource(LocalizedString.feeds),\n        )\n    }\n    var showMorePopup by remember {\n        mutableStateOf(false)\n    }\n    SimpleIconButton(\n        onClick = { showMorePopup = true },\n        imageVector = Icons.Default.MoreVert,\n        contentDescription = \"More Options\"\n    )\n    var showBlockUserConfirmDialog by remember {\n        mutableStateOf(false)\n    }\n    var showMuteDialog by remember {\n        mutableStateOf(false)\n    }\n    PopupMenu(\n        expanded = showMorePopup,\n        onDismissRequest = { showMorePopup = false },\n    ) {\n        DropDownOpenInBrowserItem {\n            showMorePopup = false\n            onOpenInBrowserClick()\n        }\n        DropDownCopyLinkItem {\n            showMorePopup = false\n            onCopyLinkClick()\n        }\n        if (uiState.isOwner) {\n            SelfAccountActions(\n                onBlockedUserListClick = onBlockedUserListClick,\n                onMuteUserListClick = onMuteUserListClick,\n                onLogoutClick = onLogoutClick,\n            )\n        } else {\n            OtherAccountActions(\n                uiState = uiState,\n                onUnmuteClick = onUnmuteClick,\n                onShowMuteDialogClick = { showMuteDialog = true },\n                onShowBlockUserConfirmDialog = { showBlockUserConfirmDialog = true },\n                onDismissMorePopupRequest = { showMorePopup = false },\n            )\n        }\n    }\n    if (showBlockUserConfirmDialog) {\n        AlertConfirmDialog(\n            content = stringResource(LocalizedString.bsky_user_detail_action_block_user_dialog_message),\n            onConfirm = {\n                showBlockUserConfirmDialog = false\n                onBlockClick()\n            },\n            onDismissRequest = { showBlockUserConfirmDialog = false },\n        )\n    }\n    if (showMuteDialog) {\n        AlertConfirmDialog(\n            content = stringResource(LocalizedString.bsky_user_detail_action_mute_user_dialog_message),\n            onConfirm = {\n                showMuteDialog = false\n                onMuteClick()\n            },\n            onDismissRequest = { showMuteDialog = false },\n        )\n    }\n}\n\n@Composable\nprivate fun SelfAccountActions(\n    onBlockedUserListClick: () -> Unit,\n    onMuteUserListClick: () -> Unit,\n    onLogoutClick: () -> Unit,\n) {\n    ModalDropdownMenuItem(\n        text = stringResource(LocalizedString.bsky_user_detail_action_muted_list),\n        imageVector = Icons.AutoMirrored.Filled.VolumeOff,\n        onClick = onMuteUserListClick,\n    )\n    ModalDropdownMenuItem(\n        text = stringResource(LocalizedString.bsky_user_detail_action_blocked_list),\n        imageVector = Icons.Default.Block,\n        onClick = onBlockedUserListClick,\n    )\n    var showLogoutDialog by remember { mutableStateOf(false) }\n    ModalDropdownMenuItem(\n        text = stringResource(LocalizedString.statusUiLogout),\n        imageVector = Icons.AutoMirrored.Filled.Logout,\n        colors = MenuDefaults.itemColors(\n            textColor = MaterialTheme.colorScheme.error,\n            leadingIconColor = MaterialTheme.colorScheme.error,\n        ),\n        onClick = { showLogoutDialog = true },\n    )\n    if (showLogoutDialog) {\n        FreadDialog(\n            onDismissRequest = { showLogoutDialog = false },\n            contentText = stringResource(LocalizedString.statusUiLogoutDialogContent),\n            onPositiveClick = {\n                showLogoutDialog = false\n                onLogoutClick()\n            },\n            onNegativeClick = { showLogoutDialog = false },\n        )\n    }\n}\n\n@Composable\nprivate fun OtherAccountActions(\n    uiState: BskyUserDetailUiState,\n    onUnmuteClick: () -> Unit,\n    onShowMuteDialogClick: () -> Unit,\n    onShowBlockUserConfirmDialog: () -> Unit,\n    onDismissMorePopupRequest: () -> Unit,\n) {\n    val fixedName = uiState.displayName?.take(10).orEmpty()\n    val muteOrUnmuteText = if (uiState.muted) {\n        stringResource(LocalizedString.bsky_user_detail_action_unmute_user)\n    } else {\n        stringResource(LocalizedString.bsky_user_detail_action_mute_user, fixedName)\n    }\n    ModalDropdownMenuItem(\n        text = muteOrUnmuteText,\n        imageVector = Icons.AutoMirrored.Filled.VolumeOff,\n        onClick = {\n            onDismissMorePopupRequest()\n            if (uiState.muted) {\n                onUnmuteClick()\n            } else {\n                onShowMuteDialogClick()\n            }\n        }\n    )\n    if (!uiState.blocked) {\n        ModalDropdownMenuItem(\n            text = stringResource(LocalizedString.bsky_user_detail_action_block_user, fixedName),\n            imageVector = Icons.Default.Block,\n            onClick = {\n                onDismissMorePopupRequest()\n                onShowBlockUserConfirmDialog()\n            },\n        )\n    }\n}\n\nprivate fun openFullImageScreen(backStack: NavBackStack<NavKey>, url: String?) {\n    if (url.isNullOrEmpty()) return\n    backStack.add(\n        ImageViewerScreenNavKey(\n            selectedIndex = 0,\n            imageList = listOf(ImageViewerImage(url = url)),\n        )\n    )\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/screen/user/detail/BskyUserDetailUiState.kt",
    "content": "package com.zhangke.fread.bluesky.internal.screen.user.detail\n\nimport com.zhangke.fread.bluesky.internal.model.BlueskyFeeds\nimport com.zhangke.fread.status.model.Relationships\n\ndata class BskyUserDetailUiState(\n    val loading: Boolean,\n    val loadError: Throwable?,\n    val did: String,\n    val handle: String?,\n    val userHomePageUrl: String?,\n    val displayName: String?,\n    val description: String?,\n    val avatar: String?,\n    val banner: String?,\n    val isOwner: Boolean,\n    val followersCount: Long?,\n    val followsCount: Long?,\n    val postsCount: Long?,\n    val relationship: Relationships?,\n    val followUri: String?,\n    val blockUri: String?,\n    val muted: Boolean,\n    val tabs: List<BlueskyFeeds>,\n) {\n\n    val blocked: Boolean get() = !blockUri.isNullOrEmpty()\n\n    val prettyHandle: String\n        get() = handle?.let {\n            if (it.startsWith(\"@\")) it else \"@$it\"\n        }.orEmpty()\n\n    companion object {\n\n        fun default(did: String): BskyUserDetailUiState {\n            return BskyUserDetailUiState(\n                loading = false,\n                loadError = null,\n                did = did,\n                handle = null,\n                isOwner = false,\n                displayName = null,\n                description = null,\n                avatar = null,\n                banner = null,\n                userHomePageUrl = null,\n                followersCount = null,\n                followsCount = null,\n                postsCount = null,\n                relationship = null,\n                followUri = null,\n                blockUri = null,\n                muted = false,\n                tabs = emptyList(),\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/screen/user/detail/BskyUserDetailViewModel.kt",
    "content": "package com.zhangke.fread.bluesky.internal.screen.user.detail\n\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport app.bsky.actor.GetProfileQueryParams\nimport app.bsky.actor.ProfileViewDetailed\nimport com.zhangke.framework.composable.TextString\nimport com.zhangke.framework.composable.textOf\nimport com.zhangke.framework.composable.toTextStringOrNull\nimport com.zhangke.framework.ktx.launchInViewModel\nimport com.zhangke.fread.bluesky.internal.account.BlueskyLoggedAccountManager\nimport com.zhangke.fread.bluesky.internal.adapter.BlueskyAccountAdapter\nimport com.zhangke.fread.bluesky.internal.client.BlueskyClientManager\nimport com.zhangke.fread.bluesky.internal.model.BlueskyFeeds\nimport com.zhangke.fread.bluesky.internal.uri.user.UserUriTransformer\nimport com.zhangke.fread.bluesky.internal.usecase.RefreshSessionUseCase\nimport com.zhangke.fread.bluesky.internal.usecase.UpdateBlockUseCase\nimport com.zhangke.fread.bluesky.internal.usecase.UpdateRelationshipType\nimport com.zhangke.fread.bluesky.internal.usecase.UpdateRelationshipUseCase\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.model.PlatformLocator\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asSharedFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.update\nimport kotlinx.coroutines.launch\nimport sh.christian.ozone.api.Did\n\nclass BskyUserDetailViewModel(\n    private val clientManager: BlueskyClientManager,\n    private val accountAdapter: BlueskyAccountAdapter,\n    private val updateRelationship: UpdateRelationshipUseCase,\n    private val updateBlock: UpdateBlockUseCase,\n    private val accountManager: BlueskyLoggedAccountManager,\n    private val refreshSession: RefreshSessionUseCase,\n    private val userUriTransformer: UserUriTransformer,\n    private val locator: PlatformLocator,\n    private val did: String,\n) : ViewModel() {\n\n    private val _uiState = MutableStateFlow(BskyUserDetailUiState.default(did = did))\n    val uiState = _uiState.asStateFlow()\n\n    private val _snackBarMessage = MutableSharedFlow<TextString>()\n    val snackBarMessage = _snackBarMessage\n\n    private val _finishPageFlow = MutableSharedFlow<Unit>()\n    val finishPageFlow = _finishPageFlow.asSharedFlow()\n\n    private var loadJob: Job? = null\n    private var sessionRefreshed = false\n\n    init {\n        loadUserDetail()\n    }\n\n    fun onPageResume() {\n        if (loadJob?.isActive == true) return\n        loadUserDetail()\n    }\n\n    private fun loadUserDetail(showLoading: Boolean = true) {\n        loadJob?.cancel()\n        loadJob = viewModelScope.launch {\n            _uiState.update { it.copy(loading = showLoading) }\n            val client = clientManager.getClient(locator)\n            val isOwner = client.loggedAccountProvider()?.did == did\n            _uiState.update { it.copy(isOwner = isOwner, tabs = createTabs(isOwner)) }\n            client.getProfileCatching(GetProfileQueryParams(Did(did)))\n                .onSuccess { detailed ->\n                    _uiState.update { state ->\n                        detailed.updateUiState(state).copy(\n                            loading = false,\n                            loadError = null,\n                        )\n                    }\n                    if (isOwner) {\n                        accountManager.updateAccountProfile(locator, detailed)\n                        if (!sessionRefreshed) {\n                            refreshSession()\n                            sessionRefreshed = true\n                        }\n                    }\n                }.onFailure {\n                    _uiState.update { state ->\n                        state.copy(\n                            loading = false,\n                            loadError = if (showLoading) it else null,\n                        )\n                    }\n                }\n        }\n    }\n\n    fun onFollowClick() {\n        launchInViewModel {\n            updateRelationship(\n                locator = locator,\n                targetDid = did,\n                type = UpdateRelationshipType.FOLLOW,\n            ).handleAndRefresh()\n        }\n    }\n\n    fun onUnfollowClick() {\n        launchInViewModel {\n            updateRelationship(\n                locator = locator,\n                targetDid = did,\n                type = UpdateRelationshipType.UNFOLLOW,\n                followUri = uiState.value.followUri,\n            ).handleAndRefresh()\n        }\n    }\n\n    fun onBlockClick() {\n        launchInViewModel {\n            updateBlock(\n                locator = locator,\n                did = did,\n                block = true,\n                blockUri = null,\n            ).handleAndRefresh()\n        }\n    }\n\n    fun onUnblockClick() {\n        launchInViewModel {\n            updateBlock(\n                locator = locator,\n                did = did,\n                block = false,\n                blockUri = uiState.value.blockUri,\n            ).handleAndRefresh()\n        }\n    }\n\n    fun onMuteClick(mute: Boolean) {\n        launchInViewModel {\n            val client = clientManager.getClient(locator)\n            val did = Did(did)\n            val result = if (mute) {\n                client.muteActorCatching(did)\n            } else {\n                client.unmuteActorCatching(did)\n            }\n            result.handleAndRefresh()\n        }\n    }\n\n    fun onLogoutClick() {\n        launchInViewModel {\n            accountManager.logout(userUriTransformer.createUserUri(did))\n            _finishPageFlow.emit(Unit)\n        }\n    }\n\n    private suspend fun <T> Result<T>.handleAndRefresh() {\n        if (isSuccess) {\n            loadUserDetail(false)\n        } else {\n            val errorMessage = exceptionOrNull()?.toTextStringOrNull()\n            _snackBarMessage.emit(\n                errorMessage ?: textOf(LocalizedString.unknownError)\n            )\n        }\n    }\n\n    private fun ProfileViewDetailed.updateUiState(uiState: BskyUserDetailUiState): BskyUserDetailUiState {\n        return uiState.copy(\n            displayName = this.displayName,\n            description = this.description,\n            handle = this.handle.handle,\n            avatar = this.avatar?.uri,\n            banner = this.banner?.uri,\n            followsCount = this.followsCount,\n            followersCount = this.followersCount,\n            postsCount = this.postsCount,\n            userHomePageUrl = \"https://bsky.app/profile/${this.handle}\",\n            followUri = this.viewer?.following?.atUri,\n            muted = this.viewer?.muted == true,\n            blockUri = this.viewer?.blocking?.atUri,\n            relationship = this.viewer?.let { accountAdapter.convertRelationship(it) },\n        )\n    }\n\n    private fun createTabs(isOwner: Boolean): List<BlueskyFeeds> {\n        return mutableListOf<BlueskyFeeds>().apply {\n            add(BlueskyFeeds.UserPosts(did))\n            add(BlueskyFeeds.UserReplies(did))\n            add(BlueskyFeeds.UserMedias(did))\n            if (isOwner) {\n                add(BlueskyFeeds.UserLikes(did))\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/screen/user/edit/EditProfileScreen.kt",
    "content": "package com.zhangke.fread.bluesky.internal.screen.user.edit\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.BoxScope\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.imePadding\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Check\nimport androidx.compose.material.icons.filled.Edit\nimport androidx.compose.material3.CircularProgressIndicator\nimport androidx.compose.material3.OutlinedTextField\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.SnackbarHost\nimport androidx.compose.material3.SnackbarHostState\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.alpha\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.text.input.TextFieldValue\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport androidx.navigation3.runtime.NavKey\nimport com.seiko.imageloader.ui.AutoSizeImage\nimport com.zhangke.framework.composable.AlertConfirmDialog\nimport com.zhangke.framework.composable.ConsumeFlow\nimport com.zhangke.framework.composable.ConsumeSnackbarFlow\nimport com.zhangke.framework.composable.SimpleIconButton\nimport com.zhangke.framework.composable.Toolbar\nimport com.zhangke.framework.composable.currentOrThrow\nimport com.zhangke.framework.composable.freadPlaceholder\nimport com.zhangke.framework.composable.pick.PickVisualMediaLauncherContainer\nimport com.zhangke.framework.composable.rememberSnackbarHostState\nimport com.zhangke.framework.nav.LocalNavBackStack\nimport com.zhangke.framework.utils.PlatformUri\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.model.PlatformLocator\nimport kotlinx.serialization.Serializable\nimport org.jetbrains.compose.resources.stringResource\n\n@Serializable\ndata class EditProfileScreenNavKey(\n    val locator: PlatformLocator,\n) : NavKey\n\n@Composable\nfun EditProfileScreen(viewModel: EditProfileViewModel) {\n    val backStack = LocalNavBackStack.currentOrThrow\n    val uiState by viewModel.uiState.collectAsState()\n    val snackbarHostState = rememberSnackbarHostState()\n    EditProfileContent(\n        uiState = uiState,\n        snackbarHostState = snackbarHostState,\n        onBackClick = backStack::removeLastOrNull,\n        onAvatarSelected = viewModel::onAvatarSelected,\n        onBannerSelected = viewModel::onBannerSelected,\n        onUserNameChanged = viewModel::onUserNameChanged,\n        onDescriptionChanged = viewModel::onDescriptionChanged,\n        onSaveClick = viewModel::onSaveClick,\n    )\n    ConsumeSnackbarFlow(snackbarHostState, viewModel.snackBarMessage)\n    ConsumeFlow(viewModel.finishScreenFlow) { backStack.removeLastOrNull() }\n}\n\n@Composable\nprivate fun EditProfileContent(\n    uiState: EditProfileUiState,\n    snackbarHostState: SnackbarHostState,\n    onBackClick: () -> Unit,\n    onAvatarSelected: (PlatformUri) -> Unit,\n    onBannerSelected: (PlatformUri) -> Unit,\n    onUserNameChanged: (TextFieldValue) -> Unit,\n    onDescriptionChanged: (TextFieldValue) -> Unit,\n    onSaveClick: () -> Unit,\n) {\n    Scaffold(\n        topBar = {\n            var showConfirmExitDialog by remember { mutableStateOf(false) }\n            Toolbar(\n                title = stringResource(LocalizedString.bsky_edit_profile_title),\n                onBackClick = {\n                    if (uiState.modified) {\n                        showConfirmExitDialog = true\n                    } else {\n                        onBackClick()\n                    }\n                },\n                actions = {\n                    if (uiState.requesting) {\n                        CircularProgressIndicator(\n                            modifier = Modifier\n                                .padding(end = 8.dp)\n                                .size(24.dp)\n                        )\n                    } else {\n                        SimpleIconButton(\n                            onClick = {\n                                onSaveClick()\n                            },\n                            imageVector = Icons.Default.Check,\n                            contentDescription = \"Save\",\n                        )\n                    }\n                }\n            )\n            if (showConfirmExitDialog) {\n                AlertConfirmDialog(\n                    content = stringResource(LocalizedString.editProfileEditConfirmMessage),\n                    onConfirm = onBackClick,\n                    onDismissRequest = { showConfirmExitDialog = false }\n                )\n            }\n        },\n        snackbarHost = {\n            SnackbarHost(\n                modifier = Modifier.padding(bottom = 60.dp),\n                hostState = snackbarHostState,\n            )\n        },\n    ) { innerPadding ->\n        Column(\n            modifier = Modifier\n                .padding(innerPadding)\n                .padding(bottom = 30.dp)\n                .verticalScroll(rememberScrollState())\n                .imePadding(),\n        ) {\n            val headerHeight = 150.dp\n            val avatarSize = 80.dp\n            Box(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .height(headerHeight + avatarSize / 2)\n            ) {\n                HeaderInEdit(\n                    uiState = uiState,\n                    headerHeight = headerHeight,\n                    onHeaderSelected = onBannerSelected,\n                )\n                AvatarInEdit(\n                    uiState = uiState,\n                    avatarSize = avatarSize,\n                    onAvatarSelected = onAvatarSelected,\n                )\n            }\n            OutlinedTextField(\n                modifier = Modifier\n                    .padding(start = 16.dp, top = 16.dp, end = 16.dp)\n                    .fillMaxWidth(),\n                value = uiState.userName,\n                onValueChange = onUserNameChanged,\n                placeholder = {\n                    Text(\n                        modifier = Modifier.alpha(0.7F),\n                        text = stringResource(LocalizedString.editProfileInputNameHint)\n                    )\n                },\n                label = {\n                    Text(text = stringResource(LocalizedString.editProfileLabelName))\n                },\n            )\n            OutlinedTextField(\n                modifier = Modifier\n                    .padding(start = 16.dp, top = 16.dp, end = 16.dp)\n                    .fillMaxWidth(),\n                value = uiState.description,\n                onValueChange = onDescriptionChanged,\n                placeholder = {\n                    Text(\n                        modifier = Modifier.alpha(0.7F),\n                        text = stringResource(LocalizedString.editProfileInputNoteHint),\n                    )\n                },\n                label = {\n                    Text(text = stringResource(LocalizedString.editProfileLabelNote))\n                },\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun HeaderInEdit(\n    uiState: EditProfileUiState,\n    headerHeight: Dp,\n    onHeaderSelected: (PlatformUri) -> Unit,\n) {\n    PickVisualMediaLauncherContainer(\n        onResult = { it.firstOrNull()?.let(onHeaderSelected) },\n    ) {\n        AutoSizeImage(\n            url = uiState.bannerLocalUri?.toString() ?: uiState.banner,\n            modifier = Modifier\n                .height(headerHeight)\n                .fillMaxWidth()\n                .freadPlaceholder(uiState.banner.isEmpty() && uiState.bannerLocalUri == null)\n                .clickable { launchImage() },\n            contentScale = ContentScale.Crop,\n            contentDescription = null,\n        )\n    }\n}\n\n@Composable\nprivate fun BoxScope.AvatarInEdit(\n    uiState: EditProfileUiState,\n    avatarSize: Dp,\n    onAvatarSelected: (PlatformUri) -> Unit,\n) {\n    Box(\n        modifier = Modifier\n            .align(Alignment.BottomStart)\n            .padding(start = 16.dp)\n            .size(avatarSize)\n            .clip(CircleShape)\n            .border(2.dp, Color.White, CircleShape)\n            .freadPlaceholder(uiState.avatar.isEmpty() && uiState.avatarLocalUri == null),\n    ) {\n        AutoSizeImage(\n            url = uiState.avatarLocalUri?.toString() ?: uiState.avatar,\n            modifier = Modifier.fillMaxSize(),\n            contentScale = ContentScale.Crop,\n            contentDescription = \"avatar\",\n        )\n        PickVisualMediaLauncherContainer(\n            onResult = {\n                it.firstOrNull()?.let(onAvatarSelected)\n            },\n        ) {\n            SimpleIconButton(\n                modifier = Modifier\n                    .fillMaxSize()\n                    .background(Color.Black.copy(alpha = 0.5F))\n                    .padding(10.dp),\n                onClick = { launchImage() },\n                imageVector = Icons.Default.Edit,\n                tint = Color.White,\n                contentDescription = \"Edit Avatar\",\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/screen/user/edit/EditProfileUiState.kt",
    "content": "package com.zhangke.fread.bluesky.internal.screen.user.edit\n\nimport androidx.compose.ui.text.input.TextFieldValue\nimport com.zhangke.framework.utils.PlatformUri\nimport com.zhangke.fread.bluesky.internal.account.BlueskyLoggedAccount\n\ndata class EditProfileUiState(\n    val loggedAccount: BlueskyLoggedAccount?,\n    val userName: TextFieldValue,\n    val description: TextFieldValue,\n    val avatar: String,\n    val banner: String,\n    val avatarLocalUri: PlatformUri?,\n    val bannerLocalUri: PlatformUri?,\n    val requesting: Boolean,\n) {\n\n    val modified: Boolean\n        get() = userName.text != loggedAccount?.userName ||\n                description.text != loggedAccount.description ||\n                avatarLocalUri != null || bannerLocalUri != null\n\n    companion object {\n\n        fun default(): EditProfileUiState {\n            return EditProfileUiState(\n                loggedAccount = null,\n                userName = TextFieldValue(\"\"),\n                description = TextFieldValue(\"\"),\n                avatar = \"\",\n                banner = \"\",\n                avatarLocalUri = null,\n                bannerLocalUri = null,\n                requesting = false,\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/screen/user/edit/EditProfileViewModel.kt",
    "content": "package com.zhangke.fread.bluesky.internal.screen.user.edit\n\nimport androidx.compose.ui.text.input.TextFieldValue\nimport androidx.lifecycle.ViewModel\nimport app.bsky.actor.Profile\nimport com.atproto.repo.GetRecordQueryParams\nimport com.atproto.repo.GetRecordResponse\nimport com.atproto.repo.PutRecordRequest\nimport com.zhangke.framework.composable.TextString\nimport com.zhangke.framework.composable.emitTextMessageFromThrowable\nimport com.zhangke.framework.composable.textOf\nimport com.zhangke.framework.ktx.launchInViewModel\nimport com.zhangke.framework.utils.PlatformUri\nimport com.zhangke.framework.utils.exceptionOrThrow\nimport com.zhangke.fread.bluesky.internal.account.BlueskyLoggedAccountManager\nimport com.zhangke.fread.bluesky.internal.client.BlueskyClientManager\nimport com.zhangke.fread.bluesky.internal.client.BskyCollections\nimport com.zhangke.fread.bluesky.internal.client.selfRkey\nimport com.zhangke.fread.bluesky.internal.usecase.UploadBlobUseCase\nimport com.zhangke.fread.bluesky.internal.utils.bskyJson\nimport com.zhangke.fread.status.model.PlatformLocator\nimport kotlinx.coroutines.async\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asSharedFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.update\nimport kotlinx.coroutines.supervisorScope\nimport sh.christian.ozone.api.Did\nimport sh.christian.ozone.api.model.Blob\n\nclass EditProfileViewModel(\n    private val accountManager: BlueskyLoggedAccountManager,\n    private val clientManager: BlueskyClientManager,\n    private val uploadBlob: UploadBlobUseCase,\n    private val locator: PlatformLocator,\n) : ViewModel() {\n\n    private val _uiState = MutableStateFlow(EditProfileUiState.default())\n    val uiState = _uiState.asStateFlow()\n\n    private val _snackBarMessage = MutableSharedFlow<TextString>()\n    val snackBarMessage = _snackBarMessage\n\n    private val _finishScreenFlow = MutableSharedFlow<Unit>()\n    val finishScreenFlow = _finishScreenFlow.asSharedFlow()\n\n    private var profile: Profile? = null\n\n    init {\n        loadLoggedUser()\n    }\n\n    private fun loadLoggedUser() {\n        launchInViewModel {\n            val account = accountManager.getAccount(locator)\n            if (account == null) {\n                _snackBarMessage.emit(textOf(\"Account not found\"))\n                return@launchInViewModel\n            }\n            _uiState.update {\n                it.copy(\n                    loggedAccount = account,\n                    userName = TextFieldValue(account.userName),\n                    description = TextFieldValue(account.description),\n                    avatar = account.avatar.orEmpty(),\n                    banner = account.user.banner.orEmpty(),\n                )\n            }\n            getProfile(account.did)\n        }\n    }\n\n    fun onBannerSelected(uri: PlatformUri) {\n        _uiState.update { it.copy(bannerLocalUri = uri) }\n    }\n\n    fun onAvatarSelected(uri: PlatformUri) {\n        _uiState.update { it.copy(avatarLocalUri = uri) }\n    }\n\n    fun onSaveClick() {\n        updateProfile()\n    }\n\n    fun onUserNameChanged(text: TextFieldValue) {\n        _uiState.update { it.copy(userName = text) }\n    }\n\n    fun onDescriptionChanged(text: TextFieldValue) {\n        _uiState.update { it.copy(description = text) }\n    }\n\n    private fun updateProfile() {\n        val account = _uiState.value.loggedAccount ?: return\n        launchInViewModel {\n            _uiState.update { it.copy(requesting = true) }\n            val currentUiState = uiState.value\n            var profile = profile\n            if (profile == null) {\n                val result = getProfile(account.did)\n                if (result.isFailure) {\n                    _uiState.update { it.copy(requesting = false) }\n                    _snackBarMessage.emitTextMessageFromThrowable(result.exceptionOrThrow())\n                    return@launchInViewModel\n                }\n                profile = result.getOrThrow()\n            }\n            val uploadResult =\n                uploadImages(currentUiState.avatarLocalUri, currentUiState.bannerLocalUri)\n            if (uploadResult.isFailure) {\n                _uiState.update { it.copy(requesting = false) }\n                _snackBarMessage.emitTextMessageFromThrowable(uploadResult.exceptionOrThrow())\n                return@launchInViewModel\n            }\n            val (avatar, banner) = uploadResult.getOrThrow()\n            val client = clientManager.getClient(locator)\n            profile = profile.copy(\n                displayName = _uiState.value.userName.text,\n                description = _uiState.value.description.text,\n                avatar = avatar ?: profile.avatar,\n                banner = banner ?: profile.banner,\n            )\n            client.putRecordCatching(\n                PutRecordRequest(\n                    repo = Did(account.did),\n                    collection = BskyCollections.profile,\n                    rkey = selfRkey,\n                    record = profile.bskyJson(),\n                )\n            ).onFailure { t ->\n                _uiState.update { it.copy(requesting = false) }\n                _snackBarMessage.emitTextMessageFromThrowable(t)\n            }.onSuccess {\n                _uiState.update { it.copy(requesting = false) }\n                _finishScreenFlow.emit(Unit)\n            }\n        }\n    }\n\n    private suspend fun uploadImages(\n        avatar: PlatformUri?,\n        banner: PlatformUri?,\n    ): Result<Pair<Blob?, Blob?>> {\n        if (avatar == null && banner == null) return Result.success(null to null)\n        if (avatar == null) return uploadBlob(locator, banner!!).map { null to it.first }\n        if (banner == null) return uploadBlob(locator, avatar).map { it.first to null }\n        return supervisorScope {\n            val avatarDeferred = async { uploadBlob(locator, avatar).map { it.first } }\n            val bannerDeferred = async { uploadBlob(locator, banner).map { it.first } }\n            val avatarResult = avatarDeferred.await()\n            val bannerResult = bannerDeferred.await()\n            if (avatarResult.isFailure || bannerResult.isFailure) {\n                val exception = avatarResult.exceptionOrNull() ?: bannerResult.exceptionOrThrow()\n                return@supervisorScope Result.failure(exception)\n            }\n            Result.success(avatarResult.getOrThrow() to bannerResult.getOrThrow())\n        }\n    }\n\n    private suspend fun getProfile(didText: String): Result<Profile> {\n        val client = clientManager.getClient(locator)\n        val did = Did(didText)\n        return client.getRecordCatching(\n            GetRecordQueryParams(\n                repo = did, collection = BskyCollections.profile, rkey = selfRkey,\n            )\n        ).map<Profile, GetRecordResponse> { it.value.bskyJson() }.onSuccess { profile = it }\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/screen/user/list/UserListItemUiState.kt",
    "content": "package com.zhangke.fread.bluesky.internal.screen.user.list\n\nimport com.zhangke.fread.status.author.BlogAuthor\n\ndata class UserListItemUiState(\n    val author: BlogAuthor,\n    val did: String,\n    val followedBy: Boolean,\n    val followingUri: String?,\n    val blockUri: String?,\n    val muted: Boolean,\n) {\n\n    val following: Boolean get() = !followingUri.isNullOrEmpty()\n\n    val blocked: Boolean get() = !blockUri.isNullOrEmpty()\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/screen/user/list/UserListScreen.kt",
    "content": "package com.zhangke.fread.bluesky.internal.screen.user.list\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.RowScope\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.SnackbarHost\nimport androidx.compose.material3.SnackbarHostState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.composable.AlertConfirmDialog\nimport com.zhangke.framework.composable.ConsumeSnackbarFlow\nimport com.zhangke.framework.composable.DefaultEmpty\nimport com.zhangke.framework.composable.DefaultFailed\nimport com.zhangke.framework.composable.StyledTextButton\nimport com.zhangke.framework.composable.TextButtonStyle\nimport com.zhangke.framework.composable.Toolbar\nimport com.zhangke.framework.composable.currentOrThrow\nimport com.zhangke.framework.composable.rememberSnackbarHostState\nimport com.zhangke.framework.composable.textString\nimport com.zhangke.framework.controller.CommonLoadableUiState\nimport com.zhangke.framework.loadable.lazycolumn.LoadableLazyColumn\nimport com.zhangke.framework.loadable.lazycolumn.rememberLoadableLazyColumnState\nimport com.zhangke.framework.nav.LocalNavBackStack\nimport com.zhangke.fread.bluesky.internal.screen.user.detail.BskyUserDetailScreenNavKey\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.ui.user.CommonUserPlaceHolder\nimport com.zhangke.fread.status.ui.user.CommonUserUi\nimport kotlinx.serialization.Serializable\nimport org.jetbrains.compose.resources.stringResource\n\n@Serializable\ndata class UserListScreenNavKey(\n    val locator: PlatformLocator,\n    val type: UserListType,\n    val postUri: String? = null,\n    val did: String? = null,\n) : NavKey\n\n@Composable\nfun UserListScreen(\n    locator: PlatformLocator,\n    type: UserListType,\n    viewModel: UserListViewModel,\n) {\n    val backStack = LocalNavBackStack.currentOrThrow\n    val snackbarHostState = rememberSnackbarHostState()\n    val uiState by viewModel.uiState.collectAsState()\n\n    UserListContent(\n        type = type,\n        uiState = uiState,\n        snackbarHostState = snackbarHostState,\n        onBackClick = backStack::removeLastOrNull,\n        onRefresh = viewModel::onRefresh,\n        onLoadMore = viewModel::onLoadMore,\n        onFollowClick = viewModel::onFollowClick,\n        onUnfollowClick = viewModel::onUnfollowClick,\n        onMuteClick = viewModel::onMuteClick,\n        onUnmuteClick = viewModel::onUnmuteClick,\n        onBlockClick = viewModel::onBlockClick,\n        onUnblockClick = viewModel::onUnblockClick,\n        onUserClick = { backStack.add(BskyUserDetailScreenNavKey(locator, it.did)) },\n    )\n    ConsumeSnackbarFlow(snackbarHostState, viewModel.snackBarMessage)\n}\n\n@Composable\nprivate fun UserListContent(\n    type: UserListType,\n    uiState: CommonLoadableUiState<UserListItemUiState>,\n    snackbarHostState: SnackbarHostState,\n    onBackClick: () -> Unit,\n    onRefresh: () -> Unit,\n    onLoadMore: () -> Unit,\n    onFollowClick: (UserListItemUiState) -> Unit,\n    onUnfollowClick: (UserListItemUiState) -> Unit,\n    onMuteClick: (UserListItemUiState) -> Unit,\n    onUnmuteClick: (UserListItemUiState) -> Unit,\n    onBlockClick: (UserListItemUiState) -> Unit,\n    onUnblockClick: (UserListItemUiState) -> Unit,\n    onUserClick: (UserListItemUiState) -> Unit,\n) {\n    Scaffold(\n        topBar = {\n            Toolbar(\n                title = type.title,\n                onBackClick = onBackClick,\n            )\n        },\n        snackbarHost = {\n            SnackbarHost(snackbarHostState)\n        },\n    ) { innerPadding ->\n        Box(modifier = Modifier.fillMaxSize().padding(innerPadding)) {\n            if (uiState.dataList.isEmpty() && uiState.initializing) {\n                LazyColumn(modifier = Modifier.fillMaxSize()) {\n                    items(30) { CommonUserPlaceHolder() }\n                }\n            } else if (uiState.dataList.isEmpty() && uiState.errorMessage != null) {\n                DefaultFailed(\n                    modifier = Modifier.fillMaxSize(),\n                    errorMessage = textString(uiState.errorMessage!!),\n                    onRetryClick = onRefresh,\n                )\n            } else {\n                val loadableState = rememberLoadableLazyColumnState(\n                    onRefresh = onRefresh,\n                    onLoadMore = onLoadMore,\n                )\n                LoadableLazyColumn(\n                    modifier = Modifier.fillMaxSize(),\n                    state = loadableState,\n                    refreshing = uiState.refreshing,\n                    loadState = uiState.loadMoreState,\n                ) {\n                    if (uiState.dataList.isNotEmpty()) {\n                        itemsIndexed(uiState.dataList) { index, item ->\n                            CommonUserUi(\n                                modifier = Modifier.clickable { onUserClick(item) },\n                                user = item.author,\n                                showDivider = index < uiState.dataList.lastIndex,\n                                actionButton = {\n                                    Spacer(modifier = Modifier.width(6.dp))\n                                    UserAction(\n                                        type = type,\n                                        user = item,\n                                        onFollowClick = onFollowClick,\n                                        onUnfollowClick = onUnfollowClick,\n                                        onMuteClick = onMuteClick,\n                                        onUnmuteClick = onUnmuteClick,\n                                        onBlockClick = onBlockClick,\n                                        onUnblockClick = onUnblockClick,\n                                    )\n                                },\n                            )\n                        }\n                    } else {\n                        item { DefaultEmpty() }\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun RowScope.UserAction(\n    type: UserListType,\n    user: UserListItemUiState,\n    onFollowClick: (UserListItemUiState) -> Unit,\n    onUnfollowClick: (UserListItemUiState) -> Unit,\n    onMuteClick: (UserListItemUiState) -> Unit,\n    onUnmuteClick: (UserListItemUiState) -> Unit,\n    onBlockClick: (UserListItemUiState) -> Unit,\n    onUnblockClick: (UserListItemUiState) -> Unit,\n) {\n    when (type) {\n        UserListType.MUTED -> {\n            var showConfirmDialog by remember { mutableStateOf(false) }\n            StyledTextButton(\n                modifier = Modifier.align(Alignment.CenterVertically),\n                text = if (user.muted) {\n                    stringResource(LocalizedString.sharedUserListActionMuted)\n                } else {\n                    stringResource(LocalizedString.sharedUserListActionMute)\n                },\n                style = if (user.muted) TextButtonStyle.STANDARD else TextButtonStyle.ALERT,\n                onClick = {\n                    if (user.muted) {\n                        onUnmuteClick(user)\n                    } else {\n                        showConfirmDialog = true\n                    }\n                },\n            )\n            if (showConfirmDialog) {\n                AlertConfirmDialog(\n                    content = stringResource(LocalizedString.sharedUserListActionMuteDialogMessage),\n                    onConfirm = { onMuteClick(user) },\n                    onDismissRequest = { showConfirmDialog = false },\n                )\n            }\n        }\n\n        UserListType.BLOCKED -> {\n            var showConfirmDialog by remember { mutableStateOf(false) }\n            StyledTextButton(\n                modifier = Modifier.align(Alignment.CenterVertically),\n                text = if (user.blocked) {\n                    stringResource(LocalizedString.sharedUserListActionBlocked)\n                } else {\n                    stringResource(LocalizedString.sharedUserListActionBlock)\n                },\n                style = if (user.blocked) TextButtonStyle.STANDARD else TextButtonStyle.ALERT,\n                onClick = {\n                    if (user.blocked) {\n                        onUnblockClick(user)\n                    } else {\n                        showConfirmDialog = false\n                    }\n                },\n            )\n            if (showConfirmDialog) {\n                AlertConfirmDialog(\n                    content = stringResource(LocalizedString.sharedUserListActionBlockDialogMessage),\n                    onConfirm = { onBlockClick(user) },\n                    onDismissRequest = { showConfirmDialog = false },\n                )\n            }\n        }\n\n        else -> {\n            var showConfirmDialog by remember { mutableStateOf(false) }\n            StyledTextButton(\n                modifier = Modifier.align(Alignment.CenterVertically),\n                text = if (user.following) {\n                    stringResource(LocalizedString.statusUiUserDetailRelationshipFollowing)\n                } else {\n                    stringResource(LocalizedString.statusUiUserDetailRelationshipNotFollow)\n                },\n                style = TextButtonStyle.STANDARD,\n                onClick = {\n                    if (user.following) {\n                        showConfirmDialog = false\n                    } else {\n                        onFollowClick(user)\n                    }\n                },\n            )\n            if (showConfirmDialog) {\n                AlertConfirmDialog(\n                    content = stringResource(LocalizedString.statusUiRelationshipBtnDialogContentCancelFollow),\n                    onConfirm = { onUnfollowClick(user) },\n                    onDismissRequest = { showConfirmDialog = false },\n                )\n            }\n        }\n    }\n}\n\nprivate val UserListType.title: String\n    @Composable get() = when (this) {\n        UserListType.LIKE -> stringResource(LocalizedString.sharedUserListTitleLikes)\n        UserListType.REBLOG -> stringResource(LocalizedString.sharedUserListTitleReblog)\n        UserListType.MUTED -> stringResource(LocalizedString.sharedUserListTitleMutes)\n        UserListType.BLOCKED -> stringResource(LocalizedString.sharedUserListTitleBlocks)\n        UserListType.FOLLOWERS -> stringResource(LocalizedString.sharedUserListTitleFollowers)\n        UserListType.FOLLOWING -> stringResource(LocalizedString.sharedUserListTitleFollowing)\n    }\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/screen/user/list/UserListType.kt",
    "content": "package com.zhangke.fread.bluesky.internal.screen.user.list\n\nimport com.zhangke.framework.utils.PlatformSerializable\nimport kotlinx.serialization.Serializable\n\n@Serializable\nenum class UserListType : PlatformSerializable {\n\n    LIKE,\n    REBLOG,\n    FOLLOWING,\n    FOLLOWERS,\n    BLOCKED,\n    MUTED,\n\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/screen/user/list/UserListViewModel.kt",
    "content": "package com.zhangke.fread.bluesky.internal.screen.user.list\n\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport app.bsky.actor.ProfileView\nimport app.bsky.feed.GetLikesQueryParams\nimport app.bsky.feed.GetRepostedByQueryParams\nimport app.bsky.graph.GetBlocksQueryParams\nimport app.bsky.graph.GetFollowersQueryParams\nimport app.bsky.graph.GetFollowsQueryParams\nimport app.bsky.graph.GetMutesQueryParams\nimport com.zhangke.framework.composable.TextString\nimport com.zhangke.framework.composable.emitInViewModel\nimport com.zhangke.framework.composable.emitTextMessageFromThrowable\nimport com.zhangke.framework.controller.CommonLoadableController\nimport com.zhangke.framework.controller.CommonLoadableUiState\nimport com.zhangke.framework.ktx.launchInViewModel\nimport com.zhangke.fread.bluesky.internal.adapter.BlueskyAccountAdapter\nimport com.zhangke.fread.bluesky.internal.client.BlueskyClientManager\nimport com.zhangke.fread.bluesky.internal.usecase.UpdateBlockUseCase\nimport com.zhangke.fread.bluesky.internal.usecase.UpdateRelationshipType\nimport com.zhangke.fread.bluesky.internal.usecase.UpdateRelationshipUseCase\nimport com.zhangke.fread.status.model.PlatformLocator\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.update\nimport sh.christian.ozone.api.AtUri\nimport sh.christian.ozone.api.Did\n\nclass UserListViewModel(\n    private val clientManager: BlueskyClientManager,\n    private val accountAdapter: BlueskyAccountAdapter,\n    private val updateRelationship: UpdateRelationshipUseCase,\n    private val updateBlock: UpdateBlockUseCase,\n    private val locator: PlatformLocator,\n    private val type: UserListType,\n    private val postUri: String?,\n    userDid: String?,\n) : ViewModel() {\n\n    private val _snackBarMessage = MutableSharedFlow<TextString>()\n    val snackBarMessage = _snackBarMessage\n\n    private val loadController = CommonLoadableController<UserListItemUiState>(\n        viewModelScope,\n        onPostSnackMessage = { _snackBarMessage.emitInViewModel(it) },\n    )\n\n    val uiState: StateFlow<CommonLoadableUiState<UserListItemUiState>> get() = loadController.uiState\n\n    private var cursor: String? = null\n\n    private val userDid: Did? = userDid?.let { Did(it) }\n\n    init {\n        loadController.initData(\n            getDataFromLocal = { emptyList() },\n            getDataFromServer = { getDataFromServer(null) },\n        )\n    }\n\n    fun onRefresh() {\n        loadController.onRefresh { getDataFromServer(null) }\n    }\n\n    fun onLoadMore() {\n        loadController.onLoadMore { getDataFromServer() }\n    }\n\n    fun onFollowClick(user: UserListItemUiState) {\n        launchInViewModel {\n            updateRelationship(\n                locator = locator,\n                targetDid = user.did,\n                type = UpdateRelationshipType.FOLLOW,\n                followUri = user.followingUri,\n            ).handleError()\n                .onSuccess { uri ->\n                    updateUserInfo(user.did) {\n                        it.copy(followingUri = uri?.atUri)\n                    }\n                }\n        }\n    }\n\n    fun onUnfollowClick(user: UserListItemUiState) {\n        launchInViewModel {\n            updateRelationship(\n                locator = locator,\n                targetDid = user.did,\n                type = UpdateRelationshipType.UNFOLLOW,\n                followUri = user.followingUri,\n            ).handleError()\n                .onSuccess {\n                    updateUserInfo(user.did) {\n                        it.copy(followingUri = null)\n                    }\n                }\n        }\n    }\n\n    fun onMuteClick(user: UserListItemUiState) {\n        launchInViewModel {\n            clientManager.getClient(locator)\n                .muteActorCatching(Did(user.did))\n                .handleError()\n                .onSuccess {\n                    updateUserInfo(user.did) {\n                        it.copy(muted = true)\n                    }\n                }\n        }\n    }\n\n    fun onUnmuteClick(user: UserListItemUiState) {\n        launchInViewModel {\n            clientManager.getClient(locator)\n                .unmuteActorCatching(Did(user.did))\n                .handleError()\n                .onSuccess {\n                    updateUserInfo(user.did) {\n                        it.copy(muted = false)\n                    }\n                }\n        }\n    }\n\n    fun onBlockClick(user: UserListItemUiState) {\n        launchInViewModel {\n            updateBlock(\n                locator = locator,\n                did = user.did,\n                block = true,\n                blockUri = user.blockUri,\n            ).handleError()\n                .onSuccess { uri ->\n                    updateUserInfo(user.did) {\n                        it.copy(blockUri = uri?.atUri)\n                    }\n                }\n        }\n    }\n\n    fun onUnblockClick(user: UserListItemUiState) {\n        launchInViewModel {\n            updateBlock(\n                locator = locator,\n                did = user.did,\n                block = false,\n                blockUri = user.blockUri,\n            ).handleError()\n                .onSuccess { uri ->\n                    updateUserInfo(user.did) {\n                        it.copy(blockUri = uri?.atUri)\n                    }\n                }\n        }\n    }\n\n    private fun updateUserInfo(did: String, block: (UserListItemUiState) -> UserListItemUiState) {\n        loadController.mutableUiState.update { state ->\n            state.copy(\n                dataList = state.dataList.map {\n                    if (it.did == did) {\n                        block(it)\n                    } else {\n                        it\n                    }\n                },\n            )\n        }\n    }\n\n    private suspend fun <T> Result<T>.handleError(): Result<T> {\n        return this.onFailure {\n            _snackBarMessage.emitTextMessageFromThrowable(it)\n        }\n    }\n\n    private suspend fun getDataFromServer(cursor: String? = this.cursor): Result<List<UserListItemUiState>> {\n        val client = clientManager.getClient(locator)\n        val pagedDataResult = when (type) {\n            UserListType.LIKE -> {\n                if (postUri == null) return Result.failure(IllegalStateException(\"PostUri is null\"))\n                client.getLikesCatching(\n                    GetLikesQueryParams(\n                        uri = AtUri(postUri),\n                        cursor = cursor\n                    )\n                )\n            }\n\n            UserListType.REBLOG -> {\n                if (postUri == null) return Result.failure(IllegalStateException(\"PostUri is null\"))\n                client.getRepostedCatching(\n                    GetRepostedByQueryParams(uri = AtUri(postUri), cursor = cursor)\n                )\n            }\n\n            UserListType.FOLLOWERS -> {\n                val did = userDid ?: client.loggedAccountProvider()?.did?.let { Did(it) }\n                if (did == null) return Result.success(emptyList())\n                client.getFollowersCatching(\n                    GetFollowersQueryParams(\n                        actor = did,\n                        cursor = cursor\n                    )\n                )\n            }\n\n            UserListType.FOLLOWING -> {\n                val did = userDid ?: client.loggedAccountProvider()?.did?.let { Did(it) }\n                if (did == null) return Result.success(emptyList())\n                client.getFollowsCatching(GetFollowsQueryParams(actor = did, cursor = cursor))\n            }\n\n            UserListType.MUTED -> {\n                client.getMutesCatching(GetMutesQueryParams(cursor = cursor))\n            }\n\n            UserListType.BLOCKED -> {\n                client.getBlocksCatching(GetBlocksQueryParams(cursor = cursor))\n            }\n        }\n        return pagedDataResult.map { data ->\n            this.cursor = data.cursor\n            data.list.map { convertToUiState(it) }\n        }\n    }\n\n    private fun convertToUiState(profile: ProfileView): UserListItemUiState {\n        return UserListItemUiState(\n            author = accountAdapter.convertToBlogAuthor(profile),\n            did = profile.did.did,\n            followingUri = profile.viewer?.following?.atUri,\n            followedBy = !profile.viewer?.followedBy?.atUri.isNullOrEmpty(),\n            blockUri = profile.viewer?.blocking?.atUri,\n            muted = profile.viewer?.muted == true,\n        )\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/tracking/BskyTrackingElements.kt",
    "content": "package com.zhangke.fread.bluesky.internal.tracking\n\nobject BskyTrackingElements {\n\n    const val USER_DETAIL_OPEN_IN_BROWSER = \"bskyUserDetailOpenInBrowser\"\n    const val USER_DETAIL_COPY_LINK = \"bskyUserDetailCopyLink\"\n}"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/uri/BlueskyUriHost.kt",
    "content": "package com.zhangke.fread.bluesky.internal.uri\n\nimport com.zhangke.fread.status.uri.FormalUri\n\nconst val BLUESKY_HOST = \"bluesky.social\"\n\nfun createBlueskyUri(path: String, queries: Map<String, String>): FormalUri {\n    return FormalUri.create(\n        host = BLUESKY_HOST,\n        path = path,\n        queries = queries,\n    )\n}\n\nval FormalUri.isBlueskyUri: Boolean get() = host == BLUESKY_HOST\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/uri/BlueskyUriPath.kt",
    "content": "package com.zhangke.fread.bluesky.internal.uri\n\nobject BlueskyUriPath {\n\n    const val PLATFORM = \"/platform\"\n    const val USER = \"/user\"\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/uri/platform/PlatformUriInsights.kt",
    "content": "package com.zhangke.fread.bluesky.internal.uri.platform\n\nimport com.zhangke.framework.network.FormalBaseUrl\nimport com.zhangke.fread.status.uri.FormalUri\n\ndata class PlatformUriInsights(\n    val uri: FormalUri,\n    val serverBaseUrl: FormalBaseUrl,\n)\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/uri/platform/PlatformUriTransformer.kt",
    "content": "package com.zhangke.fread.bluesky.internal.uri.platform\n\nimport com.zhangke.framework.network.FormalBaseUrl\nimport com.zhangke.fread.bluesky.internal.uri.BlueskyUriPath\nimport com.zhangke.fread.bluesky.internal.uri.createBlueskyUri\nimport com.zhangke.fread.bluesky.internal.uri.isBlueskyUri\nimport com.zhangke.fread.status.uri.FormalUri\n\nclass PlatformUriTransformer() {\n\n    companion object {\n        private const val QUERY_SERVER_BASE_URL = \"serverBaseUrl\"\n    }\n\n    fun build(serverBaseUrl: FormalBaseUrl): FormalUri {\n        val queries = mutableMapOf<String, String>()\n        queries[QUERY_SERVER_BASE_URL] = serverBaseUrl.toString()\n        return createBlueskyUri(\n            path = BlueskyUriPath.PLATFORM,\n            queries = queries,\n        )\n    }\n\n    fun parse(uri: FormalUri): PlatformUriInsights? {\n        if (!uri.isBlueskyUri) return null\n        if (uri.path != BlueskyUriPath.PLATFORM) return null\n        val serverBaseUrl = uri.queries[QUERY_SERVER_BASE_URL]\n        if (serverBaseUrl.isNullOrEmpty()) return null\n        return PlatformUriInsights(\n            uri,\n            FormalBaseUrl.parse(serverBaseUrl)!!\n        )\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/uri/user/UserUriInsights.kt",
    "content": "package com.zhangke.fread.bluesky.internal.uri.user\n\nimport com.zhangke.fread.status.uri.FormalUri\n\ndata class UserUriInsights(\n    val uri: FormalUri,\n    val did: String,\n)\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/uri/user/UserUriTransformer.kt",
    "content": "package com.zhangke.fread.bluesky.internal.uri.user\n\nimport com.zhangke.fread.bluesky.internal.uri.BlueskyUriPath\nimport com.zhangke.fread.bluesky.internal.uri.createBlueskyUri\nimport com.zhangke.fread.bluesky.internal.uri.isBlueskyUri\nimport com.zhangke.fread.status.uri.FormalUri\n\nclass UserUriTransformer() {\n\n    companion object {\n\n        private const val QUERY_DID = \"did\"\n    }\n\n    fun createUserUri(did: String): FormalUri {\n        val queries = mapOf(QUERY_DID to did)\n        return createBlueskyUri(\n            path = BlueskyUriPath.USER,\n            queries = queries,\n        )\n    }\n\n    fun parse(uri: FormalUri): UserUriInsights? {\n        if (!uri.isBlueskyUri) return null\n        if (uri.path != BlueskyUriPath.USER) return null\n        val did = uri.queries[QUERY_DID]\n        if (did.isNullOrEmpty()) return null\n        return UserUriInsights(uri, did)\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/usecase/BskyStatusInteractiveUseCase.kt",
    "content": "package com.zhangke.fread.bluesky.internal.usecase\n\nimport com.atproto.repo.CreateRecordRequest\nimport com.atproto.repo.CreateRecordResponse\nimport com.atproto.repo.DeleteRecordRequest\nimport com.atproto.repo.DeleteRecordResponse\nimport com.atproto.repo.StrongRef\nimport com.zhangke.fread.bluesky.internal.client.BlueskyClient\nimport com.zhangke.fread.bluesky.internal.client.BlueskyClientManager\nimport com.zhangke.fread.bluesky.internal.client.BskyCollections\nimport com.zhangke.fread.bluesky.internal.client.likeRecord\nimport com.zhangke.fread.bluesky.internal.client.repostRecord\nimport com.zhangke.fread.bluesky.internal.client.rkey\nimport com.zhangke.fread.status.blog.Blog\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.model.StatusActionType\nimport com.zhangke.fread.status.status.model.Status\nimport sh.christian.ozone.api.AtUri\nimport sh.christian.ozone.api.Cid\nimport sh.christian.ozone.api.Did\nimport sh.christian.ozone.api.Nsid\nimport sh.christian.ozone.api.RKey\nimport sh.christian.ozone.api.model.JsonContent\n\nclass BskyStatusInteractiveUseCase(\n    private val clientManager: BlueskyClientManager,\n    private val updateProfileRecord: UpdateProfileRecordUseCase,\n) {\n\n    suspend operator fun invoke(\n        locator: PlatformLocator,\n        status: Status,\n        type: StatusActionType,\n    ): Result<Status?> {\n        val client = clientManager.getClient(locator)\n        val loggedAccount =\n            client.loggedAccountProvider() ?: return Result.failure(Exception(\"No logged account\"))\n        val repo = Did(loggedAccount.did)\n        val blog = status.intrinsicBlog\n        val subject = StrongRef(\n            uri = AtUri(blog.url),\n            cid = Cid(blog.id),\n        )\n        when (type) {\n            StatusActionType.LIKE -> {\n                return if (blog.like.liked == true) {\n                    client.unlike(status, repo)\n                } else {\n                    client.like(\n                        status = status,\n                        repo = repo,\n                        subject = subject,\n                    )\n                }\n            }\n\n            StatusActionType.FORWARD -> {\n                return if (blog.forward.forward == true) {\n                    client.unForward(status, repo)\n                } else {\n                    client.forward(\n                        status = status,\n                        repo = repo,\n                        subject = subject,\n                    )\n                }\n            }\n\n            StatusActionType.DELETE -> {\n                return client.deleteRecord(\n                    repo = repo,\n                    collection = BskyCollections.feedPost,\n                    rkey = status.rkey,\n                ).map { null }\n            }\n\n            StatusActionType.PIN -> {\n                val pinned = blog.pinned\n                return updateProfileRecord(\n                    client = client,\n                    updater = { profile ->\n                        profile.copy(\n                            pinnedPost = if (pinned) {\n                                null\n                            } else {\n                                StrongRef(\n                                    uri = AtUri(blog.url),\n                                    cid = Cid(blog.id),\n                                )\n                            },\n                        )\n                    },\n                ).map {\n                    updateStatus(\n                        status = status,\n                        updateBlog = { blog ->\n                            blog.copy(pinned = !pinned)\n                        },\n                    )\n                }\n            }\n\n            else -> return Result.failure(Exception(\"Bookmark not supported\"))\n        }\n    }\n\n    private suspend fun BlueskyClient.like(\n        status: Status,\n        repo: Did,\n        subject: StrongRef,\n    ): Result<Status> {\n        return this.createRecord(\n            collection = BskyCollections.feedLike,\n            repo = repo,\n            record = likeRecord(subject),\n        ).map {\n            updateStatus(\n                status = status,\n                updateBlog = { blog ->\n                    blog.copy(\n                        like = blog.like.copy(\n                            liked = true,\n                            likedCount = (blog.like.likedCount ?: 0L) + 1L,\n                        ),\n                    )\n                },\n            )\n        }\n    }\n\n    private suspend fun BlueskyClient.forward(\n        status: Status,\n        repo: Did,\n        subject: StrongRef,\n    ): Result<Status> {\n        return this.createRecord(\n            collection = BskyCollections.feedRepost,\n            repo = repo,\n            record = repostRecord(subject),\n        ).map {\n            updateStatus(\n                status = status,\n                updateBlog = { blog ->\n                    blog.copy(\n                        forward = blog.forward.copy(\n                            forward = true,\n                            forwardCount = (blog.forward.forwardCount ?: 0L) + 1L,\n                        ),\n                    )\n                },\n            )\n        }\n    }\n\n    private suspend fun BlueskyClient.unlike(\n        status: Status,\n        repo: Did,\n    ): Result<Status> {\n        return this.deleteRecord(\n            repo = repo,\n            collection = BskyCollections.feedLike,\n            rkey = status.rkey,\n        ).map {\n            updateStatus(\n                status = status,\n                updateBlog = { blog ->\n                    blog.copy(\n                        like = blog.like.copy(\n                            liked = false,\n                            likedCount = (blog.like.likedCount ?: 1L) - 1L,\n                        ),\n                    )\n                },\n            )\n        }\n    }\n\n    private suspend fun BlueskyClient.unForward(\n        status: Status,\n        repo: Did,\n    ): Result<Status> {\n        return this.deleteRecord(\n            repo = repo,\n            collection = BskyCollections.feedRepost,\n            rkey = status.rkey,\n        ).map {\n            updateStatus(\n                status = status,\n                updateBlog = { blog ->\n                    blog.copy(\n                        forward = blog.forward.copy(\n                            forward = false,\n                            forwardCount = (blog.forward.forwardCount ?: 1L) - 1L,\n                        ),\n                    )\n                },\n            )\n        }\n    }\n\n    private suspend fun BlueskyClient.createRecord(\n        repo: Did,\n        collection: Nsid,\n        record: JsonContent,\n    ): Result<CreateRecordResponse> {\n        return this.createRecordCatching(\n            CreateRecordRequest(\n                repo = repo,\n                collection = collection,\n                record = record,\n            ),\n        )\n    }\n\n    private suspend fun BlueskyClient.deleteRecord(\n        repo: Did,\n        collection: Nsid,\n        rkey: RKey,\n    ): Result<DeleteRecordResponse> {\n        return this.deleteRecordCatching(\n            DeleteRecordRequest(\n                repo = repo,\n                collection = collection,\n                rkey = rkey,\n            ),\n        )\n    }\n\n    private fun updateStatus(\n        status: Status,\n        updateBlog: (Blog) -> Blog,\n    ): Status {\n        return when (status) {\n            is Status.NewBlog -> {\n                status.copy(\n                    blog = updateBlog(status.blog),\n                )\n            }\n\n            is Status.Reblog -> {\n                status.copy(\n                    reblog = updateBlog(status.reblog),\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/usecase/CreateRecordUseCase.kt",
    "content": "package com.zhangke.fread.bluesky.internal.usecase\n\nimport com.atproto.repo.CreateRecordRequest\nimport com.atproto.repo.CreateRecordResponse\nimport com.zhangke.fread.bluesky.internal.client.BlueskyClientManager\nimport com.zhangke.fread.status.model.PlatformLocator\nimport sh.christian.ozone.api.Did\nimport sh.christian.ozone.api.Nsid\nimport sh.christian.ozone.api.model.JsonContent\n\nclass CreateRecordUseCase(\n    private val clientManager: BlueskyClientManager,\n) {\n\n    suspend operator fun invoke(\n        locator: PlatformLocator,\n        collection: Nsid,\n        record: JsonContent,\n    ): Result<CreateRecordResponse> {\n        val repo = clientManager.getClient(locator).loggedAccountProvider()\n            ?.did?.let { Did(it) }\n            ?: return Result.failure(IllegalStateException(\"No logged account\"))\n        return clientManager.getClient(locator)\n            .createRecordCatching(\n                CreateRecordRequest(\n                    repo = repo,\n                    collection = collection,\n                    record = record,\n                )\n            )\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/usecase/DeleteRecordUseCase.kt",
    "content": "package com.zhangke.fread.bluesky.internal.usecase\n\nimport com.atproto.repo.DeleteRecordRequest\nimport com.zhangke.fread.bluesky.internal.client.BlueskyClientManager\nimport com.zhangke.fread.status.model.PlatformLocator\nimport sh.christian.ozone.api.Did\nimport sh.christian.ozone.api.Nsid\nimport sh.christian.ozone.api.RKey\n\nclass DeleteRecordUseCase(\n    private val clientManager: BlueskyClientManager,\n) {\n\n    suspend operator fun invoke(\n        locator: PlatformLocator,\n        collection: Nsid,\n        rkey: RKey,\n    ): Result<Unit> {\n        val repo = clientManager.getClient(locator).loggedAccountProvider()\n            ?.did?.let { Did(it) }\n            ?: return Result.failure(IllegalStateException(\"No logged account\"))\n        return clientManager.getClient(locator)\n            .deleteRecordCatching(\n                DeleteRecordRequest(\n                    repo = repo,\n                    collection = collection,\n                    rkey = rkey,\n                )\n            ).map { }\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/usecase/GetAllListsUseCase.kt",
    "content": "package com.zhangke.fread.bluesky.internal.usecase\n\nimport app.bsky.graph.GetListsQueryParams\nimport app.bsky.graph.ListView\nimport com.zhangke.fread.bluesky.internal.client.BlueskyClientManager\nimport com.zhangke.fread.status.model.PlatformLocator\nimport sh.christian.ozone.api.AtIdentifier\n\nclass GetAllListsUseCase(\n    private val clientManager: BlueskyClientManager,\n) {\n\n    suspend operator fun invoke(\n        locator: PlatformLocator,\n        id: AtIdentifier,\n    ): Result<List<ListView>> {\n        val client = clientManager.getClient(locator)\n        var cursor: String? = null\n        val lists = mutableListOf<ListView>()\n        while (true) {\n            val result = client.getListsCatching(GetListsQueryParams(actor = id, cursor = cursor))\n            if (result.isFailure) {\n                if (lists.isEmpty()) {\n                    return Result.failure(result.exceptionOrNull()!!)\n                } else {\n                    break\n                }\n            } else {\n                val response = result.getOrThrow()\n                lists.addAll(response.lists)\n                if (response.cursor.isNullOrBlank()) {\n                    break\n                }\n                cursor = response.cursor\n            }\n        }\n        return Result.success(lists)\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/usecase/GetAtIdentifierUseCase.kt",
    "content": "package com.zhangke.fread.bluesky.internal.usecase\n\nimport com.zhangke.framework.utils.RegexFactory\nimport com.zhangke.framework.utils.WebFinger\nimport sh.christian.ozone.api.AtIdentifier\nimport sh.christian.ozone.api.Did\nimport sh.christian.ozone.api.Handle\n\nclass GetAtIdentifierUseCase() {\n\n    operator fun invoke(text: String): AtIdentifier? {\n        // did, handle, homePageUrl\n        if (RegexFactory.didRegex.matches(text)) return Did(text)\n        val webFinger = WebFinger.create(text)\n        if (webFinger != null) {\n            val handle = webFinger.toString().let { runCatching { Handle(it) } }.getOrNull()\n            if (handle != null) return handle\n        }\n        return text.substringAfterLast(\"/\")\n            .let { WebFinger.create(it) }\n            ?.let { runCatching { Handle(it.toString()) } }\n            ?.getOrNull()\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/usecase/GetCompletedNotificationUseCase.kt",
    "content": "package com.zhangke.fread.bluesky.internal.usecase\n\nimport app.bsky.feed.GetPostsQueryParams\nimport app.bsky.feed.Like\nimport app.bsky.feed.PostView\nimport app.bsky.feed.Repost\nimport app.bsky.graph.Follow\nimport app.bsky.notification.ListNotificationsNotification\nimport app.bsky.notification.ListNotificationsQueryParams\nimport app.bsky.notification.ListNotificationsReason\nimport com.zhangke.framework.datetime.Instant\nimport com.zhangke.fread.bluesky.internal.account.BlueskyLoggedAccount\nimport com.zhangke.fread.bluesky.internal.client.BlueskyClient\nimport com.zhangke.fread.bluesky.internal.client.BlueskyClientManager\nimport com.zhangke.fread.bluesky.internal.model.CompletedBskyNotification\nimport com.zhangke.fread.bluesky.internal.model.PagedCompletedBskyNotifications\nimport com.zhangke.fread.bluesky.internal.utils.bskyJson\nimport com.zhangke.fread.status.model.PlatformLocator\nimport kotlinx.coroutines.async\nimport kotlinx.coroutines.awaitAll\nimport kotlinx.coroutines.supervisorScope\nimport sh.christian.ozone.api.AtUri\nimport kotlin.time.ExperimentalTime\n\nclass GetCompletedNotificationUseCase(\n    private val clientManager: BlueskyClientManager,\n) {\n\n    @OptIn(ExperimentalTime::class)\n    suspend operator fun invoke(\n        locator: PlatformLocator,\n        params: ListNotificationsQueryParams,\n    ): Result<PagedCompletedBskyNotifications> {\n        val client = clientManager.getClient(locator)\n        val loggedAccount = client.loggedAccountProvider()\n        val notificationSerializedCache = mutableMapOf<ListNotificationsNotification, Any>()\n        return client.listNotificationsCatching(params)\n            .mapCatching { notification ->\n                val uriList =\n                    notification.notifications.getNeedFetchPostUris(notificationSerializedCache)\n                val postList = if (uriList.isNotEmpty()) {\n                    fetchPostByUris(client, uriList).getOrThrow()\n                } else {\n                    null\n                }\n                PagedCompletedBskyNotifications(\n                    cursor = notification.cursor,\n                    notifications = notification.notifications.mapNotNull {\n                        it.convert(postList, notificationSerializedCache, loggedAccount)\n                    },\n                    priority = notification.priority,\n                    seenAt = notification.seenAt?.let { Instant(it) },\n                )\n            }\n    }\n\n    private fun ListNotificationsNotification.convert(\n        posList: List<PostView>?,\n        serializedCache: MutableMap<ListNotificationsNotification, Any>,\n        loggedAccount: BlueskyLoggedAccount?,\n    ): CompletedBskyNotification? {\n        val isOwner = loggedAccount?.did == author.did.did\n        val record: CompletedBskyNotification.Record = when (reason) {\n            ListNotificationsReason.Like -> {\n                val like: Like = (serializedCache[this] as? Like) ?: record.bskyJson()\n                CompletedBskyNotification.Record.Like(\n                    post = posList!!.firstOrNull { it.uri == like.subject.uri } ?: return null,\n                    createAt = Instant(like.createdAt),\n                )\n            }\n\n            ListNotificationsReason.Repost -> {\n                val repost: Repost = (serializedCache[this] as? Repost) ?: record.bskyJson()\n                CompletedBskyNotification.Record.Repost(\n                    post = posList!!.firstOrNull { it.uri == repost.subject.uri } ?: return null,\n                    createAt = Instant(repost.createdAt),\n                )\n            }\n\n            ListNotificationsReason.Follow -> {\n                val follow: Follow = this.record.bskyJson()\n                CompletedBskyNotification.Record.Follow(\n                    createAt = Instant(follow.createdAt),\n                )\n            }\n\n            ListNotificationsReason.Mention -> {\n                CompletedBskyNotification.Record.Mention(\n                    post = this.record.bskyJson(),\n                    cid = this.cid.cid,\n                    uri = this.uri.atUri,\n                    isOwner = isOwner,\n                )\n            }\n\n            ListNotificationsReason.Reply -> {\n                CompletedBskyNotification.Record.Reply(\n                    reply = this.record.bskyJson(),\n                    cid = this.cid.cid,\n                    uri = this.uri.atUri,\n                    isOwner = isOwner,\n                )\n            }\n\n            ListNotificationsReason.Quote -> {\n                CompletedBskyNotification.Record.Quote(\n                    quote = this.record.bskyJson(),\n                    cid = this.cid.cid,\n                    uri = this.uri.atUri,\n                    post = posList!!.firstOrNull { it.uri == this.reasonSubject!! } ?: return null,\n                    isOwner = isOwner,\n                )\n            }\n\n            ListNotificationsReason.StarterpackJoined -> {\n                CompletedBskyNotification.Record.OnlyMessage(\n                    message = \"StarterackJoined: ${this.record}\",\n                    createAt = Instant(this.indexedAt),\n                )\n            }\n\n            else -> {\n                return null\n//                CompletedBskyNotification.Record.OnlyMessage(\n//                    message = \"Unknown(${(reason as? ListNotificationsReason.Unknown)?.rawValue}): ${this.record}\",\n//                    createAt = Instant(this.indexedAt),\n//                )\n            }\n        }\n        return CompletedBskyNotification(\n            uri = this.uri.atUri,\n            cid = this.cid.cid,\n            record = record,\n            author = this.author,\n            isRead = this.isRead,\n            indexedAt = Instant(this.indexedAt),\n            labels = this.labels,\n        )\n    }\n\n    private suspend fun fetchPostByUris(\n        client: BlueskyClient,\n        uriList: List<AtUri>,\n    ): Result<List<PostView>> {\n        if (uriList.isEmpty()) return Result.success(emptyList())\n        val resultList: List<Result<List<PostView>>> = supervisorScope {\n            val grouped = uriList.chunked(15)\n            grouped.map { itemList ->\n                async { client.getPostsCatching(GetPostsQueryParams(uris = itemList)) }\n            }.awaitAll().map { result -> result.map { it.posts } }\n        }\n        val error = resultList.firstOrNull { it.isFailure }\n        if (error != null) {\n            return error\n        }\n        return Result.success(resultList.flatMap { it.getOrThrow() })\n    }\n\n    private fun List<ListNotificationsNotification>.getNeedFetchPostUris(\n        notificationSerializedCache: MutableMap<ListNotificationsNotification, Any>,\n    ): List<AtUri> {\n        return mapNotNull {\n            when (it.reason) {\n                is ListNotificationsReason.Repost -> {\n                    val repost: Repost = it.record.bskyJson()\n                    notificationSerializedCache[it] = repost\n                    repost.subject.uri\n                }\n\n                is ListNotificationsReason.Like -> {\n                    val like: Like = it.record.bskyJson()\n                    notificationSerializedCache[it] = like\n                    like.subject.uri\n                }\n\n                is ListNotificationsReason.Quote -> {\n                    it.reasonSubject!!\n                }\n\n                else -> null\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/usecase/GetFeedsStatusUseCase.kt",
    "content": "package com.zhangke.fread.bluesky.internal.usecase\n\nimport app.bsky.feed.FeedViewPost\nimport app.bsky.feed.GetActorLikesQueryParams\nimport app.bsky.feed.GetAuthorFeedFilter\nimport app.bsky.feed.GetAuthorFeedQueryParams\nimport app.bsky.feed.GetFeedQueryParams\nimport app.bsky.feed.GetListFeedQueryParams\nimport app.bsky.feed.GetTimelineQueryParams\nimport app.bsky.feed.PostView\nimport app.bsky.feed.SearchPostsQueryParams\nimport app.bsky.feed.SearchPostsSort\nimport com.zhangke.framework.utils.exceptionOrThrow\nimport com.zhangke.fread.bluesky.internal.account.BlueskyLoggedAccount\nimport com.zhangke.fread.bluesky.internal.adapter.BlueskyStatusAdapter\nimport com.zhangke.fread.bluesky.internal.client.BlueskyClientManager\nimport com.zhangke.fread.bluesky.internal.model.BlueskyFeeds\nimport com.zhangke.fread.bluesky.internal.model.BskyPagingFeeds\nimport com.zhangke.fread.bluesky.internal.repo.BlueskyPlatformRepo\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.platform.BlogPlatform\nimport sh.christian.ozone.api.AtUri\nimport sh.christian.ozone.api.Did\n\nclass GetFeedsStatusUseCase(\n    private val clientManager: BlueskyClientManager,\n    private val statusAdapter: BlueskyStatusAdapter,\n    private val blogPlatformRepo: BlueskyPlatformRepo,\n) {\n\n    suspend operator fun invoke(\n        locator: PlatformLocator,\n        feeds: BlueskyFeeds,\n        cursor: String? = null,\n    ): Result<BskyPagingFeeds> {\n        val client = clientManager.getClient(locator)\n        val platform = blogPlatformRepo.getPlatform(client.baseUrl)\n        val loggedAccount = client.loggedAccountProvider()\n        return when (feeds) {\n            is BlueskyFeeds.Feeds -> {\n                client.getFeedCatching(GetFeedQueryParams(feed = AtUri(feeds.uri), cursor = cursor))\n                    .map { it.cursor to it.feed }\n                    .convert(locator, platform, loggedAccount)\n            }\n\n            is BlueskyFeeds.List -> {\n                client.getListFeedCatching(\n                    GetListFeedQueryParams(list = AtUri(feeds.uri), cursor = cursor)\n                ).map { it.cursor to it.feed }\n                    .convert(locator, platform, loggedAccount)\n            }\n\n            is BlueskyFeeds.FollowingTimeline -> {\n                client.getTimelineCatching(GetTimelineQueryParams(cursor = cursor))\n                    .map { it.cursor to it.feed }\n                    .convert(locator, platform, loggedAccount)\n            }\n\n            is BlueskyFeeds.Hashtags -> {\n                client.searchPostsCatching(\n                    SearchPostsQueryParams(\n                        q = feeds.hashtag,\n                        sort = SearchPostsSort.Top,\n                        cursor = cursor,\n                    )\n                ).map { it.cursor to it.posts }.convertPostView(locator, platform, loggedAccount)\n            }\n\n            is BlueskyFeeds.UserPosts -> {\n                client.getAuthorFeedCatching(\n                    GetAuthorFeedQueryParams(\n                        actor = Did(feeds.did),\n                        cursor = cursor,\n                        includePins = true,\n                        filter = GetAuthorFeedFilter.PostsAndAuthorThreads,\n                    )\n                ).map { it.cursor to it.feed }.convert(locator, platform, loggedAccount)\n            }\n\n            is BlueskyFeeds.UserReplies -> {\n                client.getAuthorFeedCatching(\n                    GetAuthorFeedQueryParams(\n                        actor = Did(feeds.did),\n                        cursor = cursor,\n                        filter = GetAuthorFeedFilter.PostsWithReplies,\n                    )\n                ).map { it.cursor to it.feed }.convert(locator, platform, loggedAccount)\n            }\n\n            is BlueskyFeeds.UserMedias -> {\n                val did = feeds.did ?: loggedAccount?.did ?: return Result.failure(\n                    IllegalArgumentException(\"did is null\")\n                )\n                client.getAuthorFeedCatching(\n                    GetAuthorFeedQueryParams(\n                        actor = Did(did),\n                        cursor = cursor,\n                        filter = GetAuthorFeedFilter.PostsWithMedia,\n                    )\n                ).map { it.cursor to it.feed }.convert(locator, platform, loggedAccount)\n            }\n\n            is BlueskyFeeds.UserLikes -> {\n                val did = feeds.did ?: loggedAccount?.did ?: return Result.failure(\n                    IllegalArgumentException(\"did is null\")\n                )\n                client.getActorLikesCatching(\n                    GetActorLikesQueryParams(\n                        actor = Did(did),\n                        cursor = cursor,\n                    )\n                ).map { it.cursor to it.feed }.convert(locator, platform, loggedAccount)\n            }\n        }\n    }\n\n    private fun Result<Pair<String?, List<FeedViewPost>>>.convert(\n        locator: PlatformLocator,\n        platform: BlogPlatform,\n        loggedAccount: BlueskyLoggedAccount?,\n    ): Result<BskyPagingFeeds> {\n        if (this.isFailure) return Result.failure(this.exceptionOrThrow())\n        val (cursor, feeds) = this.getOrThrow()\n        val status = feeds.map {\n            statusAdapter.convertToUiState(\n                locator = locator,\n                feedViewPost = it,\n                platform = platform,\n                loggedAccount = loggedAccount,\n            )\n        }\n        return Result.success(BskyPagingFeeds(cursor, status))\n    }\n\n    private fun Result<Pair<String?, List<PostView>>>.convertPostView(\n        locator: PlatformLocator,\n        platform: BlogPlatform,\n        loggedAccount: BlueskyLoggedAccount?,\n    ): Result<BskyPagingFeeds> {\n        if (this.isFailure) return Result.failure(this.exceptionOrThrow())\n        val (cursor, feeds) = this.getOrThrow()\n        val status = feeds.map {\n            statusAdapter.convertToUiState(\n                locator = locator,\n                postView = it,\n                platform = platform,\n                loggedAccount = loggedAccount,\n            )\n        }\n        return Result.success(BskyPagingFeeds(cursor, status))\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/usecase/GetFollowingFeedsUseCase.kt",
    "content": "package com.zhangke.fread.bluesky.internal.usecase\n\nimport app.bsky.actor.PreferencesUnion.SavedFeedsPrefV2\nimport app.bsky.actor.SavedFeed\nimport app.bsky.actor.Type\nimport app.bsky.feed.GeneratorView\nimport app.bsky.feed.GetFeedGeneratorsQueryParams\nimport app.bsky.graph.GetListQueryParams\nimport app.bsky.graph.ListView\nimport com.zhangke.framework.utils.exceptionOrThrow\nimport com.zhangke.fread.bluesky.internal.adapter.BlueskyFeedsAdapter\nimport com.zhangke.fread.bluesky.internal.client.BlueskyClient\nimport com.zhangke.fread.bluesky.internal.client.BlueskyClientManager\nimport com.zhangke.fread.bluesky.internal.model.BlueskyFeeds\nimport com.zhangke.fread.status.model.PlatformLocator\nimport kotlinx.coroutines.async\nimport kotlinx.coroutines.awaitAll\nimport kotlinx.coroutines.supervisorScope\nimport sh.christian.ozone.api.AtUri\n\nclass GetFollowingFeedsUseCase(\n    private val clientManager: BlueskyClientManager,\n    private val feedsAdapter: BlueskyFeedsAdapter,\n) {\n\n    suspend operator fun invoke(locator: PlatformLocator): Result<List<BlueskyFeeds>> {\n        val client = clientManager.getClient(locator)\n        val preferenceResult = client.getPreferencesCatching()\n        if (preferenceResult.isFailure) return Result.failure(preferenceResult.exceptionOrThrow())\n        val preference = preferenceResult.getOrThrow()\n        val followingFeeds = preference.preferences.filterIsInstance<SavedFeedsPrefV2>()\n            .firstOrNull()\n            ?.value\n            ?.items\n        if (followingFeeds.isNullOrEmpty()) return Result.success(emptyList())\n        val generatorListResult = getGeneratorList(client, followingFeeds)\n        if (generatorListResult.isFailure) return Result.failure(generatorListResult.exceptionOrThrow())\n        val generatorList = generatorListResult.getOrThrow()\n        val allListViewsResult = getListViews(client, followingFeeds)\n        if (allListViewsResult.isFailure) return Result.failure(allListViewsResult.exceptionOrThrow())\n        val allListViews = allListViewsResult.getOrThrow()\n        return followingFeeds.mapNotNull { feed ->\n            mapFollowingFeeds(feed, generatorList, allListViews)\n        }.let { Result.success(it.distinctBy { it.id }) }\n    }\n\n    private fun mapFollowingFeeds(\n        feed: SavedFeed,\n        generatorList: List<GeneratorView>,\n        listViewList: List<ListView>,\n    ): BlueskyFeeds? = when (feed.type) {\n        Type.Feed -> {\n            val generator = generatorList.firstOrNull { it.uri.atUri == feed.value }\n            if (generator == null) {\n                null\n            } else {\n                feedsAdapter.convertToFeeds(feed, generator)\n            }\n        }\n\n        Type.List -> {\n            val listView = listViewList.firstOrNull { it.uri.atUri == feed.value }\n            if (listView == null) {\n                null\n            } else {\n                feedsAdapter.convertToList(feed, listView)\n            }\n        }\n\n        Type.Timeline -> {\n            BlueskyFeeds.FollowingTimeline(feed.pinned)\n        }\n\n        else -> null\n    }\n\n    private suspend fun getListViews(\n        client: BlueskyClient,\n        feeds: List<SavedFeed>,\n    ): Result<List<ListView>> {\n        val lists = feeds.filter { it.type == Type.List }.map { it.value }\n        if (lists.isEmpty()) return Result.success(emptyList())\n        val allResults = supervisorScope {\n            lists.map { uri ->\n                async { client.getListCatching(GetListQueryParams(AtUri(uri))) }\n            }.awaitAll()\n        }\n        if (allResults.all { it.isFailure }) {\n            return Result.failure(allResults.first().exceptionOrNull()!!)\n        }\n        return allResults.mapNotNull { it.getOrNull()?.list }.let { Result.success(it) }\n    }\n\n    private suspend fun getGeneratorList(\n        client: BlueskyClient,\n        feeds: List<SavedFeed>,\n    ): Result<List<GeneratorView>> {\n        val feedsUris = feeds.filter { it.type == Type.Feed }\n            .map { AtUri(it.value) }\n        if (feedsUris.isEmpty()) return Result.success(emptyList())\n        return client.getFeedGeneratorsCatching(\n            GetFeedGeneratorsQueryParams(feedsUris)\n        ).map { it.feeds }\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/usecase/GetStatusContextUseCase.kt",
    "content": "package com.zhangke.fread.bluesky.internal.usecase\n\nimport app.bsky.feed.GetPostThreadQueryParams\nimport app.bsky.feed.GetPostThreadResponseThreadUnion\nimport app.bsky.feed.ThreadViewPostParentUnion\nimport app.bsky.feed.ThreadViewPostReplieUnion\nimport com.zhangke.fread.bluesky.internal.account.BlueskyLoggedAccount\nimport com.zhangke.fread.bluesky.internal.adapter.BlueskyStatusAdapter\nimport com.zhangke.fread.bluesky.internal.client.BlueskyClientManager\nimport com.zhangke.fread.bluesky.internal.repo.BlueskyPlatformRepo\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.model.StatusUiState\nimport com.zhangke.fread.status.platform.BlogPlatform\nimport com.zhangke.fread.status.status.model.DescendantStatus\nimport com.zhangke.fread.status.status.model.Status\nimport com.zhangke.fread.status.status.model.StatusContext\nimport sh.christian.ozone.api.AtUri\n\nclass GetStatusContextUseCase(\n    private val clientManager: BlueskyClientManager,\n    private val blogPlatformRepo: BlueskyPlatformRepo,\n    private val statusAdapter: BlueskyStatusAdapter,\n) {\n\n    suspend operator fun invoke(\n        locator: PlatformLocator,\n        status: Status,\n    ): Result<StatusContext> {\n        val client = clientManager.getClient(locator)\n        val threadResult = client.getPostThreadCatching(\n            GetPostThreadQueryParams(\n                uri = AtUri(status.intrinsicBlog.url),\n                depth = 6,\n            )\n        )\n        if (threadResult.isFailure) return Result.failure(threadResult.exceptionOrNull()!!)\n        val loggedAccount = client.loggedAccountProvider()\n        val platform = blogPlatformRepo.getPlatform(client.baseUrl)\n        when (val thread = threadResult.getOrThrow().thread) {\n            is GetPostThreadResponseThreadUnion.Unknown,\n            is GetPostThreadResponseThreadUnion.BlockedPost,\n            is GetPostThreadResponseThreadUnion.NotFoundPost -> {\n                return Result.failure(RuntimeException(\"Post not found\"))\n            }\n\n            is GetPostThreadResponseThreadUnion.ThreadViewPost -> {\n                val threadViewPost = thread.value\n                val ancestors = buildAncestors(\n                    locator = locator,\n                    parent = threadViewPost.parent,\n                    platform = platform,\n                    loggedAccount = loggedAccount,\n                )\n                val descendants = thread.value.replies.mapNotNull { reply ->\n                    if (reply is ThreadViewPostReplieUnion.ThreadViewPost) {\n                        val statusUiState = statusAdapter.convertToUiState(\n                            locator = locator,\n                            postView = reply.value.post,\n                            platform = platform,\n                            loggedAccount = loggedAccount,\n                        )\n                        DescendantStatus(statusUiState, null)\n                    } else {\n                        null\n                    }\n                }\n                return Result.success(\n                    StatusContext(\n                        ancestors = ancestors,\n                        status = statusAdapter.convertToUiState(\n                            locator = locator,\n                            postView = threadViewPost.post,\n                            platform = platform,\n                            loggedAccount = loggedAccount,\n                        ),\n                        descendants = descendants,\n                    )\n                )\n            }\n        }\n    }\n\n    private fun buildAncestors(\n        locator: PlatformLocator,\n        parent: ThreadViewPostParentUnion?,\n        platform: BlogPlatform,\n        loggedAccount: BlueskyLoggedAccount?,\n    ): List<StatusUiState> {\n        if (parent == null) return emptyList()\n        val ancestors = mutableListOf<StatusUiState>()\n        var currentParent: ThreadViewPostParentUnion? = parent\n        while (currentParent != null) {\n            if (currentParent !is ThreadViewPostParentUnion.ThreadViewPost) break\n            val statusUiState = statusAdapter.convertToUiState(\n                locator = locator,\n                postView = currentParent.value.post,\n                platform = platform,\n                loggedAccount = loggedAccount,\n            )\n            ancestors.add(0, statusUiState)\n            currentParent = currentParent.value.parent\n        }\n        return ancestors\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/usecase/LoginToBskyUseCase.kt",
    "content": "package com.zhangke.fread.bluesky.internal.usecase\n\nimport com.zhangke.framework.network.FormalBaseUrl\nimport com.zhangke.fread.bluesky.internal.account.BlueskyLoggedAccount\nimport com.zhangke.fread.bluesky.internal.account.BlueskyLoggedAccountManager\n\nclass LoginToBskyUseCase(\n    private val accountManager: BlueskyLoggedAccountManager,\n) {\n\n    suspend operator fun invoke(\n        baseUrl: FormalBaseUrl,\n        identifier: String,\n        password: String,\n        factorToken: String? = null,\n    ): Result<BlueskyLoggedAccount> {\n        return accountManager.login(baseUrl, identifier, password, factorToken)\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/usecase/PinFeedsUseCase.kt",
    "content": "package com.zhangke.fread.bluesky.internal.usecase\n\nimport app.bsky.actor.PreferencesUnion.SavedFeedsPrefV2\nimport app.bsky.actor.SavedFeed\nimport app.bsky.actor.Type\nimport com.zhangke.fread.bluesky.internal.model.BlueskyFeeds\nimport com.zhangke.fread.status.model.PlatformLocator\nimport kotlin.uuid.ExperimentalUuidApi\nimport kotlin.uuid.Uuid\n\nclass PinFeedsUseCase(\n    private val updatePreferences: UpdatePreferencesUseCase,\n) {\n\n    suspend operator fun invoke(\n        locator: PlatformLocator,\n        feeds: BlueskyFeeds.Feeds,\n    ): Result<Unit> {\n        return updatePreferences(\n            locator = locator,\n            updater = { preference ->\n                preference.map {\n                    if (it is SavedFeedsPrefV2) {\n                        val newItems = it.value.items + feeds.toSaveFeeds()\n                        SavedFeedsPrefV2(app.bsky.actor.SavedFeedsPrefV2(newItems))\n                    } else {\n                        it\n                    }\n                }\n            },\n        )\n    }\n\n    @OptIn(ExperimentalUuidApi::class)\n    private fun BlueskyFeeds.Feeds.toSaveFeeds(): SavedFeed {\n        return SavedFeed(\n            id = Uuid.random().toString(),\n            type = Type.Feed,\n            value = this.uri,\n            pinned = true,\n        )\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/usecase/PublishingPostUseCase.kt",
    "content": "package com.zhangke.fread.bluesky.internal.usecase\n\nimport androidx.compose.ui.text.TextRange\nimport app.bsky.actor.ProfileView\nimport app.bsky.embed.AspectRatio\nimport app.bsky.embed.External\nimport app.bsky.embed.ExternalExternal\nimport app.bsky.embed.Images\nimport app.bsky.embed.ImagesImage\nimport app.bsky.embed.Record\nimport app.bsky.embed.RecordWithMedia\nimport app.bsky.embed.RecordWithMediaMediaUnion\nimport app.bsky.embed.Video\nimport app.bsky.feed.GetPostThreadQueryParams\nimport app.bsky.feed.GetPostThreadResponseThreadUnion\nimport app.bsky.feed.Post\nimport app.bsky.feed.PostEmbedUnion\nimport app.bsky.feed.PostReplyRef\nimport app.bsky.feed.PostView\nimport app.bsky.feed.Postgate\nimport app.bsky.feed.PostgateDisableRule\nimport app.bsky.feed.PostgateEmbeddingRuleUnion\nimport app.bsky.feed.Threadgate\nimport app.bsky.feed.ThreadgateAllowUnion\nimport app.bsky.feed.ThreadgateFollowerRule\nimport app.bsky.feed.ThreadgateFollowingRule\nimport app.bsky.feed.ThreadgateListRule\nimport app.bsky.feed.ThreadgateMentionRule\nimport app.bsky.richtext.Facet\nimport app.bsky.richtext.FacetByteSlice\nimport app.bsky.richtext.FacetFeatureUnion\nimport app.bsky.richtext.FacetLink\nimport app.bsky.richtext.FacetMention\nimport app.bsky.richtext.FacetTag\nimport com.atproto.repo.ApplyWritesCreate\nimport com.atproto.repo.ApplyWritesRequest\nimport com.atproto.repo.ApplyWritesRequestWriteUnion\nimport com.atproto.repo.StrongRef\nimport com.zhangke.framework.utils.LinkPreviewInfo\nimport com.zhangke.framework.utils.mapForErrorMessage\nimport com.zhangke.framework.utils.mapForMessage\nimport com.zhangke.fread.bluesky.internal.account.BlueskyLoggedAccount\nimport com.zhangke.fread.bluesky.internal.client.BlueskyClientManager\nimport com.zhangke.fread.bluesky.internal.client.BskyCollections\nimport com.zhangke.fread.bluesky.internal.client.adjustToRkey\nimport com.zhangke.fread.bluesky.internal.screen.publish.PublishPostMediaAttachment\nimport com.zhangke.fread.bluesky.internal.screen.publish.PublishPostMediaAttachmentFile\nimport com.zhangke.fread.bluesky.internal.utils.Tid\nimport com.zhangke.fread.bluesky.internal.utils.bskyJson\nimport com.zhangke.fread.common.utils.HashtagTextUtils\nimport com.zhangke.fread.common.utils.LinkTextUtils\nimport com.zhangke.fread.common.utils.MentionTextUtil\nimport com.zhangke.fread.status.blog.Blog\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.model.PostInteractionSetting\nimport com.zhangke.fread.status.model.ReplySetting\nimport kotlinx.coroutines.async\nimport kotlinx.coroutines.awaitAll\nimport kotlinx.coroutines.supervisorScope\nimport kotlinx.datetime.Clock\nimport okio.utf8Size\nimport sh.christian.ozone.api.AtUri\nimport sh.christian.ozone.api.Cid\nimport sh.christian.ozone.api.Did\nimport sh.christian.ozone.api.Language\nimport sh.christian.ozone.api.RKey\nimport sh.christian.ozone.api.Uri\n\nclass PublishingPostUseCase(\n    private val clientManager: BlueskyClientManager,\n    private val uploadImageByImageUrl: UploadImageByImageUrlUseCase,\n    private val uploadBlob: UploadBlobUseCase,\n) {\n\n    suspend operator fun invoke(\n        account: BlueskyLoggedAccount,\n        content: String,\n        selectedLanguages: List<String>,\n        interactionSetting: PostInteractionSetting,\n        replyBlog: Blog? = null,\n        attachment: PublishPostMediaAttachment? = null,\n        quoteBlog: Blog? = null,\n        mentionedUsers: Set<ProfileView> = emptySet(),\n        linkPreviewInfo: LinkPreviewInfo? = null,\n    ): Result<Unit> {\n        val client = clientManager.getClient(account.locator)\n        val rkey = Tid.generateTID()\n        val postUri = \"at://${account.did}/${BskyCollections.feedPost.nsid}/$rkey\"\n        val embedResult = buildPostEmbed(account.locator, attachment, quoteBlog, linkPreviewInfo)\n        if (embedResult.isFailure) return Result.failure(embedResult.exceptionOrNull()!!)\n        val replyResult = buildReplyRef(account.locator, replyBlog)\n        if (replyResult.isFailure) return Result.failure(replyResult.exceptionOrNull()!!)\n        val post = Post(\n            text = content,\n            langs = selectedLanguages.map { Language(it) },\n            embed = embedResult.getOrNull(),\n            createdAt = Clock.System.now(),\n            reply = replyResult.getOrNull(),\n            facets = buildFacet(content, mentionedUsers),\n        )\n        val writes = mutableListOf<ApplyWritesRequestWriteUnion>()\n        writes += ApplyWritesRequestWriteUnion.Create(\n            ApplyWritesCreate(\n                collection = BskyCollections.feedPost,\n                value = post.bskyJson(),\n                rkey = RKey(rkey),\n            )\n        )\n        writes += buildTreadAndPostGate(AtUri(postUri), interactionSetting)\n        val request = ApplyWritesRequest(\n            repo = Did(account.did),\n            validate = true,\n            writes = writes,\n        )\n        return client.applyWritesCatching(request).map { }\n    }\n\n    private suspend fun buildReplyRef(\n        locator: PlatformLocator,\n        replyBlog: Blog?\n    ): Result<PostReplyRef?> {\n        val reply = replyBlog ?: return Result.success(null)\n        val postView = getPostDetail(locator, reply.url).let {\n            if (it.isFailure) {\n                return Result.failure(\n                    it.exceptionOrNull()!!.mapForMessage(\"Get reply post failed\")\n                )\n            }\n            it.getOrThrow()\n        }\n        val post: Post = postView.record.bskyJson()\n        val replyPostRef = StrongRef(uri = postView.uri, cid = postView.cid)\n        val root = post.reply?.root ?: replyPostRef\n        return Result.success(\n            PostReplyRef(root = root, parent = replyPostRef)\n        )\n    }\n\n    private suspend fun getPostDetail(locator: PlatformLocator, uri: String): Result<PostView> {\n        val client = clientManager.getClient(locator)\n        val result = client.getPostThreadCatching(\n            GetPostThreadQueryParams(uri = AtUri(uri), depth = 1)\n        ).mapForErrorMessage(\"Get post detail failed\")\n        if (result.isFailure) return Result.failure(result.exceptionOrNull()!!)\n        val response = result.getOrThrow()\n        val threadPostView = (response.thread as? GetPostThreadResponseThreadUnion.ThreadViewPost)\n            ?: return Result.failure(IllegalStateException(\"Post not found\"))\n        return Result.success(threadPostView.value.post)\n    }\n\n    private suspend fun buildPostEmbed(\n        locator: PlatformLocator,\n        attachment: PublishPostMediaAttachment?,\n        quoteBlog: Blog?,\n        linkPreviewInfo: LinkPreviewInfo?,\n    ): Result<PostEmbedUnion?> {\n        val videoResult =\n            (attachment as? PublishPostMediaAttachment.Video)?.let { uploadVideo(locator, it.file) }\n        if (videoResult?.isFailure == true) return Result.failure(videoResult.exceptionOrNull()!!)\n        val imagesResult =\n            (attachment as? PublishPostMediaAttachment.Image)?.let { uploadImages(locator, it) }\n        if (imagesResult?.isFailure == true) return Result.failure(imagesResult.exceptionOrNull()!!)\n        val video = videoResult?.getOrNull()\n        val images = imagesResult?.getOrNull()\n        val quoteRecord = quoteBlog\n            ?.let { StrongRef(uri = AtUri(it.url), cid = Cid(it.id)) }\n            ?.let { Record(it) }\n        if (video == null && images == null && quoteRecord == null) {\n            return buildLinkPreviewEmbed(locator, linkPreviewInfo)\n        }\n        val embed = if (quoteRecord != null) {\n            if (video != null || images != null) {\n                val media = video?.let { RecordWithMediaMediaUnion.Video(it) }\n                    ?: RecordWithMediaMediaUnion.Images(images!!)\n                PostEmbedUnion.RecordWithMedia(\n                    RecordWithMedia(\n                        record = quoteRecord,\n                        media = media,\n                    )\n                )\n            } else {\n                PostEmbedUnion.Record(quoteRecord)\n            }\n        } else {\n            video?.let { PostEmbedUnion.Video(it) } ?: PostEmbedUnion.Images(images!!)\n        }\n        return Result.success(embed)\n    }\n\n    private suspend fun buildLinkPreviewEmbed(\n        locator: PlatformLocator,\n        linkPreviewInfo: LinkPreviewInfo?\n    ): Result<PostEmbedUnion?> {\n        if (linkPreviewInfo == null) return Result.success(null)\n        val thumbnail = if (linkPreviewInfo.image.isNullOrEmpty()) {\n            null\n        } else {\n            uploadImageByImageUrl(locator, linkPreviewInfo.image!!).getOrNull()\n        }\n        val external = ExternalExternal(\n            uri = Uri(linkPreviewInfo.url),\n            title = linkPreviewInfo.title,\n            description = linkPreviewInfo.description.orEmpty(),\n            thumb = thumbnail,\n        )\n        return Result.success(PostEmbedUnion.External(External(external)))\n    }\n\n    private suspend fun uploadVideo(\n        locator: PlatformLocator,\n        file: PublishPostMediaAttachmentFile,\n    ): Result<Video> {\n        return uploadBlob(locator = locator, fileUri = file.file.uri)\n            .map {\n                Video(\n                    video = it.first,\n                    alt = file.alt,\n                    aspectRatio = it.second?.convert(),\n                )\n            }\n    }\n\n    private suspend fun uploadImages(\n        locator: PlatformLocator,\n        image: PublishPostMediaAttachment.Image,\n    ): Result<Images> {\n        val resultList: List<Result<ImagesImage>> = supervisorScope {\n            image.files.map { file ->\n                async {\n                    uploadBlob(locator = locator, fileUri = file.file.uri).map {\n                        ImagesImage(\n                            image = it.first,\n                            aspectRatio = it.second?.convert(),\n                            alt = file.alt.orEmpty(),\n                        )\n                    }\n                }\n            }.awaitAll()\n        }\n        if (resultList.any { it.isFailure }) {\n            return Result.failure(resultList.first { it.isFailure }.exceptionOrNull()!!)\n        }\n        val images = Images(resultList.map { it.getOrThrow() })\n        return Result.success(images)\n    }\n\n    private fun com.zhangke.framework.utils.AspectRatio.convert(): AspectRatio {\n        return AspectRatio(\n            width = width,\n            height = height,\n        )\n    }\n\n    private fun buildTreadAndPostGate(\n        postUri: AtUri,\n        interactionSetting: PostInteractionSetting,\n    ): List<ApplyWritesRequestWriteUnion> {\n        val list = mutableListOf<ApplyWritesRequestWriteUnion>()\n        val replySetting = interactionSetting.replySetting\n        if (replySetting !is ReplySetting.Everybody) {\n            val allowList = mutableListOf<ThreadgateAllowUnion>()\n            if (replySetting is ReplySetting.Combined) {\n                allowList += buildThreadGateAllowList(replySetting)\n            }\n            val threadGate = Threadgate(\n                post = postUri,\n                allow = allowList,\n                createdAt = Clock.System.now(),\n            )\n            list += ApplyWritesRequestWriteUnion.Create(\n                ApplyWritesCreate(\n                    collection = BskyCollections.threadGate,\n                    value = threadGate.bskyJson(),\n                    rkey = postUri.toString().adjustToRkey(),\n                )\n            )\n        }\n        if (!interactionSetting.allowQuote) {\n            val postGate = Postgate(\n                createdAt = Clock.System.now(),\n                post = postUri,\n                embeddingRules = listOf(PostgateEmbeddingRuleUnion.DisableRule(PostgateDisableRule)),\n            )\n            list += ApplyWritesRequestWriteUnion.Create(\n                ApplyWritesCreate(\n                    collection = BskyCollections.postGate,\n                    value = postGate.bskyJson(),\n                    rkey = postUri.toString().adjustToRkey(),\n                )\n            )\n        }\n        return list\n    }\n\n    private fun buildThreadGateAllowList(\n        setting: ReplySetting.Combined,\n    ): List<ThreadgateAllowUnion> {\n        return buildList {\n            setting.options.forEach { option ->\n                when (option) {\n                    is ReplySetting.CombineOption.Mentioned -> {\n                        add(ThreadgateAllowUnion.MentionRule(ThreadgateMentionRule))\n                    }\n\n                    is ReplySetting.CombineOption.Following -> {\n                        add(ThreadgateAllowUnion.FollowingRule(ThreadgateFollowingRule))\n                    }\n\n                    is ReplySetting.CombineOption.Followers -> {\n                        add(ThreadgateAllowUnion.FollowerRule(ThreadgateFollowerRule))\n                    }\n\n                    is ReplySetting.CombineOption.UserInList -> {\n                        add(ThreadgateAllowUnion.ListRule(ThreadgateListRule(AtUri(option.listView.uri))))\n                    }\n                }\n            }\n        }\n    }\n\n    private fun buildFacet(\n        content: String,\n        mentionedUsers: Set<ProfileView>,\n    ): List<Facet> {\n        if (content.isEmpty()) return emptyList()\n        val facetList = mutableListOf<Facet>()\n        val hashtags = HashtagTextUtils.findHashtags(content, true)\n        for (hashtag in hashtags) {\n            val tag = content.substring(hashtag.start + 1, hashtag.end)\n            val facet = Facet(\n                index = convertIndex(hashtag, content),\n                features = listOf(\n                    FacetFeatureUnion.Tag(\n                        value = FacetTag(tag = tag),\n                    )\n                ),\n            )\n            facetList += facet\n        }\n        val userRanges = MentionTextUtil.findMentionList(content)\n            .filter { range -> hashtags.firstOrNull { it.intersects(range) } == null }\n        for (userRange in userRanges) {\n            val handle = content.substring(userRange.start, userRange.end)\n            mentionedUsers.firstOrNull { it.handle.handle == handle.removePrefix(\"@\") }\n                ?.did\n                ?.let { did ->\n                    val facet = Facet(\n                        index = convertIndex(userRange, content),\n                        features = listOf(\n                            FacetFeatureUnion.Mention(\n                                value = FacetMention(did = did),\n                            )\n                        ),\n                    )\n                    facetList += facet\n                }\n        }\n        val previousRanges = hashtags + userRanges\n        val linkRanges = LinkTextUtils.findLinks(content)\n            .filter { range ->\n                previousRanges.firstOrNull { it.intersects(range) } == null\n            }\n        for (linkRange in linkRanges) {\n            val link = content.substring(linkRange.start, linkRange.end)\n            val facetLink = if (link.lowercase().startsWith(\"http\")) {\n                FacetLink(Uri(link))\n            } else {\n                FacetLink(Uri(\"http://$link\"))\n            }\n            val facet = Facet(\n                index = convertIndex(linkRange, content),\n                features = listOf(\n                    FacetFeatureUnion.Link(facetLink)\n                ),\n            )\n            facetList += facet\n        }\n        return facetList\n    }\n\n    private fun convertIndex(range: TextRange, content: String): FacetByteSlice {\n        return FacetByteSlice(\n            byteStart = calculateUtf8Index(range.start, content),\n            byteEnd = calculateUtf8Index(range.end, content),\n        )\n    }\n\n    private fun calculateUtf8Index(\n        index: Int,\n        text: String,\n    ): Long {\n        return text.utf8Size(0, index.coerceAtMost(text.length))\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/usecase/RefreshSessionUseCase.kt",
    "content": "package com.zhangke.fread.bluesky.internal.usecase\n\nimport com.zhangke.fread.bluesky.BlueskyAccountManager\nimport com.zhangke.fread.bluesky.internal.client.BlueskyClientManager\nimport com.zhangke.fread.status.model.PlatformLocator\n\nclass RefreshSessionUseCase(\n    private val clientManager: BlueskyClientManager,\n    private val accountManager: BlueskyAccountManager,\n) {\n\n    suspend operator fun invoke() {\n        accountManager.getAllLoggedAccount()\n            .map { PlatformLocator(accountUri = it.uri, baseUrl = it.platform.baseUrl) }\n            .map { it to clientManager.getClient(it) }\n            .forEach { (role, client) ->\n                client.refreshSessionCatching()\n                    .onSuccess { clientManager.updateNewSession(role, it) }\n            }\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/usecase/UnblockUserWithoutUriUseCase.kt",
    "content": "package com.zhangke.fread.bluesky.internal.usecase\n\nimport app.bsky.actor.GetProfileQueryParams\nimport com.zhangke.fread.bluesky.internal.client.BlueskyClientManager\nimport com.zhangke.fread.status.author.BlogAuthor\nimport com.zhangke.fread.status.model.PlatformLocator\nimport sh.christian.ozone.api.Did\n\nclass UnblockUserWithoutUriUseCase(\n    private val clientManager: BlueskyClientManager,\n    private val updateBlock: UpdateBlockUseCase,\n) {\n\n    suspend operator fun invoke(\n        locator: PlatformLocator,\n        author: BlogAuthor,\n    ): Result<Unit> {\n        val client = clientManager.getClient(locator)\n        val did = author.webFinger.did\n        if (did.isNullOrEmpty()) return Result.failure(IllegalArgumentException(\"Author did is empty\"))\n        val profile = client.getProfileCatching(GetProfileQueryParams(Did(did)))\n            .getOrNull()\n        if (profile == null) {\n            return Result.failure(IllegalArgumentException(\"Failed to get profile for did: $did\"))\n        }\n        val blockingUri = profile.viewer?.blocking\n        if (blockingUri == null) {\n            return Result.success(Unit)\n        }\n        return updateBlock(\n            locator = locator,\n            did = did,\n            block = false,\n            blockUri = blockingUri.toString()\n        ).map { }\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/usecase/UnpinFeedsUseCase.kt",
    "content": "package com.zhangke.fread.bluesky.internal.usecase\n\nimport app.bsky.actor.PreferencesUnion.SavedFeedsPrefV2\nimport com.zhangke.fread.bluesky.internal.model.BlueskyFeeds\nimport com.zhangke.fread.status.model.PlatformLocator\n\nclass UnpinFeedsUseCase(\n    private val updatePreferences: UpdatePreferencesUseCase,\n) {\n\n    suspend operator fun invoke(\n        locator: PlatformLocator,\n        feeds: BlueskyFeeds.Feeds,\n    ): Result<Unit> {\n        return updatePreferences(\n            locator = locator,\n            updater = { preferences ->\n                preferences.map { preference ->\n                    if (preference is SavedFeedsPrefV2) {\n                        val newItems = preference.value.items.filter { it.value != feeds.uri }\n                        SavedFeedsPrefV2(app.bsky.actor.SavedFeedsPrefV2(newItems))\n                    } else {\n                        preference\n                    }\n                }\n            },\n        )\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/usecase/UpdateBlockUseCase.kt",
    "content": "package com.zhangke.fread.bluesky.internal.usecase\n\nimport com.zhangke.fread.bluesky.internal.client.BskyCollections\nimport com.zhangke.fread.bluesky.internal.client.adjustToRkey\nimport com.zhangke.fread.bluesky.internal.client.blockRecord\nimport com.zhangke.fread.status.model.PlatformLocator\nimport sh.christian.ozone.api.AtUri\n\nclass UpdateBlockUseCase(\n    private val createRecord: CreateRecordUseCase,\n    private val deleteRecord: DeleteRecordUseCase,\n) {\n\n    suspend operator fun invoke(\n        locator: PlatformLocator,\n        did: String,\n        block: Boolean,\n        blockUri: String?,\n    ): Result<AtUri?> {\n        return if (block) {\n            createRecord(\n                locator = locator,\n                collection = BskyCollections.block,\n                record = blockRecord(did),\n            ).map { it.uri }\n        } else {\n            if (blockUri.isNullOrEmpty()) {\n                Result.success(null)\n            } else {\n                deleteRecord(\n                    locator = locator,\n                    collection = BskyCollections.block,\n                    rkey = blockUri.adjustToRkey(),\n                ).map { null }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/usecase/UpdateHomeTabUseCase.kt",
    "content": "package com.zhangke.fread.bluesky.internal.usecase\n\nimport com.zhangke.fread.bluesky.internal.content.BlueskyContent\nimport com.zhangke.fread.common.content.FreadContentRepo\nimport com.zhangke.fread.status.model.PlatformLocator\n\nclass UpdateHomeTabUseCase(\n    private val getFeeds: GetFollowingFeedsUseCase,\n    private val contentRepo: FreadContentRepo,\n) {\n\n    suspend operator fun invoke(\n        contentId: String,\n        locator: PlatformLocator,\n    ) {\n        val content = contentRepo.getContent(contentId)\n            ?.takeIf { it is BlueskyContent }\n            ?.let { it as BlueskyContent } ?: return\n        getFeeds(locator).onSuccess { feeds ->\n            contentRepo.insertContent(content.copy(feedsList = feeds))\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/usecase/UpdatePinnedFeedsOrderUseCase.kt",
    "content": "package com.zhangke.fread.bluesky.internal.usecase\n\nimport app.bsky.actor.PreferencesUnion\nimport app.bsky.actor.SavedFeed\nimport app.bsky.actor.SavedFeedsPrefV2\nimport app.bsky.actor.Type\nimport com.zhangke.fread.bluesky.internal.model.BlueskyFeeds\nimport com.zhangke.fread.status.model.PlatformLocator\n\nclass UpdatePinnedFeedsOrderUseCase(\n    private val updatePreferences: UpdatePreferencesUseCase,\n) {\n\n    suspend operator fun invoke(\n        locator: PlatformLocator,\n        feeds: List<BlueskyFeeds>,\n    ): Result<Unit> {\n        return updatePreferences(locator) { preferences ->\n            preferences.map { preference ->\n                if (preference is PreferencesUnion.SavedFeedsPrefV2) {\n                    PreferencesUnion.SavedFeedsPrefV2(\n                        value = SavedFeedsPrefV2(reorder(preference.value.items, feeds))\n                    )\n                } else {\n                    preference\n                }\n            }\n        }\n    }\n\n    private fun reorder(list: List<SavedFeed>, newOrder: List<BlueskyFeeds>): List<SavedFeed> {\n        val uriToFeedsMap = mutableMapOf<String, SavedFeed>()\n        for (feed in list) {\n            uriToFeedsMap[feed.idForReorder] = feed\n        }\n        return newOrder.map {\n            uriToFeedsMap[it.idForReorder]!!\n        }\n    }\n\n    private val SavedFeed.idForReorder: String\n        get() {\n            return when (this.type) {\n                is Type.Timeline -> \"following\"\n                else -> value\n            }\n        }\n\n    private val BlueskyFeeds.idForReorder: String?\n        get() {\n            return when (this) {\n                is BlueskyFeeds.List -> uri\n                is BlueskyFeeds.Feeds -> uri\n                is BlueskyFeeds.FollowingTimeline -> \"following\"\n                else -> null\n            }\n        }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/usecase/UpdatePreferencesUseCase.kt",
    "content": "package com.zhangke.fread.bluesky.internal.usecase\n\nimport app.bsky.actor.PreferencesUnion\nimport app.bsky.actor.PutPreferencesRequest\nimport com.zhangke.framework.utils.exceptionOrThrow\nimport com.zhangke.fread.bluesky.internal.client.BlueskyClientManager\nimport com.zhangke.fread.status.model.PlatformLocator\n\nclass UpdatePreferencesUseCase (\n    private val clientManager: BlueskyClientManager,\n) {\n\n    suspend operator fun invoke(\n        locator: PlatformLocator,\n        updater: (List<PreferencesUnion>) -> List<PreferencesUnion>,\n    ): Result<Unit> {\n        val client = clientManager.getClient(locator)\n        val preferenceResult = client.getPreferencesCatching()\n        if (preferenceResult.isFailure) {\n            return Result.failure(preferenceResult.exceptionOrThrow())\n        }\n        val preference = preferenceResult.getOrThrow()\n        val request = PutPreferencesRequest(\n            preferences = updater(preference.preferences),\n        )\n        return clientManager.getClient(locator).putPreferencesCatching(request)\n    }\n}"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/usecase/UpdateProfileRecordUseCase.kt",
    "content": "package com.zhangke.fread.bluesky.internal.usecase\n\nimport app.bsky.actor.Profile\nimport com.atproto.repo.GetRecordQueryParams\nimport com.atproto.repo.PutRecordRequest\nimport com.atproto.repo.PutRecordResponse\nimport com.zhangke.framework.utils.exceptionOrThrow\nimport com.zhangke.fread.bluesky.internal.client.BlueskyClient\nimport com.zhangke.fread.bluesky.internal.client.BskyCollections\nimport com.zhangke.fread.bluesky.internal.client.selfRkey\nimport com.zhangke.fread.bluesky.internal.utils.bskyJson\nimport sh.christian.ozone.api.Did\n\nclass UpdateProfileRecordUseCase() {\n\n    suspend operator fun invoke(\n        client: BlueskyClient,\n        updater: (Profile) -> Profile,\n    ): Result<PutRecordResponse> {\n        val account =\n            client.loggedAccountProvider() ?: return Result.failure(Exception(\"No logged account\"))\n        val did = Did(account.did)\n        val recordResult = client.getRecordCatching(\n            GetRecordQueryParams(\n                repo = did, collection = BskyCollections.profile, rkey = selfRkey,\n            )\n        )\n        if (recordResult.isFailure) return Result.failure(recordResult.exceptionOrThrow())\n        val record = recordResult.getOrThrow()\n        val profile: Profile = record.bskyJson()\n        val newProfile = updater(profile)\n        return client.putRecordCatching(\n            PutRecordRequest(\n                repo = did,\n                collection = BskyCollections.profile,\n                rkey = selfRkey,\n                record = newProfile.bskyJson(),\n                swapRecord = record.cid,\n            )\n        )\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/usecase/UpdateRelationshipUseCase.kt",
    "content": "package com.zhangke.fread.bluesky.internal.usecase\n\nimport app.bsky.actor.GetProfileQueryParams\nimport com.zhangke.fread.bluesky.internal.client.BlueskyClientManager\nimport com.zhangke.fread.bluesky.internal.client.BskyCollections\nimport com.zhangke.fread.bluesky.internal.client.adjustToRkey\nimport com.zhangke.fread.bluesky.internal.client.followRecord\nimport com.zhangke.fread.status.model.PlatformLocator\nimport sh.christian.ozone.api.AtUri\nimport sh.christian.ozone.api.Did\n\nclass UpdateRelationshipUseCase(\n    private val clientManager: BlueskyClientManager,\n    private val createRecord: CreateRecordUseCase,\n    private val deleteRecord: DeleteRecordUseCase,\n) {\n\n    suspend operator fun invoke(\n        locator: PlatformLocator,\n        targetDid: String,\n        type: UpdateRelationshipType,\n        followUri: String? = null,\n    ): Result<AtUri?> {\n        val did = Did(targetDid)\n        when (type) {\n            UpdateRelationshipType.FOLLOW -> {\n                return createRecord(\n                    locator = locator,\n                    collection = BskyCollections.follow,\n                    record = followRecord(targetDid),\n                ).map { it.uri }\n            }\n\n            UpdateRelationshipType.UNFOLLOW -> {\n                val finalFollowUri = if (followUri.isNullOrEmpty()) {\n                    val profileResult = clientManager.getClient(locator)\n                        .getProfileCatching(GetProfileQueryParams(did))\n                    if (profileResult.isFailure) return Result.failure(profileResult.exceptionOrNull()!!)\n                    profileResult.getOrThrow().viewer?.following?.atUri\n                } else {\n                    followUri\n                }\n                if (finalFollowUri.isNullOrEmpty()) return Result.success(null)\n                return deleteRecord(\n                    locator = locator,\n                    collection = BskyCollections.follow,\n                    rkey = finalFollowUri.adjustToRkey(),\n                ).map { null }\n            }\n        }\n    }\n}\n\nenum class UpdateRelationshipType {\n\n    FOLLOW,\n    UNFOLLOW,\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/usecase/UploadBlobUseCase.kt",
    "content": "package com.zhangke.fread.bluesky.internal.usecase\n\nimport com.zhangke.framework.utils.AspectRatio\nimport com.zhangke.framework.utils.ImageCompressUtils\nimport com.zhangke.framework.utils.KB\nimport com.zhangke.framework.utils.MB\nimport com.zhangke.framework.utils.PlatformUri\nimport com.zhangke.framework.utils.VideoUtils\nimport com.zhangke.framework.utils.mapForErrorMessage\nimport com.zhangke.fread.bluesky.internal.client.BlueskyClientManager\nimport com.zhangke.fread.common.utils.PlatformUriHelper\nimport com.zhangke.fread.status.model.PlatformLocator\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.IO\nimport kotlinx.coroutines.withContext\nimport sh.christian.ozone.api.model.Blob\n\nclass UploadBlobUseCase(\n    private val clientManager: BlueskyClientManager,\n    private val platformUriHelper: PlatformUriHelper,\n) {\n\n    companion object {\n\n        private val BSKY_BLOB_MAX_SIZE = 930.KB\n    }\n\n    suspend operator fun invoke(\n        locator: PlatformLocator,\n        fileUri: PlatformUri,\n    ): Result<Pair<Blob, AspectRatio?>> {\n        return runCatching {\n            val file = platformUriHelper.read(fileUri)\n                ?: throw RuntimeException(\"File invalid!\")\n            var bytes = file.readBytes()\n                ?: throw RuntimeException(\"File invalid!\")\n            var aspect: AspectRatio? = null\n            if (file.isVideo) {\n                VideoUtils().getVideoAspect(fileUri.toString())?.let {\n                    aspect = it\n                }\n            } else {\n                val result = withContext(Dispatchers.IO) {\n                    ImageCompressUtils().compress(bytes, BSKY_BLOB_MAX_SIZE)\n                }\n                bytes = result.bytes\n                aspect = result.ratio\n            }\n            val blob = clientManager.getClient(locator).uploadBlobCatching(bytes).getOrThrow().blob\n            blob to aspect\n        }.mapForErrorMessage(\"Upload blob failed\")\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/usecase/UploadImageByImageUrlUseCase.kt",
    "content": "package com.zhangke.fread.bluesky.internal.usecase\n\nimport com.zhangke.framework.architect.http.sharedHttpClient\nimport com.zhangke.framework.utils.mapForErrorMessage\nimport com.zhangke.fread.bluesky.internal.client.BlueskyClientManager\nimport com.zhangke.fread.common.utils.StorageHelper\nimport com.zhangke.fread.status.model.PlatformLocator\nimport io.ktor.client.call.body\nimport io.ktor.client.request.get\nimport io.ktor.http.takeFrom\nimport okio.FileSystem\nimport okio.SYSTEM\nimport sh.christian.ozone.api.model.Blob\n\nclass UploadImageByImageUrlUseCase(\n    private val clientManager: BlueskyClientManager,\n    private val storageHelper: StorageHelper,\n) {\n\n    suspend operator fun invoke(locator: PlatformLocator, imageUrl: String): Result<Blob> {\n        return runCatching {\n            val fileSystem = FileSystem.SYSTEM\n            val cacheDir = storageHelper.cacheDir.resolve(\"bluesky_remote_images\")\n            fileSystem.createDirectories(cacheDir)\n            val localPath = cacheDir.resolve(\"remote-image-${imageUrl.hashCode()}.tmp\")\n            try {\n                val bytes = sharedHttpClient.get {\n                    url { takeFrom(imageUrl) }\n                }.body<ByteArray>()\n                fileSystem.write(localPath) { write(bytes) }\n                val localBytes = fileSystem.read(localPath) {\n                    readByteArray()\n                }\n                clientManager.getClient(locator).uploadBlobCatching(localBytes).getOrThrow().blob\n            } finally {\n                runCatching {\n                    if (fileSystem.exists(localPath)) {\n                        fileSystem.delete(localPath)\n                    }\n                }\n            }\n        }.mapForErrorMessage(\"Upload image by url failed\")\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/utils/AtResponseUtils.kt",
    "content": "package com.zhangke.fread.bluesky.internal.utils\n\nimport com.zhangke.framework.architect.json.globalJson\nimport com.zhangke.fread.bluesky.internal.client.expired\nimport com.zhangke.fread.status.account.AuthenticationFailureException\nimport kotlinx.serialization.json.Json\nimport kotlinx.serialization.json.decodeFromJsonElement\nimport kotlinx.serialization.json.encodeToJsonElement\nimport sh.christian.ozone.api.response.AtpResponse\n\nfun <T : Any> AtpResponse<T>.toResult(): Result<T> {\n    return when (this) {\n        is AtpResponse.Success -> Result.success(this.response)\n        is AtpResponse.Failure -> Result.failure(AtRequestException.fromResponse(this))\n    }\n}\n\ndata class AtRequestException(\n    val statusCode: Int,\n    val response: Any?,\n    val error: String?,\n    val errorMessage: String?,\n    val headers: Map<String, String>,\n) : Exception() {\n\n    val needAuthFactorTokenRequired: Boolean\n        get() = error == ERROR_FACTOR_REQUIRED\n\n    companion object {\n\n        private const val ERROR_FACTOR_REQUIRED = \"AuthFactorTokenRequired\"\n\n        fun fromResponse(response: AtpResponse.Failure<*>): Throwable {\n            if (response.error?.expired == true) {\n                return AuthenticationFailureException(response.error!!.message)\n            }\n            return AtRequestException(\n                statusCode = response.statusCode.code,\n                response = response.response,\n                error = response.error?.error,\n                errorMessage = response.error?.message,\n                headers = response.headers,\n            )\n        }\n    }\n}\n\ninternal val bskyJson by lazy {\n    Json(globalJson) {\n        ignoreUnknownKeys = true\n        classDiscriminator = \"${'$'}type\"\n    }\n}\n\ninternal inline fun <reified T, reified R> T.bskyJson(): R =\n    bskyJson.decodeFromJsonElement(bskyJson.encodeToJsonElement(this))\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/utils/PlatformLocatorUtils.kt",
    "content": "package com.zhangke.fread.bluesky.internal.utils\n\nimport com.zhangke.fread.bluesky.internal.content.BlueskyContent\nimport com.zhangke.fread.status.model.PlatformLocator\n\nfun createPlatformLocator(content: BlueskyContent): PlatformLocator {\n    return if (content.accountUri != null) {\n        PlatformLocator(baseUrl = content.baseUrl, accountUri = content.accountUri)\n    } else {\n        PlatformLocator(baseUrl = content.baseUrl)\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/commonMain/kotlin/com/zhangke/fread/bluesky/internal/utils/Tid.kt",
    "content": "@file:OptIn(ExperimentalTime::class)\n\npackage com.zhangke.fread.bluesky.internal.utils\n\nimport kotlin.random.Random\nimport kotlin.time.Clock\nimport kotlin.time.ExperimentalTime\n\nobject Tid {\n\n    private const val BASE_32_CHARS = \"234567abcdefghijklmnopqrstuvwxyz\"\n\n    fun generateTID(): String {\n        val now = Clock.System.now()\n        val currentTimeMicros = (now.epochSeconds * 1_000_000) + (now.nanosecondsOfSecond / 1_000)\n        val clockIdentifier = Random.nextBits(10).toLong()\n        val tidNumber = (currentTimeMicros shl 10) or clockIdentifier\n        return encodeBase32(tidNumber)\n    }\n\n    private fun encodeBase32(number: Long): String {\n        val base32Chars = BASE_32_CHARS\n        val stringBuilder = StringBuilder()\n        var num = number\n        for (i in 0 until 13) {\n            stringBuilder.append(base32Chars[(num and 0x1F).toInt()])\n            num = num shr 5\n        }\n        return stringBuilder.toString().reversed()\n    }\n}\n"
  },
  {
    "path": "plugins/bluesky/src/iosMain/kotlin/com/zhangke/fread/bluesky/BlueskyIosModule.kt",
    "content": "package com.zhangke.fread.bluesky\n\nimport androidx.room.Room\nimport com.zhangke.fread.bluesky.internal.db.BlueskyLoggedAccountDatabase\nimport com.zhangke.fread.common.documentDirectory\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.IO\nimport org.koin.core.module.Module\n\nactual fun Module.createPlatformModule() {\n\n    single<BlueskyLoggedAccountDatabase> {\n        val dbFilePath = getDBFilePath(BlueskyLoggedAccountDatabase.DB_NAME)\n        Room.databaseBuilder<BlueskyLoggedAccountDatabase>(\n            name = dbFilePath,\n        ).setDriver(androidx.sqlite.driver.bundled.BundledSQLiteDriver())\n            .setQueryCoroutineContext(Dispatchers.IO)\n            .build()\n    }\n}\n\nprivate fun getDBFilePath(dbName: String): String {\n    return documentDirectory() + \"/$dbName\"\n}\n"
  },
  {
    "path": "plugins/bluesky/src/test/java/com/zhangke/fread/bluesky/ExampleUnitTest.kt",
    "content": "package com.zhangke.fread.bluesky\n\nimport org.junit.Test\n\nimport org.junit.Assert.*\n\n/**\n * Example local unit test, which will execute on the development machine (host).\n *\n * See [testing documentation](http://d.android.com/tools/testing).\n */\nclass ExampleUnitTest {\n    @Test\n    fun addition_isCorrect() {\n        assertEquals(4, 2 + 2)\n    }\n}"
  },
  {
    "path": "plugins/rss/.gitignore",
    "content": "/build"
  },
  {
    "path": "plugins/rss/build.gradle.kts",
    "content": "plugins {\n    id(\"fread.project.feature.kmp\")\n    id(\"com.google.devtools.ksp\")\n    id(\"kotlin-parcelize\")\n    alias(libs.plugins.room)\n}\n\nandroid {\n    namespace = \"com.zhangke.fread.rss\"\n}\n\nkotlin {\n    sourceSets {\n        commonMain {\n            dependencies {\n                implementation(project(path = \":framework\"))\n                implementation(project(path = \":commonbiz:common\"))\n                implementation(project(path = \":bizframework:status-provider\"))\n                implementation(project(path = \":commonbiz:status-ui\"))\n                implementation(project(path = \":commonbiz:sharedscreen\"))\n\n                implementation(compose.components.resources)\n\n                implementation(libs.jetbrains.lifecycle.viewmodel)\n\n                implementation(libs.kotlinx.serialization.core)\n                implementation(libs.kotlinx.serialization.json)\n\n                implementation(libs.androidx.room)\n                implementation(libs.auto.service.annotations)\n                implementation(libs.uri.kmp)\n                implementation(libs.rssparser)\n                implementation(libs.okio)\n\n                implementation(libs.krouter.runtime)\n            }\n        }\n        commonTest {\n            dependencies {\n                implementation(kotlin(\"test\"))\n            }\n        }\n        androidMain {\n            dependencies {\n                implementation(libs.androidx.core.ktx)\n            }\n        }\n    }\n    configureCommonMainKsp()\n}\n\ndependencies {\n    kspAll(libs.androidx.room.compiler)\n    kspAll(libs.auto.service.ksp)\n    kspAll(libs.krouter.collecting.compiler)\n}\n\ncompose {\n    resources {\n        publicResClass = false\n        packageOfResClass = \"com.zhangke.fread.rss\"\n        generateResClass = always\n    }\n}\n\nroom {\n    schemaDirectory(\"$projectDir/schemas\")\n}\n"
  },
  {
    "path": "plugins/rss/consumer-rules.pro",
    "content": ""
  },
  {
    "path": "plugins/rss/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguardFiles setting in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n\n# Uncomment this to preserve the line number information for\n# debugging stack traces.\n#-keepattributes SourceFile,LineNumberTable\n\n# If you keep the line number information, uncomment this to\n# hide the original source file name.\n#-renamesourcefileattribute SourceFile"
  },
  {
    "path": "plugins/rss/src/androidMain/kotlin/com/zhangke/fread/rss/RssAndroidModule.kt",
    "content": "package com.zhangke.fread.rss\n\nimport androidx.room.Room\nimport com.prof18.rssparser.RssParser\nimport com.prof18.rssparser.RssParserBuilder\nimport com.zhangke.framework.architect.http.GlobalOkHttpClient\nimport com.zhangke.framework.utils.SystemUtils\nimport com.zhangke.fread.rss.internal.db.RssDatabases\nimport okhttp3.OkHttpClient\nimport org.koin.android.ext.koin.androidContext\nimport org.koin.core.module.Module\n\nactual fun Module.createPlatformModule() {\n    single<RssDatabases> {\n        Room.databaseBuilder(\n            androidContext(),\n            RssDatabases::class.java,\n            RssDatabases.DB_NAME,\n        ).build()\n    }\n    single<RssParser> {\n        RssParserBuilder(\n            callFactory = createRssOkHttpClient(\n                appVersion = SystemUtils.getAppVersionName(androidContext()),\n            ),\n        ).build()\n    }\n}\n\nprivate fun createRssOkHttpClient(appVersion: String): OkHttpClient {\n    val userAgent = \"Fread/$appVersion (Android) +https://fread.xyz/\"\n    return GlobalOkHttpClient.client.newBuilder()\n        .addInterceptor { chain ->\n            val request = chain.request().newBuilder()\n                .header(\"User-Agent\", userAgent)\n                .build()\n            chain.proceed(request)\n        }\n        .build()\n}\n"
  },
  {
    "path": "plugins/rss/src/androidMain/kotlin/com/zhangke/fread/rss/internal/utils/AvatarUtils.android.kt",
    "content": "package com.zhangke.fread.rss.internal.utils\n\nimport android.content.Context\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.toArgb\nimport com.zhangke.framework.ktx.ifNullOrEmpty\nimport com.zhangke.framework.network.HttpScheme\nimport com.zhangke.framework.security.Md5\nimport com.zhangke.framework.utils.BitmapUtils\nimport com.zhangke.framework.utils.appContext\nimport com.zhangke.fread.rss.internal.model.RssSource\nimport java.io.File\n\nactual object AvatarUtils {\n\n    private const val AVATAR_WIDTH = 100\n    private const val AVATAR_HEIGHT = 100\n    private const val AVATAR_DIR = \"avatars\"\n\n    private val colors = listOf(\n        Color(0xFF3880F2),\n        Color(0xFF75A5F1),\n        Color(0xFFBED7FF),\n        Color(0xFFAC82B0),\n        Color(0xFFAAB082),\n        Color(0xFF82B097),\n    )\n\n    actual fun makeSourceAvatar(\n        source: RssSource,\n    ): String? {\n        val text = source.displayName.ifNullOrEmpty { source.title }.take(2).uppercase()\n        val backgroundColor = colors.random().toArgb()\n        val bitmap = BitmapUtils.buildBitmapWithText(\n            width = AVATAR_WIDTH,\n            height = AVATAR_HEIGHT,\n            text = text,\n            backgroundColor = backgroundColor,\n        )\n        val avatarFile = getAvatarFile(source.url)\n        return try {\n            if (!avatarFile.exists()) {\n                avatarFile.parentFile?.mkdirs()\n                avatarFile.createNewFile()\n            }\n            BitmapUtils.saveToFile(bitmap, avatarFile)\n            avatarFile.absolutePath\n        } catch (e: Throwable) {\n            null\n        }\n    }\n\n    fun getAvatarFile(sourceUrl: String): File {\n        val fileName = \"${Md5.md5(sourceUrl)}.png\"\n        return File(getAvatarDirPath(), fileName)\n    }\n\n    fun isLocalAvatar(avatar: String?): Boolean {\n        avatar ?: return false\n        val fixedAvatar = avatar.lowercase()\n        return fixedAvatar.isNotEmpty() &&\n                !fixedAvatar.startsWith(HttpScheme.HTTP) &&\n                !fixedAvatar.startsWith(HttpScheme.HTTPS)\n    }\n\n    actual fun isRemoteAvatar(avatar: String?): Boolean {\n        avatar ?: return false\n        val fixedAvatar = avatar.lowercase()\n        return fixedAvatar.startsWith(HttpScheme.HTTP) || fixedAvatar.startsWith(HttpScheme.HTTPS)\n    }\n\n    private fun getAvatarDirPath(): String {\n        return appContext.getDir(AVATAR_DIR, Context.MODE_PRIVATE).absolutePath\n    }\n}\n"
  },
  {
    "path": "plugins/rss/src/commonMain/kotlin/com/zhangke/fread/rss/RssAccountManager.kt",
    "content": "package com.zhangke.fread.rss\n\nimport com.zhangke.fread.status.account.AccountRefreshResult\nimport com.zhangke.fread.status.account.IAccountManager\nimport com.zhangke.fread.status.account.LoggedAccount\nimport com.zhangke.fread.status.author.BlogAuthor\nimport com.zhangke.fread.status.model.FreadContent\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.model.Relationships\nimport com.zhangke.fread.status.platform.BlogPlatform\nimport com.zhangke.fread.status.uri.FormalUri\nimport kotlinx.coroutines.flow.Flow\n\nclass RssAccountManager() : IAccountManager {\n\n    override suspend fun getAllLoggedAccount(): List<LoggedAccount> {\n        return emptyList()\n    }\n\n    override fun getAllAccountFlow(): Flow<List<LoggedAccount>>? {\n        return null\n    }\n\n    override suspend fun triggerLaunchAuth(platform: BlogPlatform, account: LoggedAccount?) {\n        // no-op\n    }\n\n    override suspend fun refreshAllAccountInfo(): List<AccountRefreshResult> {\n        return emptyList()\n    }\n\n    override suspend fun logout(account: LoggedAccount): Boolean {\n        return false\n    }\n\n    override fun subscribeNotification() {}\n\n    override suspend fun getRelationships(\n        account: LoggedAccount,\n        accounts: List<BlogAuthor>,\n    ): Result<Map<FormalUri, Relationships>> {\n        return Result.success(emptyMap())\n    }\n\n    override suspend fun cancelFollowRequest(\n        account: LoggedAccount,\n        user: BlogAuthor,\n    ): Result<Unit>? {\n        return null\n    }\n\n    override suspend fun unblockAccount(\n        account: LoggedAccount,\n        user: BlogAuthor\n    ): Result<Unit>? {\n        return null\n    }\n\n    override suspend fun selectContentWithAccount(\n        contentList: List<FreadContent>,\n        account: LoggedAccount\n    ): List<FreadContent> {\n        return emptyList()\n    }\n}\n"
  },
  {
    "path": "plugins/rss/src/commonMain/kotlin/com/zhangke/fread/rss/RssContentManager.kt",
    "content": "package com.zhangke.fread.rss\n\nimport com.zhangke.fread.status.content.AddContentAction\nimport com.zhangke.fread.status.content.IContentManager\nimport com.zhangke.fread.status.model.ContentConfig\nimport com.zhangke.fread.status.model.FreadContent\nimport com.zhangke.fread.status.platform.BlogPlatform\n\nclass RssContentManager() : IContentManager {\n\n    override suspend fun addContent(platform: BlogPlatform, action: AddContentAction) {\n\n    }\n\n    override fun restoreContent(config: ContentConfig): FreadContent? {\n        return null\n    }\n}\n"
  },
  {
    "path": "plugins/rss/src/commonMain/kotlin/com/zhangke/fread/rss/RssModule.kt",
    "content": "package com.zhangke.fread.rss\n\nimport com.zhangke.framework.nav.NavEntryProvider\nimport com.zhangke.fread.rss.internal.adapter.BlogAuthorAdapter\nimport com.zhangke.fread.rss.internal.adapter.RssStatusAdapter\nimport com.zhangke.fread.rss.internal.platform.RssPlatformTransformer\nimport com.zhangke.fread.rss.internal.repo.RssRepo\nimport com.zhangke.fread.rss.internal.repo.RssStatusRepo\nimport com.zhangke.fread.rss.internal.rss.RssFetcher\nimport com.zhangke.fread.rss.internal.rss.RssParserWrapper\nimport com.zhangke.fread.rss.internal.screen.source.RssSourceViewModel\nimport com.zhangke.fread.rss.internal.source.RssSourceTransformer\nimport com.zhangke.fread.rss.internal.uri.RssUriTransformer\nimport com.zhangke.fread.rss.internal.webfinger.RssSourceWebFingerTransformer\nimport com.zhangke.fread.status.IStatusProvider\nimport org.koin.core.module.Module\nimport org.koin.core.module.dsl.factoryOf\nimport org.koin.core.module.dsl.singleOf\nimport org.koin.core.module.dsl.viewModelOf\nimport org.koin.dsl.bind\nimport org.koin.dsl.module\n\nval rssModule = module {\n\n    createPlatformModule()\n\n    factoryOf(::RssNavEntryProvider) bind NavEntryProvider::class\n\n    singleOf(::RssRepo)\n    singleOf(::RssParserWrapper)\n    singleOf(::RssFetcher)\n\n    factoryOf(::RssContentManager)\n    factoryOf(::RssScreenProvider)\n    factoryOf(::RssSearchEngine)\n    factoryOf(::RssAccountManager)\n    factoryOf(::RssStatusResolver)\n    factoryOf(::RssStatusSourceResolver)\n    factoryOf(::RssNotificationResolver)\n    factoryOf(::RssPublishManager)\n    factoryOf(::RssStatusRepo)\n    factoryOf(::RssStatusAdapter)\n    factoryOf(::BlogAuthorAdapter)\n    factoryOf(::RssPlatformTransformer)\n    factoryOf(::RssSourceTransformer)\n    factoryOf(::RssUriTransformer)\n    factoryOf(::RssSourceWebFingerTransformer)\n\n    viewModelOf(::RssSourceViewModel)\n\n    factoryOf(::RssStatusProvider) bind IStatusProvider::class\n}\n\nexpect fun Module.createPlatformModule()\n"
  },
  {
    "path": "plugins/rss/src/commonMain/kotlin/com/zhangke/fread/rss/RssNavEntryProvider.kt",
    "content": "package com.zhangke.fread.rss\n\nimport androidx.navigation3.runtime.EntryProviderScope\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.nav.NavEntryProvider\nimport com.zhangke.framework.utils.UrlEncoder\nimport com.zhangke.fread.rss.internal.screen.source.RssSourceScreen\nimport com.zhangke.fread.rss.internal.screen.source.RssSourceScreenNavKey\nimport kotlinx.serialization.modules.PolymorphicModuleBuilder\nimport kotlinx.serialization.modules.subclass\nimport org.koin.compose.viewmodel.koinViewModel\nimport org.koin.core.parameter.parametersOf\n\nclass RssNavEntryProvider : NavEntryProvider {\n\n    override fun EntryProviderScope<NavKey>.build() {\n        entry<RssSourceScreenNavKey> { key ->\n            RssSourceScreen(\n                viewModel = koinViewModel {\n                    parametersOf(UrlEncoder.decode(key.url))\n                },\n            )\n        }\n    }\n\n    override fun PolymorphicModuleBuilder<NavKey>.polymorph() {\n        subclass(RssSourceScreenNavKey::class)\n    }\n}\n"
  },
  {
    "path": "plugins/rss/src/commonMain/kotlin/com/zhangke/fread/rss/RssNotificationResolver.kt",
    "content": "package com.zhangke.fread.rss\n\nimport com.zhangke.fread.status.account.LoggedAccount\nimport com.zhangke.fread.status.author.BlogAuthor\nimport com.zhangke.fread.status.notification.INotificationResolver\nimport com.zhangke.fread.status.notification.PagedStatusNotification\n\nclass RssNotificationResolver() : INotificationResolver {\n\n    override suspend fun getNotifications(\n        account: LoggedAccount,\n        type: INotificationResolver.NotificationRequestType,\n        cursor: String?\n    ): Result<PagedStatusNotification>? {\n        return null\n    }\n\n    override suspend fun rejectFollowRequest(\n        account: LoggedAccount,\n        requestAuthor: BlogAuthor\n    ): Result<Unit>? {\n        return null\n    }\n\n    override suspend fun acceptFollowRequest(\n        account: LoggedAccount,\n        requestAuthor: BlogAuthor\n    ): Result<Unit>? {\n        return null\n    }\n\n    override suspend fun updateUnreadNotification(\n        account: LoggedAccount,\n        notificationLastReadId: String\n    ): Result<Unit>? {\n        return null\n    }\n}\n"
  },
  {
    "path": "plugins/rss/src/commonMain/kotlin/com/zhangke/fread/rss/RssPublishManager.kt",
    "content": "package com.zhangke.fread.rss\n\nimport com.zhangke.fread.status.account.LoggedAccount\nimport com.zhangke.fread.status.model.PublishBlogRules\nimport com.zhangke.fread.status.publish.IPublishBlogManager\nimport com.zhangke.fread.status.publish.PublishingPost\n\nclass RssPublishManager() : IPublishBlogManager {\n\n    override suspend fun getPublishBlogRules(account: LoggedAccount): Result<PublishBlogRules>? {\n        return null\n    }\n\n    override suspend fun publish(account: LoggedAccount, post: PublishingPost): Result<Unit>? {\n        return null\n    }\n}\n"
  },
  {
    "path": "plugins/rss/src/commonMain/kotlin/com/zhangke/fread/rss/RssScreenProvider.kt",
    "content": "package com.zhangke.fread.rss\n\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.nav.Tab\nimport com.zhangke.framework.utils.WebFinger\nimport com.zhangke.fread.rss.internal.screen.source.RssSourceScreenNavKey\nimport com.zhangke.fread.rss.internal.uri.RssUriTransformer\nimport com.zhangke.fread.rss.internal.uri.isRssUri\nimport com.zhangke.fread.status.blog.Blog\nimport com.zhangke.fread.status.model.FreadContent\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.model.StatusProviderProtocol\nimport com.zhangke.fread.status.platform.BlogPlatform\nimport com.zhangke.fread.status.screen.IStatusScreenProvider\nimport com.zhangke.fread.status.uri.FormalUri\n\nclass RssScreenProvider(\n    private val uriTransformer: RssUriTransformer,\n) : IStatusScreenProvider {\n\n    override fun getReplyBlogScreen(locator: PlatformLocator, blog: Blog): NavKey? {\n        return null\n    }\n\n    override fun getEditBlogScreen(locator: PlatformLocator, blog: Blog): NavKey? {\n        return null\n    }\n\n    override fun getQuoteBlogScreen(locator: PlatformLocator, blog: Blog): NavKey? {\n        return null\n    }\n\n    override fun getContentScreen(content: FreadContent, isLatestTab: Boolean): Tab? {\n        return null\n    }\n\n    override fun getEditContentConfigScreenScreen(content: FreadContent): NavKey? {\n        return null\n    }\n\n    override fun getUserDetailScreen(\n        locator: PlatformLocator,\n        uri: FormalUri,\n        userId: String?\n    ): NavKey? {\n        return getUserDetailScreenWithoutAccount(uri)\n    }\n\n    override fun getUserDetailScreenWithoutAccount(uri: FormalUri): NavKey? {\n        if (!uri.isRssUri) return null\n        val uriInsight = uriTransformer.parse(uri) ?: return null\n        val url = uriInsight.url\n        return RssSourceScreenNavKey(url)\n    }\n\n    override fun getUserDetailScreen(\n        locator: PlatformLocator,\n        webFinger: WebFinger,\n        protocol: StatusProviderProtocol,\n    ): NavKey? {\n        return null\n    }\n\n    override fun getUserDetailScreen(\n        locator: PlatformLocator,\n        did: String,\n        protocol: StatusProviderProtocol\n    ): NavKey? {\n        return null\n    }\n\n    override fun getTagTimelineScreen(\n        locator: PlatformLocator,\n        tag: String,\n        protocol: StatusProviderProtocol,\n    ): NavKey? {\n        return null\n    }\n\n    override fun getBlogFavouritedScreen(\n        locator: PlatformLocator,\n        blog: Blog,\n        protocol: StatusProviderProtocol\n    ): NavKey? {\n        return null\n    }\n\n    override fun getBlogBoostedScreen(\n        locator: PlatformLocator,\n        blog: Blog,\n        protocol: StatusProviderProtocol\n    ): NavKey? {\n        return null\n    }\n\n    override fun getExplorerTab(locator: PlatformLocator, platform: BlogPlatform): Tab? {\n        return null\n    }\n}\n"
  },
  {
    "path": "plugins/rss/src/commonMain/kotlin/com/zhangke/fread/rss/RssSearchEngine.kt",
    "content": "package com.zhangke.fread.rss\n\nimport com.zhangke.framework.network.SimpleUri\nimport com.zhangke.framework.utils.exceptionOrThrow\nimport com.zhangke.fread.rss.internal.adapter.BlogAuthorAdapter\nimport com.zhangke.fread.rss.internal.model.RssSource\nimport com.zhangke.fread.rss.internal.platform.RssPlatformTransformer\nimport com.zhangke.fread.rss.internal.repo.RssRepo\nimport com.zhangke.fread.rss.internal.source.RssSourceTransformer\nimport com.zhangke.fread.rss.internal.uri.RssUriInsight\nimport com.zhangke.fread.rss.internal.uri.RssUriTransformer\nimport com.zhangke.fread.status.author.BlogAuthor\nimport com.zhangke.fread.status.model.Hashtag\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.model.StatusUiState\nimport com.zhangke.fread.status.search.ISearchEngine\nimport com.zhangke.fread.status.search.SearchResult\nimport com.zhangke.fread.status.source.StatusSource\n\nclass RssSearchEngine(\n    private val rssPlatformTransformer: RssPlatformTransformer,\n    private val rssSourceTransformer: RssSourceTransformer,\n    private val rssRepo: RssRepo,\n    private val bogAuthorAdapter: BlogAuthorAdapter,\n    private val rssUriTransformer: RssUriTransformer,\n) : ISearchEngine {\n\n    override suspend fun search(\n        locator: PlatformLocator,\n        query: String\n    ): Result<List<SearchResult>> {\n        val authorResult = searchAuthorByUrl(query)\n        if (authorResult.isFailure) {\n            return Result.failure(authorResult.exceptionOrThrow())\n        }\n        val searchResultList = mutableListOf<SearchResult>()\n        authorResult.getOrNull()?.let {\n            searchResultList += SearchResult.Author(it)\n        }\n        return Result.success(searchResultList)\n    }\n\n    override suspend fun searchStatus(\n        locator: PlatformLocator,\n        query: String,\n        maxId: String?,\n    ): Result<List<StatusUiState>> {\n        return Result.success(emptyList())\n    }\n\n    override suspend fun searchHashtag(\n        locator: PlatformLocator,\n        query: String, offset: Int?,\n    ): Result<List<Hashtag>> {\n        return Result.success(emptyList())\n    }\n\n    override suspend fun searchAuthor(\n        locator: PlatformLocator,\n        query: String,\n        offset: Int?,\n    ): Result<List<BlogAuthor>> {\n        if (offset != null && offset > 0) {\n            return Result.success(emptyList())\n        }\n        return searchAuthorByUrl(query).map {\n            it?.let { listOf(it) } ?: emptyList()\n        }\n    }\n\n    override suspend fun searchSourceNoToken(query: String): Result<List<StatusSource>> {\n        return queryWithChannelByUrl(\n            query = query,\n            defaultResult = emptyList(),\n            block = { source, uriInsight ->\n                listOf(rssSourceTransformer.createSource(uriInsight, source))\n            }\n        )\n    }\n\n    private suspend fun searchAuthorByUrl(query: String): Result<BlogAuthor?> {\n        return queryWithChannelByUrl(\n            query = query,\n            defaultResult = null,\n            block = { channel, uriInsight ->\n                bogAuthorAdapter.createAuthor(uriInsight, channel)\n            }\n        )\n    }\n\n    private suspend fun <T> queryWithChannelByUrl(\n        query: String,\n        defaultResult: T,\n        block: suspend (RssSource, RssUriInsight) -> T,\n    ): Result<T> {\n        val url = SimpleUri.parse(query)\n            ?.toString()\n            ?.let(::fixUrl) ?: return Result.success(defaultResult)\n        val sourceResult = rssRepo.getRssSource(url)\n        if (sourceResult.isFailure) {\n            return Result.failure(sourceResult.exceptionOrThrow())\n        }\n        val source = sourceResult.getOrThrow() ?: return Result.success(defaultResult)\n        val uri = rssUriTransformer.build(url)\n        val uriInsight = RssUriInsight(uri, url)\n        val result = block(source, uriInsight)\n        return Result.success(result)\n    }\n\n    private fun fixUrl(url: String): String {\n        return url.trim().removeSuffix(\"/\")\n    }\n}\n"
  },
  {
    "path": "plugins/rss/src/commonMain/kotlin/com/zhangke/fread/rss/RssStatusProvider.kt",
    "content": "package com.zhangke.fread.rss\n\nimport com.zhangke.fread.status.IStatusProvider\nimport com.zhangke.fread.status.account.IAccountManager\nimport com.zhangke.fread.status.content.IContentManager\nimport com.zhangke.fread.status.notification.INotificationResolver\nimport com.zhangke.fread.status.platform.IPlatformResolver\nimport com.zhangke.fread.status.publish.IPublishBlogManager\nimport com.zhangke.fread.status.screen.IStatusScreenProvider\nimport com.zhangke.fread.status.search.ISearchEngine\nimport com.zhangke.fread.status.source.IStatusSourceResolver\nimport com.zhangke.fread.status.status.IStatusResolver\n\nclass RssStatusProvider(\n    contentManager: RssContentManager,\n    rssScreenProvider: RssScreenProvider,\n    rssSearchEngine: RssSearchEngine,\n    rssAccountManager: RssAccountManager,\n    rssStatusResolver: RssStatusResolver,\n    rssStatusSourceResolver: RssStatusSourceResolver,\n    rssNotificationResolver: RssNotificationResolver,\n    rssPublishManager: RssPublishManager,\n) : IStatusProvider {\n\n    override val contentManager: IContentManager = contentManager\n\n    override val screenProvider: IStatusScreenProvider = rssScreenProvider\n\n    override val searchEngine: ISearchEngine = rssSearchEngine\n\n    override val statusResolver: IStatusResolver = rssStatusResolver\n\n    override val statusSourceResolver: IStatusSourceResolver = rssStatusSourceResolver\n\n    override val accountManager: IAccountManager = rssAccountManager\n\n    override val notificationResolver: INotificationResolver = rssNotificationResolver\n\n    override val publishManager: IPublishBlogManager = rssPublishManager\n}\n"
  },
  {
    "path": "plugins/rss/src/commonMain/kotlin/com/zhangke/fread/rss/RssStatusResolver.kt",
    "content": "package com.zhangke.fread.rss\n\nimport com.zhangke.framework.network.FormalBaseUrl\nimport com.zhangke.framework.utils.exceptionOrThrow\nimport com.zhangke.fread.rss.internal.repo.RssStatusRepo\nimport com.zhangke.fread.rss.internal.uri.RssUriTransformer\nimport com.zhangke.fread.rss.internal.uri.isRssUri\nimport com.zhangke.fread.status.author.BlogAuthor\nimport com.zhangke.fread.status.blog.Blog\nimport com.zhangke.fread.status.blog.BlogPoll\nimport com.zhangke.fread.status.blog.BlogTranslation\nimport com.zhangke.fread.status.model.BlogTranslationUiState\nimport com.zhangke.fread.status.model.PagedData\nimport com.zhangke.fread.status.model.PlatformLocator\nimport com.zhangke.fread.status.model.StatusActionType\nimport com.zhangke.fread.status.model.StatusUiState\nimport com.zhangke.fread.status.model.isRss\nimport com.zhangke.fread.status.platform.BlogPlatform\nimport com.zhangke.fread.status.status.IStatusResolver\nimport com.zhangke.fread.status.status.model.Status\nimport com.zhangke.fread.status.status.model.StatusContext\nimport com.zhangke.fread.status.uri.FormalUri\n\nclass RssStatusResolver(\n    private val uriTransformer: RssUriTransformer,\n    private val rssStatusRepo: RssStatusRepo,\n) : IStatusResolver {\n\n    override suspend fun getStatus(\n        locator: PlatformLocator,\n        blogId: String?,\n        blogUri: String?,\n        platform: BlogPlatform\n    ): Result<StatusUiState>? {\n        return null\n    }\n\n    override suspend fun getStatusList(\n        uri: FormalUri,\n        limit: Int,\n        maxId: String?,\n    ): Result<PagedData<StatusUiState>>? {\n        if (!uri.isRssUri) return null\n        val uriInsight = uriTransformer.parse(uri)\n            ?: return Result.failure(IllegalArgumentException(\"Unknown uri: $uri\"))\n        if (!maxId.isNullOrEmpty()) return Result.success(PagedData(emptyList(), null))\n        val fetchResult = rssStatusRepo.getStatus(uriInsight)\n            .map { it.sortedByDescending { status -> status.createAt.epochMillis } }\n        if (fetchResult.isFailure) return Result.failure(fetchResult.exceptionOrThrow())\n        val baseUrl = FormalBaseUrl.parse(uriInsight.url) ?: return Result.failure(\n            IllegalArgumentException(\"Invalid base URL: ${uriInsight.url}\")\n        )\n        val locator = PlatformLocator(baseUrl = baseUrl)\n        val finalReturnItems = fetchResult.getOrThrow().map {\n            StatusUiState(\n                locator = locator,\n                status = it,\n                logged = false,\n                isOwner = false,\n                blogTranslationState = BlogTranslationUiState(false),\n            )\n        }\n        if (finalReturnItems.isEmpty()) return Result.success(PagedData(emptyList(), null))\n        return Result.success(PagedData(finalReturnItems, null))\n    }\n\n    override suspend fun interactive(\n        locator: PlatformLocator,\n        status: Status,\n        type: StatusActionType,\n    ): Result<Status?>? {\n        return null\n    }\n\n    override suspend fun votePoll(\n        locator: PlatformLocator,\n        blog: Blog,\n        votedOption: List<BlogPoll.Option>\n    ): Result<Status>? {\n        return null\n    }\n\n    override suspend fun getStatusContext(\n        locator: PlatformLocator,\n        status: Status,\n    ): Result<StatusContext>? {\n        if (!status.platform.protocol.isRss) return null\n        return Result.success(\n            StatusContext(\n                ancestors = emptyList(),\n                status = null,\n                descendants = emptyList(),\n            )\n        )\n    }\n\n    override suspend fun follow(locator: PlatformLocator, target: BlogAuthor): Result<Unit>? {\n        return null\n    }\n\n    override suspend fun unfollow(locator: PlatformLocator, target: BlogAuthor): Result<Unit>? {\n        return null\n    }\n\n    override suspend fun isFollowing(\n        locator: PlatformLocator,\n        target: BlogAuthor\n    ): Result<Boolean>? {\n        return null\n    }\n\n    override suspend fun translate(\n        locator: PlatformLocator,\n        status: Status,\n        lan: String,\n    ): Result<BlogTranslation>? {\n        return null\n    }\n}\n"
  },
  {
    "path": "plugins/rss/src/commonMain/kotlin/com/zhangke/fread/rss/RssStatusSourceResolver.kt",
    "content": "package com.zhangke.fread.rss\n\nimport com.zhangke.framework.utils.exceptionOrThrow\nimport com.zhangke.fread.rss.internal.repo.RssRepo\nimport com.zhangke.fread.rss.internal.rss.RssFetcher\nimport com.zhangke.fread.rss.internal.source.RssSourceTransformer\nimport com.zhangke.fread.rss.internal.uri.RssUriInsight\nimport com.zhangke.fread.rss.internal.uri.RssUriTransformer\nimport com.zhangke.fread.rss.internal.uri.isRssUri\nimport com.zhangke.fread.status.source.IStatusSourceResolver\nimport com.zhangke.fread.status.source.StatusSource\nimport com.zhangke.fread.status.uri.FormalUri\n\nclass RssStatusSourceResolver(\n    private val rssUriTransformer: RssUriTransformer,\n    private val rssSourceTransformer: RssSourceTransformer,\n    private val rssRepo: RssRepo,\n    private val rssFetcher: RssFetcher,\n) : IStatusSourceResolver {\n\n    override suspend fun resolveSourceByUri(uri: FormalUri): Result<StatusSource?> {\n        if (!uri.isRssUri) return Result.success(null)\n        val uriInsight = rssUriTransformer.parse(uri) ?: return Result.failure(\n            IllegalArgumentException(\"Unknown uri: $uri\")\n        )\n        val sourceResult = rssRepo.getRssSource(uriInsight.url)\n        if (sourceResult.isFailure) {\n            return Result.failure(sourceResult.exceptionOrThrow())\n        }\n        val source = sourceResult.getOrThrow() ?: return Result.success(null)\n        return rssSourceTransformer.createSource(uriInsight, source)\n            .let { Result.success(it) }\n    }\n\n    override suspend fun resolveRssSource(rssUrl: String): Result<StatusSource> {\n        return rssFetcher.fetchRss(rssUrl)\n            .map { it.first }\n            .map {\n                val uri = rssUriTransformer.build(rssUrl)\n                val uriInsight = RssUriInsight(uri, rssUrl)\n                rssSourceTransformer.createSource(uriInsight, it)\n            }\n    }\n}\n"
  },
  {
    "path": "plugins/rss/src/commonMain/kotlin/com/zhangke/fread/rss/internal/adapter/BlogAuthorAdapter.kt",
    "content": "package com.zhangke.fread.rss.internal.adapter\n\nimport com.zhangke.framework.ktx.ifNullOrEmpty\nimport com.zhangke.fread.rss.internal.model.RssSource\nimport com.zhangke.fread.rss.internal.uri.RssUriInsight\nimport com.zhangke.fread.rss.internal.webfinger.RssSourceWebFingerTransformer\nimport com.zhangke.fread.status.author.BlogAuthor\n\nclass BlogAuthorAdapter(\n    private val rssSourceWebFingerTransformer: RssSourceWebFingerTransformer,\n) {\n\n    fun createAuthor(\n        uriInsight: RssUriInsight,\n        source: RssSource,\n    ): BlogAuthor {\n        val webFinger = rssSourceWebFingerTransformer.create(uriInsight.url, source)\n        return BlogAuthor(\n            uri = uriInsight.rawUri,\n            webFinger = webFinger,\n            handle = webFinger.toString(),\n            name = source.displayName.ifNullOrEmpty { source.title },\n            description = source.description.orEmpty(),\n            avatar = source.thumbnail,\n            banner = null,\n            emojis = emptyList(),\n            followingCount = null,\n            followersCount = null,\n            statusesCount = null,\n        )\n    }\n}\n"
  },
  {
    "path": "plugins/rss/src/commonMain/kotlin/com/zhangke/fread/rss/internal/adapter/RssBlogMediaExtractor.kt",
    "content": "package com.zhangke.fread.rss.internal.adapter\n\nimport com.zhangke.framework.security.Md5\nimport com.zhangke.fread.status.blog.BlogMedia\nimport com.zhangke.fread.status.blog.BlogMediaType\n\ninternal object RssBlogMediaExtractor {\n\n    private val imageTagRegex = Regex(\"\"\"<img\\b[^>]*>\"\"\", setOf(RegexOption.IGNORE_CASE))\n    private val srcSetSplitRegex = Regex(\"\"\"\\s+\"\"\")\n    private val imageSourceAttributes = listOf(\n        \"src\",\n        \"data-src\",\n        \"data-original\",\n        \"data-lazy-src\",\n        \"data-actualsrc\",\n        \"data-srcset\",\n        \"srcset\",\n    )\n    private val iconKeywordRegex = Regex(\n        pattern = \"\"\"avatar|icon|logo|emoji|favicon|gravatar|sprite|badge|profile|author|masthead|publisher\"\"\",\n        options = setOf(RegexOption.IGNORE_CASE),\n    )\n\n    fun extract(\n        itemId: String,\n        articleUrl: String?,\n        contentHtml: String?,\n        descriptionHtml: String?,\n        fallbackImageUrl: String?,\n    ): List<BlogMedia> {\n        val medias = linkedMapOf<String, BlogMedia>()\n        listOfNotNull(contentHtml, descriptionHtml)\n            .forEach { html ->\n                extractFromHtml(itemId, html, articleUrl).forEach { media ->\n                    if (!medias.containsKey(media.url)) {\n                        medias[media.url] = media\n                    }\n                }\n            }\n        if (medias.isEmpty()) {\n            createMedia(\n                itemId = itemId,\n                url = normalizeUrl(fallbackImageUrl, articleUrl),\n                description = null,\n                tagSnapshot = fallbackImageUrl.orEmpty(),\n                width = null,\n                height = null,\n            )?.let { medias[it.url] = it }\n        }\n        return medias.values.toList()\n    }\n\n    private fun extractFromHtml(\n        itemId: String,\n        html: String,\n        articleUrl: String?,\n    ): List<BlogMedia> {\n        val medias = linkedMapOf<String, BlogMedia>()\n        imageTagRegex.findAll(html).forEach { match ->\n            val tag = match.value\n            val source = findImageSource(tag)\n            createMedia(\n                itemId = itemId,\n                url = normalizeUrl(source, articleUrl),\n                description = extractAttributeValue(tag, \"alt\"),\n                tagSnapshot = tag,\n                width = extractDimension(tag, \"width\"),\n                height = extractDimension(tag, \"height\"),\n            )?.let { media ->\n                if (!medias.containsKey(media.url)) {\n                    medias[media.url] = media\n                }\n            }\n        }\n        return medias.values.toList()\n    }\n\n    private fun findImageSource(tag: String): String? {\n        imageSourceAttributes.forEach { attribute ->\n            val value = extractAttributeValue(tag, attribute) ?: return@forEach\n            if (attribute.endsWith(\"srcset\", ignoreCase = true)) {\n                val srcSetValue = pickFromSrcSet(value)\n                if (!srcSetValue.isNullOrEmpty()) return srcSetValue\n            } else {\n                return value\n            }\n        }\n        return null\n    }\n\n    private fun createMedia(\n        itemId: String,\n        url: String?,\n        description: String?,\n        tagSnapshot: String,\n        width: Int?,\n        height: Int?,\n    ): BlogMedia? {\n        val normalizedUrl = url ?: return null\n        if (looksLikeIcon(normalizedUrl, description, tagSnapshot, width, height)) return null\n        return BlogMedia(\n            id = Md5.md5(\"$itemId#$normalizedUrl\"),\n            url = normalizedUrl,\n            type = BlogMediaType.IMAGE,\n            previewUrl = normalizedUrl,\n            remoteUrl = normalizedUrl,\n            description = description?.takeIf { it.isNotBlank() },\n            blurhash = null,\n            meta = null,\n        )\n    }\n\n    private fun extractAttributeValue(tag: String, attribute: String): String? {\n        val regex = Regex(\n            pattern = \"\"\"\\b$attribute\\s*=\\s*(?:\"([^\"]*)\"|'([^']*)'|([^\\s\"'=<>`]+))\"\"\",\n            options = setOf(RegexOption.IGNORE_CASE),\n        )\n        val match = regex.find(tag) ?: return null\n        val value = match.groups[1]?.value\n            ?: match.groups[2]?.value\n            ?: match.groups[3]?.value\n            ?: return null\n        return decodeHtml(value).trim().takeIf { it.isNotEmpty() }\n    }\n\n    private fun extractDimension(tag: String, attribute: String): Int? {\n        val directValue = extractAttributeValue(tag, attribute)?.toIntOrNull()\n        if (directValue != null) return directValue\n        val style = extractAttributeValue(tag, \"style\") ?: return null\n        val regex = Regex(\n            pattern = \"\"\"$attribute\\s*:\\s*(\\d+)px\"\"\",\n            options = setOf(RegexOption.IGNORE_CASE),\n        )\n        return regex.find(style)?.groupValues?.getOrNull(1)?.toIntOrNull()\n    }\n\n    private fun pickFromSrcSet(srcSet: String): String? {\n        return srcSet.split(\",\")\n            .asSequence()\n            .map { it.trim() }\n            .filter { it.isNotEmpty() }\n            .mapNotNull { candidate -> candidate.split(srcSetSplitRegex).firstOrNull() }\n            .lastOrNull()\n    }\n\n    private fun normalizeUrl(rawUrl: String?, articleUrl: String?): String? {\n        val url = decodeHtml(rawUrl.orEmpty()).trim()\n        if (url.isEmpty()) return null\n        if (url.startsWith(\"data:\", ignoreCase = true)) return null\n        if (url.startsWith(\"blob:\", ignoreCase = true)) return null\n        if (url.startsWith(\"http://\", ignoreCase = true) || url.startsWith(\n                \"https://\",\n                ignoreCase = true\n            )\n        ) {\n            return url\n        }\n        if (url.startsWith(\"//\")) {\n            val scheme = articleUrl\n                ?.substringBefore(\"://\", \"\")\n                ?.takeIf { it.isNotBlank() }\n                ?: \"https\"\n            return \"$scheme:$url\"\n        }\n        val baseUrl = articleUrl\n            ?.takeIf {\n                it.startsWith(\"http://\", ignoreCase = true) || it.startsWith(\n                    \"https://\",\n                    ignoreCase = true\n                )\n            }\n            ?: return null\n        val origin = baseUrl.origin() ?: return null\n        if (url.startsWith(\"/\")) {\n            return origin + normalizePath(url)\n        }\n        val sanitizedBase = baseUrl.substringBefore('#').substringBefore('?')\n        val directory = if (sanitizedBase.endsWith(\"/\")) {\n            sanitizedBase\n        } else {\n            sanitizedBase.substringBeforeLast(\"/\", \"$origin/\")\n        }\n        val relativePath = directory.substringAfter(origin, \"/\").trimEnd('/')\n        val normalizedPath = normalizePath(\"$relativePath/$url\")\n        return origin + normalizedPath\n    }\n\n    private fun String.origin(): String? {\n        val schemeIndex = indexOf(\"://\")\n        if (schemeIndex <= 0) return null\n        val hostStart = schemeIndex + 3\n        val hostEnd = indexOf('/', startIndex = hostStart).takeIf { it >= 0 } ?: length\n        return substring(0, hostEnd)\n    }\n\n    private fun normalizePath(pathWithQuery: String): String {\n        val fragment = pathWithQuery.substringAfter('#', \"\")\n        val pathWithoutFragment = pathWithQuery.substringBefore('#')\n        val query = pathWithoutFragment.substringAfter('?', \"\")\n        val rawPath = pathWithoutFragment.substringBefore('?')\n        val stack = mutableListOf<String>()\n        rawPath.split('/').forEach { segment ->\n            when (segment) {\n                \"\", \".\" -> Unit\n                \"..\" -> if (stack.isNotEmpty()) stack.removeAt(stack.lastIndex)\n                else -> stack += segment\n            }\n        }\n        val normalizedPath = \"/\" + stack.joinToString(\"/\")\n        return buildString {\n            append(normalizedPath)\n            if (query.isNotEmpty()) {\n                append('?')\n                append(query)\n            }\n            if (fragment.isNotEmpty()) {\n                append('#')\n                append(fragment)\n            }\n        }\n    }\n\n    private fun looksLikeIcon(\n        url: String,\n        description: String?,\n        tagSnapshot: String,\n        width: Int?,\n        height: Int?,\n    ): Boolean {\n        if (width != null && height != null && width <= 64 && height <= 64) {\n            return true\n        }\n        val signature = buildString {\n            append(url)\n            append(' ')\n            append(description.orEmpty())\n            append(' ')\n            append(tagSnapshot)\n        }\n        if (!iconKeywordRegex.containsMatchIn(signature)) return false\n        if (width == null && height == null) return true\n        return (width ?: Int.MAX_VALUE) <= 160 && (height ?: Int.MAX_VALUE) <= 160\n    }\n\n    private fun decodeHtml(value: String): String {\n        return value\n            .replace(\"&amp;\", \"&\")\n            .replace(\"&#38;\", \"&\")\n            .replace(\"&quot;\", \"\\\"\")\n            .replace(\"&#34;\", \"\\\"\")\n            .replace(\"&#39;\", \"'\")\n            .replace(\"&apos;\", \"'\")\n            .replace(\"&lt;\", \"<\")\n            .replace(\"&gt;\", \">\")\n            .replace(\"&nbsp;\", \" \")\n    }\n}\n"
  },
  {
    "path": "plugins/rss/src/commonMain/kotlin/com/zhangke/fread/rss/internal/adapter/RssStatusAdapter.kt",
    "content": "package com.zhangke.fread.rss.internal.adapter\n\nimport com.zhangke.framework.datetime.Instant\nimport com.zhangke.framework.ktx.ifNullOrEmpty\nimport com.zhangke.fread.common.utils.formatDefault\nimport com.zhangke.fread.rss.internal.model.RssChannelItem\nimport com.zhangke.fread.rss.internal.model.RssSource\nimport com.zhangke.fread.rss.internal.platform.RssPlatformTransformer\nimport com.zhangke.fread.rss.internal.uri.RssUriInsight\nimport com.zhangke.fread.status.blog.Blog\nimport com.zhangke.fread.status.model.StatusVisibility\nimport com.zhangke.fread.status.status.model.Status\n\nclass RssStatusAdapter(\n    private val blogAuthorAdapter: BlogAuthorAdapter,\n    private val rssPlatformTransformer: RssPlatformTransformer,\n) {\n\n    private val meaninglessTitleList = listOf(\n        \"unknown\",\n        \"null\",\n    )\n\n    suspend fun toStatus(\n        uriInsight: RssUriInsight,\n        source: RssSource,\n        rssItem: RssChannelItem,\n    ): Status {\n        return Status.NewBlog(\n            blog = rssItem.toBlog(uriInsight, source),\n        )\n    }\n\n    private suspend fun RssChannelItem.toBlog(uriInsight: RssUriInsight, source: RssSource): Blog {\n        val createAt = Instant(this.pubDate)\n        val blogLink = this.link.ifNullOrEmpty { uriInsight.url }\n        return Blog(\n            id = this.id,\n            author = blogAuthorAdapter.createAuthor(uriInsight, source),\n            title = removeMeaninglessTitle(this.title),\n            url = blogLink,\n            link = blogLink,\n            content = this.content.ifNullOrEmpty { this.description.ifNullOrEmpty { this.link.orEmpty() } },\n            description = this.description,\n            createAt = createAt,\n            formattedCreateAt = createAt.formatDefault(),\n            like = Blog.Like(false),\n            forward = Blog.Forward(false),\n            bookmark = Blog.Bookmark(false),\n            reply = Blog.Reply(false),\n            quote = Blog.Quote(support = false, enabled = false),\n            sensitive = false,\n            supportEdit = false,\n            spoilerText = \"\",\n            platform = rssPlatformTransformer.create(uriInsight, source),\n            mediaList = RssBlogMediaExtractor.extract(\n                itemId = this.id,\n                articleUrl = blogLink,\n                contentHtml = this.content,\n                descriptionHtml = this.description,\n                fallbackImageUrl = this.image,\n            ),\n            poll = null,\n            emojis = emptyList(),\n            mentions = emptyList(),\n            tags = emptyList(),\n            facets = emptyList(),\n            pinned = false,\n            visibility = StatusVisibility.PUBLIC,\n            embeds = emptyList(),\n            language = null,\n        )\n    }\n\n    private fun removeMeaninglessTitle(title: String): String? {\n        return if (meaninglessTitleList.contains(title.lowercase())) {\n            null\n        } else {\n            title\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/rss/src/commonMain/kotlin/com/zhangke/fread/rss/internal/db/RssChannelTable.kt",
    "content": "package com.zhangke.fread.rss.internal.db\n\nimport androidx.room.Dao\nimport androidx.room.Delete\nimport androidx.room.Entity\nimport androidx.room.Insert\nimport androidx.room.OnConflictStrategy\nimport androidx.room.PrimaryKey\nimport androidx.room.Query\nimport com.zhangke.framework.datetime.Instant\n\nprivate const val TABLE_NAME = \"channels\"\n\n@Entity(tableName = TABLE_NAME)\ndata class RssChannelEntity(\n    @PrimaryKey val url: String,\n    val homePage: String?,\n    val title: String,\n    val description: String?,\n    val displayName: String,\n    val addDate: Instant,\n    val lastUpdateDate: Instant,\n    val lastBuildDate: Instant?,\n    val updatePeriod: String?,\n    val thumbnail: String?,\n)\n\n@Dao\ninterface RssChannelDao {\n\n    @Query(\"SELECT * FROM $TABLE_NAME\")\n    suspend fun queryAll(): List<RssChannelEntity>\n\n    @Query(\"SELECT * FROM $TABLE_NAME WHERE url=:url\")\n    suspend fun queryByUrl(url: String): RssChannelEntity?\n\n    @Insert(onConflict = OnConflictStrategy.REPLACE)\n    suspend fun insert(entity: RssChannelEntity)\n\n    @Delete\n    suspend fun delete(entity: RssChannelEntity)\n\n    @Query(\"DELETE FROM $TABLE_NAME WHERE url=:url\")\n    suspend fun deleteByUri(url: String)\n}\n"
  },
  {
    "path": "plugins/rss/src/commonMain/kotlin/com/zhangke/fread/rss/internal/db/RssDatabases.kt",
    "content": "package com.zhangke.fread.rss.internal.db\n\nimport androidx.room.ConstructedBy\nimport androidx.room.Database\nimport androidx.room.RoomDatabase\nimport androidx.room.RoomDatabaseConstructor\nimport androidx.room.TypeConverters\nimport com.zhangke.fread.rss.internal.db.converter.FormalUriConverter\nimport com.zhangke.fread.rss.internal.db.converter.InstantConverter\n\n\nprivate const val DB_VERSION = 1\n\n@TypeConverters(\n    FormalUriConverter::class,\n    InstantConverter::class,\n)\n@Database(\n    entities = [RssChannelEntity::class],\n    version = DB_VERSION,\n    exportSchema = false,\n)\n@ConstructedBy(RssDatabasesConstructor::class)\nabstract class RssDatabases : RoomDatabase() {\n\n    abstract fun getRssChannelDao(): RssChannelDao\n\n    companion object {\n        const val DB_NAME = \"rss.db\"\n    }\n}\n\n// The Room compiler generates the `actual` implementations.\n@Suppress(\"NO_ACTUAL_FOR_EXPECT\")\nexpect object RssDatabasesConstructor : RoomDatabaseConstructor<RssDatabases> {\n    override fun initialize(): RssDatabases\n}\n"
  },
  {
    "path": "plugins/rss/src/commonMain/kotlin/com/zhangke/fread/rss/internal/db/converter/FormalUriConverter.kt",
    "content": "package com.zhangke.fread.rss.internal.db.converter\n\nimport androidx.room.TypeConverter\nimport com.zhangke.fread.status.uri.FormalUri\n\nclass FormalUriConverter {\n\n    @TypeConverter\n    fun convertToString(uri: FormalUri): String {\n        return uri.toString()\n    }\n\n    @TypeConverter\n    fun convertToUri(uri: String): FormalUri {\n        return FormalUri.from(uri)!!\n    }\n}\n"
  },
  {
    "path": "plugins/rss/src/commonMain/kotlin/com/zhangke/fread/rss/internal/db/converter/InstantConverter.kt",
    "content": "@file:OptIn(ExperimentalTime::class)\n\npackage com.zhangke.fread.rss.internal.db.converter\n\nimport androidx.room.TypeConverter\nimport com.zhangke.framework.datetime.Instant\nimport kotlin.time.ExperimentalTime\n\nclass InstantConverter {\n\n    @TypeConverter\n    fun convertToLong(instant: Instant): Long {\n        return instant.instant.toEpochMilliseconds()\n    }\n\n    @TypeConverter\n    fun convertToInstant(value: Long): Instant {\n        return Instant(kotlinx.datetime.Instant.fromEpochMilliseconds(value))\n    }\n}\n"
  },
  {
    "path": "plugins/rss/src/commonMain/kotlin/com/zhangke/fread/rss/internal/model/RssChannelItem.kt",
    "content": "@file:OptIn(ExperimentalTime::class)\n\npackage com.zhangke.fread.rss.internal.model\n\nimport kotlinx.datetime.Instant\nimport kotlin.time.ExperimentalTime\n\ndata class RssChannelItem(\n    val id: String,\n    val title: String,\n    val author: String?,\n    val link: String?,\n    val pubDate: Instant,\n    val description: String?,\n    val content: String?,\n    val image: String?,\n    val audio: String?,\n    val video: String?,\n    val sourceName: String?,\n    val sourceUrl: String?,\n    val categories: List<String>,\n    val commentsUrl: String?,\n)\n"
  },
  {
    "path": "plugins/rss/src/commonMain/kotlin/com/zhangke/fread/rss/internal/model/RssSource.kt",
    "content": "@file:OptIn(ExperimentalTime::class)\n\npackage com.zhangke.fread.rss.internal.model\n\nimport kotlinx.datetime.Instant\nimport kotlin.time.ExperimentalTime\n\ndata class RssSource(\n    val url: String,\n    val homePage: String?,\n    val title: String,\n    val displayName: String,\n    val addDate: Instant,\n    val lastUpdateDate: Instant,\n    val updatePeriod: String?,\n    val description: String?,\n    val thumbnail: String?,\n)\n"
  },
  {
    "path": "plugins/rss/src/commonMain/kotlin/com/zhangke/fread/rss/internal/platform/RssPlatformTransformer.kt",
    "content": "package com.zhangke.fread.rss.internal.platform\n\nimport com.zhangke.framework.network.FormalBaseUrl\nimport com.zhangke.fread.status.model.createRssProtocol\nimport com.zhangke.fread.rss.internal.model.RssSource\nimport com.zhangke.fread.rss.internal.uri.RssUriInsight\nimport com.zhangke.fread.status.platform.BlogPlatform\n\nclass RssPlatformTransformer() {\n\n    suspend fun create(\n        uriInsight: RssUriInsight,\n        source: RssSource,\n    ): BlogPlatform {\n        return BlogPlatform(\n            uri = uriInsight.rawUri.toString(),\n            name = source.title,\n            description = source.description.orEmpty(),\n            baseUrl = FormalBaseUrl.parse(uriInsight.url)!!,\n            protocol = createRssProtocol(),\n            thumbnail = source.thumbnail,\n            supportsQuotePost = false,\n        )\n    }\n}\n"
  },
  {
    "path": "plugins/rss/src/commonMain/kotlin/com/zhangke/fread/rss/internal/repo/RssRepo.kt",
    "content": "package com.zhangke.fread.rss.internal.repo\n\nimport com.zhangke.framework.datetime.Instant\nimport com.zhangke.fread.rss.internal.adapter.BlogAuthorAdapter\nimport com.zhangke.fread.rss.internal.db.RssChannelEntity\nimport com.zhangke.fread.rss.internal.db.RssDatabases\nimport com.zhangke.fread.rss.internal.model.RssChannelItem\nimport com.zhangke.fread.rss.internal.model.RssSource\nimport com.zhangke.fread.rss.internal.rss.RssFetcher\nimport com.zhangke.fread.rss.internal.uri.RssUriInsight\nimport com.zhangke.fread.rss.internal.uri.RssUriTransformer\nimport com.zhangke.fread.rss.internal.utils.AvatarUtils\nimport com.zhangke.fread.status.author.BlogAuthor\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.asSharedFlow\nimport okio.FileSystem\nimport okio.Path.Companion.toPath\nimport okio.SYSTEM\n\nclass RssRepo(\n    rssDatabases: RssDatabases,\n    private val blogAuthorAdapter: BlogAuthorAdapter,\n    private val rssUriTransformer: RssUriTransformer,\n    private val rssFetcher: RssFetcher,\n) {\n\n    private val channelDao = rssDatabases.getRssChannelDao()\n\n    private val _sourceChangedFlow = MutableSharedFlow<BlogAuthor>()\n    val sourceChangedFlow = _sourceChangedFlow.asSharedFlow()\n\n    suspend fun getRssSource(\n        url: String,\n        forceRemote: Boolean = false,\n    ): Result<RssSource?> {\n        if (!forceRemote) {\n            channelDao.queryByUrl(url)?.toRssSource()?.let { return Result.success(it) }\n        }\n        return fetchRssChannelByUrl(url)\n    }\n\n    suspend fun updateSourceName(url: String, name: String) {\n        val source = channelDao.queryByUrl(url)\n        if (source != null) {\n            val newSource = source.copy(displayName = name)\n            channelDao.insert(newSource)\n            updateAuthorFlow(url, newSource)\n        }\n    }\n\n    private suspend fun updateAuthorFlow(url: String, source: RssChannelEntity) {\n        val uri = rssUriTransformer.build(url)\n        val uriInsight = RssUriInsight(uri, url)\n        val author = blogAuthorAdapter.createAuthor(uriInsight, source.toRssSource())\n        _sourceChangedFlow.emit(author)\n    }\n\n    suspend fun getRssItems(\n        url: String,\n    ): Result<Pair<RssSource, List<RssChannelItem>>> {\n        return fetchRssItemsByUrl(url)\n    }\n\n    private suspend fun fetchRssChannelByUrl(url: String): Result<RssSource> {\n        return rssFetcher.fetchRss(url)\n            .map { mapRemoteSource(url, it.first) to it.second }\n            .onSuccess { insertSource(it.first) }\n            .map { it.first }\n    }\n\n    private suspend fun fetchRssItemsByUrl(\n        url: String\n    ): Result<Pair<RssSource, List<RssChannelItem>>> {\n        return rssFetcher.fetchRss(url)\n            .map { mapRemoteSource(url, it.first) to it.second }\n            .onSuccess { insertSource(it.first) }\n    }\n\n    private suspend fun mapRemoteSource(url: String, source: RssSource): RssSource {\n        val localSource =\n            channelDao.queryByUrl(url) ?: return source.copy(thumbnail = processThumbnail(source))\n        return source.copy(\n            displayName = localSource.displayName,\n            addDate = localSource.addDate.instant,\n            thumbnail = processThumbnail(localSource.toRssSource()),\n        )\n    }\n\n    private fun processThumbnail(source: RssSource): String? {\n        val thumbnail = source.thumbnail\n        if (AvatarUtils.isRemoteAvatar(thumbnail)) return thumbnail\n        if (!thumbnail.isNullOrEmpty() && FileSystem.SYSTEM.exists(thumbnail.toPath())) return thumbnail\n        return AvatarUtils.makeSourceAvatar(source)\n    }\n\n    private suspend fun insertSource(source: RssSource) {\n        channelDao.insert(source.toEntity())\n    }\n\n    private fun RssSource.toEntity(): RssChannelEntity {\n        return RssChannelEntity(\n            url = this.url,\n            homePage = this.homePage,\n            title = this.title,\n            description = this.description,\n            lastBuildDate = Instant(this.lastUpdateDate),\n            updatePeriod = this.updatePeriod,\n            thumbnail = this.thumbnail,\n            addDate = Instant(this.addDate),\n            lastUpdateDate = Instant(this.lastUpdateDate),\n            displayName = this.displayName,\n        )\n    }\n\n    private fun RssChannelEntity.toRssSource(): RssSource {\n        return RssSource(\n            url = this.url,\n            homePage = this.homePage,\n            title = this.title,\n            displayName = this.displayName,\n            addDate = this.addDate.instant,\n            lastUpdateDate = this.lastUpdateDate.instant,\n            description = this.description,\n            thumbnail = this.thumbnail,\n            updatePeriod = this.updatePeriod,\n        )\n    }\n}\n"
  },
  {
    "path": "plugins/rss/src/commonMain/kotlin/com/zhangke/fread/rss/internal/repo/RssStatusRepo.kt",
    "content": "package com.zhangke.fread.rss.internal.repo\n\nimport com.zhangke.fread.rss.internal.adapter.RssStatusAdapter\nimport com.zhangke.fread.rss.internal.uri.RssUriInsight\nimport com.zhangke.fread.status.status.model.Status\n\nclass RssStatusRepo(\n    private val rssStatusAdapter: RssStatusAdapter,\n    private val rssRepo: RssRepo,\n) {\n\n    suspend fun getStatus(\n        uriInsight: RssUriInsight,\n    ): Result<List<Status>> {\n        return rssRepo.getRssItems(uriInsight.url)\n            .map { (source, items) ->\n                items.map { rssStatusAdapter.toStatus(uriInsight, source, it) }\n            }\n    }\n}\n"
  },
  {
    "path": "plugins/rss/src/commonMain/kotlin/com/zhangke/fread/rss/internal/rss/RssChannel.kt",
    "content": "@file:OptIn(ExperimentalTime::class)\n\npackage com.zhangke.fread.rss.internal.rss\n\nimport kotlinx.datetime.Instant\nimport kotlin.time.ExperimentalTime\n\ndata class RssChannel(\n    val title: String,\n    val link: String?,\n    val description: String?,\n    val image: RssImage?,\n    val lastBuildDate: Instant?,\n    val updatePeriod: String?,\n    val items: List<RssItem>,\n)\n"
  },
  {
    "path": "plugins/rss/src/commonMain/kotlin/com/zhangke/fread/rss/internal/rss/RssFetcher.kt",
    "content": "package com.zhangke.fread.rss.internal.rss\n\nimport com.zhangke.fread.rss.internal.model.RssChannelItem\nimport com.zhangke.fread.rss.internal.model.RssSource\nimport kotlinx.datetime.Clock\nimport kotlin.time.ExperimentalTime\n\nclass RssFetcher(\n    private val rssParserWrapper: RssParserWrapper,\n) {\n\n    suspend fun fetchRss(url: String): Result<Pair<RssSource, List<RssChannelItem>>> {\n        val channel = try {\n            rssParserWrapper.getRssChannel(url)\n        } catch (e: Throwable) {\n            return Result.failure(e)\n        }\n        val rssSource = channel.convert(url)\n        val rssChannelItem = channel.items.map { it.convert() }\n        return Result.success(Pair(rssSource, rssChannelItem))\n    }\n\n    @OptIn(ExperimentalTime::class)\n    private fun RssChannel.convert(url: String): RssSource {\n        return RssSource(\n            url = url,\n            title = this.title,\n            homePage = this.link,\n            displayName = this.title,\n            addDate = this.lastBuildDate ?: Clock.System.now(),\n            lastUpdateDate = this.lastBuildDate ?: Clock.System.now(),\n            description = this.description,\n            updatePeriod = this.updatePeriod,\n            thumbnail = this.image?.url,\n        )\n    }\n\n    private fun RssItem.convert(): RssChannelItem {\n        return RssChannelItem(\n            id = this.id,\n            title = this.title,\n            author = this.author,\n            link = this.link,\n            pubDate = this.pubDate,\n            description = this.description,\n            content = this.content,\n            image = this.image,\n            audio = this.audio,\n            video = this.video,\n            sourceName = this.sourceName,\n            sourceUrl = this.sourceUrl,\n            categories = this.categories,\n            commentsUrl = this.commentsUrl,\n        )\n    }\n}\n"
  },
  {
    "path": "plugins/rss/src/commonMain/kotlin/com/zhangke/fread/rss/internal/rss/RssImage.kt",
    "content": "package com.zhangke.fread.rss.internal.rss\n\ndata class RssImage(\n    val title: String?,\n    val url: String,\n    val description: String?\n)\n"
  },
  {
    "path": "plugins/rss/src/commonMain/kotlin/com/zhangke/fread/rss/internal/rss/RssItem.kt",
    "content": "@file:OptIn(ExperimentalTime::class)\n\npackage com.zhangke.fread.rss.internal.rss\n\nimport kotlinx.datetime.Instant\nimport kotlin.time.ExperimentalTime\n\ndata class RssItem(\n    val id: String,\n    val title: String,\n    val author: String?,\n    val link: String?,\n    val pubDate: Instant,\n    val description: String?,\n    val content: String?,\n    val image: String?,\n    val audio: String?,\n    val video: String?,\n    val sourceName: String?,\n    val sourceUrl: String?,\n    val categories: List<String>,\n    val commentsUrl: String?,\n)\n"
  },
  {
    "path": "plugins/rss/src/commonMain/kotlin/com/zhangke/fread/rss/internal/rss/RssParserWrapper.kt",
    "content": "package com.zhangke.fread.rss.internal.rss\n\nimport com.prof18.rssparser.RssParser\nimport com.zhangke.fread.rss.internal.rss.adapter.convert\n\nclass RssParserWrapper(\n    private val rssParser: RssParser,\n) {\n    suspend fun getRssChannel(url: String): RssChannel {\n        return rssParser.getRssChannel(url).convert()\n    }\n\n    suspend fun parse(rssText: String): RssChannel {\n        return rssParser.parse(rssText).convert()\n    }\n}\n"
  },
  {
    "path": "plugins/rss/src/commonMain/kotlin/com/zhangke/fread/rss/internal/rss/adapter/RssChannelAdapter.kt",
    "content": "@file:OptIn(ExperimentalTime::class)\n\npackage com.zhangke.fread.rss.internal.rss.adapter\n\nimport com.prof18.rssparser.model.RssChannel\nimport com.zhangke.framework.ktx.ifNullOrEmpty\nimport kotlin.time.ExperimentalTime\n\nfun RssChannel.convert(): com.zhangke.fread.rss.internal.rss.RssChannel {\n    return com.zhangke.fread.rss.internal.rss.RssChannel(\n        title = this.title.ifNullOrEmpty { \"Unknown\" },\n        link = this.link,\n        description = this.description,\n        lastBuildDate = this.lastBuildDate?.let(::formatRssDate),\n        updatePeriod = this.updatePeriod,\n        image = this.image?.convert(),\n        items = this.items.map { it.convert() }.sortedByDescending { it.pubDate.toEpochMilliseconds() },\n    )\n}\n"
  },
  {
    "path": "plugins/rss/src/commonMain/kotlin/com/zhangke/fread/rss/internal/rss/adapter/RssImageAdapter.kt",
    "content": "package com.zhangke.fread.rss.internal.rss.adapter\n\nimport com.prof18.rssparser.model.RssImage\n\nfun RssImage.convert(): com.zhangke.fread.rss.internal.rss.RssImage? {\n    if (!this.isNotEmptyOrBlank()) return null\n    return com.zhangke.fread.rss.internal.rss.RssImage(\n        url = this.url!!,\n        title = this.title,\n        description = this.description,\n    )\n}\n\nprivate fun RssImage.isNotEmptyOrBlank(): Boolean {\n    return !url.isNullOrBlank() || !link.isNullOrBlank()\n}\n"
  },
  {
    "path": "plugins/rss/src/commonMain/kotlin/com/zhangke/fread/rss/internal/rss/adapter/RssItemAdapter.kt",
    "content": "@file:OptIn(ExperimentalTime::class)\n\npackage com.zhangke.fread.rss.internal.rss.adapter\n\nimport com.prof18.rssparser.model.RssItem\nimport com.zhangke.framework.date.DateParser\nimport com.zhangke.framework.ktx.ifNullOrEmpty\nimport com.zhangke.fread.common.utils.getCurrentTimeMillis\nimport kotlinx.datetime.Instant\nimport kotlinx.datetime.Clock\nimport kotlin.time.ExperimentalTime\n\nfun RssItem.convert(): com.zhangke.fread.rss.internal.rss.RssItem {\n    return com.zhangke.fread.rss.internal.rss.RssItem(\n        id = getRssItemId(this),\n        title = this.title.ifNullOrEmpty { \"Unknown\" },\n        author = this.author,\n        link = this.link,\n        pubDate = formatRssDate(this.pubDate),\n        description = this.description,\n        content = this.content,\n        image = this.image,\n        audio = this.audio,\n        video = this.video,\n        sourceName = this.sourceName,\n        sourceUrl = this.sourceUrl,\n        categories = this.categories,\n        commentsUrl = this.commentsUrl,\n    )\n}\n\nprivate fun getRssItemId(rssItem: RssItem): String {\n    if (rssItem.guid.isNullOrEmpty().not()) {\n        return rssItem.guid!!\n    }\n    return rssItem.link.ifNullOrEmpty {\n        rssItem.title.ifNullOrEmpty { getCurrentTimeMillis().toString() }\n    }\n}\n\ninternal fun formatRssDate(datetime: String?): Instant {\n    if (datetime.isNullOrEmpty()) return Clock.System.now()\n    return DateParser.parseAll(datetime) ?: Clock.System.now()\n}\n"
  },
  {
    "path": "plugins/rss/src/commonMain/kotlin/com/zhangke/fread/rss/internal/screen/RssRoutes.kt",
    "content": "package com.zhangke.fread.rss.internal.screen\n\nimport com.zhangke.framework.network.GlobalRoutes\n\nobject RssRoutes {\n\n    const val ROOT = \"${GlobalRoutes.ROOT_PREFIX}rss\"\n}\n"
  },
  {
    "path": "plugins/rss/src/commonMain/kotlin/com/zhangke/fread/rss/internal/screen/source/RssSourceScreen.kt",
    "content": "package com.zhangke.fread.rss.internal.screen.source\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Edit\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.OutlinedTextField\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.SnackbarHost\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport androidx.navigation3.runtime.NavKey\nimport com.zhangke.framework.composable.ConsumeSnackbarFlow\nimport com.zhangke.framework.composable.FreadDialog\nimport com.zhangke.framework.composable.SimpleIconButton\nimport com.zhangke.framework.composable.TextString\nimport com.zhangke.framework.composable.Toolbar\nimport com.zhangke.framework.composable.currentOrThrow\nimport com.zhangke.framework.composable.freadPlaceholder\nimport com.zhangke.framework.composable.rememberSnackbarHostState\nimport com.zhangke.framework.nav.LocalNavBackStack\nimport com.zhangke.fread.common.browser.LocalActivityBrowserLauncher\nimport com.zhangke.fread.common.browser.launchWebTabInApp\nimport com.zhangke.fread.localization.LocalizedString\nimport com.zhangke.fread.status.ui.BlogAuthorAvatar\nimport com.zhangke.fread.status.ui.richtext.FreadRichText\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.serialization.Serializable\nimport org.jetbrains.compose.resources.stringResource\n\n@Serializable\ndata class RssSourceScreenNavKey(val url: String) : NavKey\n\n@Composable\nfun RssSourceScreen(viewModel: RssSourceViewModel) {\n    val backStack = LocalNavBackStack.currentOrThrow\n    val uiState by viewModel.uiState.collectAsState()\n    RssSourceContent(\n        uiState = uiState,\n        snackBarMessageFlow = viewModel.snackBarMessageFlow,\n        onBackClick = { backStack.removeLastOrNull() },\n        onDisplayNameChanged = viewModel::onDisplayNameChanged,\n    )\n}\n\n@Composable\nprivate fun RssSourceContent(\n    uiState: RssSourceUiState,\n    snackBarMessageFlow: Flow<TextString>,\n    onBackClick: () -> Unit,\n    onDisplayNameChanged: (String) -> Unit,\n) {\n    val browserLauncher = LocalActivityBrowserLauncher.current\n    val snackBarState = rememberSnackbarHostState()\n    val coroutineScope = rememberCoroutineScope()\n    ConsumeSnackbarFlow(snackBarState, snackBarMessageFlow)\n    Scaffold(\n        topBar = {\n            Toolbar(\n                onBackClick = onBackClick,\n                title = stringResource(LocalizedString.rss_source_detail_screen_title),\n            )\n        },\n        snackbarHost = {\n            SnackbarHost(snackBarState)\n        }\n    ) { paddingValues ->\n        Column(\n            modifier = Modifier\n                .padding(paddingValues)\n                .verticalScroll(rememberScrollState())\n        ) {\n            BlogAuthorAvatar(\n                modifier = Modifier\n                    .padding(start = 16.dp, top = 20.dp)\n                    .size(80.dp),\n                imageUrl = uiState.source?.thumbnail,\n            )\n\n            Text(\n                modifier = Modifier\n                    .padding(start = 16.dp, top = 16.dp, end = 16.dp)\n                    .freadPlaceholder(uiState.source?.title.isNullOrEmpty()),\n                style = MaterialTheme.typography.titleMedium,\n                text = uiState.source?.title.orEmpty(),\n            )\n\n            FreadRichText(\n                modifier = Modifier\n                    .padding(start = 16.dp, top = 16.dp, end = 16.dp)\n                    .freadPlaceholder(uiState.source?.description.isNullOrEmpty()),\n                content = uiState.source?.description.orEmpty(),\n                mentions = emptyList(),\n                emojis = emptyList(),\n                tags = emptyList(),\n                onHashtagClick = {},\n                onMentionClick = {},\n                onUrlClick = {\n                    browserLauncher.launchWebTabInApp(coroutineScope, it)\n                },\n            )\n\n            Spacer(modifier = Modifier.height(22.dp))\n\n            val rssSource = uiState.source\n            if (rssSource != null) {\n                CustomTitleItem(\n                    displayName = rssSource.displayName,\n                    onDisplayNameChanged = onDisplayNameChanged,\n                )\n                RssInfoItem(\n                    modifier = Modifier.clickable {\n                        browserLauncher.launchWebTabInApp(coroutineScope, rssSource.url)\n                    },\n                    title = stringResource(LocalizedString.rss_source_detail_screen_url),\n                    content = rssSource.url,\n                )\n                if (rssSource.homePage.isNullOrEmpty().not()) {\n                    RssInfoItem(\n                        modifier = Modifier.clickable {\n                            browserLauncher.launchWebTabInApp(coroutineScope, rssSource.homePage)\n                        },\n                        title = stringResource(LocalizedString.rss_source_detail_screen_home_url),\n                        content = rssSource.homePage,\n                    )\n                }\n                RssInfoItem(\n                    title = stringResource(LocalizedString.rss_source_detail_screen_add_date),\n                    content = uiState.formattedAddDate.orEmpty(),\n                )\n                RssInfoItem(\n                    title = stringResource(LocalizedString.rss_source_detail_screen_last_update_date),\n                    content = uiState.formattedLastUpdateDate.orEmpty(),\n                )\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun CustomTitleItem(\n    displayName: String,\n    onDisplayNameChanged: (String) -> Unit,\n) {\n    var showEditDisplayNameDialog by remember {\n        mutableStateOf(false)\n    }\n    RssInfoItem(\n        title = stringResource(LocalizedString.rss_source_detail_screen_custom_title),\n        content = displayName,\n        option = {\n            SimpleIconButton(\n                onClick = { showEditDisplayNameDialog = true },\n                imageVector = Icons.Default.Edit,\n                contentDescription = \"Edit\",\n            )\n        },\n    )\n\n    if (showEditDisplayNameDialog) {\n        var newDisplayName by remember {\n            mutableStateOf(displayName)\n        }\n        FreadDialog(\n            title = stringResource(LocalizedString.alert),\n            onDismissRequest = {\n                showEditDisplayNameDialog = false\n            },\n            positiveButtonText = stringResource(LocalizedString.ok),\n            onPositiveClick = {\n                showEditDisplayNameDialog = false\n                if (newDisplayName != displayName) {\n                    onDisplayNameChanged(newDisplayName)\n                }\n            },\n            negativeButtonText = stringResource(LocalizedString.cancel),\n            onNegativeClick = {\n                showEditDisplayNameDialog = false\n            },\n            content = {\n                OutlinedTextField(\n                    modifier = Modifier\n                        .padding(16.dp)\n                        .fillMaxWidth(),\n                    value = newDisplayName,\n                    onValueChange = {\n                        newDisplayName = it\n                    },\n                    label = {\n                        Text(text = stringResource(LocalizedString.rss_source_detail_screen_custom_title))\n                    }\n                )\n            }\n        )\n    }\n}\n\n@Composable\nprivate fun RssInfoItem(\n    title: String,\n    content: String,\n    modifier: Modifier = Modifier,\n    option: (@Composable () -> Unit)? = null,\n) {\n    Row(\n        modifier = modifier\n            .fillMaxWidth()\n            .padding(top = 8.dp, start = 16.dp, end = 16.dp, bottom = 8.dp),\n        verticalAlignment = Alignment.CenterVertically,\n    ) {\n        Column(\n            modifier = Modifier\n                .weight(1F)\n                .padding(end = 16.dp)\n        ) {\n            Text(\n                text = title,\n                style = MaterialTheme.typography.bodyLarge,\n            )\n            Text(\n                text = content,\n                style = MaterialTheme.typography.bodyMedium,\n            )\n        }\n        if (option != null) {\n            Box(modifier = Modifier.padding(horizontal = 16.dp)) {\n                option()\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "plugins/rss/src/commonMain/kotlin/com/zhangke/fread/rss/internal/screen/source/RssSourceUiState.kt",
    "content": "package com.zhangke.fread.rss.internal.screen.source\n\nimport com.zhangke.fread.rss.internal.model.RssSource\n\ndata class RssSourceUiState(\n    val source: RssSource? = null,\n    val formattedAddDate: String?,\n    val formattedLastUpdateDate: String?,\n)\n"
  },
  {
    "path": "plugins/rss/src/commonMain/kotlin/com/zhangke/fread/rss/internal/screen/source/RssSourceViewModel.kt",
    "content": "package com.zhangke.fread.rss.internal.screen.source\n\nimport androidx.lifecycle.ViewModel\nimport com.zhangke.framework.composable.TextString\nimport com.zhangke.framework.composable.textOf\nimport com.zhangke.framework.ktx.launchInViewModel\nimport com.zhangke.fread.common.utils.formatDefault\nimport com.zhangke.fread.rss.internal.repo.RssRepo\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asSharedFlow\nimport kotlinx.coroutines.flow.asStateFlow\nclass RssSourceViewModel(\n    private val rssRepo: RssRepo,\n    private val url: String,\n) : ViewModel() {\n\n    private val _uiState = MutableStateFlow(\n        RssSourceUiState(\n            source = null,\n            formattedAddDate = null,\n            formattedLastUpdateDate = null,\n        )\n    )\n    val uiState = _uiState.asStateFlow()\n\n    private val _snackBarMessageFlow = MutableSharedFlow<TextString>()\n    val snackBarMessageFlow = _snackBarMessageFlow.asSharedFlow()\n\n    init {\n        launchInViewModel {\n            rssRepo.getRssSource(url)\n                .onFailure {\n                    it.message\n                        ?.let { textOf(it) }\n                        ?.let { _snackBarMessageFlow.emit(it) }\n                }.onSuccess {\n                    if (it == null) {\n                        _snackBarMessageFlow.emit(textOf(\"Unknown $url\"))\n                    } else {\n                        _uiState.value = _uiState.value.copy(\n                            source = it,\n                            formattedAddDate = it.addDate.formatDefault(),\n                            formattedLastUpdateDate = it.lastUpdateDate.formatDefault(),\n                        )\n                    }\n                }\n        }\n    }\n\n    fun onDisplayNameChanged(displayName: String) {\n        _uiState.value = _uiState.value.copy(\n            source = _uiState.value.source?.copy(\n                displayName = displayName\n            )\n        )\n        launchInViewModel {\n            rssRepo.updateSourceName(url, displayName)\n        }\n    }\n\n}\n"
  },
  {
    "path": "plugins/rss/src/commonMain/kotlin/com/zhangke/fread/rss/internal/source/RssSourceTransformer.kt",
    "content": "package com.zhangke.fread.rss.internal.source\n\nimport com.zhangke.fread.status.model.createRssProtocol\nimport com.zhangke.fread.rss.internal.model.RssSource\nimport com.zhangke.fread.rss.internal.uri.RssUriInsight\nimport com.zhangke.fread.status.source.StatusSource\n\nclass RssSourceTransformer() {\n\n    suspend fun createSource(\n        uriInsight: RssUriInsight,\n        source: RssSource,\n    ): StatusSource {\n        return StatusSource(\n            uri = uriInsight.rawUri,\n            name = source.title,\n            handle = source.url,\n            description = source.description.orEmpty(),\n            thumbnail = source.thumbnail,\n            protocol = createRssProtocol(),\n        )\n    }\n}\n"
  },
  {
    "path": "plugins/rss/src/commonMain/kotlin/com/zhangke/fread/rss/internal/uri/RssUri.kt",
    "content": "package com.zhangke.fread.rss.internal.uri\n"
  },
  {
    "path": "plugins/rss/src/commonMain/kotlin/com/zhangke/fread/rss/internal/uri/RssUriHost.kt",
    "content": "package com.zhangke.fread.rss.internal.uri\n\nimport com.zhangke.fread.status.uri.FormalUri\n\nconst val RSS_HOST = \"rss.com\"\n\nfun createRssUri(path: String, queries: Map<String, String>): FormalUri {\n    return FormalUri.create(\n        host = RSS_HOST,\n        path = path,\n        queries = queries,\n    )\n}\n\nval FormalUri.isRssUri: Boolean get() = host == RSS_HOST\n"
  },
  {
    "path": "plugins/rss/src/commonMain/kotlin/com/zhangke/fread/rss/internal/uri/RssUriInsight.kt",
    "content": "package com.zhangke.fread.rss.internal.uri\n\nimport com.zhangke.fread.status.uri.FormalUri\n\ndata class RssUriInsight(\n    val rawUri: FormalUri,\n    val url: String,\n)\n"
  },
  {
    "path": "plugins/rss/src/commonMain/kotlin/com/zhangke/fread/rss/internal/uri/RssUriPath.kt",
    "content": "package com.zhangke.fread.rss.internal.uri\n\nobject RssUriPath {\n\n    const val SOURCE = \"/source\"\n\n}\n"
  },
  {
    "path": "plugins/rss/src/commonMain/kotlin/com/zhangke/fread/rss/internal/uri/RssUriTransformer.kt",
    "content": "package com.zhangke.fread.rss.internal.uri\n\nimport com.zhangke.fread.status.uri.FormalUri\n\nclass RssUriTransformer() {\n\n    companion object {\n        private const val PARAM_URL = \"url\"\n    }\n\n    fun build(url: String): FormalUri {\n        return createRssUri(\n            path = RssUriPath.SOURCE,\n            queries = mapOf(PARAM_URL to url),\n        )\n    }\n\n    fun parse(uri: FormalUri): RssUriInsight? {\n        if (!uri.isRssUri) return null\n        if (uri.path != RssUriPath.SOURCE) return null\n        val sourceUrl = uri.queries[PARAM_URL]\n        if (sourceUrl.isNullOrEmpty()) return null\n        return RssUriInsight(uri, sourceUrl)\n    }\n}\n"
  },
  {
    "path": "plugins/rss/src/commonMain/kotlin/com/zhangke/fread/rss/internal/utils/AvatarUtils.kt",
    "content": "package com.zhangke.fread.rss.internal.utils\n\nimport com.zhangke.fread.rss.internal.model.RssSource\n\nexpect object AvatarUtils {\n    fun isRemoteAvatar(avatar: String?): Boolean\n\n    fun makeSourceAvatar(source: RssSource): String?\n}\n"
  },
  {
    "path": "plugins/rss/src/commonMain/kotlin/com/zhangke/fread/rss/internal/webfinger/RssSourceWebFingerTransformer.kt",
    "content": "package com.zhangke.fread.rss.internal.webfinger\n\nimport com.zhangke.framework.utils.WebFinger\nimport com.zhangke.framework.utils.toPlatformUri\nimport com.zhangke.fread.rss.internal.model.RssSource\n\nclass RssSourceWebFingerTransformer() {\n\n    fun create(url: String, source: RssSource): WebFinger {\n        val name = source.title\n        val host = try {\n            url.toPlatformUri().host ?: url\n        } catch (e: Throwable) {\n            url\n        }\n        return WebFinger.build(\n            name = name,\n            host = host,\n        )\n    }\n}\n"
  },
  {
    "path": "plugins/rss/src/commonTest/kotlin/com/zhangke/fread/rss/internal/adapter/RssBlogMediaExtractorTest.kt",
    "content": "package com.zhangke.fread.rss.internal.adapter\n\nimport kotlin.test.Test\nimport kotlin.test.assertEquals\nimport kotlin.test.assertNull\n\nclass RssBlogMediaExtractorTest {\n\n    @Test\n    fun extractImagesFromHtml() {\n        val medias = RssBlogMediaExtractor.extract(\n            itemId = \"post-1\",\n            articleUrl = \"https://example.com/posts/detail/index.html\",\n            contentHtml = \"\"\"\n                <p>Hello</p>\n                <img src=\"/images/cover.jpg\" alt=\"Cover\" />\n                <img data-src=\"../assets/body.png?size=large\" />\n                <img srcset=\"https://cdn.example.com/a-small.jpg 320w, https://cdn.example.com/a-large.jpg 1280w\" />\n            \"\"\".trimIndent(),\n            descriptionHtml = null,\n            fallbackImageUrl = null,\n        )\n\n        assertEquals(\n            listOf(\n                \"https://example.com/images/cover.jpg\",\n                \"https://example.com/posts/assets/body.png?size=large\",\n                \"https://cdn.example.com/a-large.jpg\",\n            ),\n            medias.map { it.url },\n        )\n        assertEquals(\"Cover\", medias.first().description)\n    }\n\n    @Test\n    fun ignoreLikelyIcons() {\n        val medias = RssBlogMediaExtractor.extract(\n            itemId = \"post-2\",\n            articleUrl = \"https://example.com/posts/detail.html\",\n            contentHtml = \"\"\"\n                <img src=\"https://example.com/static/avatar.png\" alt=\"author avatar\" width=\"48\" height=\"48\" />\n                <img src=\"https://example.com/images/article-header.jpg\" width=\"1200\" height=\"630\" />\n            \"\"\".trimIndent(),\n            descriptionHtml = null,\n            fallbackImageUrl = null,\n        )\n\n        assertEquals(listOf(\"https://example.com/images/article-header.jpg\"), medias.map { it.url })\n    }\n\n    @Test\n    fun fallbackToRssImageWhenHtmlHasNoEmbeddedImage() {\n        val medias = RssBlogMediaExtractor.extract(\n            itemId = \"post-3\",\n            articleUrl = \"https://example.com/posts/detail.html\",\n            contentHtml = \"<p>No image here</p>\",\n            descriptionHtml = null,\n            fallbackImageUrl = \"https://example.com/images/fallback.jpg\",\n        )\n\n        assertEquals(listOf(\"https://example.com/images/fallback.jpg\"), medias.map { it.url })\n        assertNull(medias.first().description)\n    }\n}\n"
  },
  {
    "path": "plugins/rss/src/iosMain/kotlin/com/zhangke/fread/rss/RssIosModule.kt",
    "content": "package com.zhangke.fread.rss\n\nimport androidx.room.Room\nimport androidx.sqlite.driver.bundled.BundledSQLiteDriver\nimport com.prof18.rssparser.RssParser\nimport com.prof18.rssparser.RssParserBuilder\nimport com.zhangke.fread.common.documentDirectory\nimport com.zhangke.fread.rss.internal.db.RssDatabases\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.IO\nimport org.koin.core.module.Module\nimport platform.Foundation.NSBundle\nimport platform.Foundation.NSMutableDictionary\nimport platform.Foundation.NSURLSessionConfiguration\nimport platform.Foundation.NSURLSession\n\nactual fun Module.createPlatformModule() {\n    single<RssDatabases> {\n        val dbFilePath = getDBFilePath(RssDatabases.DB_NAME)\n        Room.databaseBuilder<RssDatabases>(\n            name = dbFilePath,\n        ).setDriver(BundledSQLiteDriver())\n            .setQueryCoroutineContext(Dispatchers.IO)\n            .build()\n    }\n    single<RssParser> {\n        RssParserBuilder(\n            nsUrlSession = createRssUrlSession(\n                appVersion = NSBundle.mainBundle()\n                    .infoDictionary()\n                    ?.get(\"CFBundleShortVersionString\") as? String ?: \"\",\n            ),\n        ).build()\n    }\n}\n\nprivate fun getDBFilePath(dbName: String): String {\n    return documentDirectory() + \"/$dbName\"\n}\n\nprivate fun createRssUrlSession(appVersion: String): NSURLSession {\n    val configuration = NSURLSessionConfiguration.defaultSessionConfiguration()\n    configuration.HTTPAdditionalHeaders = buildMap {\n        put(\"User-Agent\", \"Fread/$appVersion (iOS) +https://fread.xyz/\")\n    }\n    return NSURLSession.sessionWithConfiguration(configuration)\n}\n"
  },
  {
    "path": "plugins/rss/src/iosMain/kotlin/com/zhangke/fread/rss/internal/utils/AvatarUtils.ios.kt",
    "content": "package com.zhangke.fread.rss.internal.utils\n\nimport com.zhangke.fread.rss.internal.model.RssSource\n\nactual object AvatarUtils {\n    actual fun isRemoteAvatar(avatar: String?): Boolean {\n        TODO(\"Not yet implemented\")\n    }\n\n    actual fun makeSourceAvatar(source: RssSource): String? {\n        TODO(\"Not yet implemented\")\n    }\n\n}"
  },
  {
    "path": "privacy_cn.md",
    "content": "# Fread 隐私政策\nFread 是由张可开发的应用程序。\n\n此页面用于告知访问者有关我的政策，即如果有人决定使用我的服务，我将收集、使用和披露个人信息。\n\n如果您选择使用我的服务，则表示您同意收集和使用与此政策相关的信息。\n\n我收集的个人信息用于提供和改进服务。\n\n除本隐私政策中所述外，我不会与任何人使用或共享您的信息。\n\n## 信息收集和使用\n为了获得更好的体验，在使用我们的服务时，我可能会要求您向我们提供某些个人身份信息，包括但不限于张可。\n\n该应用程序确实使用第三方服务，这些服务可能会收集用于识别您的信息。\n\n链接到应用使用的第三方服务提供商的隐私政策\n\n[Google Play 服务](https://policies.google.com/privacy)\n\n[Google Analytics for Firebase](https://firebase.google.com/policies/analytics)\n\n[Firebase Crashlytics](https://firebase.google.com/support/privacy/)\n\n## 日志数据\n\n我想通知您，每当您使用我的服务时，如果应用出现错误，我会（通过第三方产品）在您的手机上收集数据和信息，称为日志数据。此日志数据可能包括您的设备 Internet 协议（“IP”）地址、设备名称、操作系统版本、使用我的服务时的应用配置、您使用服务的时间和日期以及其他统计信息等信息。\n\n## 服务提供商\n我可能会出于以下原因雇用第三方公司和个人：\n为我们的服务提供便利；\n代表我们提供服务；\n执行与服务相关的服务；或\n帮助我们分析我们的服务使用情况。\n我想告知本服务的用户，这些第三方可以访问他们的个人信息。原因是代表我们执行分配给他们的任务。但是，他们有义务不披露或将信息用于任何其他目的。\n\n## 安全\n我重视您对向我们提供您的个人信息的信任，因此我们努力使用商业上可接受的方式来保护它。但请记住，没有任何通过互联网传输的方法或电子存储方法是 100% 安全可靠的，我无法保证其绝对安全。\n\n## 链接到其他网站\n本服务可能包含指向其他网站的链接。如果您单击第三方链接，您将被定向到该网站。请注意，这些外部网站不是由我运营的。因此，我强烈建议您查看这些网站的隐私政策。我无法控制也不对任何第三方网站或服务的内容、隐私政策或做法承担任何责任。\n\n## 儿童隐私\n这些服务不针对 13 岁以下的任何人。我不会故意收集 13 岁以下儿童的个人身份信息。如果我发现 13 岁以下的儿童向我提供了个人信息，我会立即从我们的服务器中删除这些信息。如果您是父母或监护人，并且您知道您的孩子向我们提供了个人信息，请与我联系，以便我能够采取必要的措施。\n\n## 本隐私政策的变更\n我可能会不时更新我们的隐私政策。因此，建议您定期查看此页面以了解任何更改。我将通过在此页面上发布新的隐私政策来通知您任何更改。\n\n本政策自 2027-07-18 起生效\n\n## 联系我们\n如果您对我的隐私政策有任何疑问或建议，请随时通过 zhangkeport@gmail.com 与我联系。"
  },
  {
    "path": "privacy_en.md",
    "content": "# Fread Privacy Policy\nZhangKe built the Fread app. This SERVICE is provided by ZhangKe is intended for use as is.\n\nThis page is used to inform visitors regarding my policies with the collection, use, and disclosure of Personal Information if anyone decided to use my Service.\n\nIf you choose to use my Service, then you agree to the collection and use of information in relation to this policy. \nThe Personal Information that I collect is used for providing and improving the Service. \nI will not use or share your information with anyone except as described in this Privacy Policy.\n\n## Information Collection and Use\nFor a better experience, while using our Service, I may require you to provide us with certain personally identifiable information, including but not limited to ZhangKe. \n\nThe app does use third-party services that may collect information used to identify you.\n\nLink to the privacy policy of third-party service providers used by the app\n\n[Google Play Services](https://policies.google.com/privacy)\n\n[Google Analytics for Firebase](https://firebase.google.com/policies/analytics)\n\n[Firebase Crashlytics](https://firebase.google.com/support/privacy/)\n\n## Log Data\n\nI want to inform you that whenever you use my Service, in a case of an error in the app I collect data and information (through third-party products) on your phone called Log Data. This Log Data may include information such as your device Internet Protocol (“IP”) address, device name, operating system version, the configuration of the app when utilizing my Service, the time and date of your use of the Service, and other statistics.\n\n## Service Providers\nI may employ third-party companies and individuals due to the following reasons:\n\nTo facilitate our Service;\nTo provide the Service on our behalf;\nTo perform Service-related services; or\nTo assist us in analyzing how our Service is used.\nI want to inform users of this Service that these third parties have access to their Personal Information. The reason is to perform the tasks assigned to them on our behalf. However, they are obligated not to disclose or use the information for any other purpose.\n\n## Security\nI value your trust in providing us your Personal Information, thus we are striving to use commercially acceptable means of protecting it. But remember that no method of transmission over the internet, or method of electronic storage is 100% secure and reliable, and I cannot guarantee its absolute security.\n\n## Links to Other Sites\nThis Service may contain links to other sites. If you click on a third-party link, you will be directed to that site. Note that these external sites are not operated by me. Therefore, I strongly advise you to review the Privacy Policy of these websites. I have no control over and assume no responsibility for the content, privacy policies, or practices of any third-party sites or services.\n\n## Children’s Privacy\nThese Services do not address anyone under the age of 13. I do not knowingly collect personally identifiable information from children under 13 years of age. In the case I discover that a child under 13 has provided me with personal information, I immediately delete this from our servers. If you are a parent or guardian and you are aware that your child has provided us with personal information, please contact me so that I will be able to do the necessary actions.\n\n## Changes to This Privacy Policy\nI may update our Privacy Policy from time to time. Thus, you are advised to review this page periodically for any changes. I will notify you of any changes by posting the new Privacy Policy on this page.\n\nThis policy is effective as of 2027-07-18\n\n## Contact Us\nIf you have any questions or suggestions about my Privacy Policy, do not hesitate to contact me at zhangkeport@gmail.com.\n"
  },
  {
    "path": "settings.gradle.kts",
    "content": "pluginManagement {\n    includeBuild(\"build-logic\")\n    repositories {\n        gradlePluginPortal()\n        google()\n        mavenCentral()\n        mavenLocal()\n        maven { setUrl(\"https://jitpack.io\") }\n        maven { setUrl(\"https://plugins.gradle.org/m2/\") }\n    }\n}\ndependencyResolutionManagement {\n    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)\n    repositories {\n        google()\n        mavenCentral()\n        mavenLocal()\n        maven(\"https://jitpack.io\")\n        maven(\"https://plugins.gradle.org/m2/\")\n        maven(\"https://s01.oss.sonatype.org/content/repositories/snapshots/\")\n        maven(\"https://oss.sonatype.org/content/repositories/snapshots/\")\n    }\n}\nrootProject.name = \"Fread\"\n\nval moduleExists = File(\"./plugins/fread-firebase\").exists()\nval disableFirebase = gradle.startParameter.projectProperties[\"disableFirebase\"]\nval enableFirebaseModule = moduleExists && disableFirebase != \"true\"\ngradle.extra[\"enableFirebaseModule\"] = enableFirebaseModule\nprintln(\"--------disableFirebase:$disableFirebase, moduleExists: $moduleExists------------- enableFirebaseModule: ${gradle.extra[\"enableFirebaseModule\"]}\")\n\ninclude(\":framework\")\ninclude(\":bizframework:status-provider\")\ninclude(\":commonbiz:common\")\ninclude(\":commonbiz:analytics\")\ninclude(\":commonbiz:status-ui\")\ninclude(\":commonbiz:sharedscreen\")\ninclude(\":feature:explore\")\ninclude(\":feature:feeds\")\ninclude(\":feature:notifications\")\ninclude(\":feature:profile\")\ninclude(\":plugins:rss\")\ninclude(\":plugins:activitypub-app\")\ninclude(\":app-hosting\")\ninclude(\":app\")\ninclude(\":thirds:halilibo-richtext-ui\")\ninclude(\":thirds:halilibo-richtext-material3\")\ninclude(\":plugins:bluesky\")\nif (enableFirebaseModule) {\n    include(\":plugins:fread-firebase\")\n}\ninclude(\":localization\")\n"
  },
  {
    "path": "thirds/halilibo-richtext-material3/build.gradle.kts",
    "content": "plugins {\n    id(\"fread.project.framework.kmp\")\n}\n\nandroid {\n    namespace = \"com.halilibo.richtext.material3\"\n}\n\nkotlin {\n    sourceSets {\n        val commonMain by getting {\n            dependencies {\n                implementation(compose.runtime)\n                implementation(compose.foundation)\n\n                @OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class)\n                implementation(compose.material3)\n\n                api(project(\":thirds:halilibo-richtext-ui\"))\n            }\n        }\n        val commonTest by getting\n    }\n}"
  },
  {
    "path": "thirds/halilibo-richtext-material3/src/androidMain/AndroidManifest.xml",
    "content": "<manifest />"
  },
  {
    "path": "thirds/halilibo-richtext-material3/src/commonMain/kotlin/com/halilibo/richtext/ui/material3/RichText.kt",
    "content": "package com.halilibo.richtext.ui.material3\n\nimport androidx.compose.material3.LocalContentColor\nimport androidx.compose.material3.LocalTextStyle\nimport androidx.compose.material3.ProvideTextStyle\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.compositionLocalOf\nimport androidx.compose.ui.Modifier\nimport com.halilibo.richtext.ui.BasicRichText\nimport com.halilibo.richtext.ui.RichTextScope\nimport com.halilibo.richtext.ui.RichTextStyle\nimport com.halilibo.richtext.ui.RichTextThemeProvider\n\n/**\n * RichText implementation that integrates with Material 3 design.\n *\n * If the consumer app has small composition trees or only uses RichText in\n * a single place, it would be ideal to call this function instead of wrapping\n * everything under [RichTextMaterialTheme].\n */\n@Composable\npublic fun RichText(\n  modifier: Modifier = Modifier,\n  style: RichTextStyle? = null,\n  children: @Composable RichTextScope.() -> Unit\n) {\n  RichTextMaterialTheme {\n    BasicRichText(\n      modifier = modifier,\n      style = style,\n      children = children\n    )\n  }\n}\n\n/**\n * Wraps the given [child] with Material Theme integration for [BasicRichText].\n *\n * This function also keeps track of the parent context by using CompositionLocals\n * to not apply Material Theming if it already exists in the current composition.\n */\n@Composable\ninternal fun RichTextMaterialTheme(\n  child: @Composable () -> Unit\n) {\n  val isApplied = LocalMaterialThemingApplied.current\n\n  if (!isApplied) {\n    RichTextThemeProvider(\n      textStyleProvider = { LocalTextStyle.current },\n      contentColorProvider = { LocalContentColor.current },\n      textStyleBackProvider = { textStyle, content ->\n        ProvideTextStyle(textStyle, content)\n      },\n      contentColorBackProvider = { color, content ->\n        CompositionLocalProvider(LocalContentColor provides color) {\n          content()\n        }\n      }\n    ) {\n      CompositionLocalProvider(LocalMaterialThemingApplied provides true) {\n        child()\n      }\n    }\n  } else {\n    child()\n  }\n}\n\nprivate val LocalMaterialThemingApplied = compositionLocalOf { false }"
  },
  {
    "path": "thirds/halilibo-richtext-ui/build.gradle.kts",
    "content": "plugins {\n    id(\"fread.project.framework.kmp\")\n}\n\nandroid {\n    namespace = \"com.halilibo.richtext.ui\"\n}\n\nkotlin {\n    sourceSets {\n        val commonMain by getting {\n            dependencies {\n                implementation(compose.runtime)\n                implementation(compose.foundation)\n            }\n        }\n        val commonTest by getting\n    }\n}"
  },
  {
    "path": "thirds/halilibo-richtext-ui/src/androidMain/AndroidManifest.xml",
    "content": "<manifest />"
  },
  {
    "path": "thirds/halilibo-richtext-ui/src/androidMain/kotlin/com/halilibo/richtext/ui/CodeBlock.android.kt",
    "content": "package com.halilibo.richtext.ui\n\nimport androidx.compose.foundation.horizontalScroll\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\n\n@Composable\ninternal actual fun RichTextScope.CodeBlockLayout(\n  wordWrap: Boolean,\n  children: @Composable RichTextScope.(Modifier) -> Unit\n) {\n  if (!wordWrap) {\n    val scrollState = rememberScrollState()\n    children(Modifier.horizontalScroll(scrollState))\n  } else {\n    children(Modifier)\n  }\n}"
  },
  {
    "path": "thirds/halilibo-richtext-ui/src/androidMain/kotlin/com/halilibo/richtext/ui/util/UUID.android.kt",
    "content": "package com.halilibo.richtext.ui.util\n\nimport java.util.UUID\n\ninternal actual fun randomUUID(): String {\n  return UUID.randomUUID().toString()\n}"
  },
  {
    "path": "thirds/halilibo-richtext-ui/src/commonMain/kotlin/com/halilibo/richtext/ui/BasicRichText.kt",
    "content": "@file:Suppress(\"RemoveEmptyParenthesesFromAnnotationEntry\")\n\npackage com.halilibo.richtext.ui\n\nimport androidx.compose.foundation.layout.Arrangement.spacedBy\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalDensity\n\n/**\n * Draws some rich text. Entry point to the compose-richtext library.\n */\n@Composable\npublic fun BasicRichText(\n  modifier: Modifier = Modifier,\n  style: RichTextStyle? = null,\n  children: @Composable RichTextScope.() -> Unit\n) {\n  with(RichTextScope) {\n    RestartListLevel {\n      WithStyle(style) {\n        val resolvedStyle = currentRichTextStyle.resolveDefaults()\n        val blockSpacing = with(LocalDensity.current) {\n          resolvedStyle.paragraphSpacing!!.toDp()\n        }\n\n        Column(modifier = modifier, verticalArrangement = spacedBy(blockSpacing)) {\n          children()\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "thirds/halilibo-richtext-ui/src/commonMain/kotlin/com/halilibo/richtext/ui/BlockQuote.kt",
    "content": "@file:Suppress(\"RemoveEmptyParenthesesFromAnnotationEntry\")\n\npackage com.halilibo.richtext.ui\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.Immutable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.layout.Layout\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.unit.IntOffset\nimport androidx.compose.ui.unit.TextUnit\nimport androidx.compose.ui.unit.offset\nimport androidx.compose.ui.unit.sp\nimport com.halilibo.richtext.ui.BlockQuoteGutter.BarGutter\n\ninternal val DefaultBlockQuoteGutter = BarGutter()\n\n/**\n * A composable function that draws the gutter beside a [BlockQuote].\n *\n * [BarGutter] is provided as the reasonable default of a simple vertical line.\n */\npublic interface BlockQuoteGutter {\n  @Composable public fun RichTextScope.drawGutter()\n\n  @Immutable\n  public data class BarGutter(\n    val startMargin: TextUnit = 6.sp,\n    val barWidth: TextUnit = 3.sp,\n    val endMargin: TextUnit = 6.sp,\n    val color: (contentColor: Color) -> Color = { it.copy(alpha = .25f) }\n  ) : BlockQuoteGutter {\n\n    @Composable\n    override fun RichTextScope.drawGutter() {\n      with(LocalDensity.current) {\n        val color = color(currentContentColor)\n\n        val modifier = remember(startMargin, endMargin, barWidth, color) {\n          // Padding must come before width.\n          Modifier\n            .padding(\n              start = startMargin.toDp(),\n              end = endMargin.toDp()\n            )\n            .width(barWidth.toDp())\n            .background(color, RoundedCornerShape(50))\n        }\n\n        Box(modifier)\n      }\n    }\n  }\n}\n\n/**\n * Draws a block quote, with a [BlockQuoteGutter] drawn beside the children on the start side.\n */\n@Composable public fun RichTextScope.BlockQuote(children: @Composable RichTextScope.() -> Unit) {\n  val gutter = currentRichTextStyle.resolveDefaults().blockQuoteGutter!!\n  val spacing = with(LocalDensity.current) {\n    currentRichTextStyle.resolveDefaults().paragraphSpacing!!.toDp() / 2\n  }\n\n  Layout(content = {\n    with(gutter) { drawGutter() }\n    BasicRichText(\n      modifier = Modifier.padding(top = spacing, bottom = spacing),\n      children = children\n    )\n  }) { measurables, constraints ->\n    val gutterMeasurable = measurables[0]\n    val contentsMeasurable = measurables[1]\n\n    // First get the width of the gutter, so we can measure the contents with\n    // the smaller width if bounded.\n    val gutterWidth = gutterMeasurable.minIntrinsicWidth(constraints.maxHeight)\n\n    // Measure the contents with the confined width.\n    // This must be done before measuring the gutter so that the gutter gets\n    // the correct height.\n    val contentsConstraints = constraints.offset(horizontal = -gutterWidth)\n    val contentsPlaceable = contentsMeasurable.measure(contentsConstraints)\n    val layoutWidth = contentsPlaceable.width + gutterWidth\n    val layoutHeight = contentsPlaceable.height\n\n    // Measure the gutter to fit in its min intrinsic width and exactly the\n    // height of the contents.\n    val gutterConstraints = constraints.copy(\n      maxWidth = gutterWidth,\n      minHeight = layoutHeight,\n      maxHeight = layoutHeight\n    )\n    val gutterPlaceable = gutterMeasurable.measure(gutterConstraints)\n\n    layout(layoutWidth, layoutHeight) {\n      gutterPlaceable.place(IntOffset.Zero)\n      contentsPlaceable.place(gutterWidth, 0)\n    }\n  }\n}\n"
  },
  {
    "path": "thirds/halilibo-richtext-ui/src/commonMain/kotlin/com/halilibo/richtext/ui/CodeBlock.kt",
    "content": "package com.halilibo.richtext.ui\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.Immutable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.text.TextStyle\nimport androidx.compose.ui.text.font.FontFamily\nimport androidx.compose.ui.unit.TextUnit\nimport androidx.compose.ui.unit.sp\n\n/**\n * Defines how [CodeBlock]s are rendered.\n *\n * @param textStyle The [TextStyle] to use for the block.\n * @param modifier The [Modifier] to use for the block.\n * @param padding The amount of space between the edge of the text and the edge of the background.\n * @param wordWrap Whether a code block breaks the lines or scrolls horizontally.\n */\n@Immutable\npublic data class CodeBlockStyle(\n  val textStyle: TextStyle? = null,\n  val modifier: Modifier? = null,\n  val padding: TextUnit? = null,\n  val wordWrap: Boolean? = null\n) {\n  public companion object {\n    public val Default: CodeBlockStyle = CodeBlockStyle()\n  }\n}\n\nprivate val DefaultCodeBlockTextStyle = TextStyle(\n  fontFamily = FontFamily.Monospace\n)\ninternal val DefaultCodeBlockBackgroundColor: Color = Color.LightGray.copy(alpha = .5f)\nprivate val DefaultCodeBlockModifier: Modifier =\n  Modifier.background(color = DefaultCodeBlockBackgroundColor)\nprivate val DefaultCodeBlockPadding: TextUnit = 16.sp\nprivate const val DefaultCodeWordWrap: Boolean = true\n\ninternal fun CodeBlockStyle.resolveDefaults() = CodeBlockStyle(\n  textStyle = textStyle ?: DefaultCodeBlockTextStyle,\n  modifier = modifier ?: DefaultCodeBlockModifier,\n  padding = padding ?: DefaultCodeBlockPadding,\n  wordWrap = wordWrap ?: DefaultCodeWordWrap\n)\n\n/**\n * A specially-formatted block of text that typically uses a monospace font with a tinted\n * background.\n *\n * @param wordWrap Overrides word wrap preference coming from [CodeBlockStyle]\n */\n@Composable public fun RichTextScope.CodeBlock(\n  text: String,\n  wordWrap: Boolean? = null\n) {\n  CodeBlock(wordWrap = wordWrap) {\n    Text(text)\n  }\n}\n\n/**\n * A specially-formatted block of text that typically uses a monospace font with a tinted\n * background.\n *\n * @param wordWrap Overrides word wrap preference coming from [CodeBlockStyle]\n */\n@Composable public fun RichTextScope.CodeBlock(\n  wordWrap: Boolean? = null,\n  children: @Composable RichTextScope.() -> Unit\n) {\n  val codeBlockStyle = currentRichTextStyle.resolveDefaults().codeBlockStyle!!\n  val textStyle = currentTextStyle.merge(codeBlockStyle.textStyle)\n  val modifier = codeBlockStyle.modifier!!\n  val blockPadding = with(LocalDensity.current) {\n    codeBlockStyle.padding!!.toDp()\n  }\n  val resolvedWordWrap = wordWrap ?: codeBlockStyle.wordWrap!!\n\n  CodeBlockLayout(\n    wordWrap = resolvedWordWrap\n  ) { layoutModifier ->\n    Box(\n      modifier = layoutModifier\n        .then(modifier)\n        .padding(blockPadding)\n    ) {\n      textStyleBackProvider(textStyle) {\n        children()\n      }\n    }\n  }\n}\n\n/**\n * Desktop composable adds an optional horizontal scrollbar.\n */\n@Composable\ninternal expect fun RichTextScope.CodeBlockLayout(\n  wordWrap: Boolean,\n  children: @Composable RichTextScope.(Modifier) -> Unit\n)\n"
  },
  {
    "path": "thirds/halilibo-richtext-ui/src/commonMain/kotlin/com/halilibo/richtext/ui/FormattedList.kt",
    "content": "@file:Suppress(\"ComposableNaming\")\n\npackage com.halilibo.richtext.ui\n\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.text.selection.DisableSelection\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.Immutable\nimport androidx.compose.runtime.compositionLocalOf\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.paint\nimport androidx.compose.ui.graphics.painter.Painter\nimport androidx.compose.ui.layout.Layout\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.unit.Constraints\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.IntSize\nimport androidx.compose.ui.unit.TextUnit\nimport androidx.compose.ui.unit.sp\nimport com.halilibo.richtext.ui.ListType.Ordered\nimport com.halilibo.richtext.ui.ListType.Unordered\nimport kotlin.math.max\n\npublic enum class ListType {\n  /**\n   * An ordered (numbered) list.\n   */\n  Ordered,\n\n  /**\n   * An unordered (bullet) list.\n   */\n  Unordered\n}\n\n/**\n * Defines how to draw list markers for [FormattedList]s that are [Ordered].\n *\n * These are typically some sort of ordinal text.\n */\npublic interface OrderedMarkers {\n  @Composable public fun drawMarker(\n    level: Int,\n    index: Int\n  )\n\n  public companion object {\n    /**\n     * Creates an [OrderedMarkers] from an arbitrary composable given the indentation level and\n     * the index.\n     */\n    public operator fun invoke(\n      drawMarker: @Composable (level: Int, index: Int) -> Unit\n    ): OrderedMarkers = object : OrderedMarkers {\n      @Composable override fun drawMarker(\n        level: Int,\n        index: Int\n      ) {\n        drawMarker(level, index)\n      }\n    }\n  }\n}\n\n/**\n * Creates an [OrderedMarkers] that will cycle through the values in [markers] for each\n * indentation level given the index.\n */\npublic fun RichTextScope.textOrderedMarkers(\n  vararg markers: (index: Int) -> String\n): OrderedMarkers =\n  OrderedMarkers { level, index ->\n    Text(markers[level % markers.size](index))\n  }\n\n/**\n * Defines how to draw list markers for [FormattedList]s that are [Unordered].\n *\n * These are typically some sort of bullet point.\n */\npublic interface UnorderedMarkers {\n  @Composable public fun drawMarker(level: Int)\n\n  public companion object {\n    /**\n     * Creates an [UnorderedMarkers] from an arbitrary composable given the indentation level.\n     */\n    public operator fun invoke(drawMarker: @Composable (level: Int) -> Unit): UnorderedMarkers =\n      object : UnorderedMarkers {\n        @Composable override fun drawMarker(level: Int) = drawMarker(level)\n      }\n  }\n}\n\n/**\n * Creates an [UnorderedMarkers] that will cycle through the values in [markers] for each\n * indentation level.\n */\n@Composable\npublic fun RichTextScope.textUnorderedMarkers(\n  vararg markers: String\n): UnorderedMarkers = UnorderedMarkers {\n  Text(markers[it % markers.size])\n}\n\n/**\n * Creates an [UnorderedMarkers] that will cycle through the values in [painters] for each\n * indentation level.\n */\npublic fun painterUnorderedMarkers(vararg painters: Painter): UnorderedMarkers = UnorderedMarkers {\n  Box(Modifier.paint(painters[it % painters.size]))\n}\n\n/**\n * Defines how [FormattedList]s should look.\n *\n * @param markerIndent The padding before each marker.\n * @param contentsIndent The padding after each marker.\n */\n@Immutable\npublic data class ListStyle(\n  val markerIndent: TextUnit? = null,\n  val contentsIndent: TextUnit? = null,\n  val itemSpacing: TextUnit? = null,\n  val orderedMarkers: (RichTextScope.() -> OrderedMarkers)? = null,\n  val unorderedMarkers: (@Composable RichTextScope.() -> UnorderedMarkers)? = null\n) {\n  public companion object {\n    public val Default: ListStyle = ListStyle()\n  }\n}\n\nprivate val DefaultMarkerIndent = 8.sp\nprivate val DefaultContentsIndent = 4.sp\nprivate val DefaultItemSpacing = 4.sp\nprivate val DefaultOrderedMarkers: RichTextScope.() -> OrderedMarkers = {\n  textOrderedMarkers(\n    { \"${it + 1}.\" },\n    {\n      ('a'..'z').drop(it % 26)\n        .first() + \".\"\n    },\n    { \"${it + 1})\" },\n    {\n      ('a'..'z').drop(it % 26)\n        .first() + \")\"\n    }\n  )\n}\n\nprivate val DefaultUnorderedMarkers: @Composable RichTextScope.() -> UnorderedMarkers = {\n  textUnorderedMarkers(\"•\", \"◦\", \"▸\", \"▹\")\n}\n\ninternal fun ListStyle.resolveDefaults(): ListStyle = ListStyle(\n  markerIndent = markerIndent ?: DefaultMarkerIndent,\n  contentsIndent = contentsIndent ?: DefaultContentsIndent,\n  itemSpacing = itemSpacing ?: DefaultItemSpacing,\n  orderedMarkers = orderedMarkers ?: DefaultOrderedMarkers,\n  unorderedMarkers = unorderedMarkers ?: DefaultUnorderedMarkers\n)\n\nprivate val LocalListLevel = compositionLocalOf { 0 }\n\n/**\n * Composes [children] with their [LocalListLevel] reset back to 0.\n */\n@Composable internal fun RestartListLevel(children: @Composable () -> Unit) {\n  CompositionLocalProvider(LocalListLevel provides 0) {\n    children()\n  }\n}\n\n/**\n * Creates a formatted list such as a bullet list or numbered list.\n *\n * @sample com.halilibo.richtext.ui.previews.OrderedListPreview\n * @sample com.halilibo.richtext.ui.previews.UnorderedListPreview\n */\n// inline is required for https://github.com/halilozercan/compose-richtext/issues/7\n@Suppress(\"NOTHING_TO_INLINE\")\n@Composable public inline fun RichTextScope.FormattedList(\n  listType: ListType,\n  vararg children: @Composable RichTextScope.() -> Unit\n): Unit = FormattedList(listType, children.asList(), 0) { it() }\n\n/**\n * Creates a formatted list such as a bullet list or numbered list.\n *\n * @sample com.halilibo.richtext.ui.previews.OrderedListPreview\n * @sample com.halilibo.richtext.ui.previews.UnorderedListPreview\n */\n@Composable public fun <T> RichTextScope.FormattedList(\n  listType: ListType,\n  items: List<T>,\n  startIndex: Int = 0,\n  drawItem: @Composable RichTextScope.(T) -> Unit\n) {\n  val listStyle = currentRichTextStyle.resolveDefaults().listStyle!!\n  val density = LocalDensity.current\n  val markerIndent = with(density) { listStyle.markerIndent!!.toDp() }\n  val contentsIndent = with(density) { listStyle.contentsIndent!!.toDp() }\n  val itemSpacing = with(density) { listStyle.itemSpacing!!.toDp() }\n  val currentLevel = LocalListLevel.current\n\n  PrefixListLayout(\n    count = items.size,\n    itemSpacing = itemSpacing,\n    prefixPadding = PaddingValues(start = markerIndent, end = contentsIndent),\n    prefixForIndex = { index ->\n      when (listType) {\n        Ordered -> listStyle.orderedMarkers!!().drawMarker(currentLevel, startIndex + index)\n        Unordered -> listStyle.unorderedMarkers!!().drawMarker(currentLevel)\n      }\n    },\n    itemForIndex = { index ->\n      BasicRichText(\n        style = currentRichTextStyle.copy(paragraphSpacing = listStyle.itemSpacing),\n      ) {\n        CompositionLocalProvider(LocalListLevel provides currentLevel + 1) {\n          drawItem(items[index])\n        }\n      }\n    }\n  )\n}\n\n@Composable private fun PrefixListLayout(\n  count: Int,\n  itemSpacing: Dp,\n  prefixPadding: PaddingValues,\n  prefixForIndex: @Composable (index: Int) -> Unit,\n  itemForIndex: @Composable (index: Int) -> Unit\n) {\n  Layout(content = {\n    // List markers aren't selectable.\n    DisableSelection {\n      // Draw the markers first.\n      for (i in 0 until count) {\n        // TODO Use the padding in the calculation directly instead of wrapping.\n        Box(Modifier.padding(prefixPadding)) {\n          prefixForIndex(i)\n        }\n      }\n    }\n\n    // Then draw the items.\n    for (i in 0 until count) {\n      itemForIndex(i)\n    }\n  }) { measurables, constraints ->\n    check(measurables.size == count * 2)\n    val prefixMeasureables = measurables.asSequence()\n      .take(count)\n    val itemMeasurables = measurables.asSequence()\n      .drop(count)\n\n    // Measure the prefixes first.\n    val prefixPlaceables = prefixMeasureables.map { marker ->\n      marker.measure(Constraints())\n    }\n      .toList()\n    val widestPrefix = prefixPlaceables.maxByOrNull { it.width }!!\n\n    // Then measure the items, offset to the right to allow space for the prefixes and gap.\n    val itemConstraints = constraints.copy(\n      maxWidth = (constraints.maxWidth - widestPrefix.width).coerceAtLeast(0)\n    )\n    val itemPlaceables = itemMeasurables.map { item ->\n      item.measure(itemConstraints)\n    }\n      .toList()\n    val widestItem = itemPlaceables.maxByOrNull { it.width }!!\n\n    val listWidth = widestPrefix.width + widestItem.width\n    val itemsHeight = itemPlaceables.sumOf { it.height } +\n        (itemPlaceables.size - 1) * itemSpacing.roundToPx()\n    val prefixesHeight = prefixPlaceables.sumOf { it.height } +\n        (prefixPlaceables.size - 1) * itemSpacing.roundToPx()\n\n    val listHeight = maxOf(itemsHeight, prefixesHeight)\n    layout(listWidth, listHeight) {\n      var y = 0\n\n      // Flow the rows vertically, much like Column.\n      for (i in 0 until count) {\n        val prefix = prefixPlaceables[i]\n        val item = itemPlaceables[i]\n        val rowHeight = max(prefix.height, item.height) + itemSpacing.roundToPx()\n        val size = IntSize(\n          width = widestPrefix.width - prefix.width,\n          height = rowHeight - prefix.height\n        )\n        val prefixOffset = Alignment.TopEnd.align(\n          size = size,\n          space = size,\n          layoutDirection = layoutDirection\n        )\n\n        prefix.placeRelative(prefixOffset.x, y + prefixOffset.y)\n        item.placeRelative(widestPrefix.width, y)\n        y += rowHeight\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "thirds/halilibo-richtext-ui/src/commonMain/kotlin/com/halilibo/richtext/ui/Heading.kt",
    "content": "@file:Suppress(\"RemoveEmptyParenthesesFromAnnotationEntry\")\n\npackage com.halilibo.richtext.ui\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.takeOrElse\nimport androidx.compose.ui.platform.LocalLayoutDirection\nimport androidx.compose.ui.semantics.heading\nimport androidx.compose.ui.semantics.semantics\nimport androidx.compose.ui.text.TextStyle\nimport androidx.compose.ui.text.font.FontStyle.Companion.Italic\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.resolveDefaults\nimport androidx.compose.ui.unit.sp\n\n\n/**\n * Function that computes the [TextStyle] for the given header level, given the current [TextStyle]\n * for this point in the composition. Note that the [TextStyle] passed into this function will be\n * fully resolved. The returned style will then be _merged_ with the passed-in text style, so any\n * unspecified properties will be inherited.\n */\n// TODO factor a generic \"block style\" thing out, use for code block, quote block, and this, to\n// also allow controlling top/bottom space.\npublic typealias HeadingStyle = (level: Int, textStyle: TextStyle) -> TextStyle\n\ninternal val DefaultHeadingStyle: HeadingStyle = { level, textStyle ->\n  when (level) {\n    0 -> TextStyle(\n        fontSize = 36.sp,\n        fontWeight = FontWeight.Bold\n    )\n    1 -> TextStyle(\n        fontSize = 26.sp,\n        fontWeight = FontWeight.Bold\n    )\n    2 -> TextStyle(\n        fontSize = 22.sp,\n        fontWeight = FontWeight.Bold,\n        color = textStyle.color.copy(alpha = .7F)\n    )\n    3 -> TextStyle(\n        fontSize = 20.sp,\n        fontWeight = FontWeight.Bold,\n        fontStyle = Italic\n    )\n    4 -> TextStyle(\n        fontSize = 18.sp,\n        fontWeight = FontWeight.Bold,\n        color = textStyle.color.copy(alpha = .7F)\n    )\n    5 -> TextStyle(\n        fontWeight = FontWeight.Bold,\n        color = textStyle.color.copy(alpha = .5f)\n    )\n    else -> textStyle\n  }\n}\n\n/**\n * A section heading.\n *\n * @param level The non-negative rank of the header, with 0 being the most important.\n */\n@Composable public fun RichTextScope.Heading(\n  level: Int,\n  text: String\n) {\n  Heading(level) {\n    Text(text, Modifier.semantics { heading() })\n  }\n}\n\n/**\n * A section heading.\n *\n * @param level The non-negative rank of the header, with 0 being the most important.\n */\n@Composable public fun RichTextScope.Heading(\n  level: Int,\n  children: @Composable RichTextScope.() -> Unit\n) {\n  require(level >= 0) { \"Level must be at least 0\" }\n\n  val incomingStyle = currentTextStyle.let {\n    it.copy(color = it.color.takeOrElse { currentContentColor })\n  }\n  val currentTextStyle = resolveDefaults(incomingStyle, LocalLayoutDirection.current)\n\n  val headingStyleFunction = currentRichTextStyle.resolveDefaults().headingStyle!!\n  val headingTextStyle = headingStyleFunction(level, currentTextStyle)\n  val mergedTextStyle = currentTextStyle.merge(headingTextStyle)\n\n  textStyleBackProvider(mergedTextStyle) {\n    children()\n  }\n}\n"
  },
  {
    "path": "thirds/halilibo-richtext-ui/src/commonMain/kotlin/com/halilibo/richtext/ui/HorizontalRule.kt",
    "content": "package com.halilibo.richtext.ui\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.unit.dp\n\n/**\n * A simple horizontal line drawn with the current content color.\n */\n@Composable public fun RichTextScope.HorizontalRule() {\n  val color = currentContentColor.copy(alpha = .2f)\n  val spacing = with(LocalDensity.current) {\n    currentRichTextStyle.resolveDefaults().paragraphSpacing!!.toDp()\n  }\n  Box(\n    Modifier\n      .padding(top = spacing, bottom = spacing)\n      .fillMaxWidth()\n      .height(1.dp)\n      .background(color)\n  )\n}\n"
  },
  {
    "path": "thirds/halilibo-richtext-ui/src/commonMain/kotlin/com/halilibo/richtext/ui/InfoPanel.kt",
    "content": "package com.halilibo.richtext.ui\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.Stable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.text.TextStyle\nimport androidx.compose.ui.unit.dp\nimport com.halilibo.richtext.ui.InfoPanelType.Danger\nimport com.halilibo.richtext.ui.InfoPanelType.Primary\nimport com.halilibo.richtext.ui.InfoPanelType.Secondary\nimport com.halilibo.richtext.ui.InfoPanelType.Success\nimport com.halilibo.richtext.ui.InfoPanelType.Warning\n\n@Stable\npublic data class InfoPanelStyle(\n  val contentPadding: PaddingValues? = null,\n  val background: @Composable ((InfoPanelType) -> Modifier)? = null,\n  val textStyle: @Composable ((InfoPanelType) -> TextStyle)? = null\n) {\n  public companion object {\n    public val Default: InfoPanelStyle = InfoPanelStyle()\n  }\n}\n\npublic enum class InfoPanelType {\n  Primary,\n  Secondary,\n  Success,\n  Danger,\n  Warning\n}\n\nprivate val DefaultContentPadding = PaddingValues(8.dp)\nprivate val DefaultInfoPanelBackground = @Composable { infoPanelType: InfoPanelType ->\n  remember {\n    val (borderColor, backgroundColor) = when (infoPanelType) {\n      Primary -> Color(0xffb8daff) to Color(0xffcce5ff)\n      Secondary -> Color(0xffd6d8db) to Color(0xffe2e3e5)\n      Success -> Color(0xffc3e6cb) to Color(0xffd4edda)\n      Danger -> Color(0xfff5c6cb) to Color(0xfff8d7da)\n      Warning -> Color(0xffffeeba) to Color(0xfffff3cd)\n    }\n\n    Modifier\n      .border(1.dp, borderColor, RoundedCornerShape(4.dp))\n      .background(backgroundColor, RoundedCornerShape(4.dp))\n  }\n}\n\nprivate val DefaultInfoPanelTextStyle = @Composable { infoPanelType: InfoPanelType ->\n  remember {\n    val color = when(infoPanelType) {\n      Primary -> Color(0xff004085)\n      Secondary -> Color(0xff383d41)\n      Success -> Color(0xff155724)\n      Danger -> Color(0xff721c24)\n      Warning -> Color(0xff856404)\n    }\n    TextStyle(color = color)\n  }\n}\n\ninternal fun InfoPanelStyle.resolveDefaults() = InfoPanelStyle(\n  contentPadding = contentPadding ?: DefaultContentPadding,\n  background = background ?: DefaultInfoPanelBackground,\n  textStyle = textStyle ?: DefaultInfoPanelTextStyle\n)\n\n/**\n * A panel to show content similar to Bootstrap alerts, categorized as [InfoPanelType].\n * This composable is a shortcut to show only [text] in an info panel.\n */\n@Composable\npublic fun RichTextScope.InfoPanel(\n  infoPanelType: InfoPanelType,\n  text: String\n) {\n  InfoPanel(infoPanelType) {\n    Text(text)\n  }\n}\n\n/**\n * A panel to show content similar to Bootstrap alerts, categorized as [InfoPanelType].\n */\n@Composable\npublic fun RichTextScope.InfoPanel(\n  infoPanelType: InfoPanelType,\n  content: @Composable () -> Unit\n) {\n  val infoPanelStyle = currentRichTextStyle.resolveDefaults().infoPanelStyle!!\n  val backgroundModifier = infoPanelStyle.background!!.invoke(infoPanelType)\n  val infoPanelTextStyle = infoPanelStyle.textStyle!!.invoke(infoPanelType)\n\n  val resolvedTextStyle = currentTextStyle.merge(infoPanelTextStyle)\n\n  textStyleBackProvider(resolvedTextStyle) {\n    Box(modifier = backgroundModifier.padding(infoPanelStyle.contentPadding!!)) {\n      content()\n    }\n  }\n}\n"
  },
  {
    "path": "thirds/halilibo-richtext-ui/src/commonMain/kotlin/com/halilibo/richtext/ui/RichTextLocals.kt",
    "content": "package com.halilibo.richtext.ui\n\nimport androidx.compose.foundation.text.BasicText\nimport androidx.compose.foundation.text.InlineTextContent\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.compositionLocalOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.takeOrElse\nimport androidx.compose.ui.input.pointer.pointerInput\nimport androidx.compose.ui.text.AnnotatedString\nimport androidx.compose.ui.text.TextLayoutResult\nimport androidx.compose.ui.text.TextStyle\nimport androidx.compose.ui.text.style.TextOverflow\nimport com.halilibo.richtext.ui.util.detectTapGesturesIf\n\n/**\n * Carries the text style in Composition tree. [Heading], [CodeBlock],\n * [BlockQuote] are designed to change the ongoing [TextStyle] in composition,\n * so that their children can use the modified text style implicitly.\n *\n * LocalTextStyle also exists in Material package but this one is internal\n * to RichText.\n */\ninternal val LocalInternalTextStyle = compositionLocalOf { TextStyle.Default }\n\n/**\n * Carries the content color in Composition tree. Default TextStyle\n * does not have text color specified. It defaults to [Color.Black]\n * in the \"resolve chain\" but Dark Mode is an exception. To also resolve\n * for Dark Mode, content color should be passed to [RichTextScope].\n */\ninternal val LocalInternalContentColor = compositionLocalOf { Color.Black }\n\n/**\n * The current [TextStyle].\n */\ninternal val RichTextScope.currentTextStyle: TextStyle\n  @Composable get() = textStyleProvider()\n\n/**\n * The current content [Color].\n */\ninternal val RichTextScope.currentContentColor: Color\n  @Composable get() = contentColorProvider()\n\n/**\n * Intended for preview composables.\n */\n@Composable\ninternal fun RichTextScope.Text(\n  text: String,\n  modifier: Modifier = Modifier,\n  onTextLayout: (TextLayoutResult) -> Unit = {},\n  overflow: TextOverflow = TextOverflow.Clip,\n  softWrap: Boolean = true,\n  maxLines: Int = Int.MAX_VALUE\n) {\n  val textColor = currentTextStyle.color.takeOrElse { currentContentColor }\n  val style = currentTextStyle.copy(color = textColor)\n\n  BasicText(\n    text = text,\n    modifier = modifier,\n    style = style,\n    onTextLayout = onTextLayout,\n    overflow = overflow,\n    softWrap = softWrap,\n    maxLines = maxLines\n  )\n}\n\n@Composable\ninternal fun RichTextScope.Text(\n  text: AnnotatedString,\n  modifier: Modifier = Modifier,\n  onTextLayout: (TextLayoutResult) -> Unit = {},\n  overflow: TextOverflow = TextOverflow.Clip,\n  softWrap: Boolean = true,\n  maxLines: Int = Int.MAX_VALUE,\n  inlineContent: Map<String, InlineTextContent> = mapOf(),\n) {\n  val textColor = currentTextStyle.color.takeOrElse { currentContentColor }\n  val style = currentTextStyle.copy(color = textColor)\n\n  BasicText(\n    text = text,\n    modifier = modifier,\n    style = style,\n    onTextLayout = onTextLayout,\n    overflow = overflow,\n    softWrap = softWrap,\n    maxLines = maxLines,\n    inlineContent = inlineContent\n  )\n}\n"
  },
  {
    "path": "thirds/halilibo-richtext-ui/src/commonMain/kotlin/com/halilibo/richtext/ui/RichTextScope.kt",
    "content": "@file:Suppress(\"RemoveEmptyParenthesesFromAnnotationEntry\")\n\npackage com.halilibo.richtext.ui\n\nimport androidx.compose.runtime.CompositionLocal\nimport androidx.compose.runtime.Immutable\nimport androidx.compose.runtime.State\n\n/**\n * Scope object for composables that can draw rich text.\n *\n * RichTextScope facilitates a context for RichText elements. It does not\n * behave like a [State] or a [CompositionLocal]. Starting from [BasicRichText],\n * this scope carries information that should not be passed down as a state.\n */\n@Immutable\npublic object RichTextScope\n"
  },
  {
    "path": "thirds/halilibo-richtext-ui/src/commonMain/kotlin/com/halilibo/richtext/ui/RichTextStyle.kt",
    "content": "package com.halilibo.richtext.ui\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.Immutable\nimport androidx.compose.runtime.compositionLocalOf\nimport androidx.compose.ui.unit.TextUnit\nimport androidx.compose.ui.unit.sp\nimport com.halilibo.richtext.ui.string.RichTextStringStyle\n\ninternal val LocalRichTextStyle = compositionLocalOf { RichTextStyle.Default }\ninternal val DefaultParagraphSpacing: TextUnit = 8.sp\n\n/**\n * Configures all formatting attributes for drawing rich text.\n *\n * @param paragraphSpacing The amount of space in between blocks of text.\n * @param headingStyle The [HeadingStyle] that defines how [Heading]s are drawn.\n * @param listStyle The [ListStyle] used to format [FormattedList]s.\n * @param blockQuoteGutter The [BlockQuoteGutter] used to draw [BlockQuote]s.\n * @param codeBlockStyle The [CodeBlockStyle] that defines how [CodeBlock]s are drawn.\n * @param tableStyle The [TableStyle] used to render [Table]s.\n * @param stringStyle The [RichTextStringStyle] used to render\n * [RichTextString][com.halilibo.richtext.ui.string.RichTextString]s\n */\n@Immutable\npublic data class RichTextStyle(\n  val paragraphSpacing: TextUnit? = null,\n  val headingStyle: HeadingStyle? = null,\n  val listStyle: ListStyle? = null,\n  val blockQuoteGutter: BlockQuoteGutter? = null,\n  val codeBlockStyle: CodeBlockStyle? = null,\n  val tableStyle: TableStyle? = null,\n  val infoPanelStyle: InfoPanelStyle? = null,\n  val stringStyle: RichTextStringStyle? = null\n) {\n  public companion object {\n    public val Default: RichTextStyle = RichTextStyle()\n  }\n}\n\npublic fun RichTextStyle.merge(otherStyle: RichTextStyle?): RichTextStyle = RichTextStyle(\n  paragraphSpacing = otherStyle?.paragraphSpacing ?: paragraphSpacing,\n  headingStyle = otherStyle?.headingStyle ?: headingStyle,\n  listStyle = otherStyle?.listStyle ?: listStyle,\n  blockQuoteGutter = otherStyle?.blockQuoteGutter ?: blockQuoteGutter,\n  codeBlockStyle = otherStyle?.codeBlockStyle ?: codeBlockStyle,\n  tableStyle = otherStyle?.tableStyle ?: tableStyle,\n  infoPanelStyle = otherStyle?.infoPanelStyle ?: infoPanelStyle,\n  stringStyle = stringStyle?.merge(otherStyle?.stringStyle) ?: otherStyle?.stringStyle\n)\n\npublic fun RichTextStyle.resolveDefaults(): RichTextStyle = RichTextStyle(\n  paragraphSpacing = paragraphSpacing ?: DefaultParagraphSpacing,\n  headingStyle = headingStyle ?: DefaultHeadingStyle,\n  listStyle = (listStyle ?: ListStyle.Default).resolveDefaults(),\n  blockQuoteGutter = blockQuoteGutter ?: DefaultBlockQuoteGutter,\n  codeBlockStyle = (codeBlockStyle ?: CodeBlockStyle.Default).resolveDefaults(),\n  tableStyle = (tableStyle ?: TableStyle.Default).resolveDefaults(),\n  infoPanelStyle = (infoPanelStyle ?: InfoPanelStyle.Default).resolveDefaults(),\n  stringStyle = (stringStyle ?: RichTextStringStyle.Default).resolveDefaults()\n)\n\n/**\n * The current [RichTextStyle].\n */\npublic val RichTextScope.currentRichTextStyle: RichTextStyle\n  @Composable get() = LocalRichTextStyle.current\n\n/**\n * Sets the [RichTextStyle] for its [children].\n */\n@Composable\npublic fun RichTextScope.WithStyle(\n  style: RichTextStyle?,\n  children: @Composable RichTextScope.() -> Unit\n) {\n  if (style == null) {\n    children()\n  } else {\n    val mergedStyle = LocalRichTextStyle.current.merge(style)\n    CompositionLocalProvider(LocalRichTextStyle provides mergedStyle) {\n      children()\n    }\n  }\n}\n"
  },
  {
    "path": "thirds/halilibo-richtext-ui/src/commonMain/kotlin/com/halilibo/richtext/ui/RichTextThemeConfiguration.kt",
    "content": "package com.halilibo.richtext.ui\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.ProvidableCompositionLocal\nimport androidx.compose.runtime.compositionLocalOf\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.text.TextStyle\n\ninternal typealias TextStyleProvider = @Composable () -> TextStyle\ninternal typealias TextStyleBackProvider = @Composable (TextStyle, @Composable () -> Unit) -> Unit\ninternal typealias ContentColorProvider = @Composable () -> Color\ninternal typealias ContentColorBackProvider = @Composable (Color, @Composable () -> Unit) -> Unit\n\ninternal data class RichTextThemeConfiguration(\n  val textStyleProvider: TextStyleProvider = { LocalInternalTextStyle.current },\n  val textStyleBackProvider: TextStyleBackProvider = { newTextStyle, content ->\n    CompositionLocalProvider(LocalInternalTextStyle provides newTextStyle) {\n      content()\n    }\n  },\n  val contentColorProvider: ContentColorProvider = { LocalInternalContentColor.current },\n  val contentColorBackProvider: ContentColorBackProvider = { newColor, content ->\n    CompositionLocalProvider(LocalInternalContentColor provides newColor) {\n      content()\n    }\n  }\n) {\n  companion object {\n    internal val Default = RichTextThemeConfiguration()\n  }\n}\n\ninternal val LocalRichTextThemeConfiguration: ProvidableCompositionLocal<RichTextThemeConfiguration> =\n  compositionLocalOf { RichTextThemeConfiguration() }\n\n/**\n * Easy access delegations for [RichTextThemeProvider] within [RichTextScope]\n */\ninternal val RichTextScope.textStyleProvider: @Composable () -> TextStyle\n  @Composable get() = LocalRichTextThemeConfiguration.current.textStyleProvider\n\ninternal val RichTextScope.textStyleBackProvider: @Composable (TextStyle, @Composable () -> Unit) -> Unit\n  @Composable get() = LocalRichTextThemeConfiguration.current.textStyleBackProvider\n\ninternal val RichTextScope.contentColorProvider: @Composable () -> Color\n  @Composable get() = LocalRichTextThemeConfiguration.current.contentColorProvider\n\ninternal val RichTextScope.contentColorBackProvider: @Composable (Color, @Composable () -> Unit) -> Unit\n  @Composable get() = LocalRichTextThemeConfiguration.current.contentColorBackProvider"
  },
  {
    "path": "thirds/halilibo-richtext-ui/src/commonMain/kotlin/com/halilibo/richtext/ui/RichTextThemeProvider.kt",
    "content": "package com.halilibo.richtext.ui\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.text.TextStyle\n\n/**\n * Entry point for integrating app's own typography and theme system with RichText.\n *\n * API for this integration is highly influenced by how compose-material theming\n * is designed. RichText library assumes that almost all Theme/Design systems would\n * have composition locals that provide text style downstream.\n *\n * Moreover, text style should not include text color by best practice. Content color\n * exists to figure text color in current context. Light/Dark theming leverages content\n * color to influence not just text but other parts of theming as well.\n *\n * @param textStyleProvider Returns the current text style.\n * @param textStyleBackProvider RichText sometimes updates the current text style\n * e.g. Heading, CodeBlock, and etc. New style should be passed to the outer\n * theming to indicate that there is a need for update, so that children Text\n * composables use the correct styling.\n * @param contentColorProvider Returns the current content color.\n * @param contentColorBackProvider Similar to [textStyleBackProvider], does the same job\n * for content color.\n */\n@Composable\npublic fun RichTextThemeProvider(\n  textStyleProvider: @Composable (() -> TextStyle)? = null,\n  textStyleBackProvider: @Composable ((TextStyle, @Composable () -> Unit) -> Unit)? = null,\n  contentColorProvider: @Composable (() -> Color)? = null,\n  contentColorBackProvider: @Composable ((Color, @Composable () -> Unit) -> Unit)? = null,\n  content: @Composable () -> Unit\n) {\n  CompositionLocalProvider(\n    LocalRichTextThemeConfiguration provides\n        RichTextThemeConfiguration(\n          textStyleProvider = textStyleProvider\n            ?: RichTextThemeConfiguration.Default.textStyleProvider,\n          textStyleBackProvider = textStyleBackProvider\n            ?: RichTextThemeConfiguration.Default.textStyleBackProvider,\n          contentColorProvider = contentColorProvider\n            ?: RichTextThemeConfiguration.Default.contentColorProvider,\n          contentColorBackProvider = contentColorBackProvider\n            ?: RichTextThemeConfiguration.Default.contentColorBackProvider,\n        )\n  ) {\n    content()\n  }\n}\n"
  },
  {
    "path": "thirds/halilibo-richtext-ui/src/commonMain/kotlin/com/halilibo/richtext/ui/SimpleTableLayout.kt",
    "content": "package com.halilibo.richtext.ui\n\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.Immutable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.layout.SubcomposeLayout\nimport androidx.compose.ui.unit.Constraints\nimport androidx.compose.ui.unit.constrain\nimport kotlin.math.roundToInt\n\n/**\n * The offsets of rows and columns of a [SimpleTableLayout], centered inside their spacing.\n *\n * E.g. If a table is given a cell spacing of 2px, then the first column and row offset will each\n * be 1px.\n */\n@Immutable\ninternal data class TableLayoutResult(\n  val rowOffsets: List<Float>,\n  val columnOffsets: List<Float>\n)\n\n/**\n * A simple table that sizes all columns equally.\n *\n * @param cellSpacing The space in between each cell, and between each outer cell and the edge of\n * the table.\n */\n@OptIn(ExperimentalStdlibApi::class)\n@Composable\ninternal fun SimpleTableLayout(\n  columns: Int,\n  rows: List<List<@Composable () -> Unit>>,\n  drawDecorations: (TableLayoutResult) -> Modifier,\n  cellSpacing: Float,\n  modifier: Modifier\n) {\n  SubcomposeLayout(modifier = modifier) { constraints ->\n    val measurables = subcompose(false) {\n      rows.forEach { row ->\n        check(row.size == columns)\n        row.forEach { cell ->\n          cell()\n        }\n      }\n    }\n\n    val rowMeasurables = measurables.chunked(columns)\n    check(rowMeasurables.size == rows.size)\n\n    check(constraints.hasBoundedWidth) { \"Table must have bounded width\" }\n    // Divide the width by the number of columns, then leave room for the padding.\n    val cellSpacingWidth = cellSpacing * (columns + 1)\n    val cellWidth = (constraints.maxWidth - cellSpacingWidth) / columns\n    val cellSpacingHeight = cellSpacing * (rowMeasurables.size + 1)\n    // TODO Handle bounded height constraints.\n    // val cellMaxHeight = if (!constraints.hasBoundedHeight) {\n    //   Float.MAX_VALUE\n    // } else {\n    //   // Divide the height by the number of rows, then leave room for the padding.\n    //   (constraints.maxHeight - cellSpacingHeight) / rowMeasurables.size\n    // }\n    val cellConstraints = Constraints(maxWidth = cellWidth.roundToInt()).constrain(constraints)\n\n    val rowPlaceables = rowMeasurables.map { cellMeasurables ->\n      cellMeasurables.map { cell ->\n        cell.measure(cellConstraints)\n      }\n    }\n    val rowHeights = rowPlaceables.map { row -> row.maxByOrNull { it.height }!!.height }\n\n    val tableWidth = constraints.maxWidth\n    val tableHeight = (rowHeights.sumOf { it } + cellSpacingHeight).roundToInt()\n    layout(tableWidth, tableHeight) {\n      var y = cellSpacing\n      val rowOffsets = mutableListOf<Float>()\n      val columnOffsets = mutableListOf<Float>()\n\n      rowPlaceables.forEachIndexed { rowIndex, cellPlaceables ->\n        rowOffsets += y - cellSpacing / 2f\n        var x = cellSpacing\n\n        cellPlaceables.forEach { cell ->\n          if (rowIndex == 0) {\n            columnOffsets.add(x - cellSpacing / 2f)\n          }\n          cell.place(x.roundToInt(), y.roundToInt())\n          x += cellWidth + cellSpacing\n        }\n\n        if (rowIndex == 0) {\n          // Add the right-most edge.\n          columnOffsets.add(x - cellSpacing / 2f)\n        }\n\n        y += rowHeights[rowIndex] + cellSpacing\n      }\n\n      rowOffsets.add(y - cellSpacing / 2f)\n\n      // Compose and draw the borders.\n      val layoutResult = TableLayoutResult(rowOffsets, columnOffsets)\n      subcompose(true) {\n        Box(modifier = drawDecorations(layoutResult))\n      }.single()\n        .measure(Constraints.fixed(tableWidth, tableHeight))\n        .placeRelative(0, 0)\n    }\n  }\n}\n"
  },
  {
    "path": "thirds/halilibo-richtext-ui/src/commonMain/kotlin/com/halilibo/richtext/ui/Table.kt",
    "content": "@file:Suppress(\"RemoveEmptyParenthesesFromAnnotationEntry\")\n\npackage com.halilibo.richtext.ui\n\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.Immutable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clipToBounds\nimport androidx.compose.ui.draw.drawBehind\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.takeOrElse\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.text.TextStyle\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.TextUnit\nimport androidx.compose.ui.unit.sp\nimport kotlin.math.max\n\n/**\n * Defines the visual style for a [Table].\n *\n * @param headerTextStyle The [TextStyle] used for header rows.\n * @param cellPadding The spacing between the contents of each cell and the borders.\n * @param borderColor The [Color] of the table border.\n * @param borderStrokeWidth The width of the table border.\n */\n@Immutable\npublic data class TableStyle(\n  val headerTextStyle: TextStyle? = null,\n  val cellPadding: TextUnit? = null,\n  val borderColor: Color? = null,\n  val borderStrokeWidth: Float? = null\n) {\n  public companion object {\n    public val Default: TableStyle = TableStyle()\n  }\n}\n\nprivate val DefaultTableHeaderTextStyle = TextStyle(fontWeight = FontWeight.Bold)\nprivate val DefaultCellPadding = 8.sp\nprivate val DefaultBorderColor = Color.Unspecified\nprivate const val DefaultBorderStrokeWidth = 1f\n\ninternal fun TableStyle.resolveDefaults() = TableStyle(\n    headerTextStyle = headerTextStyle ?: DefaultTableHeaderTextStyle,\n    cellPadding = cellPadding ?: DefaultCellPadding,\n    borderColor = borderColor ?: DefaultBorderColor,\n    borderStrokeWidth = borderStrokeWidth ?: DefaultBorderStrokeWidth\n)\n\npublic interface RichTextTableRowScope {\n  public fun row(children: RichTextTableCellScope.() -> Unit)\n}\n\npublic interface RichTextTableCellScope {\n  public fun cell(children: @Composable RichTextScope.() -> Unit)\n}\n\n@Immutable\nprivate data class TableRow(val cells: List<@Composable RichTextScope.() -> Unit>)\n\nprivate class TableBuilder : RichTextTableRowScope {\n  val rows = mutableListOf<RowBuilder>()\n\n  override fun row(children: RichTextTableCellScope.() -> Unit) {\n    rows += RowBuilder().apply(children)\n  }\n}\n\nprivate class RowBuilder : RichTextTableCellScope {\n  var row = TableRow(emptyList())\n\n  override fun cell(children: @Composable RichTextScope.() -> Unit) {\n    row = TableRow(row.cells + children)\n  }\n}\n\n/**\n * Draws a table with an optional header row, and an arbitrary number of body rows.\n *\n * The style of the table is defined by the [RichTextStyle.tableStyle]&nbsp;[TableStyle].\n */\n@OptIn(ExperimentalStdlibApi::class)\n@Composable\npublic fun RichTextScope.Table(\n  modifier: Modifier = Modifier,\n  headerRow: (RichTextTableCellScope.() -> Unit)? = null,\n  bodyRows: RichTextTableRowScope.() -> Unit\n) {\n  val tableStyle = currentRichTextStyle.resolveDefaults().tableStyle!!\n  val contentColor = currentContentColor\n  val header = remember(headerRow) {\n    headerRow?.let { RowBuilder().apply(headerRow).row }\n  }\n  val rows = remember(bodyRows) {\n    TableBuilder().apply(bodyRows).rows.map { it.row }\n  }\n  val columns = remember(header, rows) {\n    max(\n        header?.cells?.size ?: 0,\n        rows.maxByOrNull { it.cells.size }?.cells?.size ?: 0\n    )\n  }\n  val headerStyle = currentTextStyle.merge(tableStyle.headerTextStyle)\n  val cellPadding = with(LocalDensity.current) {\n    tableStyle.cellPadding!!.toDp()\n  }\n  val cellModifier = Modifier\n      .clipToBounds()\n      .padding(cellPadding)\n\n  val styledRows = remember(header, rows, cellModifier) {\n    buildList {\n      header?.let { headerRow ->\n        // Type inference seems to puke without explicit parameters.\n        @Suppress(\"RemoveExplicitTypeArguments\")\n        add(headerRow.cells.map<@Composable RichTextScope.() -> Unit, @Composable () -> Unit> { cell ->\n          @Composable {\n            textStyleBackProvider(headerStyle) {\n              BasicRichText(\n                modifier = cellModifier,\n                children = cell\n              )\n            }\n          }\n        })\n      }\n\n      rows.mapTo(this) { row ->\n        @Suppress(\"RemoveExplicitTypeArguments\")\n        row.cells.map<@Composable RichTextScope.() -> Unit, @Composable () -> Unit> { cell ->\n          @Composable {\n            BasicRichText(\n              modifier = cellModifier,\n              children = cell\n            )\n          }\n        }\n      }\n    }\n  }\n\n  // For some reason borders don't get drawn in the Preview, but they work on-device.\n  SimpleTableLayout(\n      columns = columns,\n      rows = styledRows,\n      cellSpacing = tableStyle.borderStrokeWidth!!,\n      drawDecorations = { layoutResult ->\n        Modifier.drawTableBorders(\n            rowOffsets = layoutResult.rowOffsets,\n            columnOffsets = layoutResult.columnOffsets,\n            borderColor = tableStyle.borderColor!!.takeOrElse { contentColor },\n            borderStrokeWidth = tableStyle.borderStrokeWidth\n        )\n      },\n      modifier = modifier\n  )\n}\n\nprivate fun Modifier.drawTableBorders(\n  rowOffsets: List<Float>,\n  columnOffsets: List<Float>,\n  borderColor: Color,\n  borderStrokeWidth: Float\n) = drawBehind {\n  // Draw horizontal borders.\n  rowOffsets.forEach { position ->\n    drawLine(\n        borderColor,\n        start = Offset(0f, position),\n        end = Offset(size.width, position),\n        borderStrokeWidth\n    )\n  }\n\n  // Draw vertical borders.\n  columnOffsets.forEach { position ->\n    drawLine(\n        borderColor,\n        Offset(position, 0f),\n        Offset(position, size.height),\n        borderStrokeWidth\n    )\n  }\n}"
  },
  {
    "path": "thirds/halilibo-richtext-ui/src/commonMain/kotlin/com/halilibo/richtext/ui/string/InlineContent.kt",
    "content": "@file:Suppress(\"RemoveEmptyParenthesesFromAnnotationEntry\", \"FunctionName\")\n\npackage com.halilibo.richtext.ui.string\n\nimport androidx.compose.foundation.text.InlineTextContent\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.runtime.structuralEqualityPolicy\nimport androidx.compose.ui.layout.Layout\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.text.Placeholder\nimport androidx.compose.ui.text.PlaceholderVerticalAlign\nimport androidx.compose.ui.text.PlaceholderVerticalAlign.Companion.AboveBaseline\nimport androidx.compose.ui.unit.Constraints\nimport androidx.compose.ui.unit.Density\nimport androidx.compose.ui.unit.IntSize\nimport androidx.compose.ui.unit.sp\n\n/**\n * A Composable that can be embedded inline in a [RichTextString] by passing to\n * [RichTextString.Builder.appendInlineContent].\n *\n * @param initialSize Optional function to calculate the initial size of the content. Not specifying\n * this may cause flicker.\n * @param placeholderVerticalAlign Used to specify how a placeholder is vertically aligned within a\n * text line.\n */\npublic class InlineContent(\n  internal val initialSize: (Density.() -> IntSize)? = null,\n  internal val placeholderVerticalAlign: PlaceholderVerticalAlign = AboveBaseline,\n  internal val content: @Composable Density.(alternateText: String) -> Unit\n)\n\n/**\n * Converts a map of [InlineContent]s into a map of [InlineTextContent] that is ready to pass to\n * the core Text composable. Whenever any of the contents resize themselves, or if the map changes,\n * a new map will be returned with updated [Placeholder]s.\n */\n@Composable internal fun manageInlineTextContents(\n  inlineContents: Map<String, InlineContent>,\n  textConstraints: Constraints\n): Map<String, InlineTextContent> {\n  val density = LocalDensity.current\n\n  return inlineContents.mapValues { (_, content) ->\n    reifyInlineContent(\n      content,\n      Constraints(maxWidth = textConstraints.maxWidth, maxHeight = textConstraints.maxHeight),\n      density\n    )\n  }\n}\n\n/**\n * Given an [InlineContent] function, wraps it in a [InlineTextContent] that will allow the content\n * to measure itself inside the enclosing layout's maximum constraints, and automatically return a\n * new [InlineTextContent] whenever the content changes size to update how much space is reserved\n * in the text layout for the content.\n */\n@Composable private fun reifyInlineContent(\n  content: InlineContent,\n  contentConstraints: Constraints,\n  density: Density\n): InlineTextContent {\n  var size by remember {\n    mutableStateOf(\n      content.initialSize?.invoke(density),\n      structuralEqualityPolicy()\n    )\n  }\n\n  with(density) {\n    // If size is null, content hasn't been measured yet, so just draw with zero width for now.\n    // Set the height to 1 em so we can calculate how many pixels in an EM.\n    val placeholder = Placeholder(\n      width = size?.width?.toSp() ?: 0.sp,\n      height = size?.height?.toSp() ?: 1.sp,\n      placeholderVerticalAlign = content.placeholderVerticalAlign\n    )\n\n    return InlineTextContent(placeholder) { alternateText ->\n      Layout(content = { content.content(this, alternateText) }) { measurables, _ ->\n        // Measure the content with the constraints for the parent Text layout, not the actual.\n        // This allows it to determine exactly how large it needs to be so we can update the\n        // placeholder.\n        val contentPlaceable = measurables.singleOrNull()?.measure(contentConstraints)\n          ?: return@Layout layout(0, 0) {}\n\n        if (contentPlaceable.width != size?.width\n          || contentPlaceable.height != size?.height\n        ) {\n          size = IntSize(contentPlaceable.width, contentPlaceable.height)\n        }\n\n        layout(contentPlaceable.width, contentPlaceable.height) {\n          contentPlaceable.place(0, 0)\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "thirds/halilibo-richtext-ui/src/commonMain/kotlin/com/halilibo/richtext/ui/string/RichTextString.kt",
    "content": "@file:Suppress(\"RemoveEmptyParenthesesFromAnnotationEntry\", \"SuspiciousCollectionReassignment\")\n\npackage com.halilibo.richtext.ui.string\n\nimport androidx.compose.foundation.text.appendInlineContent\nimport androidx.compose.runtime.Immutable\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.text.AnnotatedString\nimport androidx.compose.ui.text.LinkAnnotation\nimport androidx.compose.ui.text.LinkInteractionListener\nimport androidx.compose.ui.text.SpanStyle\nimport androidx.compose.ui.text.TextLinkStyles\nimport androidx.compose.ui.text.buildAnnotatedString\nimport androidx.compose.ui.text.font.FontFamily\nimport androidx.compose.ui.text.font.FontStyle\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.BaselineShift\nimport androidx.compose.ui.text.style.TextDecoration\nimport androidx.compose.ui.unit.sp\nimport com.halilibo.richtext.ui.DefaultCodeBlockBackgroundColor\nimport com.halilibo.richtext.ui.string.RichTextString.Builder\nimport com.halilibo.richtext.ui.string.RichTextString.Format\nimport com.halilibo.richtext.ui.string.RichTextString.Format.Bold\nimport com.halilibo.richtext.ui.string.RichTextString.Format.Code\nimport com.halilibo.richtext.ui.string.RichTextString.Format.Companion.FormatAnnotationScope\nimport com.halilibo.richtext.ui.string.RichTextString.Format.Italic\nimport com.halilibo.richtext.ui.string.RichTextString.Format.Link\nimport com.halilibo.richtext.ui.string.RichTextString.Format.Strikethrough\nimport com.halilibo.richtext.ui.string.RichTextString.Format.Subscript\nimport com.halilibo.richtext.ui.string.RichTextString.Format.Superscript\nimport com.halilibo.richtext.ui.string.RichTextString.Format.Underline\nimport com.halilibo.richtext.ui.util.randomUUID\nimport kotlin.LazyThreadSafetyMode.NONE\n\n/** Copied from inline content. */\n@PublishedApi\ninternal const val REPLACEMENT_CHAR: String = \"\\uFFFD\"\n\n/**\n * Defines the [SpanStyle]s that are used for various [RichTextString] formatting directives.\n */\n@Immutable\npublic class RichTextStringStyle(\n  public val boldStyle: SpanStyle? = null,\n  public val italicStyle: SpanStyle? = null,\n  public val underlineStyle: SpanStyle? = null,\n  public val strikethroughStyle: SpanStyle? = null,\n  public val subscriptStyle: SpanStyle? = null,\n  public val superscriptStyle: SpanStyle? = null,\n  public val codeStyle: SpanStyle? = null,\n  public val linkStyle: TextLinkStyles? = null\n) {\n  internal fun merge(otherStyle: RichTextStringStyle?): RichTextStringStyle {\n    if (otherStyle == null) return this\n    return RichTextStringStyle(\n      boldStyle = boldStyle.merge(otherStyle.boldStyle),\n      italicStyle = italicStyle.merge(otherStyle.italicStyle),\n      underlineStyle = underlineStyle.merge(otherStyle.underlineStyle),\n      strikethroughStyle = strikethroughStyle.merge(otherStyle.strikethroughStyle),\n      subscriptStyle = subscriptStyle.merge(otherStyle.subscriptStyle),\n      superscriptStyle = superscriptStyle.merge(otherStyle.superscriptStyle),\n      codeStyle = codeStyle.merge(otherStyle.codeStyle),\n      linkStyle = linkStyle?.merge(otherStyle.linkStyle) ?: otherStyle.linkStyle\n    )\n  }\n\n  internal fun resolveDefaults(): RichTextStringStyle =\n    RichTextStringStyle(\n      boldStyle = boldStyle ?: Bold.DefaultStyle,\n      italicStyle = italicStyle ?: Italic.DefaultStyle,\n      underlineStyle = underlineStyle ?: Underline.DefaultStyle,\n      strikethroughStyle = strikethroughStyle ?: Strikethrough.DefaultStyle,\n      subscriptStyle = subscriptStyle ?: Subscript.DefaultStyle,\n      superscriptStyle = superscriptStyle ?: Superscript.DefaultStyle,\n      codeStyle = codeStyle ?: Code.DefaultStyle,\n      linkStyle = linkStyle ?: Link.DefaultStyle\n    )\n\n  override fun equals(other: Any?): Boolean {\n    if (this === other) return true\n    if (other !is RichTextStringStyle) return false\n\n    if (boldStyle != other.boldStyle) return false\n    if (italicStyle != other.italicStyle) return false\n    if (underlineStyle != other.underlineStyle) return false\n    if (strikethroughStyle != other.strikethroughStyle) return false\n    if (subscriptStyle != other.subscriptStyle) return false\n    if (superscriptStyle != other.superscriptStyle) return false\n    if (codeStyle != other.codeStyle) return false\n    if (linkStyle != other.linkStyle) return false\n\n    return true\n  }\n\n  override fun hashCode(): Int {\n    var result = boldStyle?.hashCode() ?: 0\n    result = 31 * result + (italicStyle?.hashCode() ?: 0)\n    result = 31 * result + (underlineStyle?.hashCode() ?: 0)\n    result = 31 * result + (strikethroughStyle?.hashCode() ?: 0)\n    result = 31 * result + (subscriptStyle?.hashCode() ?: 0)\n    result = 31 * result + (superscriptStyle?.hashCode() ?: 0)\n    result = 31 * result + (codeStyle?.hashCode() ?: 0)\n    result = 31 * result + (linkStyle?.hashCode() ?: 0)\n    return result\n  }\n\n  override fun toString(): String {\n    return \"RichTextStringStyle(boldStyle=$boldStyle, \" +\n        \"italicStyle=$italicStyle, \" +\n        \"underlineStyle=$underlineStyle, \" +\n        \"strikethroughStyle=$strikethroughStyle, \" +\n        \"subscriptStyle=$subscriptStyle, \" +\n        \"superscriptStyle=$superscriptStyle, \" +\n        \"codeStyle=$codeStyle, \" +\n        \"linkStyle=$linkStyle)\"\n  }\n\n  public companion object {\n    public val Default: RichTextStringStyle = RichTextStringStyle()\n\n    private fun SpanStyle?.merge(otherStyle: SpanStyle?): SpanStyle? =\n      this?.merge(otherStyle) ?: otherStyle\n  }\n}\n\n/**\n * Convenience function for creating a [RichTextString] using a [Builder].\n */\npublic inline fun richTextString(builder: Builder.() -> Unit): RichTextString =\n  Builder().apply(builder)\n    .toRichTextString()\n\n/**\n * A special type of [AnnotatedString] that is formatted using higher-level directives that are\n * configured using a [RichTextStringStyle].\n */\n@Immutable\npublic class RichTextString internal constructor(\n  private val taggedString: AnnotatedString,\n  internal val formatObjects: Map<String, Any>\n) {\n  private val length: Int get() = taggedString.length\n  public val text: String get() = taggedString.text\n\n  public operator fun plus(other: RichTextString): RichTextString =\n    Builder(length + other.length).run {\n      append(this@RichTextString)\n      append(other)\n      toRichTextString()\n    }\n\n  internal fun toAnnotatedString(\n    style: RichTextStringStyle,\n    contentColor: Color\n  ): AnnotatedString =\n    buildAnnotatedString {\n      append(taggedString)\n\n      // Get all of our format annotations.\n      val tags = taggedString.getStringAnnotations(FormatAnnotationScope, 0, taggedString.length)\n      // And apply their actual SpanStyles to the string.\n      tags.forEach { range ->\n        val format = Format.findTag(range.item, formatObjects) ?: return@forEach\n        format.getAnnotation(style, contentColor)\n          ?.let { annotation ->\n            if (annotation is SpanStyle) {\n              addStyle(annotation, range.start, range.end)\n            } else if (annotation is LinkAnnotation.Url) {\n              addLink(annotation, range.start, range.end)\n            }\n          }\n      }\n    }\n\n  internal fun getInlineContents(): Map<String, InlineContent> =\n    formatObjects.asSequence()\n      .mapNotNull { (tag, format) ->\n        tag.removePrefix(\"inline:\")\n          // If no prefix was found then we ignore it.\n          .takeUnless { it === tag }\n          ?.let {\n            Pair(it, format as InlineContent)\n          }\n      }\n      .toMap()\n\n  override fun equals(other: Any?): Boolean {\n    if (this === other) return true\n    if (other !is RichTextString) return false\n\n    if (taggedString != other.taggedString) return false\n    if (formatObjects != other.formatObjects) return false\n\n    return true\n  }\n\n  override fun hashCode(): Int {\n    var result = taggedString.hashCode()\n    result = 31 * result + formatObjects.hashCode()\n    return result\n  }\n\n  public sealed class Format(private val simpleTag: String? = null) {\n\n    /**\n     * This function should either return [SpanStyle] or [LinkAnnotation.Url]. In future releases of\n     * Compose these classes will have a common supertype called `AnnotatedString.Annotation`. Then\n     * we can stop returning [Any].\n     */\n    internal open fun getAnnotation(\n      richTextStyle: RichTextStringStyle,\n      contentColor: Color\n    ): Any? = null\n\n    public object Italic : Format(\"italic\") {\n      internal val DefaultStyle = SpanStyle(fontStyle = FontStyle.Italic)\n      override fun getAnnotation(\n        richTextStyle: RichTextStringStyle,\n        contentColor: Color\n      ) = richTextStyle.italicStyle\n    }\n\n    public object Bold : Format(simpleTag = \"foo\") {\n      internal val DefaultStyle = SpanStyle(fontWeight = FontWeight.Bold)\n      override fun getAnnotation(\n        richTextStyle: RichTextStringStyle,\n        contentColor: Color\n      ) = richTextStyle.boldStyle\n    }\n\n    public object Underline : Format(\"underline\") {\n      internal val DefaultStyle = SpanStyle(textDecoration = TextDecoration.Underline)\n      override fun getAnnotation(\n        richTextStyle: RichTextStringStyle,\n        contentColor: Color\n      ) = richTextStyle.underlineStyle\n    }\n\n    public object Strikethrough : Format(\"strikethrough\") {\n      internal val DefaultStyle = SpanStyle(textDecoration = TextDecoration.LineThrough)\n      override fun getAnnotation(\n        richTextStyle: RichTextStringStyle,\n        contentColor: Color\n      ) = richTextStyle.strikethroughStyle\n    }\n\n    public object Subscript : Format(\"subscript\") {\n      internal val DefaultStyle = SpanStyle(\n        baselineShift = BaselineShift(-0.2f),\n        // TODO this should be relative to current font size\n        fontSize = 10.sp\n      )\n\n      override fun getAnnotation(\n        richTextStyle: RichTextStringStyle,\n        contentColor: Color\n      ) = richTextStyle.subscriptStyle\n    }\n\n    public object Superscript : Format(\"superscript\") {\n      internal val DefaultStyle = SpanStyle(\n        baselineShift = BaselineShift.Superscript,\n        fontSize = 10.sp\n      )\n\n      override fun getAnnotation(\n        richTextStyle: RichTextStringStyle,\n        contentColor: Color\n      ) = richTextStyle.superscriptStyle\n    }\n\n    public object Code : Format(\"code\") {\n      internal val DefaultStyle = SpanStyle(\n        fontFamily = FontFamily.Monospace,\n        fontWeight = FontWeight.Medium,\n        background = DefaultCodeBlockBackgroundColor\n      )\n\n      override fun getAnnotation(\n        richTextStyle: RichTextStringStyle,\n        contentColor: Color\n      ) = richTextStyle.codeStyle\n    }\n\n    public class Link(\n      public val destination: String,\n      public val linkInteractionListener: LinkInteractionListener? = null\n    ) : Format() {\n      override fun getAnnotation(\n        richTextStyle: RichTextStringStyle,\n        contentColor: Color\n      ) = LinkAnnotation.Url(\n        url = destination,\n        styles = richTextStyle.linkStyle,\n        linkInteractionListener = linkInteractionListener\n      )\n\n      override fun equals(other: Any?): Boolean {\n        if (this === other) return true\n        if (other !is Link) return false\n\n        if (destination != other.destination) return false\n        if (linkInteractionListener != other.linkInteractionListener) return false\n\n        return true\n      }\n\n      override fun hashCode(): Int {\n        var result = destination.hashCode()\n        result = 31 * result + linkInteractionListener.hashCode()\n        return result\n      }\n\n      override fun toString(): String {\n        return \"Link(destination='$destination', linkInteractionListener=$linkInteractionListener)\"\n      }\n\n      internal companion object {\n        val DefaultStyle = TextLinkStyles(\n          style = SpanStyle(color = Color.Blue),\n          hoveredStyle = SpanStyle(\n            textDecoration = TextDecoration.Underline,\n            color = Color.Blue\n          )\n        )\n      }\n    }\n\n    internal fun registerTag(tags: MutableMap<String, Any>): String {\n      simpleTag?.let { return it }\n      val uuid = randomUUID()\n      tags[uuid] = this\n      return \"format:$uuid\"\n    }\n\n    internal companion object {\n      val FormatAnnotationScope = Format::class.qualifiedName!!\n\n      // For some reason, if this isn't lazy, Bold will always be null. Is Compose messing up static\n      // initialization order?\n      private val simpleTags by lazy(NONE) {\n        listOf(Bold, Italic, Underline, Strikethrough, Subscript, Superscript, Code)\n      }\n\n      fun findTag(\n        tag: String,\n        tags: Map<String, Any>\n      ): Format? {\n        val stripped = tag.removePrefix(\"format:\")\n        return if (stripped === tag) {\n          // If the original string was returned, it means the string did not have the prefix.\n          simpleTags.firstOrNull { it.simpleTag == tag }\n        } else {\n          tags[stripped] as? Format\n        }\n      }\n    }\n  }\n\n  public class Builder(capacity: Int = 16) {\n    private val builder = AnnotatedString.Builder(capacity)\n    private val formatObjects = mutableMapOf<String, Any>()\n\n    public fun addFormat(\n      format: Format,\n      start: Int,\n      end: Int\n    ) {\n      val tag = format.registerTag(formatObjects)\n      builder.addStringAnnotation(FormatAnnotationScope, tag, start, end)\n    }\n\n    public fun pushFormat(format: Format): Int {\n      val tag = format.registerTag(formatObjects)\n      return builder.pushStringAnnotation(FormatAnnotationScope, tag)\n    }\n\n    public fun pop(): Unit = builder.pop()\n\n    public fun pop(index: Int): Unit = builder.pop(index)\n\n    public fun append(text: String): Unit = builder.append(text)\n\n    public fun append(text: RichTextString) {\n      builder.append(text.taggedString)\n      formatObjects.putAll(text.formatObjects)\n    }\n\n    public fun appendInlineContent(\n      alternateText: String = REPLACEMENT_CHAR,\n      content: InlineContent\n    ) {\n      val tag = randomUUID()\n      formatObjects[\"inline:$tag\"] = content\n      builder.appendInlineContent(tag, alternateText)\n    }\n\n    /**\n     * Provides access to the underlying builder, which can be used to add arbitrary formatting,\n     * including mixed with formatting from this Builder.\n     */\n    public fun <T> withAnnotatedString(block: AnnotatedString.Builder.() -> T): T = builder.block()\n\n    public fun toRichTextString(): RichTextString =\n      RichTextString(\n        builder.toAnnotatedString(),\n        formatObjects.toMap()\n      )\n  }\n}\n\npublic inline fun Builder.withFormat(\n  format: Format,\n  block: Builder.() -> Unit\n) {\n  val index = pushFormat(format)\n  block()\n  pop(index)\n}\n\nprivate fun TextLinkStyles.merge(other: TextLinkStyles?): TextLinkStyles {\n  return if (other == null) {\n    TextLinkStyles()\n  } else {\n    TextLinkStyles(\n      style = this.style?.merge(other.style) ?: other.style,\n      focusedStyle = this.style?.merge(other.focusedStyle) ?: other.focusedStyle,\n      hoveredStyle = this.style?.merge(other.hoveredStyle) ?: other.hoveredStyle,\n      pressedStyle = this.style?.merge(other.pressedStyle) ?: other.pressedStyle,\n    )\n  }\n}"
  },
  {
    "path": "thirds/halilibo-richtext-ui/src/commonMain/kotlin/com/halilibo/richtext/ui/string/Text.kt",
    "content": "package com.halilibo.richtext.ui.string\n\nimport androidx.compose.foundation.layout.BoxWithConstraints\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.text.AnnotatedString\nimport androidx.compose.ui.text.TextLayoutResult\nimport androidx.compose.ui.text.style.TextOverflow\nimport com.halilibo.richtext.ui.RichTextScope\nimport com.halilibo.richtext.ui.Text\nimport com.halilibo.richtext.ui.currentContentColor\nimport com.halilibo.richtext.ui.currentRichTextStyle\nimport com.halilibo.richtext.ui.string.RichTextString.Format\n\n/**\n * Renders a [RichTextString] as created with [richTextString].\n *\n * @sample com.halilibo.richtext.ui.previews.TextPreview\n */\n@Composable\npublic fun RichTextScope.Text(\n  text: RichTextString,\n  modifier: Modifier = Modifier,\n  onTextLayout: (TextLayoutResult) -> Unit = {},\n  softWrap: Boolean = true,\n  overflow: TextOverflow = TextOverflow.Clip,\n  maxLines: Int = Int.MAX_VALUE\n) {\n  val style = currentRichTextStyle.stringStyle\n  val contentColor = currentContentColor\n  val annotated = remember(text, style, contentColor) {\n    val resolvedStyle = (style ?: RichTextStringStyle.Default).resolveDefaults()\n    text.toAnnotatedString(resolvedStyle, contentColor)\n  }\n\n  val inlineContents = remember(text) { text.getInlineContents() }\n\n  if (inlineContents.isEmpty()) {\n    Text(\n      text = annotated,\n      onTextLayout = onTextLayout,\n      softWrap = softWrap,\n      overflow = overflow,\n      maxLines = maxLines\n    )\n  } else {\n    // expensive constraints reading path\n    BoxWithConstraints(modifier = modifier) {\n      val inlineTextContents = manageInlineTextContents(\n        inlineContents = inlineContents,\n        textConstraints = constraints\n      )\n\n      Text(\n        text = annotated,\n        onTextLayout = onTextLayout,\n        inlineContent = inlineTextContents,\n        softWrap = softWrap,\n        overflow = overflow,\n        maxLines = maxLines,\n      )\n    }\n  }\n}\n\nprivate fun AnnotatedString.getConsumableAnnotations(textFormatObjects: Map<String, Any>, offset: Int): Sequence<Format.Link> =\n  getStringAnnotations(Format.FormatAnnotationScope, offset, offset)\n    .asSequence()\n    .mapNotNull {\n      Format.findTag(\n        it.item,\n        textFormatObjects\n      ) as? Format.Link\n    }\n"
  },
  {
    "path": "thirds/halilibo-richtext-ui/src/commonMain/kotlin/com/halilibo/richtext/ui/util/ConditionalTapGestureDetector.kt",
    "content": "@file:OptIn(ExperimentalComposeUiApi::class)\n\npackage com.halilibo.richtext.ui.util\n\nimport androidx.compose.foundation.gestures.GestureCancellationException\nimport androidx.compose.foundation.gestures.PressGestureScope\nimport androidx.compose.foundation.gestures.awaitEachGesture\nimport androidx.compose.foundation.gestures.awaitFirstDown\nimport androidx.compose.ui.ExperimentalComposeUiApi\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.input.pointer.AwaitPointerEventScope\nimport androidx.compose.ui.input.pointer.PointerEventPass\nimport androidx.compose.ui.input.pointer.PointerEventTimeoutCancellationException\nimport androidx.compose.ui.input.pointer.PointerInputChange\nimport androidx.compose.ui.input.pointer.PointerInputScope\nimport androidx.compose.ui.input.pointer.changedToUp\nimport androidx.compose.ui.input.pointer.isOutOfBounds\nimport androidx.compose.ui.platform.ViewConfiguration\nimport androidx.compose.ui.unit.Density\nimport androidx.compose.ui.util.fastAll\nimport androidx.compose.ui.util.fastAny\nimport androidx.compose.ui.util.fastForEach\nimport kotlinx.coroutines.coroutineScope\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.sync.Mutex\n\nprivate val NoPressGesture: suspend PressGestureScope.(Offset) -> Unit = { }\n\n/**\n * If predicate returns true: detects tap, double-tap, and long press gestures and calls [onTap],\n * [onDoubleTap], and [onLongPress], respectively, when detected. [onPress] is called when the press\n * is detected and the [PressGestureScope.tryAwaitRelease] and [PressGestureScope.awaitRelease]\n * can be used to detect when pointers have released or the gesture was canceled.\n * The first pointer down and final pointer up are consumed, and in the\n * case of long press, all changes after the long press is detected are consumed.\n *\n * Each function parameter receives an [Offset] representing the position relative to the containing\n * element. The [Offset] can be outside the actual bounds of the element itself meaning the numbers\n * can be negative or larger than the element bounds if the touch target is smaller than the\n * [ViewConfiguration.minimumTouchTargetSize].\n *\n * When [onDoubleTap] is provided, the tap gesture is detected only after\n * the [ViewConfiguration.doubleTapMinTimeMillis] has passed and [onDoubleTap] is called if the\n * second tap is started before [ViewConfiguration.doubleTapTimeoutMillis]. If [onDoubleTap] is not\n * provided, then [onTap] is called when the pointer up has been received.\n *\n * After the initial [onPress], if the pointer moves out of the input area, the position change\n * is consumed, or another gesture consumes the down or up events, the gestures are considered\n * canceled. That means [onDoubleTap], [onLongPress], and [onTap] will not be called after a\n * gesture has been canceled.\n *\n * If the first down event is consumed somewhere else, the entire gesture will be skipped,\n * including [onPress].\n */\npublic suspend fun PointerInputScope.detectTapGesturesIf(\n  predicate: (Offset) -> Boolean = { true },\n  onDoubleTap: ((Offset) -> Unit)? = null,\n  onLongPress: ((Offset) -> Unit)? = null,\n  onPress: suspend PressGestureScope.(Offset) -> Unit = NoPressGesture,\n  onTap: ((Offset) -> Unit)? = null\n): Unit = coroutineScope {\n  // special signal to indicate to the sending side that it shouldn't intercept and consume\n  // cancel/up events as we're only require down events\n  val pressScope =\n    PressGestureScopeImpl(this@detectTapGesturesIf)\n\n  awaitEachGesture {\n    val down = awaitFirstDown()\n    if (!predicate(down.position)) {\n      pressScope.reset()\n      return@awaitEachGesture\n    }\n    down.consume()\n    pressScope.reset()\n    if (onPress !== NoPressGesture) launch {\n      pressScope.onPress(down.position)\n    }\n    val longPressTimeout = onLongPress?.let {\n      viewConfiguration.longPressTimeoutMillis\n    } ?: (Long.MAX_VALUE / 2)\n    var upOrCancel: PointerInputChange? = null\n    try {\n      // wait for first tap up or long press\n      upOrCancel = withTimeout(longPressTimeout) {\n        waitForUpOrCancellation()\n      }\n      if (upOrCancel == null) {\n        pressScope.cancel() // tap-up was canceled\n      } else {\n        upOrCancel.consume()\n        pressScope.release()\n      }\n    } catch (_: PointerEventTimeoutCancellationException) {\n      onLongPress?.invoke(down.position)\n      consumeUntilUp()\n      pressScope.release()\n    }\n\n    if (upOrCancel != null) {\n      // tap was successful.\n      if (onDoubleTap == null) {\n        onTap?.invoke(upOrCancel.position) // no need to check for double-tap.\n      } else {\n        // check for second tap\n        val secondDown = awaitSecondDown(upOrCancel)\n\n        if (secondDown == null) {\n          onTap?.invoke(upOrCancel.position) // no valid second tap started\n        } else {\n          // Second tap down detected\n          pressScope.reset()\n          if (onPress !== NoPressGesture) {\n            launch { pressScope.onPress(secondDown.position) }\n          }\n\n          try {\n            // Might have a long second press as the second tap\n            withTimeout(longPressTimeout) {\n              val secondUp = waitForUpOrCancellation()\n              if (secondUp != null) {\n                secondUp.consume()\n                pressScope.release()\n                onDoubleTap(secondUp.position)\n              } else {\n                pressScope.cancel()\n                onTap?.invoke(upOrCancel.position)\n              }\n            }\n          } catch (e: PointerEventTimeoutCancellationException) {\n            // The first tap was valid, but the second tap is a long press.\n            // notify for the first tap\n            onTap?.invoke(upOrCancel.position)\n\n            // notify for the long press\n            onLongPress?.invoke(secondDown.position)\n            consumeUntilUp()\n            pressScope.release()\n          }\n        }\n      }\n    }\n  }\n}\n\n/**\n * Consumes all pointer events until nothing is pressed and then returns. This method assumes\n * that something is currently pressed.\n */\nprivate suspend fun AwaitPointerEventScope.consumeUntilUp() {\n  do {\n    val event = awaitPointerEvent()\n    event.changes.fastForEach { it.consume() }\n  } while (event.changes.fastAny { it.pressed })\n}\n\n/**\n * Waits for [ViewConfiguration.doubleTapTimeoutMillis] for a second press event. If a\n * second press event is received before the time out, it is returned or `null` is returned\n * if no second press is received.\n */\nprivate suspend fun AwaitPointerEventScope.awaitSecondDown(\n  firstUp: PointerInputChange\n): PointerInputChange? = withTimeoutOrNull(viewConfiguration.doubleTapTimeoutMillis) {\n  val minUptime = firstUp.uptimeMillis + viewConfiguration.doubleTapMinTimeMillis\n  var change: PointerInputChange\n  // The second tap doesn't count if it happens before DoubleTapMinTime of the first tap\n  do {\n    change = awaitFirstDown()\n  } while (change.uptimeMillis < minUptime)\n  change\n}\n\n/**\n * Reads events until all pointers are up or the gesture was canceled. The gesture\n * is considered canceled when a pointer leaves the event region, a position change\n * has been consumed or a pointer down change event was consumed in the [PointerEventPass.Main]\n * pass. If the gesture was not canceled, the final up change is returned or `null` if the\n * event was canceled.\n */\nprivate suspend fun AwaitPointerEventScope.waitForUpOrCancellation(): PointerInputChange? {\n  while (true) {\n    val event = awaitPointerEvent(PointerEventPass.Main)\n    if (event.changes.fastAll { it.changedToUp() }) {\n      // All pointers are up\n      return event.changes[0]\n    }\n\n    if (event.changes.fastAny {\n        it.isConsumed || it.isOutOfBounds(size, extendedTouchPadding)\n      }\n    ) {\n      return null // Canceled\n    }\n\n    // Check for cancel by position consumption. We can look on the Final pass of the\n    // existing pointer event because it comes after the Main pass we checked above.\n    val consumeCheck = awaitPointerEvent(PointerEventPass.Final)\n    if (consumeCheck.changes.fastAny { it.isConsumed }) {\n      return null\n    }\n  }\n}\n\n/**\n * [detectTapGesturesIf]'s implementation of [PressGestureScope].\n */\nprivate class PressGestureScopeImpl(\n  density: Density\n) : PressGestureScope, Density by density {\n  private var isReleased = false\n  private var isCanceled = false\n  private val mutex = Mutex(locked = false)\n\n  /**\n   * Called when a gesture has been canceled.\n   */\n  fun cancel() {\n    isCanceled = true\n    mutex.unlock()\n  }\n\n  /**\n   * Called when all pointers are up.\n   */\n  fun release() {\n    isReleased = true\n    mutex.unlock()\n  }\n\n  /**\n   * Called when a new gesture has started.\n   */\n  fun reset() {\n    mutex.tryLock() // If tryAwaitRelease wasn't called, this will be unlocked.\n    isReleased = false\n    isCanceled = false\n  }\n\n  override suspend fun awaitRelease() {\n    if (!tryAwaitRelease()) {\n      throw GestureCancellationException(\"The press gesture was canceled.\")\n    }\n  }\n\n  override suspend fun tryAwaitRelease(): Boolean {\n    if (!isReleased && !isCanceled) {\n      mutex.lock()\n    }\n    return isReleased\n  }\n}\n"
  },
  {
    "path": "thirds/halilibo-richtext-ui/src/commonMain/kotlin/com/halilibo/richtext/ui/util/UUID.kt",
    "content": "package com.halilibo.richtext.ui.util\n\ninternal expect fun randomUUID(): String"
  },
  {
    "path": "thirds/halilibo-richtext-ui/src/iosMain/kotlin/com/halilibo/richtext/ui/CodeBlock.ios.kt",
    "content": "package com.halilibo.richtext.ui\n\nimport androidx.compose.foundation.horizontalScroll\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\n\n@Composable\ninternal actual fun RichTextScope.CodeBlockLayout(\n  wordWrap: Boolean,\n  children: @Composable RichTextScope.(Modifier) -> Unit\n) {\n  if (!wordWrap) {\n    val scrollState = rememberScrollState()\n    children(Modifier.horizontalScroll(scrollState))\n  } else {\n    children(Modifier)\n  }\n}"
  },
  {
    "path": "thirds/halilibo-richtext-ui/src/iosMain/kotlin/com/halilibo/richtext/ui/util/UUID.ios.kt",
    "content": "package com.halilibo.richtext.ui.util\n\nimport platform.Foundation.NSUUID\n\ninternal actual fun randomUUID(): String {\n  return NSUUID().UUIDString()\n}\n"
  }
]