Compare commits

..

No commits in common. "master" and "2.2.2+49" have entirely different histories.

273 changed files with 15576 additions and 61817 deletions

View File

@ -1,87 +0,0 @@
name: Bug report
description: Create a report to help us address issues you are facing
title: "[Bug] "
labels: [Bug]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to make us better!
- type: checkboxes
id: duplication
attributes:
label:
options:
- label: This issue is not duplicated with any other open or closed issues
required: true
- type: textarea
id: description
attributes:
label: Describe the bug
description: A clear and concise description of what the bug is
placeholder: |
Example:
App crashes on startup every time after changing settings.
validations:
required: true
- type: textarea
id: expected
attributes:
label: Expected behavior
description: A clear and concise description of what you expected to happen
placeholder: |
Example:
App started normally, everything worked fine.
validations:
required: true
- type: textarea
id: reproduce
attributes:
label: Steps to reproduce
description: Steps to reproduce the bug
placeholder: |
Example:
1. Change "HyperNet Server" to "127.0.1" in "Network" settings
2. Restart the app
3. Crash
validations:
required: true
- type: textarea
id: environment
attributes:
label: Device information
description: Provide details about your system environment
placeholder: |
Example:
Device: Google Pixel 8 Pro
System: Baklava (BP22.250124.009)
Version*: 2.3.2
validations:
required: true
- type: textarea
id: screenshots
attributes:
label: Screenshots
description: If applicable, add screenshots to help explain your problem
placeholder: |
Example:
setting_items.jpg
crash_screen.jpg
validations:
required: false
- type: textarea
id: additional
attributes:
label: Additional context
description: Add any other context about the problem here
placeholder: |
Crash report or other useful informations
validations:
required: false

View File

@ -1,83 +0,0 @@
name: 问题反馈
description: 提交 Bug 或其它问题的反馈
title: "[Bug] 标题"
labels: [Bug]
body:
- type: markdown
attributes:
value: |
非常感谢,你将要提交的反馈会让我们变得更好!
- type: checkboxes
id: duplication
attributes:
label:
options:
- label: 我已经搜索并确认此 issue 不与其它任何 issue 重复
required: true
- type: textarea
id: description
attributes:
label: 问题描述
description: 清楚且详细地描述你遇到的 Bug 或问题
placeholder: |
发生了什么?生动地描述你所看到的一切
validations:
required: true
- type: textarea
id: expected
attributes:
label: 期望表现
description: 清楚且详细地描述你期望发生的事
placeholder: |
什么功能应该正常运行,运行后会有什么结果
什么界面应该正常显示,应该会显示什么内容
validations:
required: true
- type: textarea
id: reproduce
attributes:
label: 复现步骤
description: 能够复现问题的每一步
placeholder: |
1. 尽可能详细地描述每一步
2. 更改的设置、添加的好友...
3. 这里也可以描述你看到的界面
validations:
required: true
- type: textarea
id: environment
attributes:
label: 环境/版本
description: 提供运行时的环境信息
placeholder: |
示例:
设备型号: Google Pixel 8 Pro
系统板本: Baklava (BP22.250124.009)
程序版本: 2.3.2
validations:
required: true
- type: textarea
id: screenshots
attributes:
label: 屏幕截图/录制
description: 提供截屏或录屏来更好地描述问题
placeholder: |
错误显示的界面/崩溃时的界面、先前改动的设置
validations:
required: false
- type: textarea
id: additional
attributes:
label: 更多信息
description: 任何与问题有关且有用的信息
placeholder: |
崩溃报告、日志,或是你的用户名
validations:
required: false

View File

@ -1,5 +0,0 @@
blank_issues_enabled: true
contact_links:
- name: Solsynth Releases
url: https://files.solsynth.dev/production01/solian
about: Another place to download released apps

View File

@ -1,59 +0,0 @@
name: Feature request
description: Suggest features you want to add or suggest to modify existing features
title: "[Feature] "
labels: [Feature]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to make us better!
- type: checkboxes
id: duplication
attributes:
label:
options:
- label: This issue is not duplicated with any other open or closed issues
required: true
- type: textarea
id: description
attributes:
label: Describe the feature
description: A clear and concise description of what the feature is
placeholder: |
Example:
A Quick Settings tile to start the service, long press to launch the app.
validations:
required: true
- type: textarea
id: reasons
attributes:
label: Reason for adding
description: Explain why this feature would be useful to you
placeholder: |
Example:
Start the service quickly from the Quick Settings tile and save lots of time.
validations:
required: true
- type: textarea
id: examples
attributes:
label: Example(s)
description: Post screenshots/drawings/links/etc of the feature request, or proof-of-concept images about the feature
placeholder: |
Example:
shazam_toggle.jpg
nekobox_switch.jpg
validations:
required: false
- type: textarea
id: additional
attributes:
label: Additional context
description: Add any other context about the feature here
validations:
required: false

View File

@ -1,49 +0,0 @@
name: 功能建议
description: 提出你想要添加或更改的功能
title: "[Feature] 标题"
labels: [Feature]
body:
- type: markdown
attributes:
value: |
非常感谢,你将要提交的请求会让我们变得更好!
- type: checkboxes
id: duplication
attributes:
label:
options:
- label: 我已经搜索并确认此 issue 不与其它任何 issue 重复
required: true
- type: textarea
id: description
attributes:
label: 功能描述
description: 清楚且详细地描述要添加/更改后的功能
validations:
required: true
- type: textarea
id: reasons
attributes:
label: 添加/更改理由
description: 解释为什么要这样做,对用户有什么好处
validations:
required: true
- type: textarea
id: examples
attributes:
label: 功能示例
description: 相似/已存在功能的截图,或画出大致的界面
validations:
required: false
- type: textarea
id: additional
attributes:
label: 更多信息
description: 任何与功能有关且有用的信息,或已存在功能的代码/仓库
validations:
required: false

View File

@ -38,44 +38,4 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: build-output-windows
path: build/windows/x64/runner/Release
build-linux:
runs-on: ubuntu-latest
steps:
- name: Clone repository
uses: actions/checkout@v4
- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
channel: stable
- run: |
sudo apt-get update -y
sudo apt-get install -y ninja-build libgtk-3-dev
sudo apt-get install -y libmpv-dev mpv
sudo apt-get install -y libayatana-appindicator3-dev
sudo apt-get install -y keybinder-3.0
sudo apt-get install -y libnotify-dev
sudo apt-get install -y libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
sudo apt-get install -y gstreamer-1.0
- run: flutter pub get
- run: flutter build linux
- name: Archive production artifacts
uses: actions/upload-artifact@v4
with:
name: build-output-linux
path: build/linux/x64/release/bundle
- name: Build AppImage
run: |
rm -r Solian.AppDir | true
mkdir Solian.AppDir
cp -r build/linux/x64/release/bundle/* Solian.AppDir
cp -r buildtools/appimage_config/* Solian.AppDir
cp assets/icon/icon-light-radius.png Solian.AppDir
sudo chmod +x buildtools/appimagetool-x86_64.AppImage
sudo chmod +x Solian.AppDir/AppRun
./buildtools/appimagetool-x86_64.AppImage Solian.AppDir
- name: Archive production artifacts
uses: actions/upload-artifact@v4
with:
name: build-output-linux-appimage
path: './*.AppImage*'
path: build/windows/x64/runner/Release

View File

@ -1,12 +1,12 @@
{
"sync": {
"region": "solian",
"region": "solian-next",
"configPath": "roadsign.toml"
},
"deployments": [
{
"region": "solian",
"site": "solian-web",
"region": "solian-next",
"site": "solian-next-web",
"path": "build/web"
}
]

View File

@ -1,34 +0,0 @@
# Code of Conduct
Welcome to the Solar Network / HyperNet project!
We're welcome for any contribution, from bug reports to feature requests to code contributions.
To get started, start from fork the repository.
## Project Structure
The current repository you're visiting is the front-end project for the Solar Network project. It's built by Flutter and also manages all feature requests and issues reports in this repository.
The backend of the Solar Network is written in Go and is a microservices app. The code is stored separately in different repositories. They're linked in the README.MD, you can have a look and try to contribute if you want.
## Commit Messages
We're using the gitmoji to clarify the reason and changes of the commit. To learn more about gitmoji, visit https://gitmoji.dev
## Translations & Localization
We're not accepting translation and localization improvements, or fixes on the GitHub or Solsynth Git Repository. If you want to contribute to those, please head to our Weblate: https://i18n.solsynth.dev. You will able to sign up / in via your Solar Network Account (Solarpass)
## New Features
To contribute new features, please create an issue or mention the feature you want in our official development chat channel. You should discuss the feature with us and the community first. You shouldn't just create a Pull Request for the feature you want, it will not be merged.
## Bug Reports / Ask for help
Read the error message, check for the update (including pre-releases), and wiki before creating an issue. At the same time, be respectful and don't argue with our developers and contributors in the development chat or GitHub issue. Otherwise your issue may got deleted and your Solar Network Account may got a strike.
-----------
We appreciate every single commit you contributed. Let's work together and create a better Solar Network!

View File

@ -1,68 +0,0 @@
# Solar Network
![](https://solsynth.dev/_next/static/media/alpha.e779a584.webp)
Hello there! Welcome to the main repository of the HyperNet (also known as the Solar Network). The code here is mainly about the front-end app (also known as Solian). But you can still post issues here to get help and request new features!
## Sub Projects
HyperNet, the Solar Network is a microservices project in which the backends are stored in separate repositories. Here is a simple index for it.
- The Core, Gateway: [Nexus](https://github.com/Solsynth/HyperNet.Nexus)
- The Auth Service: [Passport](https://github.com/Solsynth/HyperNet.Passport)
- The Posting Service: [Interactive](https://github.com/Solsynth/HyperNet.Interactive)
- The Messaging Service: [Messaging](https://github.com/Solsynth/HyperNet.Messaging)
- The Wallet Service: [Wallet](https://github.com/Solsynth/HyperNet.Wallet)
- The Crawler: [Reader](https://github.com/Solsynth/HyperNet.Reader)
- The Attachments Service: [Paperclip](https://github.com/Solsynth/HyperNet.Paperclip)
- Some others may not be listed, you can search in the organization with `HyperNet.` It's the prefix of all HyperNet projects.
## Tech Stack
For those people who want to know the tech stack of this project, the front-end was built by Flutter, which provides cross-platform ability.
The backend was built in Go and PostgreSQL with our very own microservice framework included in the nexus.
If you want to contribute to the project, learn more about the [Code of Conduct](./CODE_OF_CONDUCT.md).
## Getting Started
The content below will lead you to the world of Solar Network.
### For Normal Users
1. Go to the Github Releases page, and download the latest release / pre-release according to your platform.
- **What's the difference between stable and pre-release?** The pre-release is untested by the other users and includes the new cutting-edge features, usually the pre-release is the feature drop. At the same time, due to we're not doing the API versioning, some breaking changes may break the stable release, so use the pre-release one instead.
2. Create an account on the Solar Network
3. Go to your email inbox to confirm your registration
4. Start exploring!
### For Developers
To make the Solar Network App run in debug mode on your machine, you need to install the flutter development environment, for more environments, head to https://flutter.dev.
For the Linux platform, you need to install those extra development libs:
```bash
sudo apt-get update -y
sudo apt-get install -y ninja-build libgtk-3-dev
sudo apt-get install -y libmpv-dev mpv
sudo apt-get install -y libayatana-appindicator3-dev
sudo apt-get install -y keybinder-3.0
sudo apt-get install -y libnotify-dev
sudo apt-get install -y libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
sudo apt-get install -y gstreamer-1.0
```
Then, use the flutter run for the app running in debug mode.
```bash
flutter pub get
```
If you want to build the release version, use the flutter build command. Learn more from the flutter docs.
```bash
flutter build <platform>
```

2
android/.gitignore vendored
View File

@ -11,5 +11,3 @@ GeneratedPluginRegistrant.java
key.properties
**/*.keystore
**/*.jks
app/.cxx

View File

@ -10,15 +10,10 @@ plugins {
}
dependencies {
// implementation('org.jitsi.react:jitsi-meet-sdk:11.1.1') { transitive = true }
// implementation 'com.facebook.fresco:webpsupport:2.6.0'
// implementation 'com.facebook.fresco:animated-webp:2.6.0'
// implementation 'com.facebook.react:react-android:0.75.5'
// implementation 'com.facebook.react:hermes-android:0.75.5'
implementation 'com.google.android.material:material:1.12.0'
implementation 'androidx.glance:glance:1.1.1'
implementation 'androidx.glance:glance-appwidget:1.1.1'
implementation 'androidx.compose.foundation:foundation-layout-android:1.7.8'
implementation 'androidx.compose.foundation:foundation-layout-android:1.7.6'
implementation 'com.google.code.gson:gson:2.10.1'
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
implementation 'io.coil-kt.coil3:coil-compose:3.0.4'
@ -78,10 +73,8 @@ android {
}
release {
signingConfig = signingConfigs.release
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}

View File

@ -1,96 +0,0 @@
-keepclassmembers class kotlin.Metadata { *; }
-keep class dev.solsynth.solian.** { *; }
-keep public class dev.solsynth.solian.data.** { public *; }
-keepclassmembers class dev.solsynth.solian.data.** { *; }
-keepattributes *Annotation*
-keepattributes Signature
-keepattributes EnclosingMethod
-keep class com.google.gson.** { *; }
-keepclassmembers class * {
@com.google.gson.annotations.SerializedName <fields>;
}
-dontwarn com.facebook.imagepipeline.nativecode.WebpTranscoder
-keep,allowobfuscation @interface com.facebook.proguard.annotations.DoNotStrip
-keep,allowobfuscation @interface com.facebook.proguard.annotations.KeepGettersAndSetters
# Do not strip any method/class that is annotated with @DoNotStrip
-keep @com.facebook.proguard.annotations.DoNotStrip class *
-keepclassmembers class * {
@com.facebook.proguard.annotations.DoNotStrip *;
}
-keep @com.facebook.proguard.annotations.DoNotStripAny class * {
*;
}
-keepclassmembers @com.facebook.proguard.annotations.KeepGettersAndSetters class * {
void set*(***);
*** get*();
}
-keep class * implements com.facebook.react.bridge.JavaScriptModule { *; }
-keep class * implements com.facebook.react.bridge.NativeModule { *; }
-keepclassmembers,includedescriptorclasses class * { native <methods>; }
-keepclassmembers class * { @com.facebook.react.uimanager.annotations.ReactProp <methods>; }
-keepclassmembers class * { @com.facebook.react.uimanager.annotations.ReactPropGroup <methods>; }
-dontwarn com.facebook.react.**
-keep,includedescriptorclasses class com.facebook.react.bridge.** { *; }
-keep,includedescriptorclasses class com.facebook.react.turbomodule.core.** { *; }
# hermes
-keep class com.facebook.jni.** { *; }
# okio
-keep class sun.misc.Unsafe { *; }
-dontwarn java.nio.file.*
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
-dontwarn okio.**
# yoga
-keep,allowobfuscation @interface com.facebook.yoga.annotations.DoNotStrip
-keep @com.facebook.yoga.annotations.DoNotStrip class *
-keepclassmembers class * {
@com.facebook.yoga.annotations.DoNotStrip *;
}
# WebRTC
-keep class org.webrtc.** { *; }
-dontwarn org.chromium.build.BuildHooksAndroid
# Jisti Meet SDK
-keep class org.jitsi.meet.** { *; }
-keep class org.jitsi.meet.sdk.** { *; }
# We added the following when we switched minifyEnabled on. Probably because we
# ran the app and hit problems...
-keep class com.facebook.react.bridge.CatalystInstanceImpl { *; }
-keep class com.facebook.react.bridge.ExecutorToken { *; }
-keep class com.facebook.react.bridge.JavaScriptExecutor { *; }
-keep class com.facebook.react.bridge.ModuleRegistryHolder { *; }
-keep class com.facebook.react.bridge.ReadableType { *; }
-keep class com.facebook.react.bridge.queue.NativeRunnable { *; }
-keep class com.facebook.react.devsupport.** { *; }
-dontwarn com.facebook.react.devsupport.**
-dontwarn com.google.appengine.**
-dontwarn com.squareup.okhttp.**
-dontwarn javax.servlet.**
# ^^^ We added the above when we switched minifyEnabled on.
# Rule to avoid build errors related to SVGs.
-keep public class com.horcrux.svg.** {*;}
# https://github.com/facebook/fresco/issues/2638
-keep public class com.facebook.imageutils.** {
public *;
}

View File

@ -1,4 +1,4 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />
<uses-permission android:name="android.permission.INTERNET" />
@ -9,25 +9,19 @@
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="29" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<application
tools:replace="android:label"
android:label="Solian"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:requestLegacyExternalStorage="true">
<meta-data
android:name="flutterEmbedding"
android:value="2" />
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleInstance"
android:launchMode="singleTask"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"

View File

@ -54,7 +54,7 @@ class CheckInWidget : GlanceAppWidget() {
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
.registerTypeAdapter(Instant::class.java, InstantAdapter())
.create()
val resultTierSymbols = listOf("Bad", "Poor", "Medium", "Good", "Great")
val resultTierSymbols = listOf("大凶", "", "中平", "", "大吉")
val prefs = currentState.preferences
val checkInRaw: String? = prefs.getString("pas_check_in_record", null)
@ -120,7 +120,7 @@ class CheckInWidget : GlanceAppWidget() {
}
Text(
text = "You haven't divined today",
text = "You haven't checked in today",
style = TextStyle(fontSize = 15.sp, color = GlanceTheme.colors.onSurface)
)
}

14
android/app/src/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1,14 @@
-keepclassmembers class kotlin.Metadata { *; }
-keep class dev.solsynth.solian.** { *; }
-keep public class dev.solsynth.solian.data.** { public *; }
-keepclassmembers class dev.solsynth.solian.data.** { *; }
-keepattributes *Annotation*
-keepattributes Signature
-keepattributes EnclosingMethod
-keep class com.google.gson.** { *; }
-keepclassmembers class * {
@com.google.gson.annotations.SerializedName <fields>;
}

View File

@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-all.zip

View File

@ -10,22 +10,18 @@ pluginManagement {
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
maven {
url "https://github.com/jitsi/jitsi-maven-repository/raw/master/releases"
}
google()
mavenCentral()
gradlePluginPortal()
maven { url 'https://www.jitpack.io' }
}
}
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version '8.9.1' apply false
id "com.android.application" version '8.7.3' apply false
// START: FlutterFire Configuration
id "com.google.gms.google-services" version "4.4.2" apply false
id "com.google.firebase.crashlytics" version "3.0.3" apply false
id "com.google.gms.google-services" version "4.3.15" apply false
id "com.google.firebase.crashlytics" version "2.8.1" apply false
// END: FlutterFire Configuration
id "org.jetbrains.kotlin.android" version "1.8.22" apply false
}

View File

@ -1,11 +0,0 @@
meta {
name: Trigger Fediverse Scan
type: http
seq: 1
}
post {
url: {{endpoint}}/cgi/co/admin/fediverse
body: none
auth: inherit
}

View File

@ -1,11 +0,0 @@
meta {
name: Check Status
type: http
seq: 1
}
get {
url: {{endpoint}}/directory/status
body: none
auth: none
}

View File

@ -1,11 +0,0 @@
meta {
name: List Services
type: http
seq: 2
}
get {
url: {{endpoint}}/directory/services
body: none
auth: none
}

View File

@ -12,9 +12,9 @@ post {
body:json {
{
"alias": "Deadge",
"name": "Dead",
"attachment_id": "pcbFd0u4zgdM39HM",
"pack_id": 4
"alias": "AteChip",
"name": "Cat ate chips",
"attachment_id": "d0b692cc64054463",
"pack_id": 2
}
}

View File

@ -1,11 +0,0 @@
meta {
name: Get Sticker Packs
type: http
seq: 3
}
get {
url: {{endpoint}}/cgi/uc/stickers/packs
body: none
auth: none
}

View File

@ -1,15 +0,0 @@
meta {
name: Get Stickers
type: http
seq: 4
}
get {
url: {{endpoint}}/cgi/uc/stickers?take=10
body: none
auth: none
}
params:query {
take: 10
}

View File

@ -1,18 +0,0 @@
meta {
name: Deal Abuse Report
type: http
seq: 3
}
put {
url: {{endpoint}}/cgi/id/reports/abuse/6/status
body: json
auth: inherit
}
body:json {
{
"status": "rejected",
"message": "Not a good reason"
}
}

View File

@ -15,10 +15,12 @@ body:json {
"client_id": "{{third_client_id}}",
"client_secret":"{{third_client_tk}}",
"type": "general",
"subject": "关于迁移服务器完成的提示",
"subtitle": "一条来自 Solar Network 团队的运营信息",
"content": "我们已经将所有用户数据迁移到新版服务器,刚刚发布新的 DNS因为部分 DNS 缓存的影响。可能更改不会生效,可以使用 nslookup / ping 检查解析地址是否未 8. 开头,您可以主动刷新 DNS。谢谢",
"metadata": {},
"subject": "Merry Christmas!",
"subtitle": "一条来自 Solar Network 团队的信息",
"content": "今天是 12 月 25 日 (UTC+8),小羊祝您圣诞快乐 🎄",
"metadata": {
"image": "6EqsYQwmFRCkbmhR"
},
"priority": 10
}
}

View File

@ -1,23 +0,0 @@
meta {
name: Developer Notify One User
type: http
seq: 2
}
post {
url: {{endpoint}}/cgi/id/dev/notify/328
body: json
auth: inherit
}
body:json {
{
"client_id": "{{third_client_id}}",
"client_secret":"{{third_client_tk}}",
"type": "general",
"subject": "处理该发布者 @vedal987 的决定",
"subtitle": "一条来自 Solar Network 客户支持的信息",
"content": "您的发布者违反了我们用户协议中的「禁止冒充他人」的相关条例,经管理决定,将相关内容隐藏。冒充他人的判定无论作者是否有主观意志,只要造成了误解我们就有责任处理。希望您能理解,本次决定未作出任何帐号相关的连带处罚。",
"priority": 10
}
}

View File

@ -1,20 +0,0 @@
meta {
name: Give Punishment
type: http
seq: 4
}
post {
url: {{endpoint}}/cgi/id/punishments
body: json
auth: inherit
}
body:json {
{
"reason": "吹哨管理条例 / 滥用吹哨功能,累积三次复核无效吹哨。处以禁用吹哨功能 30 天。",
"type": 1,
"perm_nodes": {"FlagPost":false},
"account_id": 5
}
}

View File

@ -1,11 +0,0 @@
meta {
name: List News Sources
type: http
seq: 3
}
get {
url: {{endpoint}}/cgi/re/well-known/sources
body: none
auth: inherit
}

View File

@ -1,17 +0,0 @@
meta {
name: List News
type: http
seq: 2
}
get {
url: {{endpoint}}/cgi/re/news?take=10&offset=0&source=shadiao
body: none
auth: none
}
params:query {
take: 10
offset: 0
source: shadiao
}

View File

@ -1,15 +0,0 @@
meta {
name: Trigger Scan News
type: http
seq: 1
}
post {
url: {{endpoint}}/cgi/re/admin/scan
body: json
auth: inherit
}
body:json {
{}
}

View File

@ -1,20 +0,0 @@
meta {
name: Create Order
type: http
seq: 1
}
post {
url: {{endpoint}}/cgi/wa/orders
body: json
auth: none
}
body:json {
{
"client_id": "highland-mc",
"client_secret": "(3^DLAvo3v",
"remark": "我是秦始皇,现在被困香港,现在 SN 转我 500 源点,帮助我回到咸阳,到时候封你为太监一职。",
"amount": 500
}
}

View File

@ -1,21 +0,0 @@
meta {
name: Create Transaction
type: http
seq: 3
}
post {
url: {{endpoint}}/cgi/wa/transactions
body: json
auth: none
}
body:json {
{
"client_id": "alphabot",
"client_secret": "_uR0sVnHTh",
"remark": "新年红包",
"amount": 150,
"payee_id": 18
}
}

View File

@ -1,20 +0,0 @@
meta {
name: Get Order
type: http
seq: 2
}
get {
url: {{endpoint}}/cgi/wa/orders/4
body: none
auth: none
}
body:json {
{
"client_id": "highland-mc",
"client_secret": "(3^DLAvo3v",
"remark": "我是秦始皇,现在被困香港,现在 SN 转我 500 源点,帮助我回到咸阳,到时候封你为太监一职。",
"amount": 500
}
}

View File

@ -1,20 +0,0 @@
meta {
name: Get Transaction
type: http
seq: 4
}
get {
url: {{endpoint}}/cgi/wa/transactions/67
body: none
auth: inherit
}
body:json {
{
"client_id": "highland-mc",
"client_secret": "(3^DLAvo3v",
"remark": "我是秦始皇,现在被困香港,现在 SN 转我 500 源点,帮助我回到咸阳,到时候封你为太监一职。",
"amount": 500
}
}

View File

@ -1,11 +0,0 @@
meta {
name: Run Database Maintenance
type: http
seq: 1
}
post {
url: {{endpoint}}/wt/maintenance/database
body: none
auth: inherit
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 509 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 228 KiB

View File

@ -17,17 +17,12 @@
"screenAccountProfileEdit": "Edit Profile",
"screenAbuseReport": "Abuse Reports",
"screenSettings": "Settings",
"screenAccountSettings": "Account Settings",
"screenFactorSettings": "Auth Factors",
"screenAccountWallet": "Wallet",
"screenNews": "News",
"screenAlbum": "Album",
"screenChat": "Chat",
"screenChatManage": "Edit Channel",
"screenChatNew": "New Channel",
"screenRealm": "Realm",
"screenRealmManage": "Edit Realm",
"screenRealmDiscovery": "Realm Discovery",
"screenRealmNew": "New Realm",
"screenNotification": "Notification",
"screenPostSearch": "Search Posts",
@ -108,18 +103,8 @@
},
"loginEnterPassword": "Enter the code",
"loginSuccess": "Logged in as {}",
"authFactorDelete": "Delete Auth Factor",
"authFactorDeleteDescription": "Are you sure you want delete auth factor {}?",
"authFactorPassword": "Password",
"authFactorPasswordDescription": "The password you set when you registered.",
"authFactorEmail": "Email verification code",
"authFactorEmailDescription": "An one-time code sent to the email address you set when you registered.",
"authFactorTOTP": "Time-based OTP",
"authFactorTOTPDescription": "A one-time code generated by a TOTP authenticator such as Google Authenticator or Authy.",
"authFactorInAppNotify": "In-app notification",
"authFactorInAppNotifyDescription": "A one-time code sent via in-app notification.",
"authFactorAdd": "Add a factor",
"authFactorAddSubtitle": "Provide another way to login your account.",
"accountIntroTitle": "Hello there!",
"accountIntroSubtitle": "Pick an option below to get started.",
"accountLogout": "Logout",
@ -128,14 +113,8 @@
"accountLogoutConfirm": "You will need to re-enter your account password, even if you have already done so. This is required to login again.",
"accountPublishers": "Your publishers",
"accountPublishersSubtitle": "Manage your publish identities.",
"accountSettings": "Account Settings",
"accountSettingsSubtitle": "Manage your account and make it yours.",
"accountProfileEdit": "Edit Profile",
"accountProfileEdit": "Edit your profile",
"accountProfileEditSubtitle": "Make your Solarpass account more looks like you.",
"accountWallet": "Wallet",
"accountWalletSubtitle": "View your balance and transactions.",
"factorSettings": "Auth Factors",
"factorSettingsSubtitle": "Manage your authentication factors.",
"accountProfileEditApplied": "Profile modification applied.",
"publishersNew": "New Publisher",
"publisherNewSubtitle": "Create a new publisher identity.",
@ -153,19 +132,11 @@
"publisherRunBy": "Run by {}",
"fieldPublisherBelongToRealm": "Belongs to",
"fieldPublisherBelongToRealmUnset": "Unset Publisher Belongs to Realm",
"writePost": "Compose",
"postTypeStory": "Story",
"postTypeArticle": "Article",
"postTypeQuestion": "Question",
"postTypeVideo": "Video",
"writePostTypeStory": "Post a story",
"writePostTypeArticle": "Write an article",
"writePostTypeQuestion": "Ask a question",
"writePostTypeVideo": "Post a video",
"fieldPostPublisher": "Post publisher",
"fieldPostContent": "What happened?!",
"fieldPostTitle": "Title",
"fieldPostQuestionReward": "Answer Rewards (Source Points)",
"fieldPostDescription": "Description",
"fieldPostTags": "Tags",
"fieldPostCategories": "Categories",
@ -175,9 +146,9 @@
"postPosted": "Post has been posted.",
"postPublishedAt": "Published At",
"postPublishedUntil": "Published Until",
"postEditingNotice": "You're about to editing a post that posted by {}.",
"postReplyingNotice": "You're about to reply to a post that posted by {}.",
"postRepostingNotice": "You're about to repost a post that posted by {}.",
"postEditingNotice": "You're about to editing a post that posted {}.",
"postReplyingNotice": "You're about to reply to a post that posted {}.",
"postRepostingNotice": "You're about to repost a post that posted {}.",
"postReact": "React",
"postReactions": "Reactions of Post",
"postReactionUpvote": {
@ -207,20 +178,9 @@
"one": "{} comment",
"other": "{} comments"
},
"postCommentExpand": "Show comments",
"settingsAppearance": "Appearance",
"settingsCustomFonts": "Custom Fonts",
"settingsCustomFontsDescription": "Set custom fonts for the application.",
"settingsCustomFontFamily": "Custom Font Family",
"settingsCustomFontFamilyHint": "Use comma to separate fonts, higher priority comes first",
"settingsCustomFontApplied": "Custom font has been applied.",
"settingsDisplayLanguage": "Display Language",
"settingsDisplayLanguageDescription": "Set the application language.",
"settingsDisplayLanguageSystem": "Follow System",
"settingsAppBarTransparent": "Transparent App Bar",
"settingsAppBarTransparentDescription": "Enable transparent effect for the app bar.",
"settingsDrawerPreferCollapse": "Prefer Drawer Collapse",
"settingsDrawerPreferCollapseDescription": "Make the drawer to collapse even when the screen is wide enough.",
"settingsBackgroundImage": "Background Image",
"settingsBackgroundImageDescription": "Set the background image that will be applied globally.",
"settingsBackgroundImageClear": "Clear Existing Background Image",
@ -231,13 +191,6 @@
"settingsColorSchemeDescription": "Set the application primary color.",
"settingsColorSeed": "Color Seed",
"settingsColorSeedDescription": "Select one of the present color schemes.",
"settingsFeatures": "Features",
"settingsNotifyWithHaptic": "Haptic when Notified",
"settingsNotifyWithHapticDescription": "Vibrate lightly when a new notification appears in the foreground.",
"settingsExpandPostLink": "Expand Post Link",
"settingsExpandPostLinkDescription": "Expand the post link in the post list.",
"settingsExpandChatLink": "Expand Chat Link",
"settingsExpandChatLinkDescription": "Expand the chat link in the chat list.",
"settingsNetwork": "Network",
"settingsNetworkServer": "HyperNet Server",
"settingsNetworkServerDescription": "Set the HyperNet server address, choose ours or build your own.",
@ -256,15 +209,12 @@
"settingsMisc": "Misc",
"settingsMiscAbout": "About",
"settingsMiscAboutDescription": "View the version information of Solian.",
"settingsAccountLanguage": "Account Language",
"settingsAccountLanguageDescription": "Set the language for email, notification, and other account-related content.",
"sensitiveContent": "Sensitive Content",
"sensitiveContentCollapsed": "Sensitive content has been collapsed.",
"sensitiveContentDescription": "This content has been marked as sensitive, and may not be suitable for all viewers.",
"sensitiveContentReveal": "Reveal",
"serverConnecting": "Connecting...",
"serverDisconnected": "Connection Lost",
"serverConnected": "Connected",
"serverConnecting": "Connecting to server...",
"serverDisconnected": "Lost connection from server",
"fieldChatAlias": "Channel Alias",
"fieldChatAliasHint": "The unique channel alias within the site, used to represent the channel in URL, leave blank to auto generate. Should be URL-Safe.",
"fieldChatName": "Name",
@ -338,14 +288,11 @@
"fieldAttachmentRandomId": "Random ID",
"fieldAttachmentAlt": "Alternative text",
"addAttachmentFromAlbum": "Add from album",
"addAttachmentFromFiles": "Add from files",
"addAttachmentFromClipboard": "Paste file",
"addAttachmentFromCameraPhoto": "Take photo",
"addAttachmentFromCameraVideo": "Take video",
"addAttachmentFromRandomId": "Link via RID",
"attachmentDetailInfo": "Attachment details",
"attachmentPastedImage": "Pasted Image",
"attachmentInsertedImage": "Inserted Image",
"attachmentInsertLink": "Insert Link",
"attachmentSetAsPostThumbnail": "Set as post thumbnail",
"attachmentUnsetAsPostThumbnail": "Unset as post thumbnail",
@ -432,7 +379,7 @@
"callMessageEnded": "Call lasted {}",
"callMessageStarted": "Call started",
"dailyCheckIn": "Check In",
"dailyCheckInNone": "You haven't divined today",
"dailyCheckInNone": "You haven't checked in today",
"dailyCheckAction": "Check in right now!",
"dailyCheckDetail": "Can't understand the symbol? Master, help me understand it!",
"dailyCheckDetailTitle": "{}'s fortune details",
@ -524,13 +471,8 @@
"accountBirthday": "Born on {}",
"accountBadge": "Badge",
"accountCheckInNoRecords": "No check-in records",
"badgeCompanyStaff": "Staff",
"badgeCompanyStaff": "Solsynth Staff",
"badgeSiteMigration": "Solar Network Native",
"badgeCommunitySurvey": "Survey Participant",
"badgeCommunityVerified": "Verified User",
"badgeCommunityContributor": "Great Contributor",
"badgeSiteAnniversary": "Anniversary",
"badgeUserBirthday": "Birthday",
"accountStatus": "Status",
"accountStatusOnline": "Online",
"accountStatusOffline": "Offline",
@ -543,7 +485,6 @@
"attachmentSaved": "Saved to album",
"attachmentSavedDesktop": "Saved to Downloads folder",
"openInAlbum": "Open in album",
"openInBrowser": "Open in browser",
"postAbuseReport": "Report Post",
"postAbuseReportDescription": "Report posts that violate our user agreement and community guidelines to help us improve the content on Solar Network. Please describe how this post violates the relevant rules. Do not include any sensitive information. We will process your report within 24 hours.",
"abuseReport": "Abuse Report",
@ -566,7 +507,6 @@
"termAcceptNextWithAgree": "By clicking the \"Next\", it means you agree to our terms and its updates.",
"unauthorized": "Unauthorized",
"unauthorizedDescription": "Login to explore the entire Solar Network.",
"projectDetail": "Project Details",
"serviceStatus": "Service Status",
"termRelated": "Related Terms",
"appDetails": "App Details",
@ -580,15 +520,11 @@
"postImageShareAds": "Explore posts on the Solar Network",
"postShare": "Share",
"postShareImage": "Share via Image",
"postGetInsight": "Get Insight",
"postGetInsightTitle": "AI Insight",
"postGetInsightDescription": "AI may make mistakes, check important information.",
"appInitializing": "Initializing",
"poweredBy": "Powered by {}",
"shareIntent": "Share",
"shareIntentDescription": "What do you want to do with the content you are sharing?",
"shareIntentPostStory": "Post a Story",
"shareIntentSendChannel": "Share to Channel",
"updateAvailable": "Update Available",
"updateOngoing": "Updating, please wait...",
"custom": "Custom",
@ -601,8 +537,6 @@
"colorSchemeWhite": "White",
"colorSchemeBlack": "Black",
"colorSchemeApplied": "Color scheme has been applied, may need restart the app to take effect.",
"postFeaturedComment": "Featured Comment",
"postCategory": "Category",
"postCategoryTechnology": "Technology",
"postCategoryGaming": "Gaming",
"postCategoryLife": "Life",
@ -613,357 +547,5 @@
"postCategoryKnowledge": "Knowledge",
"postCategoryLiterature": "Literature",
"postCategoryFunny": "Funny",
"postCategoryUncategorized": "Uncategorized",
"newsAllSources": "All News",
"newsReadingProviderSwap": "Swap",
"newsReadingFromReader": "You're reading from HyperNet.Reader",
"newsReadingFromOriginal": "You're reading the original article",
"newsDisclaimer": "This article is fetched from the Internet, we do not guarantee its authenticity, please judge for yourself. All content in this article belongs to the original author.",
"newsToday": "Today's News",
"totpPostSetup": "One More Thing",
"totpPostSetupDescription": "Scan the QR Code below with Google Authenticator, Microsoft Authenticator, 1Password, Authy, Bitwarden or any of kind of authenticator app which supports TOTP.",
"totpNeverShare": "Never share this QR Code",
"needHelp": "Need Help?",
"needHelpLaunch": "Check out our Goatpedia!",
"walletCreate": "Create a Wallet",
"walletCreateSubtitle": "Create a wallet to start using Source Points",
"walletCreatePassword": "Set a payment password for your new wallet below",
"walletCurrencyShort": "SRC",
"walletCurrency": {
"one": "{} Source Point",
"other": "{} Source Points"
},
"aiThinkingProcess": "AI Thinking Process",
"accountSettingsApplied": "Account settings have been applied.",
"trayMenuExit": "Exit",
"postQuestionUnanswered": "Unanswered Question",
"postQuestionUnansweredWithReward": "Unanswered Question, reward source points {}",
"postQuestionAnswered": "Answered Question",
"postQuestionAnswerSelect": "Select as Answer",
"postQuestionAnswerTitle": "Selected Question",
"postQuestionAnswerSelected": "Answer has been selected, reward has been applied.",
"postVideoUpload": "Upload Video",
"realmJoin": "Join Realm",
"realmCommunityHint": "This realm is a community realm, you can freely join.",
"realmCommunityPublicChannelsHint": "The public channels in this realm",
"realmCommunityPublishersHint": "The publishers in this realm",
"realmJoined": "Joined realm {}.",
"join": "Join",
"pollEditorNew": "New Poll",
"pollEditorEdit": "Edit Poll",
"pollEditorDelete": "Delete Poll",
"pollEditorDeleteDescription": "Are you sure you want to delete this poll? This operation is irreversible.",
"pollEditorUnlink": "Unlink Poll",
"pollOptionAdd": "Add Option",
"pollOptionName": "Option Name",
"pollLinkExisting": "Link existing poll",
"pollAnswered": "Answered the poll.",
"pollVotes": {
"one": "{} vote",
"other": "{} votes"
},
"publisherDelete": "Delete Publisher {}",
"publisherDeleteDescription": "Are you sure you want to delete this publisher? This operation is irreversible.",
"channelIsPublic": "Public Channel",
"channelIsPublicDescription": "The channel is public, anyone can join.",
"channelIsCommunity": "Community Channel",
"channelIsCommunityDescription": "Currently, community channel has nothing special yet.",
"realmIsPublic": "Public Realm",
"realmIsPublicDescription": "The realm is public, anyone can join.",
"realmIsCommunity": "Community Realm",
"realmIsCommunityDescription": "Community realm will be displayed on the discover page.",
"realmLeave": "Leave Realm",
"realmLeaveDescription": "Leave the current realm and delete the realm's identity.",
"checkInResultTier1": "Worst",
"checkInResultTier2": "Worse",
"checkInResultTier3": "Normal",
"checkInResultTier4": "Better",
"checkInResultTier5": "Best",
"flagPostAction": "Flag the Post",
"flagPost": "Flag objectionable content",
"flagPostDescription": "If flagged users takes 50% or more of the views, the post will be collapsed. You cannot revoke the action.",
"flaggedPost": "Post has been flagged.",
"postViews": {
"zero": "No views",
"one": "{} view",
"other": "{} views"
},
"attachmentBillingUploaded": "Used space",
"attachmentBillingDiscount": "Free space",
"attachmentBillingRatio": "Usage",
"attachmentBillingHint": "Sliding Window Pricing®\nFees will only apply if the size of the file uploaded within 24 hours exceeds the free space.",
"postThumbnail": "Post Thumbnail",
"accountRealms": "Realms",
"postInGlobal": "Global",
"postInGlobalDescription": "Do not link this post with any realm.",
"postChannelGlobal": "Global",
"postChannelFriends": "Friends",
"postChannelFollowing": "Following",
"postChannelRealm": "Realms",
"postFilterReset": "Reset Filter",
"postFilterResetDescription": "Clear filter and show all posts.",
"postFilterWithCategory": "Viewing posts in {}",
"databaseSize": "Database Size",
"databaseDelete": "Delete Database",
"databaseDeleteDescription": "Remove the database on your local disk, the content will be fetched from server again.",
"databaseDeleted": "The local database has been deleted.",
"settingsEnablePushNotifications": "Enable Push Notifications",
"settingsEnablePushNotificationsDescription": "Re-enable and request permission to receive push notifications. Just in case it didn't run automatically.",
"settingsEnabledPushNotifications": "Push notification has been enabled.",
"screenStickers": "Stickers",
"stickersDiscovery": "Discovery",
"stickersOwned": "Owned",
"stickersCreated": "Created",
"stickersAdd": "Add Sticker Pack",
"stickersAdded": "Sticker pack has been added.",
"add": "Add",
"stickersRemoved": "Sticker pack has been removed, you can add it again anytime.",
"stickersReload": "Reload Stickers",
"stickersReloadDescription": "Reload stickers from the server, update the sticker picker.",
"stickersReloaded": "Sticker packs has been reloaded.",
"stickersPackDelete": "Delete Pack {}",
"stickersPackDeleteDescription": "Are you sure you want to delete this sticker pack? This operation is irreversible.",
"stickersPackDeleted": "Sticker pack has been deleted.",
"stickersDelete": "Delete Sticker {}",
"stickersDeleteDescription": "Are you sure you want to delete this sticker? This operation is irreversible.",
"stickersDeleted": "Sticker has been deleted.",
"fieldStickerName": "Sticker Name",
"fieldStickerAlias": "Sticker Alias",
"fieldStickerAliasHint": "The unique sticker placeholder with the pack prefix.",
"fieldStickerPackName": "Name",
"fieldStickerPackDescription": "Description",
"fieldStickerPackPrefix": "Prefix",
"fieldStickerAttachment": "Attachment",
"stickersNew": "New Sticker",
"stickersNewDescription": "Create a new sticker belongs to this pack.",
"stickersPackNew": "New Sticker Pack",
"trayMenuShow": "Show",
"trayMenuMuteNotification": "Do Not Disturb",
"update": "Update",
"forceUpdate": "Force Update",
"forceUpdateDescription": "Force to show the application update popup, even the new version is not available.",
"debugLogging": "Runtime Logs",
"runtimeLogsOpen": "Open Logs",
"runtimeLogsDescription": "Show the runtime logs to help debugging.",
"signinResetPasswordHint": "Please enter the username / email address to help us to find your account and reset your password.",
"cacheSize": "Cache Size",
"cacheDelete": "Clean Cache",
"cacheDeleteDescription": "Remove the cached images and other resources from your disk, the content will be downloaded from server again.",
"cacheDeleted": "All cache has been cleaned up.",
"userNoDescription": "No description.",
"fieldTimeZone": "Time Zone",
"fieldGender": "Gender",
"fieldPronouns": "Pronouns",
"fieldLocation": "Location",
"fieldLinks": "Links",
"fieldLinkName": "Name",
"fieldLinkUrl": "URL",
"screenAccountBadges": "Badges",
"accountBadges": "Badges",
"accountBadgesDescription": "View and manage your badges.",
"badgeActivated": "Activated badge {}.",
"viewDetailedAttachment": "Details",
"screenKeyPairs": "Key Pairs",
"accountKeyPairs": "Key Pairs",
"accountKeyPairsDescription": "Manage the key pairs which used to encrypt messages.",
"enrollNewKeyPair": "Enroll New One",
"enrollNewKeyPairDescription": "Generate a new key pair.",
"keyPairHasPrivateKey": "With private key",
"decrypting": "Decrypting……",
"decryptingKeyNotFound": "Key not found or exchange failed, the other party may not be online",
"messageUnablePreview": "Unable preview",
"messageUnablePreviewEncrypted": "Unable preview encrypted message",
"postViewInGlobalDescription": "Do not view the post in the specific realm.",
"postDraftSaved": "The draft has been saved.",
"postDraftBox": "Draft Box",
"postShuffle": "Read Randomly",
"checkInStreak": {
"zero": "No streak",
"one": "{} day streak",
"other": "{} days streak"
},
"accountChangeStatus": "Change Status",
"accountStatusSilent": "Do not Disturb",
"accountStatusSilentDesc": "The notification will stop popping up",
"accountStatusInvisible": "Invisible",
"accountStatusInvisibleDesc": "Will show as offline, but all features still remain normal",
"accountCustomStatus": "Custom Status",
"accountCustomStatusDescription": "Customize your status.",
"accountClearStatus": "Clear Status",
"accountClearStatusDescription": "Clear your status, and let server decide which status you are for you.",
"fieldAccountStatusLabel": "Status Text",
"fieldAccountStatusClearAt": "Clear At",
"accountStatusNegative": "Negative",
"accountStatusNeutral": "Neutral",
"accountStatusPositive": "Positive",
"mixedFeed": "Mixed Feed",
"mixedFeedDescription": "The Explore screen may not only display the user's posts, but may also contain other content. However, this mode does not apply to classification and filtering.",
"filterFeed": "Exploring Adjust",
"feedUnknownItem": "Unable to display this content, the current version of the client does not support the type of content, please try to update the application afterwards.",
"serviceStatusOperational": "All services operational",
"serviceStatusDowngraded": "Some services downgraded",
"serviceStatusFailed": "All services unavailable",
"serviceStatusFailedDescription": "The server is down or the maintenance is just finished.",
"serviceNameInsights": "Summarize and Insights",
"serviceNameInteractive": "Posts, Reactions and Explore",
"serviceNameReader": "News and Link Previews",
"serviceNameMessaging": "Chat",
"serviceNameMatrix": "Matrix Software and Game Marketplace",
"serviceNamePaperclip": "Attachments, Images and Files",
"serviceNameWallet": "Source Points Wallet",
"serviceNamePassport": "Authorization and Authentication",
"accountActionEvent": "Action Events",
"accountActionEventDescription": "View your action event logs.",
"eventMetadata": "Metadata",
"accountAuthTickets": "Auth Sessions",
"accountAuthTicketsDescription": "View and manage your auth sessions.",
"authTicketCreatedAt": "Issued at {}",
"authTicketExpiredAt": "Expired at {}",
"authTicketLastGrantAt": "Last granted at {}",
"authTicketCurrent": "Current",
"accountUnconfirmedTitle": "Unconfirmed Account",
"accountUnconfirmedSubtitle": "Your account is unconfirmed, which will make most features unavailable and your account will be destroyed in 24 hours. You should receive an email in your inbox with a confirmation link.",
"accountUnconfirmedUnreceived": "Didn't receive the email?",
"accountUnconfirmedResend": "Resend one",
"accountUnconfirmedResendSuccessful": "Email has been resent, you can resend it again in 60 minutes.",
"stickerPickerEmpty": "Sticker list is empty",
"stickerPickerEmptyHint": "To start using stickers, you need to add a sticker pack first.",
"goto": "Go to {}",
"accountContactMethods": "Contact Methods",
"accountContactMethodsDescription": "Manage your contact methods.",
"accountContactMethodsNameEmail": "Email address",
"accountContactMethodsNamePhone": "Phone number",
"accountContactMethodsNameAddress": "Address",
"accountContactMethodsPrimary": "Primary",
"accountContactMethodsVerified": "Verified",
"accountContactMethodsPublic": "Public",
"accountContactMethodsAdd": "Add Contact Method",
"accountContactMethodsEdit": "Edit Contact Method",
"accountContactMethodsAddDescription": "Add a new contact method.",
"fieldContactContent": "Contact method",
"accountContactMethodsPublicHint": "This contact method will be displayed publicly on your profile.",
"accountContactMethodsDelete": "Delete Contact Method",
"accountContactMethodsDeleteDescription": "Are you sure you want to delete contact method {}? This operation is irreversible.",
"postCommentAdd": "Write a comment",
"translate": "Translate",
"translating": "Translating…",
"translated": "Translated",
"settingsAutoTranslate": "Auto Translate",
"settingsAutoTranslateDescription": "Automatically translate text when viewing posts and messages.",
"trayMenuHide": "Hide",
"accountSettingsNotify": "Notify Settings",
"accountSettingsNotifyDescription": "Adjust the types of notifications you receive.",
"accountSettingsSecurity": "Security Settings",
"accountSettingsSecurityDescription": "Adjust your account security settings.",
"save": "Save",
"notificationTopicPostFeedback": "Post Feedback",
"notificationTopicPostReply": "Post Replies",
"notificationTopicPostSubscription": "Post Subscriptions",
"notificationTopicMessaging": "New Messages",
"notificationTopicMessagingCall": "Incoming Calls",
"notificationTopicGeneral": "General",
"authMaximumAuthSteps": "Maximum Authenticate Steps",
"authMaximumAuthStepsDescription": {
"one": "Maximum ask for {} step authenticate",
"other": "Maximum ask for {} steps authenticate"
},
"authAlwaysRisky": "Always Risky",
"authAlwaysRiskyDescription": "Always ask for the highest steps count of authentication when logging in.",
"chatUnjoined": "Unjoined Channel",
"chatUnjoinedDescription": "You haven't joined this channel, so you can't send messages either view messages in it.",
"chatUnjoinedPublicDescription": "Fortunately, this is a public channel, so you can join it as you want.",
"chatJoin": "Join the Channel",
"appInitStarting": "Starting",
"appInitNetwork": "Initializing Network",
"appInitUserdata": "Initializing User Data",
"appInitWebsocket": "Establishing Solar Link",
"appInitNotification": "Initializing Push Notifications",
"appInitKeyPair": "Initializing Key Pairs",
"appInitStickers": "Initializing Stickers",
"appInitUserDirectory": "Initializing User Directory",
"appInitRealm": "Initializing Realms",
"appInitChat": "Initializing Chat",
"appInitDone": "Completed",
"community": "Community",
"realmCommunity": "{}'s Community",
"postTotalCount": {
"one": "Total {} post",
"other": "Total {} posts"
},
"settingsHideBottomNav": "Hide Bottom Navigation",
"settingsHideBottomNavDescription": "Hide the bottom navigation bar, and show the navigation buttons in the drawer.",
"reCaptcha": "reCaptcha",
"friends": "Friends",
"friendsDescription": "Manage your friendships.",
"album": "Album",
"albumDescription": "View albums and manage attachments.",
"stickers": "Stickers",
"stickersDescription": "View sticker packs and manage stickers.",
"navBottomUnauthorizedCaption": "Or create an account",
"walletCurrencyGoldenShort": "GDP",
"walletCurrencyGolden": {
"one": "{} Golden Point",
"other": "{} Golden Points"
},
"walletTransactionTypeNormal": "Source Point",
"walletTransactionTypeGolden": "Golden Point",
"accountProgram": "Programs",
"accountProgramDescription": "Explore the available member programs.",
"accountProgramJoin": "Join Program",
"accountProgramJoinRequirements": "Requirements",
"accountProgramJoinPricing": "Pricing",
"accountProgramJoinPricingHint": "Billed every (30 days) month.",
"accountProgramLeaveHint": "After leaving the program, the source points will not be refunded.",
"accountProgramJoined": "Joined Program.",
"accountProgramAlreadyJoined": "Joined",
"accountProgramLeft": "Left Program.",
"leave": "Leave",
"attachmentFailedToLoadMedia": "Unable to load media file, please try again later. If this error occurs repeatedly, the source file may not exist or the network connection may be abnormal.",
"accountPunishments": "Punishments",
"accountPunishmentsDescription": "View your account's reputation status.",
"punishmentType0": "Strike",
"punishmentType1": "Limited",
"punishmentType2": "Banned",
"punishmentOverall": "Overall Status",
"punishmentStatusNormal": "All abilities normal",
"punishmentStatusWarned": "All abilities normal, but at least one strike is in effect",
"punishmentStatusLimited": "Some abilities limited, at least one limited punishment is in effect",
"punishmentStatusLimitedFully": "All abilities limited, at least one completely limited punishment is in effect",
"punishmentStatusBanned": "All services are terminated, banned",
"punishmentCreatedAt": "Applied since {}",
"punishmentExpiredAt": "Expired at {}",
"punishmentExpiredNever": "Never expired",
"punishmentModerator": "Moderator who made this punishment",
"punishmentMadeBySystem": "Made by auto-mod system",
"settingsAprilFoolFeatures": "April Fool Features",
"settingsAprilFoolFeaturesDescription": "Enable April Fool features during April Fool, this option will only be visible during April Fool.",
"settingsSoundEffects": "Sound Effects",
"settingsSoundEffectsDescription": "Enable the sound effects around the app.",
"settingsResetMemorizedWindowSize": "Reset Window Size",
"settingsResetMemorizedWindowSizeDescription": "Reset the memorized window size, and set it to the default size.",
"chatDirect": "Direct Messages",
"back": "Back",
"badgeProgramDeveloper": "Developer Program Member",
"badgeProgramStellar": "A Stellar",
"badgeProgramModerator": "Community Moderator",
"postEditedHint": "edited",
"splashScreenServer": "Server",
"splashScreenServerName": "Potato",
"splashScreenCaption": "Trying to establishing connection with HyperNet™",
"attachmentEditor": "Attachment editor",
"attachmentEditorUnUploadHint": "This attachment is not uploaded, metadata editing is unavailable, and you can crop this attachment.",
"attachmentEditorUploadHint": "This attachment is uploaded.",
"attachmentRating": "Rating",
"fieldAttachmentRating": "Content Rating",
"fieldAttachmentQuality": "Quality Rating",
"attachmentReferenceLink": "Use external attachment",
"fieldAttachmentReferenceLink": "Reference Link",
"attachmentReferenceLinkDescription": "It will be used as the source file of the attachment. The link needs to allow cross-origin access.",
"fieldAttachmentMimetype": "Mimetype",
"postVideoLive": "Live Stream",
"postVideoLiveDescription": "This is a live video, you can embed the source site by yourself.",
"postVideoRendererWeb": "WebView Rendering",
"postVideoRendererWebDescription": "Use WebView to render the content",
"fieldPostVideoUrl": "Video URL",
"fieldPostVideoUrlDescription": "The URL of the video content, it can be a webpage, and will be rendered by iFrame / WebView."
"postCategoryUncategorized": "Uncategorized"
}

View File

@ -15,17 +15,12 @@
"screenAccountProfileEdit": "编辑资料",
"screenAbuseReport": "滥用检举",
"screenSettings": "设置",
"screenAccountSettings": "账号设置",
"screenFactorSettings": "验证因子",
"screenAccountWallet": "钱包",
"screenNews": "新闻",
"screenAlbum": "相册",
"screenChat": "聊天",
"screenChatManage": "编辑聊天频道",
"screenChatNew": "新建聊天频道",
"screenRealm": "领域",
"screenRealmManage": "编辑领域",
"screenRealmDiscovery": "发现领域",
"screenRealmNew": "新建领域",
"screenNotification": "通知",
"screenPostSearch": "搜索帖子",
@ -92,18 +87,8 @@
},
"loginEnterPassword": "验证代码",
"loginSuccess": "登录为 {}",
"authFactorDelete": "删除验证因子",
"authFactorDeleteDescription": "你确定要删除 {} 验证因子吗?",
"authFactorPassword": "密码",
"authFactorPasswordDescription": "注册时选择设置的密码。",
"authFactorEmail": "电邮一次性验证码",
"authFactorEmailDescription": "由我们生成并发送到绑定的的电子邮箱的一次性验证码。",
"authFactorTOTP": "时序验证码",
"authFactorTOTPDescription": "使用 Google Authenticator 或 Authy 等验证器生成的一次性验证码。",
"authFactorInAppNotify": "应用内通知验证码",
"authFactorInAppNotifyDescription": "通过站内通知推送的一次性验证码。",
"authFactorAdd": "添加新验证因子",
"authFactorAddSubtitle": "给你的帐户登陆时提供另一个方案。",
"accountIntroTitle": "喜欢您来!",
"accountIntroSubtitle": "登陆以探索更广大的世界。",
"accountLogout": "退出登录",
@ -112,14 +97,8 @@
"accountLogoutConfirm": "您需要重新输入账号密码,甚至可能需要多步验证来再次登陆。",
"accountPublishers": "你的发布者",
"accountPublishersSubtitle": "管理你的公共形象。",
"accountSettings": "帐号设置",
"accountSettingsSubtitle": "管理你的帐号并让它更好的服务你。",
"accountProfileEdit": "编辑资料",
"accountProfileEditSubtitle": "使你的 Solarpass 账户更像你。",
"accountWallet": "钱包",
"accountWalletSubtitle": "查看你的余额和交易记录。",
"factorSettings": "验证因子",
"factorSettingsSubtitle": "管理你的登陆验证方式。",
"accountProfileEditApplied": "个人资料修改已被应用。",
"publishersNew": "新发布者",
"publisherNewSubtitle": "创建一个新的公共身份。",
@ -137,19 +116,11 @@
"publisherRunBy": "由 {} 管理",
"fieldPublisherBelongToRealm": "所属领域",
"fieldPublisherBelongToRealmUnset": "未设置发布者所属领域",
"writePost": "撰写",
"postTypeStory": "动态",
"postTypeArticle": "文章",
"postTypeQuestion": "问题",
"postTypeVideo": "视频",
"writePostTypeStory": "发动态",
"writePostTypeArticle": "写文章",
"writePostTypeQuestion": "提问题",
"writePostTypeVideo": "发视频",
"fieldPostPublisher": "帖子发布者",
"fieldPostContent": "发生什么事了?!",
"fieldPostTitle": "标题",
"fieldPostQuestionReward": "回答奖励源点",
"fieldPostDescription": "描述",
"fieldPostTags": "标签",
"fieldPostCategories": "分类",
@ -205,16 +176,7 @@
"one": "{} 条评论",
"other": "{} 条评论"
},
"postCommentExpand": "展开评论",
"settingsAppearance": "外观",
"settingsCustomFonts": "自定义字体",
"settingsCustomFontsDescription": "设置应用程序使用的字体。",
"settingsCustomFontFamily": "应用字体",
"settingsCustomFontFamilyHint": "使用英文逗号分割每一种字体,越前优先级越高",
"settingsCustomFontApplied": "自定义字体已经应用。",
"settingsDisplayLanguage": "显示语言",
"settingsDisplayLanguageDescription": "设置应用程序使用的语言",
"settingsDisplayLanguageSystem": "跟随系统",
"settingsBackgroundImage": "背景图片",
"settingsBackgroundImageDescription": "设置应用全局生效的的背景图片。",
"settingsBackgroundImageClear": "清除现存背景图",
@ -223,19 +185,10 @@
"settingsThemeMaterial3Description": "将应用主题设置为 Material 3 设计范式的主题。",
"settingsAppBarTransparent": "透明顶栏",
"settingsAppBarTransparentDescription": "为顶栏启用透明效果。",
"settingsDrawerPreferCollapse": "侧边栏偏好折叠",
"settingsDrawerPreferCollapseDescription": "将侧边栏优先折叠,即使屏幕宽度足够大去放下整个侧边栏。",
"settingsColorScheme": "主题色",
"settingsColorSchemeDescription": "设置应用主题色。",
"settingsColorSeed": "预设色彩主题",
"settingsColorSeedDescription": "选择一个预设色彩主题。",
"settingsFeatures": "功能",
"settingsNotifyWithHaptic": "新通知时振动",
"settingsNotifyWithHapticDescription": "在应用在前台时收到新通知出现时出发轻量的振动。",
"settingsExpandPostLink": "展开帖子链接",
"settingsExpandPostLinkDescription": "在帖子列表中展开显示帖子中的链接。",
"settingsExpandChatLink": "展开聊天链接",
"settingsExpandChatLinkDescription": "在聊天信息中展开显示内容中的链接。",
"settingsNetwork": "网络",
"settingsNetworkServer": "HyperNet 服务器",
"settingsNetworkServerDescription": "设置 HyperNet 服务器地址,选择我们提供的,或者自己搭建。",
@ -254,15 +207,12 @@
"settingsMisc": "杂项",
"settingsMiscAbout": "关于",
"settingsMiscAboutDescription": "查看 Solian 的版本信息。",
"settingsAccountLanguage": "帐号偏好语言",
"settingsAccountLanguageDescription": "设置邮件、通知和其他帐号相关内容的语言。",
"sensitiveContent": "敏感内容",
"sensitiveContentCollapsed": "敏感内容已折叠。",
"sensitiveContentDescription": "此内容已被标记,可能不适合所有人查看。",
"sensitiveContentReveal": "显示内容",
"serverConnecting": "正在连接…",
"serverDisconnected": "已断开连接",
"serverConnected": "已连接",
"serverConnecting": "正在连接服务器…",
"serverDisconnected": "已与服务器断开连接",
"fieldChatAlias": "频道别名",
"fieldChatAliasHint": "全站范围内唯一的频道别名,用于在 URL 中表示该频道,留空则自动生成。应遵循 URL-Safe 的原则。",
"fieldChatName": "名称",
@ -336,14 +286,11 @@
"fieldAttachmentRandomId": "访问 ID",
"fieldAttachmentAlt": "概述文字",
"addAttachmentFromAlbum": "从相册中添加附件",
"addAttachmentFromFiles": "从文件中添加附件",
"addAttachmentFromClipboard": "粘贴附件",
"addAttachmentFromCameraPhoto": "拍摄照片",
"addAttachmentFromCameraVideo": "拍摄视频",
"addAttachmentFromRandomId": "通过访问 ID 链接",
"attachmentDetailInfo": "附件详细信息",
"attachmentPastedImage": "粘贴的图片",
"attachmentInsertedImage": "插入的图片",
"attachmentInsertLink": "插入连接",
"attachmentSetAsPostThumbnail": "设置为帖子缩略图",
"attachmentUnsetAsPostThumbnail": "取消设置为帖子缩略图",
@ -522,13 +469,8 @@
"accountBirthday": "出生于 {}",
"accountBadge": "徽章",
"accountCheckInNoRecords": "暂无运势记录",
"badgeCompanyStaff": "工作人员",
"badgeCompanyStaff": "索尔辛茨士大夫 · 员工",
"badgeSiteMigration": "Solar Network 原住民",
"badgeCommunitySurvey": "调研参与者",
"badgeCommunityVerified": "认证用户",
"badgeCommunityContributor": "优秀社区贡献者",
"badgeSiteAnniversary": "周年纪念",
"badgeUserBirthday": "生日纪念",
"accountStatus": "状态",
"accountStatusOnline": "在线",
"accountStatusOffline": "离线",
@ -541,7 +483,6 @@
"attachmentSaved": "已保存到相册",
"attachmentSavedDesktop": "已保存到下载目录",
"openInAlbum": "在相册中打开",
"openInBrowser": "在浏览器中打开",
"postAbuseReport": "检举帖子",
"postAbuseReportDescription": "检举不符合我们用户协议以及社区准则的帖子,来帮助我们更好的维护 Solar Network 上的内容。请在下面描述该帖子如何违反我么的相关规定。请勿填写任何敏感信息。我们将会在 24 小时内处理您的检举。",
"abuseReport": "检举",
@ -564,7 +505,6 @@
"termAcceptNextWithAgree": "点击 “下一步”,即表示你同意我们的各项条款,包括其之后的更新。",
"unauthorized": "未登陆",
"unauthorizedDescription": "登陆以探索整个 Solar Network。",
"projectDetail": "项目详情",
"serviceStatus": "服务状态",
"termRelated": "相关条款",
"appDetails": "应用程序详情",
@ -578,15 +518,11 @@
"postImageShareAds": "来 Solar Network 探索更多有趣帖子",
"postShare": "分享",
"postShareImage": "分享帖图",
"postGetInsight": "获取见解",
"postGetInsightTitle": "AI 见解",
"postGetInsightDescription": "AI 可能会出错,检查信息真实性。",
"appInitializing": "正在初始化",
"poweredBy": "由 {} 提供支持",
"shareIntent": "分享",
"shareIntentDescription": "您想对您分享的内容做些什么?",
"shareIntentPostStory": "发布动态",
"shareIntentSendChannel": "分享到聊天频道",
"updateAvailable": "检测到更新可用",
"updateOngoing": "正在更新,请稍后……",
"custom": "自定义",
@ -599,8 +535,6 @@
"colorSchemeWhite": "白色",
"colorSchemeBlack": "黑色",
"colorSchemeApplied": "主题色已应用,可能需要重启来生效。",
"postFeaturedComment": "精选评论",
"postCategory": "分类",
"postCategoryTechnology": "技术",
"postCategoryGaming": "游戏",
"postCategoryLife": "生活",
@ -611,356 +545,5 @@
"postCategoryKnowledge": "知识",
"postCategoryLiterature": "文学",
"postCategoryFunny": "搞笑",
"postCategoryUncategorized": "未分类",
"newsAllSources": "所有新闻",
"newsReadingProviderSwap": "切换",
"newsReadingFromReader": "你正在从 HyperNet.Reader 阅读文章",
"newsReadingFromOriginal": "你正在阅读原始文章",
"newsDisclaimer": "本文由 HyperNet.Reader 从互联网上获取,我们不担保其内容的真实性,请自行判断。本文章的所有内容版权归原作者所有。",
"newsToday": "快讯",
"totpPostSetup": "还有一件事",
"totpPostSetupDescription": "使用 Google Authenticator, Microsoft Authenticator, 1Password, Authy, Bitwarden 或其他支持 TOTP 的验证器扫描本 QR Code 来添加。",
"totpNeverShare": "永远不要分享这个 QR Code",
"needHelp": "需要帮助?",
"needHelpLaunch": "查看我们的山羊维基!",
"walletCreate": "创建钱包",
"walletCreateSubtitle": "创建于一个钱包来开始使用源点。",
"walletCreatePassword": "在下方设置你的付款密码",
"walletCurrencyShort": "源点",
"walletCurrency": {
"one": "{} 源点",
"other": "{} 源点"
},
"aiThinkingProcess": "AI 思考过程",
"accountSettingsApplied": "帐号设置已应用。",
"trayMenuExit": "退出",
"postQuestionUnanswered": "未解答的问题",
"postQuestionUnansweredWithReward": "未解答的问题,悬赏源点 {}",
"postQuestionAnswered": "已解答的问题",
"postQuestionAnswerTitle": "精选解答",
"postQuestionAnswerSelect": "选择解答",
"postQuestionAnswerSelected": "解答已选择,奖励已发放。",
"postVideoUpload": "上传视频",
"realmJoin": "加入领域",
"realmCommunityHint": "该领域是一个社区领域,你可以自由加入。",
"realmCommunityPublicChannelsHint": "该领域包含的公共频道",
"realmCommunityPublishersHint": "该领域的发布者",
"realmJoined": "已加入领域 {}。",
"join": "加入",
"pollEditorNew": "新投票",
"pollEditorEdit": "编辑投票",
"pollEditorDelete": "删除投票",
"pollEditorDeleteDescription": "你确定要删除这个投票吗?该操作不可撤销。",
"pollEditorUnlink": "解除链接",
"pollOptionAdd": "添加选项",
"pollOptionName": "选项名称",
"pollLinkExisting": "链接现有投票",
"pollAnswered": "答案已经反馈。",
"pollVotes": {
"one": "{} 票",
"other": "{} 票"
},
"publisherDelete": "删除发布者 {}",
"publisherDeleteDescription": "你确定要删除这个发布者吗?该操作不可撤销。",
"channelIsPublic": "公开频道",
"channelIsPublicDescription": "该频道是公开的,任何人都可以加入。",
"channelIsCommunity": "社区频道",
"channelIsCommunityDescription": "目前来说,社区频道还没有什么特别之处。",
"realmIsPublic": "公开领域",
"realmIsPublicDescription": "该领域是公开的,任何人都可以加入。",
"realmIsCommunity": "社区领域",
"realmIsCommunityDescription": "社区领域会显示在发现页面上。",
"realmLeave": "离开领域",
"realmLeaveDescription": "离开当前领域,并且删除领域中的身份。",
"checkInResultTier1": "大凶",
"checkInResultTier2": "凶",
"checkInResultTier3": "中平",
"checkInResultTier4": "吉",
"checkInResultTier5": "大吉",
"flagPostAction": "吹哨",
"flagPost": "吹哨不良内容",
"flagPostDescription": "吹哨不良内容,如果吹哨用户占浏览量的 50% 或以上,则帖子会被折叠。吹哨后不可撤销。",
"flaggedPost": "哨子已经吹响。",
"postViews": {
"zero": "{} 次浏览",
"one": "{} 次浏览",
"other": "{} 次浏览"
},
"attachmentBillingUploaded": "已占用的字节数",
"attachmentBillingDiscount": "免费的字节数",
"attachmentBillingHint": "滑动窗口计价®\n在24小时内上传的文件大小超出免费空间才会适用扣费。",
"postThumbnail": "帖子缩略图",
"accountRealms": "领域",
"postInGlobal": "全站",
"postInGlobalDescription": "不关联此帖子与任何领域。",
"postChannelGlobal": "全站",
"postChannelFriends": "好友",
"postChannelFollowing": "关注",
"postChannelRealm": "领域",
"postFilterReset": "重置过滤器",
"postFilterResetDescription": "清除过滤器并显示所有帖子。",
"postFilterWithCategory": "查看{}区中的帖子",
"databaseSize": "数据库大小",
"databaseDelete": "删除数据库",
"databaseDeleteDescription": "删除本地数据库,内容将从服务器重新获取。",
"databaseDeleted": "本地数据库已被删除。",
"settingsEnablePushNotifications": "启用推送数据",
"settingsEnablePushNotificationsDescription": "重新启用并请求推送权限,以防自动激活失败。",
"settingsEnabledPushNotifications": "推送通知已经注册。",
"screenStickers": "贴图",
"stickersDiscovery": "发现",
"stickersOwned": "由我拥有",
"stickersCreated": "由我发布",
"stickersAdd": "添加贴图包",
"stickersAdded": "贴图包已添加。",
"add": "添加",
"stickersRemoved": "贴图包已被移除,你可以随时再次添加回来。",
"stickersReload": "重载贴图包",
"stickersReloadDescription": "从服务器重新加载添加过的贴图,更新贴图选择器。",
"stickersReloaded": "贴图包已重载。",
"stickersPackDelete": "删除贴图包 {}",
"stickersPackDeleteDescription": "你确定要删除这个贴图包吗?这个操作不可撤销。",
"stickersPackDeleted": "贴图包已被删除。",
"stickersDelete": "删除贴图 {}",
"stickersDeleteDescription": "你确定要删除这个贴图吗?这个操作不可撤销。",
"stickersDeleted": "贴图已被删除。",
"fieldStickerName": "贴图名称",
"fieldStickerAlias": "贴图别名",
"fieldStickerAliasHint": "和贴图包前缀组合成为本贴图的唯一占位符。",
"fieldStickerPackName": "名称",
"fieldStickerPackDescription": "描述",
"fieldStickerPackPrefix": "贴图包前缀",
"fieldStickerAttachment": "附件",
"stickersNew": "新建贴图",
"stickersNewDescription": "创建一个新的贴图。",
"stickersPackNew": "新建贴图包",
"trayMenuShow": "显示",
"trayMenuMuteNotification": "静音通知",
"update": "更新",
"forceUpdate": "强制更新",
"forceUpdateDescription": "强制更新应用程序,即使有更新的版本可能不可用。",
"runtimeLogs": "运行时日志",
"runtimeLogsOpen": "打开日志文件",
"runtimeLogsDescription": "显示运行时的日志记录。",
"signinResetPasswordHint": "请输入用户名/电子邮箱地址以帮助我们找到您的帐户并重置密码。",
"cacheSize": "缓存资源大小",
"cacheDelete": "清除缓存",
"cacheDeleteDescription": "从磁盘中移除缓存的图片和其他资源,内容将从服务器重新下载。",
"cacheDeleted": "所有缓存已被清除。",
"userNoDescription": "这个人很懒,没有留下什么……",
"fieldTimeZone": "时区",
"fieldGender": "性别",
"fieldPronouns": "人称代词",
"fieldLocation": "位置",
"fieldLinks": "链接",
"fieldLinkName": "名称",
"fieldLinkUrl": "链接",
"screenAccountBadges": "徽章",
"accountBadges": "徽章",
"accountBadgesDescription": "查看并管理你的徽章。",
"badgeActivated": "已佩戴徽章 {}。",
"viewDetailedAttachment": "查看附件详情",
"screenKeyPairs": "密钥对",
"accountKeyPairs": "密钥对",
"accountKeyPairsDescription": "管理用于加密信息的密钥对。",
"enrollNewKeyPair": "新建密钥对",
"enrollNewKeyPairDescription": "生成一对新密钥对。",
"keyPairHasPrivateKey": "有私钥",
"decrypting": "解密中……",
"decryptingKeyNotFound": "未找到密钥对或交换失败,对方可能不在线",
"messageUnablePreview": "无法预览消息",
"messageUnablePreviewEncrypted": "无法预览加密消息",
"postViewInGlobalDescription": "不查看特定领域的帖子。",
"postDraftSaved": "已保存为草稿。",
"postDraftBox": "草稿箱",
"postShuffle": "随便看看",
"checkInStreak": {
"zero": "无连击",
"one": "连续签到 {} 天",
"other": "连续签到 {} 天"
},
"accountChangeStatus": "修改状态",
"accountStatusSilent": "请勿打扰",
"accountStatusSilentDesc": "将会暂停所有通知推送",
"accountStatusInvisible": "隐身",
"accountStatusInvisibleDesc": "将会在他人界面显示离线,但不影响功能使用",
"accountCustomStatus": "自定义状态",
"accountCustomStatusDescription": "客制化你的状态。",
"accountClearStatus": "清除状态",
"accountClearStatusDescription": "清除你的状态,并让服务器决定你的状态。",
"fieldAccountStatusLabel": "状态文字",
"fieldAccountStatusClearAt": "清除时间",
"accountStatusNegative": "负面",
"accountStatusNeutral": "中性",
"accountStatusPositive": "正面",
"mixedFeed": "混合推荐流",
"mixedFeedDescription": "探索页面可能不只会展示用户的帖子,更可能包含其他的内容。但该模式不适用分类和过滤。",
"filterFeed": "探索队列调整",
"feedUnknownItem": "无法显示该内容,当前版本客户端不支持该类型的内容,请尝试更新应用程序后再试。",
"serviceStatusOperational": "所有服务正常",
"serviceStatusDowngraded": "部分服务异常",
"serviceStatusFailed": "服务状态异常",
"serviceStatusFailedDescription": "服务器炸了或者刚刚执行完维护程序。",
"serviceNameInsights": "总结、见解与洞察",
"serviceNameInteractive": "帖子与互动",
"serviceNameReader": "新闻与链接展开",
"serviceNameMessaging": "即使聊天",
"serviceNameMatrix": "矩阵市场",
"serviceNamePaperclip": "附件",
"serviceNameWallet": "源点钱包",
"serviceNamePassport": "身份验证与授权",
"accountActionEvent": "操作日志",
"accountActionEventDescription": "查看你的操作日志。",
"eventMetadata": "元数据",
"accountAuthTickets": "授权会话",
"accountAuthTicketsDescription": "查看和管理你的授权会话。",
"authTicketCreatedAt": "签发于 {}",
"authTicketExpiredAt": "到期于 {}",
"authTicketLastGrantAt": "上次刷新于 {}",
"authTicketCurrent": "当前会话",
"accountUnconfirmedTitle": "尚未未确认账户",
"accountUnconfirmedSubtitle": "您的账户尚未确认,这会导致大部分功能不可用,并且您的帐户将在 24 小时后自毁。您绑定的邮箱的收件箱应该会有一封邮件指引您确认您的帐户。",
"accountUnconfirmedUnreceived": "未收到邮件?",
"accountUnconfirmedResend": "重新发送一封",
"accountUnconfirmedResendSuccessful": "邮件已重新发送,你可以在 60 分钟后再重发一封。",
"stickerPickerEmpty": "贴图列表为空",
"stickerPickerEmptyHint": "想要开始使用贴图,请先添加贴图包。",
"goto": "跳转到 {}",
"accountContactMethods": "联系方式",
"accountContactMethodsDescription": "管理你的联系方式。",
"accountContactMethodsNameEmail": "电子邮箱",
"accountContactMethodsNamePhone": "电话",
"accountContactMethodsNameAddress": "地址",
"accountContactMethodsPrimary": "主要的",
"accountContactMethodsVerified": "已验证",
"accountContactMethodsPublic": "公开的",
"accountContactMethodsAdd": "添加联系方式",
"accountContactMethodsEdit": "编辑联系方式",
"accountContactMethodsAddDescription": "添加新的联系方式。",
"fieldContactContent": "联系方式",
"accountContactMethodsPublicHint": "这个联系方式公开地显示在个人资料中。",
"accountContactMethodsDelete": "删除联系方式",
"accountContactMethodsDeleteDescription": "你确定要删除联系方式 {} 吗?这个操作不可撤销。",
"postCommentAdd": "撰写一条评论",
"translate": "翻译",
"translating": "正在翻译……",
"translated": "已翻译",
"settingsAutoTranslate": "自动翻译",
"settingsAutoTranslateDescription": "在查看帖子、消息时自动翻译文本。",
"trayMenuHide": "隐藏",
"accountSettingsNotify": "通知设置",
"accountSettingsNotifyDescription": "调整你所收到的通知种类。",
"accountSettingsSecurity": "安全设置",
"accountSettingsSecurityDescription": "调整你的帐户安全设置。",
"save": "保存",
"notificationTopicPostFeedback": "帖子数据反馈",
"notificationTopicPostReply": "帖子回复",
"notificationTopicPostSubscription": "帖子订阅",
"notificationTopicMessaging": "消息",
"notificationTopicMessagingCall": "通话",
"notificationTopicGeneral": "杂项",
"authMaximumAuthSteps": "最大验证步骤",
"authMaximumAuthStepsDescription": {
"one": "登入时最多要求 {} 步验证",
"other": "登入时最多要求 {} 步验证"
},
"authAlwaysRisky": "总是风险",
"authAlwaysRiskyDescription": "在登入时始终按最高标准要求验证。",
"chatUnjoined": "未加入频道",
"chatUnjoinedDescription": "你没有加入这个频道,所以你也无法发送消息或者查看这个频道中的消息。",
"chatUnjoinedPublicDescription": "但幸运的是,这是一个公开频道,所以你可以主动加入。",
"chatJoin": "加入频道",
"appInitStarting": "启动中",
"appInitNetwork": "正在初始化网络",
"appInitUserdata": "正在初始化用户数据",
"appInitWebsocket": "正在建立 Solar Link",
"appInitNotification": "正在初始化推送通知",
"appInitKeyPair": "正在初始化密钥对",
"appInitStickers": "正在初始化贴图包",
"appInitUserDirectory": "正在初始化用户目录",
"appInitRealm": "正在初始化领域信息",
"appInitChat": "正在初始化聊天",
"appInitDone": "完成",
"community": "社区",
"realmCommunity": "{}的社区",
"postTotalCount": {
"zero": "没有帖子",
"one": "共 {} 条帖子"
},
"settingsHideBottomNav": "隐藏底部导航栏",
"settingsHideBottomNavDescription": "隐藏底部导航栏,在侧边栏抽屉显示导航按钮。",
"reCaptcha": "人机验证",
"friends": "好友",
"friendsDescription": "管理好友关系。",
"album": "相册",
"albumDescription": "查看相册与管理上传附件。",
"stickers": "贴图",
"stickersDescription": "查看贴图包与管理贴图。",
"navBottomUnauthorizedCaption": "或者注册一个账号",
"walletCurrencyGoldenShort": "金点",
"walletCurrencyGolden": {
"one": "{} 金点",
"other": "{} 金点"
},
"walletTransactionTypeNormal": "源点",
"walletTransactionTypeGolden": "金点",
"accountProgram": "计划",
"accountProgramDescription": "了解可用的成员计划。",
"accountProgramJoin": "加入计划",
"accountProgramJoinRequirements": "要求",
"accountProgramJoinPricing": "价格",
"accountProgramJoinPricingHint": "按月30 天)收费",
"accountProgramLeaveHint": "离开计划后,之前花费的源点不会退款。",
"accountProgramJoined": "已加入计划。",
"accountProgramLeft": "已离开计划。",
"accountProgramAlreadyJoined": "已加入",
"leave": "离开",
"attachmentFailedToLoadMedia": "无法加载媒体文件,请稍后重试。若此错误重复出现,可能源文件不存在或者网络连接异常。",
"accountPunishments": "处分",
"accountPunishmentsDescription": "查看你帐号的信誉状态。",
"punishmentType0": "警告",
"punishmentType1": "停权",
"punishmentType2": "封禁",
"punishmentOverall": "总体状态",
"punishmentStatusNormal": "所有功能正常",
"punishmentStatusWarned": "所有功能正常,但有警告生效",
"punishmentStatusLimited": "部份功能暂时受限,有至少一个停权生效",
"punishmentStatusLimitedFully": "所有功能暂时受限,有至少一个完全停权生效",
"punishmentStatusBanned": "所有服务终止,已被封禁",
"punishmentCreatedAt": "宣布于 {}",
"punishmentExpiredAt": "到期于 {}",
"punishmentExpiredNever": "永久生效",
"punishmentModerator": "责任管理员",
"punishmentMadeBySystem": "由系统自动裁决",
"settingsAprilFoolFeatures": "愚人节特性",
"settingsAprilFoolFeaturesDescription": "在愚人节期间启用愚人节特性,该选项只会在愚人节期间显示。",
"settingsSoundEffects": "声音效果",
"settingsSoundEffectsDescription": "在一些场合下启用声音特效。",
"settingsResetMemorizedWindowSize": "重置窗口大小",
"settingsResetMemorizedWindowSizeDescription": "重置记忆的窗口大小,以重新设置为默认大小。",
"chatDirect": "私信",
"back": "返回",
"badgeProgramDeveloper": "开发者计划成员",
"badgeProgramStellar": "一颗恒星",
"badgeProgramModerator": "社区管理员",
"postEditedHint": "已编辑",
"splashScreenServer": "服务器",
"splashScreenServerName": "土豆",
"splashScreenCaption": "正在尝试与 HyperNet™ 取得太阳链连接",
"attachmentEditor": "附件编辑器",
"attachmentEditorUnUploadHint": "该附件未上传,元数据编辑不可用,同时你可以裁剪本附件。",
"attachmentEditorUploadHint": "该附件已上传。",
"attachmentRating": "评级",
"fieldAttachmentRating": "内容分级",
"fieldAttachmentQuality": "质量评分",
"attachmentReferenceLink": "引用外部附件",
"fieldAttachmentReferenceLink": "引用连接",
"attachmentReferenceLinkDescription": "作为附件的源文件。需要链接允许跨域访问。",
"fieldAttachmentMimetype": "文件类型",
"postVideoLive": "直播",
"postVideoLiveDescription": "这是一条直播影片,允许用户自行嵌入源站。",
"postVideoRendererWeb": "网页渲染器",
"postVideoRendererWebDescription": "使用 WebView 渲染内容。",
"fieldPostVideoUrl": "视频流地址",
"fieldPostVideoUrlDescription": "视频内容的地址,可以为网页,将会使用 iFrame / WebView 渲染。"
"postCategoryUncategorized": "未分类"
}

View File

@ -15,17 +15,12 @@
"screenAccountProfileEdit": "編輯資料",
"screenAbuseReport": "濫用檢舉",
"screenSettings": "設置",
"screenAccountSettings": "賬號設置",
"screenFactorSettings": "驗證因子",
"screenAccountWallet": "錢包",
"screenNews": "新聞",
"screenAlbum": "相冊",
"screenChat": "聊天",
"screenChatManage": "編輯聊天頻道",
"screenChatNew": "新建聊天頻道",
"screenRealm": "領域",
"screenRealmManage": "編輯領域",
"screenRealmDiscovery": "發現領域",
"screenRealmNew": "新建領域",
"screenNotification": "通知",
"screenPostSearch": "搜索帖子",
@ -92,18 +87,8 @@
},
"loginEnterPassword": "驗證代碼",
"loginSuccess": "登錄為 {}",
"authFactorDelete": "刪除驗證因子",
"authFactorDeleteDescription": "你確定要刪除 {} 驗證因子嗎?",
"authFactorPassword": "密碼",
"authFactorPasswordDescription": "註冊時選擇設置的密碼。",
"authFactorEmail": "電郵一次性驗證碼",
"authFactorEmailDescription": "由我們生成併發送到綁定的的電子郵箱的一次性驗證碼。",
"authFactorTOTP": "時序驗證碼",
"authFactorTOTPDescription": "使用 Google Authenticator 或 Authy 等驗證器生成的一次性驗證碼。",
"authFactorInAppNotify": "應用內通知驗證碼",
"authFactorInAppNotifyDescription": "通過站內通知推送的一次性驗證碼。",
"authFactorAdd": "添加新驗證因子",
"authFactorAddSubtitle": "給你的帳户登陸時提供另一個方案。",
"accountIntroTitle": "喜歡您來!",
"accountIntroSubtitle": "登陸以探索更廣大的世界。",
"accountLogout": "退出登錄",
@ -112,14 +97,8 @@
"accountLogoutConfirm": "您需要重新輸入賬號密碼,甚至可能需要多步驗證來再次登陸。",
"accountPublishers": "你的發佈者",
"accountPublishersSubtitle": "管理你的公共形象。",
"accountSettings": "帳號設置",
"accountSettingsSubtitle": "管理你的帳號並讓它更好的服務你。",
"accountProfileEdit": "編輯資料",
"accountProfileEditSubtitle": "使你的 Solarpass 賬户更像你。",
"accountWallet": "錢包",
"accountWalletSubtitle": "查看你的餘額和交易記錄。",
"factorSettings": "驗證因子",
"factorSettingsSubtitle": "管理你的登陸驗證方式。",
"accountProfileEditApplied": "個人資料修改已被應用。",
"publishersNew": "新發布者",
"publisherNewSubtitle": "創建一個新的公共身份。",
@ -137,19 +116,11 @@
"publisherRunBy": "由 {} 管理",
"fieldPublisherBelongToRealm": "所屬領域",
"fieldPublisherBelongToRealmUnset": "未設置發佈者所屬領域",
"writePost": "撰寫",
"postTypeStory": "動態",
"postTypeArticle": "文章",
"postTypeQuestion": "問題",
"postTypeVideo": "視頻",
"writePostTypeStory": "發動態",
"writePostTypeArticle": "寫文章",
"writePostTypeQuestion": "提問題",
"writePostTypeVideo": "發視頻",
"fieldPostPublisher": "帖子發佈者",
"fieldPostContent": "發生什麼事了?!",
"fieldPostTitle": "標題",
"fieldPostQuestionReward": "回答獎勵源點",
"fieldPostDescription": "描述",
"fieldPostTags": "標籤",
"fieldPostCategories": "分類",
@ -205,16 +176,7 @@
"one": "{} 條評論",
"other": "{} 條評論"
},
"postCommentExpand": "展開評論",
"settingsAppearance": "外觀",
"settingsCustomFonts": "自定義字體",
"settingsCustomFontsDescription": "設置應用程序使用的字體。",
"settingsCustomFontFamily": "應用字體",
"settingsCustomFontFamilyHint": "使用英文逗號分割每一種字體,越前優先級越高",
"settingsCustomFontApplied": "自定義字體已經應用。",
"settingsDisplayLanguage": "顯示語言",
"settingsDisplayLanguageDescription": "設置應用程序使用的語言",
"settingsDisplayLanguageSystem": "跟隨系統",
"settingsBackgroundImage": "背景圖片",
"settingsBackgroundImageDescription": "設置應用全局生效的的背景圖片。",
"settingsBackgroundImageClear": "清除現存背景圖",
@ -223,19 +185,10 @@
"settingsThemeMaterial3Description": "將應用主題設置為 Material 3 設計範式的主題。",
"settingsAppBarTransparent": "透明頂欄",
"settingsAppBarTransparentDescription": "為頂欄啓用透明效果。",
"settingsDrawerPreferCollapse": "側邊欄偏好摺疊",
"settingsDrawerPreferCollapseDescription": "將側邊欄優先摺疊,即使屏幕寬度足夠大去放下整個側邊欄。",
"settingsColorScheme": "主題色",
"settingsColorSchemeDescription": "設置應用主題色。",
"settingsColorSeed": "預設色彩主題",
"settingsColorSeedDescription": "選擇一個預設色彩主題。",
"settingsFeatures": "功能",
"settingsNotifyWithHaptic": "新通知時振動",
"settingsNotifyWithHapticDescription": "在應用在前台時收到新通知出現時出發輕量的振動。",
"settingsExpandPostLink": "展開帖子鏈接",
"settingsExpandPostLinkDescription": "在帖子列表中展開顯示帖子中的鏈接。",
"settingsExpandChatLink": "展開聊天鏈接",
"settingsExpandChatLinkDescription": "在聊天信息中展開顯示內容中的鏈接。",
"settingsNetwork": "網絡",
"settingsNetworkServer": "HyperNet 服務器",
"settingsNetworkServerDescription": "設置 HyperNet 服務器地址,選擇我們提供的,或者自己搭建。",
@ -254,15 +207,12 @@
"settingsMisc": "雜項",
"settingsMiscAbout": "關於",
"settingsMiscAboutDescription": "查看 Solian 的版本信息。",
"settingsAccountLanguage": "帳號偏好語言",
"settingsAccountLanguageDescription": "設置郵件、通知和其他帳號相關內容的語言。",
"sensitiveContent": "敏感內容",
"sensitiveContentCollapsed": "敏感內容已摺疊。",
"sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。",
"sensitiveContentReveal": "顯示內容",
"serverConnecting": "正在連接…",
"serverDisconnected": "已斷開連接",
"serverConnected": "已連接",
"serverConnecting": "正在連接服務器…",
"serverDisconnected": "已與服務器斷開連接",
"fieldChatAlias": "頻道別名",
"fieldChatAliasHint": "全站範圍內唯一的頻道別名,用於在 URL 中表示該頻道,留空則自動生成。應遵循 URL-Safe 的原則。",
"fieldChatName": "名稱",
@ -336,14 +286,11 @@
"fieldAttachmentRandomId": "訪問 ID",
"fieldAttachmentAlt": "概述文字",
"addAttachmentFromAlbum": "從相冊中添加附件",
"addAttachmentFromFiles": "從文件中添加附件",
"addAttachmentFromClipboard": "粘貼附件",
"addAttachmentFromCameraPhoto": "拍攝照片",
"addAttachmentFromCameraVideo": "拍攝視頻",
"addAttachmentFromRandomId": "通過訪問 ID 鏈接",
"attachmentDetailInfo": "附件詳細信息",
"attachmentPastedImage": "粘貼的圖片",
"attachmentInsertedImage": "插入的圖片",
"attachmentInsertLink": "插入連接",
"attachmentSetAsPostThumbnail": "設置為帖子縮略圖",
"attachmentUnsetAsPostThumbnail": "取消設置為帖子縮略圖",
@ -522,13 +469,8 @@
"accountBirthday": "出生於 {}",
"accountBadge": "徽章",
"accountCheckInNoRecords": "暫無運勢記錄",
"badgeCompanyStaff": "工作人員",
"badgeCompanyStaff": "索爾辛茨士大夫 · 員工",
"badgeSiteMigration": "Solar Network 原住民",
"badgeCommunitySurvey": "調研參與者",
"badgeCommunityVerified": "認證用户",
"badgeCommunityContributor": "優秀社區貢獻者",
"badgeSiteAnniversary": "週年紀念",
"badgeUserBirthday": "生日紀念",
"accountStatus": "狀態",
"accountStatusOnline": "在線",
"accountStatusOffline": "離線",
@ -563,7 +505,6 @@
"termAcceptNextWithAgree": "點擊 “下一步”,即表示你同意我們的各項條款,包括其之後的更新。",
"unauthorized": "未登陸",
"unauthorizedDescription": "登陸以探索整個 Solar Network。",
"projectDetail": "項目詳情",
"serviceStatus": "服務狀態",
"termRelated": "相關條款",
"appDetails": "應用程序詳情",
@ -577,15 +518,11 @@
"postImageShareAds": "來 Solar Network 探索更多有趣帖子",
"postShare": "分享",
"postShareImage": "分享帖圖",
"postGetInsight": "獲取見解",
"postGetInsightTitle": "AI 見解",
"postGetInsightDescription": "AI 可能會出錯,檢查信息真實性。",
"appInitializing": "正在初始化",
"poweredBy": "由 {} 提供支持",
"shareIntent": "分享",
"shareIntentDescription": "您想對您分享的內容做些什麼?",
"shareIntentPostStory": "發佈動態",
"shareIntentSendChannel": "分享到聊天頻道",
"updateAvailable": "檢測到更新可用",
"updateOngoing": "正在更新,請稍後……",
"custom": "自定義",
@ -598,8 +535,6 @@
"colorSchemeWhite": "白色",
"colorSchemeBlack": "黑色",
"colorSchemeApplied": "主題色已應用,可能需要重啓來生效。",
"postFeaturedComment": "精選評論",
"postCategory": "分類",
"postCategoryTechnology": "技術",
"postCategoryGaming": "遊戲",
"postCategoryLife": "生活",
@ -610,290 +545,5 @@
"postCategoryKnowledge": "知識",
"postCategoryLiterature": "文學",
"postCategoryFunny": "搞笑",
"postCategoryUncategorized": "未分類",
"newsAllSources": "所有新聞",
"newsReadingProviderSwap": "切換",
"newsReadingFromReader": "你正在從 HyperNet.Reader 閲讀文章",
"newsReadingFromOriginal": "你正在閲讀原始文章",
"newsDisclaimer": "本文由 HyperNet.Reader 從互聯網上獲取,我們不擔保其內容的真實性,請自行判斷。本文章的所有內容版權歸原作者所有。",
"newsToday": "快訊",
"totpPostSetup": "還有一件事",
"totpPostSetupDescription": "使用 Google Authenticator, Microsoft Authenticator, 1Password, Authy, Bitwarden 或其他支持 TOTP 的驗證器掃描本 QR Code 來添加。",
"totpNeverShare": "永遠不要分享這個 QR Code",
"needHelp": "需要幫助?",
"needHelpLaunch": "查看我們的山羊維基!",
"walletCreate": "創建錢包",
"walletCreateSubtitle": "創建於一個錢包來開始使用源點。",
"walletCreatePassword": "在下方設置你的付款密碼",
"walletCurrencyShort": "源點",
"walletCurrency": {
"one": "{} 源點",
"other": "{} 源點"
},
"aiThinkingProcess": "AI 思考過程",
"accountSettingsApplied": "帳號設置已應用。",
"trayMenuExit": "退出",
"postQuestionUnanswered": "未解答的問題",
"postQuestionUnansweredWithReward": "未解答的問題,懸賞源點 {}",
"postQuestionAnswered": "已解答的問題",
"postQuestionAnswerTitle": "精選解答",
"postQuestionAnswerSelect": "選擇解答",
"postQuestionAnswerSelected": "解答已選擇,獎勵已發放。",
"postVideoUpload": "上傳視頻",
"realmJoin": "加入領域",
"realmCommunityHint": "該領域是一個社區領域,你可以自由加入。",
"realmCommunityPublicChannelsHint": "該領域包含的公共頻道",
"realmCommunityPublishersHint": "該領域的發佈者",
"realmJoined": "已加入領域 {}。",
"join": "加入",
"pollEditorNew": "新投票",
"pollEditorEdit": "編輯投票",
"pollEditorDelete": "刪除投票",
"pollEditorDeleteDescription": "你確定要刪除這個投票嗎?該操作不可撤銷。",
"pollEditorUnlink": "解除鏈接",
"pollOptionAdd": "添加選項",
"pollOptionName": "選項名稱",
"pollLinkExisting": "鏈接現有投票",
"pollAnswered": "答案已經反饋。",
"pollVotes": {
"one": "{} 票",
"other": "{} 票"
},
"publisherDelete": "刪除發佈者 {}",
"publisherDeleteDescription": "你確定要刪除這個發佈者嗎?該操作不可撤銷。",
"channelIsPublic": "公開頻道",
"channelIsPublicDescription": "該頻道是公開的,任何人都可以加入。",
"channelIsCommunity": "社區頻道",
"channelIsCommunityDescription": "目前來説,社區頻道還沒有什麼特別之處。",
"realmIsPublic": "公開領域",
"realmIsPublicDescription": "該領域是公開的,任何人都可以加入。",
"realmIsCommunity": "社區領域",
"realmIsCommunityDescription": "社區領域會顯示在發現頁面上。",
"realmLeave": "離開領域",
"realmLeaveDescription": "離開當前領域,並且刪除領域中的身份。",
"checkInResultTier1": "大凶",
"checkInResultTier2": "兇",
"checkInResultTier3": "中平",
"checkInResultTier4": "吉",
"checkInResultTier5": "大吉",
"flagPostAction": "吹哨",
"flagPost": "吹哨不良內容",
"flagPostDescription": "吹哨不良內容,如果吹哨用户佔瀏覽量的 50% 或以上,則帖子會被摺疊。吹哨後不可撤銷。",
"flaggedPost": "哨子已經吹響。",
"postViews": {
"zero": "{} 次瀏覽",
"one": "{} 次瀏覽",
"other": "{} 次瀏覽"
},
"attachmentBillingUploaded": "已佔用的字節數",
"attachmentBillingDiscount": "免費的字節數",
"attachmentBillingHint": "滑動窗口計價®\n在24小時內上傳的文件大小超出免費空間才會適用扣費。",
"postThumbnail": "帖子縮略圖",
"accountRealms": "領域",
"postInGlobal": "全站",
"postInGlobalDescription": "不關聯此帖子與任何領域。",
"postChannelGlobal": "全站",
"postChannelFriends": "好友",
"postChannelFollowing": "關注",
"postChannelRealm": "領域",
"postFilterReset": "重置過濾器",
"postFilterResetDescription": "清除過濾器並顯示所有帖子。",
"postFilterWithCategory": "查看{}區中的帖子",
"databaseSize": "數據庫大小",
"databaseDelete": "刪除數據庫",
"databaseDeleteDescription": "刪除本地數據庫,內容將從服務器重新獲取。",
"databaseDeleted": "本地數據庫已被刪除。",
"settingsEnablePushNotifications": "啓用推送數據",
"settingsEnablePushNotificationsDescription": "重新啓用並請求推送權限,以防自動激活失敗。",
"settingsEnabledPushNotifications": "推送通知已經註冊。",
"screenStickers": "貼圖",
"stickersDiscovery": "發現",
"stickersOwned": "由我擁有",
"stickersCreated": "由我發佈",
"stickersAdd": "添加貼圖包",
"stickersAdded": "貼圖包已添加。",
"add": "添加",
"stickersRemoved": "貼圖包已被移除,你可以隨時再次添加回來。",
"stickersReload": "重載貼圖包",
"stickersReloadDescription": "從服務器重新加載添加過的貼圖,更新貼圖選擇器。",
"stickersReloaded": "貼圖包已重載。",
"stickersPackDelete": "刪除貼圖包 {}",
"stickersPackDeleteDescription": "你確定要刪除這個貼圖包嗎?這個操作不可撤銷。",
"stickersPackDeleted": "貼圖包已被刪除。",
"stickersDelete": "刪除貼圖 {}",
"stickersDeleteDescription": "你確定要刪除這個貼圖嗎?這個操作不可撤銷。",
"stickersDeleted": "貼圖已被刪除。",
"fieldStickerName": "貼圖名稱",
"fieldStickerAlias": "貼圖別名",
"fieldStickerAliasHint": "和貼圖包前綴組合成為本貼圖的唯一佔位符。",
"fieldStickerPackName": "名稱",
"fieldStickerPackDescription": "描述",
"fieldStickerPackPrefix": "貼圖包前綴",
"fieldStickerAttachment": "附件",
"stickersNew": "新建貼圖",
"stickersNewDescription": "創建一個新的貼圖。",
"stickersPackNew": "新建貼圖包",
"trayMenuShow": "顯示",
"trayMenuMuteNotification": "靜音通知",
"update": "更新",
"forceUpdate": "強制更新",
"forceUpdateDescription": "強制更新應用程序,即使有更新的版本可能不可用。",
"runtimeLogs": "運行時日誌",
"runtimeLogsOpen": "打開日誌文件",
"runtimeLogsDescription": "顯示運行時的日誌記錄。",
"signinResetPasswordHint": "請輸入用户名/電子郵箱地址以幫助我們找到您的帳户並重置密碼。",
"cacheSize": "緩存資源大小",
"cacheDelete": "清除緩存",
"cacheDeleteDescription": "從磁盤中移除緩存的圖片和其他資源,內容將從服務器重新下載。",
"cacheDeleted": "所有緩存已被清除。",
"userNoDescription": "這個人很懶,沒有留下什麼……",
"fieldTimeZone": "時區",
"fieldGender": "性別",
"fieldPronouns": "人稱代詞",
"fieldLocation": "位置",
"fieldLinks": "鏈接",
"fieldLinkName": "名稱",
"fieldLinkUrl": "鏈接",
"screenAccountBadges": "徽章",
"accountBadges": "徽章",
"accountBadgesDescription": "查看並管理你的徽章。",
"badgeActivated": "已佩戴徽章 {}。",
"viewDetailedAttachment": "查看附件詳情",
"screenKeyPairs": "密鑰對",
"accountKeyPairs": "密鑰對",
"accountKeyPairsDescription": "管理用於加密信息的密鑰對。",
"enrollNewKeyPair": "新建密鑰對",
"enrollNewKeyPairDescription": "生成一對新密鑰對。",
"keyPairHasPrivateKey": "有私鑰",
"decrypting": "解密中……",
"decryptingKeyNotFound": "未找到密鑰對或交換失敗,對方可能不在線",
"messageUnablePreview": "無法預覽消息",
"messageUnablePreviewEncrypted": "無法預覽加密消息",
"postViewInGlobalDescription": "不查看特定領域的帖子。",
"postDraftSaved": "已保存為草稿。",
"postDraftBox": "草稿箱",
"postShuffle": "隨便看看",
"checkInStreak": {
"zero": "無連擊",
"one": "連續簽到 {} 天",
"other": "連續簽到 {} 天"
},
"accountChangeStatus": "修改狀態",
"accountStatusSilent": "請勿打擾",
"accountStatusSilentDesc": "將會暫停所有通知推送",
"accountStatusInvisible": "隱身",
"accountStatusInvisibleDesc": "將會在他人界面顯示離線,但不影響功能使用",
"accountCustomStatus": "自定義狀態",
"accountCustomStatusDescription": "客製化你的狀態。",
"accountClearStatus": "清除狀態",
"accountClearStatusDescription": "清除你的狀態,並讓服務器決定你的狀態。",
"fieldAccountStatusLabel": "狀態文字",
"fieldAccountStatusClearAt": "清除時間",
"accountStatusNegative": "負面",
"accountStatusNeutral": "中性",
"accountStatusPositive": "正面",
"mixedFeed": "混合推薦流",
"mixedFeedDescription": "探索頁面可能不只會展示用户的帖子,更可能包含其他的內容。但該模式不適用分類和過濾。",
"filterFeed": "探索隊列調整",
"feedUnknownItem": "無法顯示該內容,當前版本客户端不支持該類型的內容,請嘗試更新應用程序後再試。",
"serviceStatusOperational": "所有服務正常",
"serviceStatusDowngraded": "部分服務異常",
"serviceStatusFailed": "服務狀態異常",
"serviceStatusFailedDescription": "服務器炸了或者剛剛執行完維護程序。",
"serviceNameInsights": "總結、見解與洞察",
"serviceNameInteractive": "帖子與互動",
"serviceNameReader": "新聞與鏈接展開",
"serviceNameMessaging": "即使聊天",
"serviceNameMatrix": "矩陣市場",
"serviceNamePaperclip": "附件",
"serviceNameWallet": "源點錢包",
"serviceNamePassport": "身份驗證與授權",
"accountActionEvent": "操作日誌",
"accountActionEventDescription": "查看你的操作日誌。",
"eventMetadata": "元數據",
"accountAuthTickets": "授權會話",
"accountAuthTicketsDescription": "查看和管理你的授權會話。",
"authTicketCreatedAt": "簽發於 {}",
"authTicketExpiredAt": "到期於 {}",
"authTicketLastGrantAt": "上次刷新於 {}",
"authTicketCurrent": "當前會話",
"accountUnconfirmedTitle": "尚未未確認賬户",
"accountUnconfirmedSubtitle": "您的賬户尚未確認,這會導致大部分功能不可用,並且您的帳户將在 24 小時後自毀。您綁定的郵箱的收件箱應該會有一封郵件指引您確認您的帳户。",
"accountUnconfirmedUnreceived": "未收到郵件?",
"accountUnconfirmedResend": "重新發送一封",
"accountUnconfirmedResendSuccessful": "郵件已重新發送,你可以在 60 分鐘後再重發一封。",
"stickerPickerEmpty": "貼圖列表為空",
"stickerPickerEmptyHint": "想要開始使用貼圖,請先添加貼圖包。",
"goto": "跳轉到 {}",
"accountContactMethods": "聯繫方式",
"accountContactMethodsDescription": "管理你的聯繫方式。",
"accountContactMethodsNameEmail": "電子郵箱",
"accountContactMethodsNamePhone": "電話",
"accountContactMethodsNameAddress": "地址",
"accountContactMethodsPrimary": "主要的",
"accountContactMethodsVerified": "已驗證",
"accountContactMethodsPublic": "公開的",
"accountContactMethodsAdd": "添加聯繫方式",
"accountContactMethodsEdit": "編輯聯繫方式",
"accountContactMethodsAddDescription": "添加新的聯繫方式。",
"fieldContactContent": "聯繫方式",
"accountContactMethodsPublicHint": "這個聯繫方式公開地顯示在個人資料中。",
"accountContactMethodsDelete": "刪除聯繫方式",
"accountContactMethodsDeleteDescription": "你確定要刪除聯繫方式 {} 嗎?這個操作不可撤銷。",
"postCommentAdd": "撰寫一條評論",
"translate": "翻譯",
"translating": "正在翻譯……",
"translated": "已翻譯",
"settingsAutoTranslate": "自動翻譯",
"settingsAutoTranslateDescription": "在查看帖子、消息時自動翻譯文本。",
"trayMenuHide": "隱藏",
"accountSettingsNotify": "通知設置",
"accountSettingsNotifyDescription": "調整你所收到的通知種類。",
"accountSettingsSecurity": "安全設置",
"accountSettingsSecurityDescription": "調整你的帳户安全設置。",
"save": "保存",
"notificationTopicPostFeedback": "帖子數據反饋",
"notificationTopicPostReply": "帖子回覆",
"notificationTopicPostSubscription": "帖子訂閲",
"notificationTopicMessaging": "消息",
"notificationTopicMessagingCall": "通話",
"notificationTopicGeneral": "雜項",
"authMaximumAuthSteps": "最大驗證步驟",
"authMaximumAuthStepsDescription": {
"one": "登入時最多要求 {} 步驗證",
"other": "登入時最多要求 {} 步驗證"
},
"authAlwaysRisky": "總是風險",
"authAlwaysRiskyDescription": "在登入時始終按最高標準要求驗證。",
"chatUnjoined": "未加入頻道",
"chatUnjoinedDescription": "你沒有加入這個頻道,所以你也無法發送消息或者查看這個頻道中的消息。",
"chatUnjoinedPublicDescription": "但幸運的是,這是一個公開頻道,所以你可以主動加入。",
"chatJoin": "加入頻道",
"appInitStarting": "啓動中",
"appInitNetwork": "正在初始化網絡",
"appInitUserdata": "正在初始化用户數據",
"appInitWebsocket": "正在建立 Solar Link",
"appInitNotification": "正在初始化推送通知",
"appInitKeyPair": "正在初始化密鑰對",
"appInitStickers": "正在初始化貼圖包",
"appInitUserDirectory": "正在初始化用户目錄",
"appInitRealm": "正在初始化領域信息",
"appInitChat": "正在初始化聊天",
"appInitDone": "完成",
"community": "社區",
"realmCommunity": "{}的社區",
"postTotalCount": {
"zero": "沒有帖子",
"one": "共 {} 條帖子"
},
"settingsHideBottomNav": "隱藏底部導航欄",
"settingsHideBottomNavDescription": "隱藏底部導航欄,在側邊欄抽屜顯示導航按鈕。",
"reCaptcha": "人機驗證",
"friends": "好友",
"friendsDescription": "管理好友關係。",
"album": "相冊",
"albumDescription": "查看相冊與管理上傳附件。",
"stickers": "貼圖",
"stickersDescription": "查看貼圖包與管理貼圖。",
"navBottomUnauthorizedCaption": "或者註冊一個賬號"
"postCategoryUncategorized": "未分類"
}

View File

@ -15,17 +15,12 @@
"screenAccountProfileEdit": "編輯資料",
"screenAbuseReport": "濫用檢舉",
"screenSettings": "設置",
"screenAccountSettings": "賬號設置",
"screenFactorSettings": "驗證因子",
"screenAccountWallet": "錢包",
"screenNews": "新聞",
"screenAlbum": "相冊",
"screenChat": "聊天",
"screenChatManage": "編輯聊天頻道",
"screenChatNew": "新建聊天頻道",
"screenRealm": "領域",
"screenRealmManage": "編輯領域",
"screenRealmDiscovery": "發現領域",
"screenRealmNew": "新建領域",
"screenNotification": "通知",
"screenPostSearch": "搜索帖子",
@ -92,18 +87,8 @@
},
"loginEnterPassword": "驗證代碼",
"loginSuccess": "登錄為 {}",
"authFactorDelete": "刪除驗證因子",
"authFactorDeleteDescription": "你確定要刪除 {} 驗證因子嗎?",
"authFactorPassword": "密碼",
"authFactorPasswordDescription": "註冊時選擇設置的密碼。",
"authFactorEmail": "電郵一次性驗證碼",
"authFactorEmailDescription": "由我們生成併發送到綁定的的電子郵箱的一次性驗證碼。",
"authFactorTOTP": "時序驗證碼",
"authFactorTOTPDescription": "使用 Google Authenticator 或 Authy 等驗證器生成的一次性驗證碼。",
"authFactorInAppNotify": "應用內通知驗證碼",
"authFactorInAppNotifyDescription": "通過站內通知推送的一次性驗證碼。",
"authFactorAdd": "添加新驗證因子",
"authFactorAddSubtitle": "給你的帳戶登陸時提供另一個方案。",
"accountIntroTitle": "喜歡您來!",
"accountIntroSubtitle": "登陸以探索更廣大的世界。",
"accountLogout": "退出登錄",
@ -112,14 +97,8 @@
"accountLogoutConfirm": "您需要重新輸入賬號密碼,甚至可能需要多步驗證來再次登陸。",
"accountPublishers": "你的發佈者",
"accountPublishersSubtitle": "管理你的公共形象。",
"accountSettings": "帳號設置",
"accountSettingsSubtitle": "管理你的帳號並讓它更好的服務你。",
"accountProfileEdit": "編輯資料",
"accountProfileEditSubtitle": "使你的 Solarpass 賬戶更像你。",
"accountWallet": "錢包",
"accountWalletSubtitle": "查看你的餘額和交易記錄。",
"factorSettings": "驗證因子",
"factorSettingsSubtitle": "管理你的登陸驗證方式。",
"accountProfileEditApplied": "個人資料修改已被應用。",
"publishersNew": "新發布者",
"publisherNewSubtitle": "創建一個新的公共身份。",
@ -137,19 +116,11 @@
"publisherRunBy": "由 {} 管理",
"fieldPublisherBelongToRealm": "所屬領域",
"fieldPublisherBelongToRealmUnset": "未設置發佈者所屬領域",
"writePost": "撰寫",
"postTypeStory": "動態",
"postTypeArticle": "文章",
"postTypeQuestion": "問題",
"postTypeVideo": "視頻",
"writePostTypeStory": "發動態",
"writePostTypeArticle": "寫文章",
"writePostTypeQuestion": "提問題",
"writePostTypeVideo": "發視頻",
"fieldPostPublisher": "帖子發佈者",
"fieldPostContent": "發生什麼事了?!",
"fieldPostTitle": "標題",
"fieldPostQuestionReward": "回答獎勵源點",
"fieldPostDescription": "描述",
"fieldPostTags": "標籤",
"fieldPostCategories": "分類",
@ -205,16 +176,7 @@
"one": "{} 條評論",
"other": "{} 條評論"
},
"postCommentExpand": "展開評論",
"settingsAppearance": "外觀",
"settingsCustomFonts": "自定義字體",
"settingsCustomFontsDescription": "設置應用程序使用的字體。",
"settingsCustomFontFamily": "應用字體",
"settingsCustomFontFamilyHint": "使用英文逗號分割每一種字體,越前優先級越高",
"settingsCustomFontApplied": "自定義字體已經應用。",
"settingsDisplayLanguage": "顯示語言",
"settingsDisplayLanguageDescription": "設置應用程序使用的語言",
"settingsDisplayLanguageSystem": "跟隨系統",
"settingsBackgroundImage": "背景圖片",
"settingsBackgroundImageDescription": "設置應用全局生效的的背景圖片。",
"settingsBackgroundImageClear": "清除現存背景圖",
@ -223,19 +185,10 @@
"settingsThemeMaterial3Description": "將應用主題設置為 Material 3 設計範式的主題。",
"settingsAppBarTransparent": "透明頂欄",
"settingsAppBarTransparentDescription": "為頂欄啟用透明效果。",
"settingsDrawerPreferCollapse": "側邊欄偏好摺疊",
"settingsDrawerPreferCollapseDescription": "將側邊欄優先摺疊,即使屏幕寬度足夠大去放下整個側邊欄。",
"settingsColorScheme": "主題色",
"settingsColorSchemeDescription": "設置應用主題色。",
"settingsColorSeed": "預設色彩主題",
"settingsColorSeedDescription": "選擇一個預設色彩主題。",
"settingsFeatures": "功能",
"settingsNotifyWithHaptic": "新通知時振動",
"settingsNotifyWithHapticDescription": "在應用在前臺時收到新通知出現時出發輕量的振動。",
"settingsExpandPostLink": "展開帖子鏈接",
"settingsExpandPostLinkDescription": "在帖子列表中展開顯示帖子中的鏈接。",
"settingsExpandChatLink": "展開聊天鏈接",
"settingsExpandChatLinkDescription": "在聊天信息中展開顯示內容中的鏈接。",
"settingsNetwork": "網絡",
"settingsNetworkServer": "HyperNet 服務器",
"settingsNetworkServerDescription": "設置 HyperNet 服務器地址,選擇我們提供的,或者自己搭建。",
@ -254,15 +207,12 @@
"settingsMisc": "雜項",
"settingsMiscAbout": "關於",
"settingsMiscAboutDescription": "查看 Solian 的版本信息。",
"settingsAccountLanguage": "帳號偏好語言",
"settingsAccountLanguageDescription": "設置郵件、通知和其他帳號相關內容的語言。",
"sensitiveContent": "敏感內容",
"sensitiveContentCollapsed": "敏感內容已摺疊。",
"sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。",
"sensitiveContentReveal": "顯示內容",
"serverConnecting": "正在連接…",
"serverDisconnected": "已斷開連接",
"serverConnected": "已連接",
"serverConnecting": "正在連接服務器…",
"serverDisconnected": "已與服務器斷開連接",
"fieldChatAlias": "頻道別名",
"fieldChatAliasHint": "全站範圍內唯一的頻道別名,用於在 URL 中表示該頻道,留空則自動生成。應遵循 URL-Safe 的原則。",
"fieldChatName": "名稱",
@ -336,14 +286,11 @@
"fieldAttachmentRandomId": "訪問 ID",
"fieldAttachmentAlt": "概述文字",
"addAttachmentFromAlbum": "從相冊中添加附件",
"addAttachmentFromFiles": "從文件中添加附件",
"addAttachmentFromClipboard": "粘貼附件",
"addAttachmentFromCameraPhoto": "拍攝照片",
"addAttachmentFromCameraVideo": "拍攝視頻",
"addAttachmentFromRandomId": "通過訪問 ID 鏈接",
"attachmentDetailInfo": "附件詳細信息",
"attachmentPastedImage": "粘貼的圖片",
"attachmentInsertedImage": "插入的圖片",
"attachmentInsertLink": "插入連接",
"attachmentSetAsPostThumbnail": "設置為帖子縮略圖",
"attachmentUnsetAsPostThumbnail": "取消設置為帖子縮略圖",
@ -522,13 +469,8 @@
"accountBirthday": "出生於 {}",
"accountBadge": "徽章",
"accountCheckInNoRecords": "暫無運勢記錄",
"badgeCompanyStaff": "工作人員",
"badgeCompanyStaff": "索爾辛茨士大夫 · 員工",
"badgeSiteMigration": "Solar Network 原住民",
"badgeCommunitySurvey": "調研參與者",
"badgeCommunityVerified": "認證用戶",
"badgeCommunityContributor": "優秀社區貢獻者",
"badgeSiteAnniversary": "週年紀念",
"badgeUserBirthday": "生日紀念",
"accountStatus": "狀態",
"accountStatusOnline": "在線",
"accountStatusOffline": "離線",
@ -563,7 +505,6 @@
"termAcceptNextWithAgree": "點擊 “下一步”,即表示你同意我們的各項條款,包括其之後的更新。",
"unauthorized": "未登陸",
"unauthorizedDescription": "登陸以探索整個 Solar Network。",
"projectDetail": "項目詳情",
"serviceStatus": "服務狀態",
"termRelated": "相關條款",
"appDetails": "應用程序詳情",
@ -577,15 +518,11 @@
"postImageShareAds": "來 Solar Network 探索更多有趣帖子",
"postShare": "分享",
"postShareImage": "分享帖圖",
"postGetInsight": "獲取見解",
"postGetInsightTitle": "AI 見解",
"postGetInsightDescription": "AI 可能會出錯,檢查信息真實性。",
"appInitializing": "正在初始化",
"poweredBy": "由 {} 提供支持",
"shareIntent": "分享",
"shareIntentDescription": "您想對您分享的內容做些什麼?",
"shareIntentPostStory": "發佈動態",
"shareIntentSendChannel": "分享到聊天頻道",
"updateAvailable": "檢測到更新可用",
"updateOngoing": "正在更新,請稍後……",
"custom": "自定義",
@ -598,8 +535,6 @@
"colorSchemeWhite": "白色",
"colorSchemeBlack": "黑色",
"colorSchemeApplied": "主題色已應用,可能需要重啟來生效。",
"postFeaturedComment": "精選評論",
"postCategory": "分類",
"postCategoryTechnology": "技術",
"postCategoryGaming": "遊戲",
"postCategoryLife": "生活",
@ -610,290 +545,5 @@
"postCategoryKnowledge": "知識",
"postCategoryLiterature": "文學",
"postCategoryFunny": "搞笑",
"postCategoryUncategorized": "未分類",
"newsAllSources": "所有新聞",
"newsReadingProviderSwap": "切換",
"newsReadingFromReader": "你正在從 HyperNet.Reader 閱讀文章",
"newsReadingFromOriginal": "你正在閱讀原始文章",
"newsDisclaimer": "本文由 HyperNet.Reader 從互聯網上獲取,我們不擔保其內容的真實性,請自行判斷。本文章的所有內容版權歸原作者所有。",
"newsToday": "快訊",
"totpPostSetup": "還有一件事",
"totpPostSetupDescription": "使用 Google Authenticator, Microsoft Authenticator, 1Password, Authy, Bitwarden 或其他支持 TOTP 的驗證器掃描本 QR Code 來添加。",
"totpNeverShare": "永遠不要分享這個 QR Code",
"needHelp": "需要幫助?",
"needHelpLaunch": "查看我們的山羊維基!",
"walletCreate": "創建錢包",
"walletCreateSubtitle": "創建於一個錢包來開始使用源點。",
"walletCreatePassword": "在下方設置你的付款密碼",
"walletCurrencyShort": "源點",
"walletCurrency": {
"one": "{} 源點",
"other": "{} 源點"
},
"aiThinkingProcess": "AI 思考過程",
"accountSettingsApplied": "帳號設置已應用。",
"trayMenuExit": "退出",
"postQuestionUnanswered": "未解答的問題",
"postQuestionUnansweredWithReward": "未解答的問題,懸賞源點 {}",
"postQuestionAnswered": "已解答的問題",
"postQuestionAnswerTitle": "精選解答",
"postQuestionAnswerSelect": "選擇解答",
"postQuestionAnswerSelected": "解答已選擇,獎勵已發放。",
"postVideoUpload": "上傳視頻",
"realmJoin": "加入領域",
"realmCommunityHint": "該領域是一個社區領域,你可以自由加入。",
"realmCommunityPublicChannelsHint": "該領域包含的公共頻道",
"realmCommunityPublishersHint": "該領域的發佈者",
"realmJoined": "已加入領域 {}。",
"join": "加入",
"pollEditorNew": "新投票",
"pollEditorEdit": "編輯投票",
"pollEditorDelete": "刪除投票",
"pollEditorDeleteDescription": "你確定要刪除這個投票嗎?該操作不可撤銷。",
"pollEditorUnlink": "解除鏈接",
"pollOptionAdd": "添加選項",
"pollOptionName": "選項名稱",
"pollLinkExisting": "鏈接現有投票",
"pollAnswered": "答案已經反饋。",
"pollVotes": {
"one": "{} 票",
"other": "{} 票"
},
"publisherDelete": "刪除發佈者 {}",
"publisherDeleteDescription": "你確定要刪除這個發佈者嗎?該操作不可撤銷。",
"channelIsPublic": "公開頻道",
"channelIsPublicDescription": "該頻道是公開的,任何人都可以加入。",
"channelIsCommunity": "社區頻道",
"channelIsCommunityDescription": "目前來說,社區頻道還沒有什麼特別之處。",
"realmIsPublic": "公開領域",
"realmIsPublicDescription": "該領域是公開的,任何人都可以加入。",
"realmIsCommunity": "社區領域",
"realmIsCommunityDescription": "社區領域會顯示在發現頁面上。",
"realmLeave": "離開領域",
"realmLeaveDescription": "離開當前領域,並且刪除領域中的身份。",
"checkInResultTier1": "大凶",
"checkInResultTier2": "兇",
"checkInResultTier3": "中平",
"checkInResultTier4": "吉",
"checkInResultTier5": "大吉",
"flagPostAction": "吹哨",
"flagPost": "吹哨不良內容",
"flagPostDescription": "吹哨不良內容,如果吹哨用戶佔瀏覽量的 50% 或以上,則帖子會被摺疊。吹哨後不可撤銷。",
"flaggedPost": "哨子已經吹響。",
"postViews": {
"zero": "{} 次瀏覽",
"one": "{} 次瀏覽",
"other": "{} 次瀏覽"
},
"attachmentBillingUploaded": "已佔用的字節數",
"attachmentBillingDiscount": "免費的字節數",
"attachmentBillingHint": "滑動窗口計價®\n在24小時內上傳的文件大小超出免費空間才會適用扣費。",
"postThumbnail": "帖子縮略圖",
"accountRealms": "領域",
"postInGlobal": "全站",
"postInGlobalDescription": "不關聯此帖子與任何領域。",
"postChannelGlobal": "全站",
"postChannelFriends": "好友",
"postChannelFollowing": "關注",
"postChannelRealm": "領域",
"postFilterReset": "重置過濾器",
"postFilterResetDescription": "清除過濾器並顯示所有帖子。",
"postFilterWithCategory": "查看{}區中的帖子",
"databaseSize": "數據庫大小",
"databaseDelete": "刪除數據庫",
"databaseDeleteDescription": "刪除本地數據庫,內容將從服務器重新獲取。",
"databaseDeleted": "本地數據庫已被刪除。",
"settingsEnablePushNotifications": "啟用推送數據",
"settingsEnablePushNotificationsDescription": "重新啟用並請求推送權限,以防自動激活失敗。",
"settingsEnabledPushNotifications": "推送通知已經註冊。",
"screenStickers": "貼圖",
"stickersDiscovery": "發現",
"stickersOwned": "由我擁有",
"stickersCreated": "由我發佈",
"stickersAdd": "添加貼圖包",
"stickersAdded": "貼圖包已添加。",
"add": "添加",
"stickersRemoved": "貼圖包已被移除,你可以隨時再次添加回來。",
"stickersReload": "重載貼圖包",
"stickersReloadDescription": "從服務器重新加載添加過的貼圖,更新貼圖選擇器。",
"stickersReloaded": "貼圖包已重載。",
"stickersPackDelete": "刪除貼圖包 {}",
"stickersPackDeleteDescription": "你確定要刪除這個貼圖包嗎?這個操作不可撤銷。",
"stickersPackDeleted": "貼圖包已被刪除。",
"stickersDelete": "刪除貼圖 {}",
"stickersDeleteDescription": "你確定要刪除這個貼圖嗎?這個操作不可撤銷。",
"stickersDeleted": "貼圖已被刪除。",
"fieldStickerName": "貼圖名稱",
"fieldStickerAlias": "貼圖別名",
"fieldStickerAliasHint": "和貼圖包前綴組合成為本貼圖的唯一佔位符。",
"fieldStickerPackName": "名稱",
"fieldStickerPackDescription": "描述",
"fieldStickerPackPrefix": "貼圖包前綴",
"fieldStickerAttachment": "附件",
"stickersNew": "新建貼圖",
"stickersNewDescription": "創建一個新的貼圖。",
"stickersPackNew": "新建貼圖包",
"trayMenuShow": "顯示",
"trayMenuMuteNotification": "靜音通知",
"update": "更新",
"forceUpdate": "強制更新",
"forceUpdateDescription": "強制更新應用程序,即使有更新的版本可能不可用。",
"runtimeLogs": "運行時日誌",
"runtimeLogsOpen": "打開日誌文件",
"runtimeLogsDescription": "顯示運行時的日誌記錄。",
"signinResetPasswordHint": "請輸入用戶名/電子郵箱地址以幫助我們找到您的帳戶並重置密碼。",
"cacheSize": "緩存資源大小",
"cacheDelete": "清除緩存",
"cacheDeleteDescription": "從磁盤中移除緩存的圖片和其他資源,內容將從服務器重新下載。",
"cacheDeleted": "所有緩存已被清除。",
"userNoDescription": "這個人很懶,沒有留下什麼……",
"fieldTimeZone": "時區",
"fieldGender": "性別",
"fieldPronouns": "人稱代詞",
"fieldLocation": "位置",
"fieldLinks": "鏈接",
"fieldLinkName": "名稱",
"fieldLinkUrl": "鏈接",
"screenAccountBadges": "徽章",
"accountBadges": "徽章",
"accountBadgesDescription": "查看並管理你的徽章。",
"badgeActivated": "已佩戴徽章 {}。",
"viewDetailedAttachment": "查看附件詳情",
"screenKeyPairs": "密鑰對",
"accountKeyPairs": "密鑰對",
"accountKeyPairsDescription": "管理用於加密信息的密鑰對。",
"enrollNewKeyPair": "新建密鑰對",
"enrollNewKeyPairDescription": "生成一對新密鑰對。",
"keyPairHasPrivateKey": "有私鑰",
"decrypting": "解密中……",
"decryptingKeyNotFound": "未找到密鑰對或交換失敗,對方可能不在線",
"messageUnablePreview": "無法預覽消息",
"messageUnablePreviewEncrypted": "無法預覽加密消息",
"postViewInGlobalDescription": "不查看特定領域的帖子。",
"postDraftSaved": "已保存為草稿。",
"postDraftBox": "草稿箱",
"postShuffle": "隨便看看",
"checkInStreak": {
"zero": "無連擊",
"one": "連續簽到 {} 天",
"other": "連續簽到 {} 天"
},
"accountChangeStatus": "修改狀態",
"accountStatusSilent": "請勿打擾",
"accountStatusSilentDesc": "將會暫停所有通知推送",
"accountStatusInvisible": "隱身",
"accountStatusInvisibleDesc": "將會在他人界面顯示離線,但不影響功能使用",
"accountCustomStatus": "自定義狀態",
"accountCustomStatusDescription": "客製化你的狀態。",
"accountClearStatus": "清除狀態",
"accountClearStatusDescription": "清除你的狀態,並讓服務器決定你的狀態。",
"fieldAccountStatusLabel": "狀態文字",
"fieldAccountStatusClearAt": "清除時間",
"accountStatusNegative": "負面",
"accountStatusNeutral": "中性",
"accountStatusPositive": "正面",
"mixedFeed": "混合推薦流",
"mixedFeedDescription": "探索頁面可能不只會展示用戶的帖子,更可能包含其他的內容。但該模式不適用分類和過濾。",
"filterFeed": "探索隊列調整",
"feedUnknownItem": "無法顯示該內容,當前版本客戶端不支持該類型的內容,請嘗試更新應用程序後再試。",
"serviceStatusOperational": "所有服務正常",
"serviceStatusDowngraded": "部分服務異常",
"serviceStatusFailed": "服務狀態異常",
"serviceStatusFailedDescription": "服務器炸了或者剛剛執行完維護程序。",
"serviceNameInsights": "總結、見解與洞察",
"serviceNameInteractive": "帖子與互動",
"serviceNameReader": "新聞與鏈接展開",
"serviceNameMessaging": "即使聊天",
"serviceNameMatrix": "矩陣市場",
"serviceNamePaperclip": "附件",
"serviceNameWallet": "源點錢包",
"serviceNamePassport": "身份驗證與授權",
"accountActionEvent": "操作日誌",
"accountActionEventDescription": "查看你的操作日誌。",
"eventMetadata": "元數據",
"accountAuthTickets": "授權會話",
"accountAuthTicketsDescription": "查看和管理你的授權會話。",
"authTicketCreatedAt": "簽發於 {}",
"authTicketExpiredAt": "到期於 {}",
"authTicketLastGrantAt": "上次刷新於 {}",
"authTicketCurrent": "當前會話",
"accountUnconfirmedTitle": "尚未未確認賬戶",
"accountUnconfirmedSubtitle": "您的賬戶尚未確認,這會導致大部分功能不可用,並且您的帳戶將在 24 小時後自毀。您綁定的郵箱的收件箱應該會有一封郵件指引您確認您的帳戶。",
"accountUnconfirmedUnreceived": "未收到郵件?",
"accountUnconfirmedResend": "重新發送一封",
"accountUnconfirmedResendSuccessful": "郵件已重新發送,你可以在 60 分鐘後再重發一封。",
"stickerPickerEmpty": "貼圖列表為空",
"stickerPickerEmptyHint": "想要開始使用貼圖,請先添加貼圖包。",
"goto": "跳轉到 {}",
"accountContactMethods": "聯繫方式",
"accountContactMethodsDescription": "管理你的聯繫方式。",
"accountContactMethodsNameEmail": "電子郵箱",
"accountContactMethodsNamePhone": "電話",
"accountContactMethodsNameAddress": "地址",
"accountContactMethodsPrimary": "主要的",
"accountContactMethodsVerified": "已驗證",
"accountContactMethodsPublic": "公開的",
"accountContactMethodsAdd": "添加聯繫方式",
"accountContactMethodsEdit": "編輯聯繫方式",
"accountContactMethodsAddDescription": "添加新的聯繫方式。",
"fieldContactContent": "聯繫方式",
"accountContactMethodsPublicHint": "這個聯繫方式公開地顯示在個人資料中。",
"accountContactMethodsDelete": "刪除聯繫方式",
"accountContactMethodsDeleteDescription": "你確定要刪除聯繫方式 {} 嗎?這個操作不可撤銷。",
"postCommentAdd": "撰寫一條評論",
"translate": "翻譯",
"translating": "正在翻譯……",
"translated": "已翻譯",
"settingsAutoTranslate": "自動翻譯",
"settingsAutoTranslateDescription": "在查看帖子、消息時自動翻譯文本。",
"trayMenuHide": "隱藏",
"accountSettingsNotify": "通知設置",
"accountSettingsNotifyDescription": "調整你所收到的通知種類。",
"accountSettingsSecurity": "安全設置",
"accountSettingsSecurityDescription": "調整你的帳戶安全設置。",
"save": "保存",
"notificationTopicPostFeedback": "帖子數據反饋",
"notificationTopicPostReply": "帖子回覆",
"notificationTopicPostSubscription": "帖子訂閱",
"notificationTopicMessaging": "消息",
"notificationTopicMessagingCall": "通話",
"notificationTopicGeneral": "雜項",
"authMaximumAuthSteps": "最大驗證步驟",
"authMaximumAuthStepsDescription": {
"one": "登入時最多要求 {} 步驗證",
"other": "登入時最多要求 {} 步驗證"
},
"authAlwaysRisky": "總是風險",
"authAlwaysRiskyDescription": "在登入時始終按最高標準要求驗證。",
"chatUnjoined": "未加入頻道",
"chatUnjoinedDescription": "你沒有加入這個頻道,所以你也無法發送消息或者查看這個頻道中的消息。",
"chatUnjoinedPublicDescription": "但幸運的是,這是一個公開頻道,所以你可以主動加入。",
"chatJoin": "加入頻道",
"appInitStarting": "啟動中",
"appInitNetwork": "正在初始化網絡",
"appInitUserdata": "正在初始化用戶數據",
"appInitWebsocket": "正在建立 Solar Link",
"appInitNotification": "正在初始化推送通知",
"appInitKeyPair": "正在初始化密鑰對",
"appInitStickers": "正在初始化貼圖包",
"appInitUserDirectory": "正在初始化用戶目錄",
"appInitRealm": "正在初始化領域信息",
"appInitChat": "正在初始化聊天",
"appInitDone": "完成",
"community": "社區",
"realmCommunity": "{}的社區",
"postTotalCount": {
"zero": "沒有帖子",
"one": "共 {} 條帖子"
},
"settingsHideBottomNav": "隱藏底部導航欄",
"settingsHideBottomNavDescription": "隱藏底部導航欄,在側邊欄抽屜顯示導航按鈕。",
"reCaptcha": "人機驗證",
"friends": "好友",
"friendsDescription": "管理好友關係。",
"album": "相冊",
"albumDescription": "查看相冊與管理上傳附件。",
"stickers": "貼圖",
"stickersDescription": "查看貼圖包與管理貼圖。",
"navBottomUnauthorizedCaption": "或者註冊一個賬號"
"postCategoryUncategorized": "未分類"
}

View File

@ -4,8 +4,4 @@ targets:
json_serializable:
options:
explicit_to_json: true
field_rename: snake
drift_dev:
options:
databases:
my_database: lib/database/database.dart
field_rename: snake

View File

@ -1,4 +0,0 @@
#!/bin/sh
cd "$(dirname "$0")"
exec ./surface

View File

@ -1,8 +0,0 @@
[Desktop Entry]
Version=1.0
Type=Application
Terminal=false
Name=Solian
Exec=surface %u
Icon=icon-light-radius
Categories=Network;

14
debian/debian.yml vendored
View File

@ -1,14 +0,0 @@
flutter_app:
command: surface
arch: x64
parent: /usr/local/lib
nonInteractive: false
control:
Package: solian
Version: 2.3.2
Architecture: amd64
Priority: optional
Depends: mpv keybinder-3.0
Maintainer: Solsynth LLC
Description: The Solar Network Desktop Application

View File

@ -1,9 +0,0 @@
[Desktop Entry]
Version=2.3.2
Name=Solian
GenericName=Solian
Comment=The Solar Network Desktop Application
Terminal=false
Type=Application
Categories=Social Networking
Keywords=social;social network;chat;solar network

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 232 KiB

View File

@ -1 +0,0 @@
{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.2.0"},"options":{"store_date_time_values_as_text":false},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"sn_local_chat_channel","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"alias","getter_name":"alias","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"content","getter_name":"content","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const SnChannelConverter()","dart_type_name":"SnChannel"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":1,"references":[],"type":"table","data":{"name":"sn_local_chat_message","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"channel_id","getter_name":"channelId","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"content","getter_name":"content","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const SnMessageConverter()","dart_type_name":"SnChatMessage"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}}]}

View File

@ -1 +0,0 @@
{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.2.0"},"options":{"store_date_time_values_as_text":false},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"sn_local_chat_channel","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"alias","getter_name":"alias","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"content","getter_name":"content","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const SnChannelConverter()","dart_type_name":"SnChannel"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":1,"references":[],"type":"table","data":{"name":"sn_local_chat_message","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"channel_id","getter_name":"channelId","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"content","getter_name":"content","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const SnMessageConverter()","dart_type_name":"SnChatMessage"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CAST(strftime(\\'%s\\', CURRENT_TIMESTAMP) AS INTEGER)')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":2,"references":[],"type":"table","data":{"name":"sn_local_key_pair","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"account_id","getter_name":"accountId","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"public_key","getter_name":"publicKey","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"private_key","getter_name":"privateKey","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_active","getter_name":"isActive","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_active\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_active\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"]}}]}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your project
platform :ios, '15.1'
platform :ios, '13.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'

View File

@ -1,9 +1,8 @@
PODS:
- Alamofire (5.10.2)
- audioplayers_darwin (0.0.1):
- Flutter
- connectivity_plus (0.0.1):
- Flutter
- FlutterMacOS
- croppy (0.0.1):
- Flutter
- device_info_plus (0.0.1):
@ -39,65 +38,63 @@ PODS:
- DKPhotoGallery/Resource (0.0.19):
- SDWebImage
- SwiftyGif
- fast_rsa (0.6.0):
- Flutter
- file_picker (0.0.1):
- DKImagePickerController/PhotoGallery
- Flutter
- file_saver (0.0.1):
- Flutter
- Firebase/Analytics (11.10.0):
- Firebase/Analytics (11.4.0):
- Firebase/Core
- Firebase/Core (11.10.0):
- Firebase/Core (11.4.0):
- Firebase/CoreOnly
- FirebaseAnalytics (~> 11.10.0)
- Firebase/CoreOnly (11.10.0):
- FirebaseCore (~> 11.10.0)
- Firebase/Messaging (11.10.0):
- FirebaseAnalytics (~> 11.4.0)
- Firebase/CoreOnly (11.4.0):
- FirebaseCore (= 11.4.0)
- Firebase/Messaging (11.4.0):
- Firebase/CoreOnly
- FirebaseMessaging (~> 11.10.0)
- firebase_analytics (11.4.5):
- Firebase/Analytics (= 11.10.0)
- FirebaseMessaging (~> 11.4.0)
- firebase_analytics (11.3.6):
- Firebase/Analytics (= 11.4.0)
- firebase_core
- Flutter
- firebase_core (3.13.0):
- Firebase/CoreOnly (= 11.10.0)
- firebase_core (3.9.0):
- Firebase/CoreOnly (= 11.4.0)
- Flutter
- firebase_messaging (15.2.5):
- Firebase/Messaging (= 11.10.0)
- firebase_messaging (15.1.6):
- Firebase/Messaging (= 11.4.0)
- firebase_core
- Flutter
- FirebaseAnalytics (11.10.0):
- FirebaseAnalytics/AdIdSupport (= 11.10.0)
- FirebaseCore (~> 11.10.0)
- FirebaseAnalytics (11.4.0):
- FirebaseAnalytics/AdIdSupport (= 11.4.0)
- FirebaseCore (~> 11.0)
- FirebaseInstallations (~> 11.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
- GoogleUtilities/MethodSwizzler (~> 8.0)
- GoogleUtilities/Network (~> 8.0)
- "GoogleUtilities/NSData+zlib (~> 8.0)"
- nanopb (~> 3.30910.0)
- FirebaseAnalytics/AdIdSupport (11.10.0):
- FirebaseCore (~> 11.10.0)
- FirebaseAnalytics/AdIdSupport (11.4.0):
- FirebaseCore (~> 11.0)
- FirebaseInstallations (~> 11.0)
- GoogleAppMeasurement (= 11.10.0)
- GoogleAppMeasurement (= 11.4.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
- GoogleUtilities/MethodSwizzler (~> 8.0)
- GoogleUtilities/Network (~> 8.0)
- "GoogleUtilities/NSData+zlib (~> 8.0)"
- nanopb (~> 3.30910.0)
- FirebaseCore (11.10.0):
- FirebaseCoreInternal (~> 11.10.0)
- FirebaseCore (11.4.0):
- FirebaseCoreInternal (~> 11.0)
- GoogleUtilities/Environment (~> 8.0)
- GoogleUtilities/Logger (~> 8.0)
- FirebaseCoreInternal (11.10.0):
- FirebaseCoreInternal (11.6.0):
- "GoogleUtilities/NSData+zlib (~> 8.0)"
- FirebaseInstallations (11.10.0):
- FirebaseCore (~> 11.10.0)
- FirebaseInstallations (11.4.0):
- FirebaseCore (~> 11.0)
- GoogleUtilities/Environment (~> 8.0)
- GoogleUtilities/UserDefaults (~> 8.0)
- PromisesObjC (~> 2.4)
- FirebaseMessaging (11.10.0):
- FirebaseCore (~> 11.10.0)
- FirebaseMessaging (11.4.0):
- FirebaseCore (~> 11.0)
- FirebaseInstallations (~> 11.0)
- GoogleDataTransport (~> 10.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
@ -108,40 +105,32 @@ PODS:
- Flutter (1.0.0)
- flutter_app_update (0.0.1):
- Flutter
- flutter_inappwebview_ios (0.0.1):
- Flutter
- flutter_inappwebview_ios/Core (= 0.0.1)
- OrderedSet (~> 6.0.3)
- flutter_inappwebview_ios/Core (0.0.1):
- Flutter
- OrderedSet (~> 6.0.3)
- flutter_native_splash (2.4.3):
- Flutter
- flutter_timezone (0.0.1):
- Flutter
- flutter_udid (0.0.1):
- Flutter
- SAMKeychain
- flutter_webrtc (0.12.2):
- Flutter
- WebRTC-SDK (= 125.6422.06)
- gal (1.0.0):
- Flutter
- FlutterMacOS
- Giphy (2.2.12):
- libwebp
- GoogleAppMeasurement (11.10.0):
- GoogleAppMeasurement/AdIdSupport (= 11.10.0)
- GoogleAppMeasurement (11.4.0):
- GoogleAppMeasurement/AdIdSupport (= 11.4.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
- GoogleUtilities/MethodSwizzler (~> 8.0)
- GoogleUtilities/Network (~> 8.0)
- "GoogleUtilities/NSData+zlib (~> 8.0)"
- nanopb (~> 3.30910.0)
- GoogleAppMeasurement/AdIdSupport (11.10.0):
- GoogleAppMeasurement/WithoutAdIdSupport (= 11.10.0)
- GoogleAppMeasurement/AdIdSupport (11.4.0):
- GoogleAppMeasurement/WithoutAdIdSupport (= 11.4.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
- GoogleUtilities/MethodSwizzler (~> 8.0)
- GoogleUtilities/Network (~> 8.0)
- "GoogleUtilities/NSData+zlib (~> 8.0)"
- nanopb (~> 3.30910.0)
- GoogleAppMeasurement/WithoutAdIdSupport (11.10.0):
- GoogleAppMeasurement/WithoutAdIdSupport (11.4.0):
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
- GoogleUtilities/MethodSwizzler (~> 8.0)
- GoogleUtilities/Network (~> 8.0)
@ -183,28 +172,15 @@ PODS:
- Flutter
- in_app_review (2.0.0):
- Flutter
- jitsi_meet_flutter_sdk (11.1.1):
- Kingfisher (8.1.3)
- livekit_client (2.3.4):
- Flutter
- JitsiMeetSDK (= 11.1.1)
- JitsiMeetSDK (11.1.1):
- Giphy (= 2.2.12)
- JitsiWebRTC (~> 124.0)
- JitsiWebRTC (124.0.2)
- Kingfisher (8.3.1)
- libwebp (1.5.0):
- libwebp/demux (= 1.5.0)
- libwebp/mux (= 1.5.0)
- libwebp/sharpyuv (= 1.5.0)
- libwebp/webp (= 1.5.0)
- libwebp/demux (1.5.0):
- libwebp/webp
- libwebp/mux (1.5.0):
- libwebp/demux
- libwebp/sharpyuv (1.5.0)
- libwebp/webp (1.5.0):
- libwebp/sharpyuv
- flutter_webrtc
- WebRTC-SDK (= 125.6422.06)
- media_kit_libs_ios_video (1.0.4):
- Flutter
- media_kit_native_event_loop (1.0.0):
- Flutter
- media_kit_video (0.0.1):
- Flutter
- nanopb (3.30910.0):
@ -212,7 +188,6 @@ PODS:
- nanopb/encode (= 3.30910.0)
- nanopb/decode (3.30910.0)
- nanopb/encode (3.30910.0)
- OrderedSet (6.0.3)
- package_info_plus (0.4.5):
- Flutter
- pasteboard (0.0.1):
@ -226,9 +201,11 @@ PODS:
- receive_sharing_intent (1.8.1):
- Flutter
- SAMKeychain (1.5.3)
- SDWebImage (5.21.0):
- SDWebImage/Core (= 5.21.0)
- SDWebImage/Core (5.21.0)
- screen_brightness_ios (0.1.0):
- Flutter
- SDWebImage (5.20.0):
- SDWebImage/Core (= 5.20.0)
- SDWebImage/Core (5.20.0)
- share_plus (0.0.1):
- Flutter
- shared_preferences_foundation (0.0.1):
@ -237,28 +214,6 @@ PODS:
- sqflite_darwin (0.0.4):
- Flutter
- FlutterMacOS
- sqlite3 (3.49.1):
- sqlite3/common (= 3.49.1)
- sqlite3/common (3.49.1)
- sqlite3/dbstatvtab (3.49.1):
- sqlite3/common
- sqlite3/fts5 (3.49.1):
- sqlite3/common
- sqlite3/math (3.49.1):
- sqlite3/common
- sqlite3/perf-threadsafe (3.49.1):
- sqlite3/common
- sqlite3/rtree (3.49.1):
- sqlite3/common
- sqlite3_flutter_libs (0.0.1):
- Flutter
- FlutterMacOS
- sqlite3 (~> 3.49.1)
- sqlite3/dbstatvtab
- sqlite3/fts5
- sqlite3/math
- sqlite3/perf-threadsafe
- sqlite3/rtree
- SwiftyGif (5.4.5)
- url_launcher_ios (0.0.1):
- Flutter
@ -268,16 +223,15 @@ PODS:
- Flutter
- wakelock_plus (0.0.1):
- Flutter
- WebRTC-SDK (125.6422.06)
- workmanager (0.0.1):
- Flutter
DEPENDENCIES:
- Alamofire
- audioplayers_darwin (from `.symlinks/plugins/audioplayers_darwin/ios`)
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/darwin`)
- croppy (from `.symlinks/plugins/croppy/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- fast_rsa (from `.symlinks/plugins/fast_rsa/ios`)
- file_picker (from `.symlinks/plugins/file_picker/ios`)
- file_saver (from `.symlinks/plugins/file_saver/ios`)
- firebase_analytics (from `.symlinks/plugins/firebase_analytics/ios`)
@ -285,27 +239,27 @@ DEPENDENCIES:
- firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
- Flutter (from `Flutter`)
- flutter_app_update (from `.symlinks/plugins/flutter_app_update/ios`)
- flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`)
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
- flutter_timezone (from `.symlinks/plugins/flutter_timezone/ios`)
- flutter_udid (from `.symlinks/plugins/flutter_udid/ios`)
- flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`)
- gal (from `.symlinks/plugins/gal/darwin`)
- home_widget (from `.symlinks/plugins/home_widget/ios`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- in_app_review (from `.symlinks/plugins/in_app_review/ios`)
- jitsi_meet_flutter_sdk (from `.symlinks/plugins/jitsi_meet_flutter_sdk/ios`)
- Kingfisher (~> 8.0)
- livekit_client (from `.symlinks/plugins/livekit_client/ios`)
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
- media_kit_native_event_loop (from `.symlinks/plugins/media_kit_native_event_loop/ios`)
- media_kit_video (from `.symlinks/plugins/media_kit_video/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- pasteboard (from `.symlinks/plugins/pasteboard/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`)
- screen_brightness_ios (from `.symlinks/plugins/screen_brightness_ios/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
- sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- video_compress (from `.symlinks/plugins/video_compress/ios`)
- volume_controller (from `.symlinks/plugins/volume_controller/ios`)
@ -323,33 +277,24 @@ SPEC REPOS:
- FirebaseCoreInternal
- FirebaseInstallations
- FirebaseMessaging
- Giphy
- GoogleAppMeasurement
- GoogleDataTransport
- GoogleUtilities
- JitsiMeetSDK
- JitsiWebRTC
- Kingfisher
- libwebp
- nanopb
- OrderedSet
- PromisesObjC
- SAMKeychain
- SDWebImage
- sqlite3
- SwiftyGif
- WebRTC-SDK
EXTERNAL SOURCES:
audioplayers_darwin:
:path: ".symlinks/plugins/audioplayers_darwin/ios"
connectivity_plus:
:path: ".symlinks/plugins/connectivity_plus/ios"
:path: ".symlinks/plugins/connectivity_plus/darwin"
croppy:
:path: ".symlinks/plugins/croppy/ios"
device_info_plus:
:path: ".symlinks/plugins/device_info_plus/ios"
fast_rsa:
:path: ".symlinks/plugins/fast_rsa/ios"
file_picker:
:path: ".symlinks/plugins/file_picker/ios"
file_saver:
@ -364,14 +309,12 @@ EXTERNAL SOURCES:
:path: Flutter
flutter_app_update:
:path: ".symlinks/plugins/flutter_app_update/ios"
flutter_inappwebview_ios:
:path: ".symlinks/plugins/flutter_inappwebview_ios/ios"
flutter_native_splash:
:path: ".symlinks/plugins/flutter_native_splash/ios"
flutter_timezone:
:path: ".symlinks/plugins/flutter_timezone/ios"
flutter_udid:
:path: ".symlinks/plugins/flutter_udid/ios"
flutter_webrtc:
:path: ".symlinks/plugins/flutter_webrtc/ios"
gal:
:path: ".symlinks/plugins/gal/darwin"
home_widget:
@ -380,10 +323,12 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/image_picker_ios/ios"
in_app_review:
:path: ".symlinks/plugins/in_app_review/ios"
jitsi_meet_flutter_sdk:
:path: ".symlinks/plugins/jitsi_meet_flutter_sdk/ios"
livekit_client:
:path: ".symlinks/plugins/livekit_client/ios"
media_kit_libs_ios_video:
:path: ".symlinks/plugins/media_kit_libs_ios_video/ios"
media_kit_native_event_loop:
:path: ".symlinks/plugins/media_kit_native_event_loop/ios"
media_kit_video:
:path: ".symlinks/plugins/media_kit_video/ios"
package_info_plus:
@ -396,14 +341,14 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/permission_handler_apple/ios"
receive_sharing_intent:
:path: ".symlinks/plugins/receive_sharing_intent/ios"
screen_brightness_ios:
:path: ".symlinks/plugins/screen_brightness_ios/ios"
share_plus:
:path: ".symlinks/plugins/share_plus/ios"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
sqflite_darwin:
:path: ".symlinks/plugins/sqflite_darwin/darwin"
sqlite3_flutter_libs:
:path: ".symlinks/plugins/sqlite3_flutter_libs/darwin"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
video_compress:
@ -417,67 +362,60 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496
audioplayers_darwin: ccf9c770ee768abb07e26d90af093f7bab1c12ab
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
croppy: 979e8ddc254f4642bffe7d52dc7193354b27ba30
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
connectivity_plus: 18382e7311ba19efcaee94442b23b32507b20695
croppy: b6199bc8d56bd2e03cc11609d1c47ad9875c1321
device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
fast_rsa: d99f8e1809a4a312fa9216d830186869b2e9eb65
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6
Firebase: 1fe1c0a7d9aaea32efe01fbea5f0ebd8d70e53a2
firebase_analytics: 1998960b8fa16fd0cd9e77a6f9fd35a2009ad65e
firebase_core: 2d4534e7b489907dcede540c835b48981d890943
firebase_messaging: 75bc93a4df25faccad67f6662ae872ac9ae69b64
FirebaseAnalytics: 4e42333f02cf78ed93703a5c36f36dd518aebdef
FirebaseCore: 8344daef5e2661eb004b177488d6f9f0f24251b7
FirebaseCoreInternal: ef4505d2afb1d0ebbc33162cb3795382904b5679
FirebaseInstallations: 9980995bdd06ec8081dfb6ab364162bdd64245c3
FirebaseMessaging: 2b9f56aa4ed286e1f0ce2ee1d413aabb8f9f5cb9
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808
Firebase: cf1b19f21410b029b6786a54e9764a0cacad3c99
firebase_analytics: 2815af29d49c1a994652abd37a5b001a88bc7b75
firebase_core: b62a5080210edad3f2934314a8b2c6f5124e8e10
firebase_messaging: 98619a0572d82cfb3668e78859ba9f1110e268c9
FirebaseAnalytics: 3feef9ae8733c567866342a1000691baaa7cad49
FirebaseCore: e0510f1523bc0eb21653cac00792e1e2bd6f1771
FirebaseCoreInternal: d98ab91e2d80a56d7b246856a8885443b302c0c2
FirebaseInstallations: 6ef4a1c7eb2a61ee1f74727d7f6ce2e72acf1414
FirebaseMessaging: f8a160d99c2c2e5babbbcc90c4a3e15db036aee2
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_app_update: 816fdb2e30e4832a7c45e3f108d391c42ef040a9
flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
flutter_timezone: 7c838e17ffd4645d261e87037e5bebf6d38fe544
flutter_udid: f7c3884e6ec2951efe4f9de082257fc77c4d15e9
gal: baecd024ebfd13c441269ca7404792a7152fde89
Giphy: 83628960ed04e1c3428ff1b4fb2b027f65e82f50
GoogleAppMeasurement: 36684bfb3ee034e2b42b4321eb19da3a1b81e65d
flutter_app_update: 65f61da626cb111d1b24674abc4b01728d7723bc
flutter_native_splash: f71420956eb811e6d310720fee915f1d42852e7a
flutter_udid: b2417673f287ee62817a1de3d1643f47b9f508ab
flutter_webrtc: 1a53bd24f97bcfeff512f13699e721897f261563
gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5
GoogleAppMeasurement: 987769c4ca6b968f2479fbcc9fe3ce34af454b8e
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
home_widget: f169fc41fd807b4d46ab6615dc44d62adbf9f64f
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
in_app_review: 5596fe56fab799e8edb3561c03d053363ab13457
jitsi_meet_flutter_sdk: 0283a60730922d608fbad9872e07afdd5bb3578a
JitsiMeetSDK: 4e1c269aaaed8f2cb7b0fff2d3c00f08359b170e
JitsiWebRTC: b47805ab5668be38e7ee60e2258f49badfe8e1d0
Kingfisher: 3204d23de16b5ea53541c44ca5a8efb55741dec3
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854
media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474
home_widget: 0434835a4c9a75704264feff6be17ea40e0f0d57
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
in_app_review: a31b5257259646ea78e0e35fc914979b0031d011
Kingfisher: f2af9028b16baf9dc6c07c570072bc41cbf009ef
livekit_client: 4eaa7a2968fc7e7c57888f43d90394547cc8d9e9
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
pasteboard: 49088aeb6119d51f976a421db60d8e1ab079b63c
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
pasteboard: 982969ebaa7c78af3e6cc7761e8f5e77565d9ce0
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00
receive_sharing_intent: 79c848f5b045674ad60b9fea3bafea59962ad2c1
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
SDWebImage: f84b0feeb08d2d11e6a9b843cb06d75ebf5b8868
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983
sqlite3_flutter_libs: f6acaa2172e6bb3e2e70c771661905080e8ebcf2
screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625
SDWebImage: 73c6079366fea25fa4bb9640d5fb58f0893facd8
share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
video_compress: f2133a07762889d67f0711ac831faa26f956980e
volume_controller: 3657a1f65bedb98fa41ff7dc5793537919f31b12
wakelock_plus: 04623e3f525556020ebd4034310f20fe7fda8b49
workmanager: 01be2de7f184bd15de93a1812936a2b7f42ef07e
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
video_compress: fce97e4fb1dfd88175aa07d2ffc8a2f297f87fbe
volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9
wakelock_plus: 373cfe59b235a6dd5837d0fb88791d2f13a90d56
WebRTC-SDK: 79942c006ea64f6fb48d7da8a4786dfc820bc1db
workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6
PODFILE CHECKSUM: d278ce52a331dda323590121247d2046cd085ae7
PODFILE CHECKSUM: 9b244e02f87527430136c8d21cbdcf1cd586b6bc
COCOAPODS: 1.16.2

View File

@ -961,7 +961,7 @@
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Solian;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -1521,7 +1521,7 @@
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Solian;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -1549,7 +1549,7 @@
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Solian;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",

View File

@ -59,7 +59,6 @@
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">

View File

@ -2,8 +2,6 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSCalendarsUsageDescription</key>
<string>Grant access to Calander help us to shows Solar Calander with your own events.</string>
<key>AppGroupId</key>
<string>group.solsynth.solian</string>
<key>CADisableMinimumFrameDurationOnPhone</key>
@ -81,8 +79,6 @@
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>

View File

@ -123,59 +123,48 @@ class NotificationService: UNNotificationServiceExtension {
}
if let imageIdentifier = metadata["image"] as? String {
attachMedia(to: content, withIdentifier: [imageIdentifier], fileType: UTType.jpeg, doScaleDown: true)
attachMedia(to: content, withIdentifier: imageIdentifier, fileType: UTType.jpeg, doScaleDown: true)
} else if let avatarIdentifier = metadata["avatar"] as? String {
attachMedia(to: content, withIdentifier: [avatarIdentifier], fileType: UTType.jpeg, doScaleDown: true)
} else if let imagesIdentifier = metadata["images"] as? Array<String> {
attachMedia(to: content, withIdentifier: imagesIdentifier, fileType: UTType.jpeg, doScaleDown: true)
attachMedia(to: content, withIdentifier: avatarIdentifier, fileType: UTType.jpeg, doScaleDown: true)
} else {
contentHandler?(content)
}
}
private func attachMedia(to content: UNMutableNotificationContent, withIdentifier identifier: Array<String>, fileType type: UTType?, doScaleDown scaleDown: Bool = false) {
let attachmentUrls = identifier.compactMap { element in
return getAttachmentUrl(for: element)
}
guard !attachmentUrls.isEmpty else {
print("Invalid URLs for attachments: \(attachmentUrls)")
private func attachMedia(to content: UNMutableNotificationContent, withIdentifier identifier: String, fileType type: UTType?, doScaleDown scaleDown: Bool = false) {
let attachmentUrl = getAttachmentUrl(for: identifier)
guard let remoteUrl = URL(string: attachmentUrl) else {
print("Invalid URL for attachment: \(attachmentUrl)")
return
}
let targetSize = 800
let scaleProcessor = ResizingImageProcessor(referenceSize: CGSize(width: targetSize, height: targetSize), mode: .aspectFit)
for attachmentUrl in attachmentUrls {
guard let remoteUrl = URL(string: attachmentUrl) else {
print("Invalid URL for attachment: \(attachmentUrl)")
continue // Skip this URL and move to the next one
}
KingfisherManager.shared.retrieveImage(with: remoteUrl, options: scaleDown ? [
.processor(scaleProcessor)
] : nil) { [weak self] result in
guard let self = self else { return }
switch result {
case .success(let retrievalResult):
// The image is either retrieved from cache or downloaded
let tempDirectory = FileManager.default.temporaryDirectory
let cachedFileUrl = tempDirectory.appendingPathComponent(UUID().uuidString) // Unique identifier for each file
do {
// Write the image data to a temporary file for UNNotificationAttachment
try retrievalResult.image.pngData()?.write(to: cachedFileUrl)
self.attachLocalMedia(to: content, fileType: type?.identifier, from: cachedFileUrl, withIdentifier: attachmentUrl)
} catch {
print("Failed to write media to temporary file: \(error.localizedDescription)")
self.contentHandler?(content)
}
case .failure(let error):
print("Failed to retrieve image: \(error.localizedDescription)")
KingfisherManager.shared.retrieveImage(with: remoteUrl, options: scaleDown ? [
.processor(scaleProcessor)
] : nil) { [weak self] result in
guard let self = self else { return }
switch result {
case .success(let retrievalResult):
// The image is either retrieved from cache or downloaded
let tempDirectory = FileManager.default.temporaryDirectory
let cachedFileUrl = tempDirectory.appendingPathComponent(identifier)
do {
// Write the image data to a temporary file for UNNotificationAttachment
try retrievalResult.image.pngData()?.write(to: cachedFileUrl)
self.attachLocalMedia(to: content, fileType: type?.identifier, from: cachedFileUrl, withIdentifier: identifier)
} catch {
print("Failed to write media to temporary file: \(error.localizedDescription)")
self.contentHandler?(content)
}
case .failure(let error):
print("Failed to retrieve image: \(error.localizedDescription)")
self.contentHandler?(content)
}
}
}

View File

@ -15,14 +15,14 @@ struct CheckInProvider: TimelineProvider {
func getSnapshot(in context: Context, completion: @escaping (CheckInEntry) -> ()) {
let prefs = UserDefaults(suiteName: "group.solsynth.solian")
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'"
let jsonDecoder = JSONDecoder()
jsonDecoder.dateDecodingStrategy = .formatted(dateFormatter)
jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase
let checkInRaw = prefs?.string(forKey: "pas_check_in_record")
var checkIn: SolarCheckInRecord?
if let checkInRaw = checkInRaw {
@ -31,7 +31,7 @@ struct CheckInProvider: TimelineProvider {
checkIn = nil
}
}
let entry = CheckInEntry(
date: Date(),
checkIn: checkIn
@ -54,11 +54,11 @@ struct CheckInEntry: TimelineEntry {
struct CheckInWidgetEntryView : View {
var entry: CheckInProvider.Entry
private let resultTierSymbols: [String] = ["Bad", "Poor", "Medium", "Good", "Great"]
private let resultTierSymbols: [String] = ["大凶", "", "中平", "", "大吉"]
func checkIn() -> Void {}
func seeDetail() -> Void {}
var body: some View {
@ -68,9 +68,9 @@ struct CheckInWidgetEntryView : View {
Text(resultTierSymbols[checkIn.resultTier]).font(.system(size: 27, weight: .bold))
Text("+\(checkIn.resultExperience) EXP").font(.system(size: 15, design: .monospaced))
}.padding(.horizontal, 4)
Spacer()
HStack {
VStack(alignment: .leading) {
Text(
@ -82,7 +82,7 @@ struct CheckInWidgetEntryView : View {
format: .dateTime.day().month()
).font(.system(size: 13))
}.padding(.leading, 4)
Button("See Detail", systemImage: "arrow.right", action: seeDetail)
.labelStyle(.iconOnly)
.buttonBorderShape(.circle)
@ -91,11 +91,11 @@ struct CheckInWidgetEntryView : View {
} else {
VStack(alignment: .leading) {
Text("Check In").font(.system(size: 19, weight: .bold))
Text("You haven't divined today").font(.system(size: 15))
Text("You haven't check in today").font(.system(size: 15))
}.padding(.horizontal, 4)
Spacer()
HStack(alignment: .bottom) {
Button("Check In", systemImage: "checkmark", action: checkIn).labelStyle(.iconOnly).buttonBorderShape(.circle).frame(maxWidth: .infinity, alignment: .trailing)
}

View File

@ -2,15 +2,11 @@ import 'dart:async';
import 'dart:convert';
import 'dart:math' as math;
import 'package:collection/collection.dart';
import 'package:dio/dio.dart';
import 'package:drift/drift.dart';
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:provider/provider.dart';
import 'package:surface/database/database.dart';
import 'package:surface/logger.dart';
import 'package:surface/providers/channel.dart';
import 'package:surface/providers/database.dart';
import 'package:surface/providers/keypair.dart';
import 'package:surface/providers/sn_attachment.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/user_directory.dart';
@ -20,15 +16,13 @@ import 'package:surface/types/websocket.dart';
import 'package:uuid/uuid.dart';
class ChatMessageController extends ChangeNotifier {
static const kChatMessageBoxPrefix = 'nex_chat_messages_';
static const kSingleBatchLoadLimit = 100;
late final SnNetworkProvider _sn;
late final UserDirectoryProvider _ud;
late final WebSocketProvider _ws;
late final SnAttachmentProvider _attach;
late final DatabaseProvider _dt;
late final ChatChannelProvider _ct;
late final KeyPairProvider _kp;
StreamSubscription? _wsSubscription;
@ -37,20 +31,16 @@ class ChatMessageController extends ChangeNotifier {
_ud = context.read<UserDirectoryProvider>();
_ws = context.read<WebSocketProvider>();
_attach = context.read<SnAttachmentProvider>();
_ct = context.read<ChatChannelProvider>();
_dt = context.read<DatabaseProvider>();
_kp = context.read<KeyPairProvider>();
}
bool isPending = true;
bool isLoading = false;
bool isAggressiveLoading = false;
int? messageTotal;
bool get isAllLoaded =>
messageTotal != null && messages.length >= messageTotal!;
bool get isAllLoaded => messageTotal != null && messages.length >= messageTotal!;
String? _boxKey;
SnChannel? channel;
SnChannelMember? profile;
@ -61,19 +51,29 @@ class ChatMessageController extends ChangeNotifier {
/// Stored as a list of nonce to provide the loading state
final List<String> unconfirmedMessages = List.empty(growable: true);
Box<SnChatMessage>? get _box => (_boxKey == null || isPending) ? null : Hive.box<SnChatMessage>(_boxKey!);
final List<SnChannelMember> typingMembers = List.empty(growable: true);
final Map<int, Timer> typingInactiveTimer = {};
Future<void> initialize(SnChannel chan) async {
channel = chan;
// Fetch channel profile
profile = await _ct.getChannelProfile(channel!);
// Initialize local data
_boxKey = '$kChatMessageBoxPrefix${chan.id}';
await Hive.openBox<SnChatMessage>(_boxKey!);
_wsSubscription = _ws.pk.stream.listen((event) {
// Fetch channel profile
final resp = await _sn.client.get(
'/cgi/im/channels/${chan.keyPath}/me',
);
profile = SnChannelMember.fromJson(
resp.data as Map<String, dynamic>,
);
_wsSubscription = _ws.stream.stream.listen((event) {
switch (event.method) {
case 'events.new':
if (event.payload?['channel_id'] != channel?.id) break;
final payload = SnChatMessage.fromJson(event.payload!);
_addMessage(payload);
break;
@ -86,8 +86,7 @@ class ChatMessageController extends ChangeNotifier {
notifyListeners();
}
typingInactiveTimer[member.id]?.cancel();
typingInactiveTimer[member.id] =
Timer(const Duration(seconds: 3), () {
typingInactiveTimer[member.id] = Timer(const Duration(seconds: 3), () {
typingMembers.removeWhere((x) => x.id == member.id);
typingInactiveTimer.remove(member.id);
notifyListeners();
@ -129,16 +128,10 @@ class ChatMessageController extends ChangeNotifier {
}
Future<void> _saveMessageToLocal(Iterable<SnChatMessage> messages) async {
await _dt.db.snLocalChatMessage.insertAll(
messages.map(
(ele) => SnLocalChatMessageCompanion.insert(
id: Value(ele.id),
content: ele,
channelId: channel!.id,
createdAt: Value(ele.createdAt),
),
),
onConflict: DoNothing());
if (_box == null) return;
await _box!.putAll({
for (final message in messages) message.id: message,
});
}
Future<void> _addUnconfirmedMessage(SnChatMessage message) async {
@ -187,27 +180,11 @@ class ChatMessageController extends ChangeNotifier {
} else {
messages.insert(0, message);
}
notifyListeners();
await _applyMessage(message);
notifyListeners();
if (isCheckedUpdate) {
await _dt.db.snLocalChatMessage.insertOne(
SnLocalChatMessageCompanion.insert(
id: Value(message.id),
content: message,
channelId: channel!.id,
createdAt: Value(message.createdAt),
),
onConflict: DoUpdate(
(_) => SnLocalChatMessageCompanion.custom(
content: Constant(jsonEncode(message.toJson())),
),
),
);
} else {
incomeStrandedQueue.add(message);
}
if (_box == null) return;
await _box!.put(message.id, message);
}
Future<void> _applyMessage(SnChatMessage message) async {
@ -216,56 +193,29 @@ class ChatMessageController extends ChangeNotifier {
switch (message.type) {
case 'messages.edit':
if (message.relatedEventId != null) {
final idx =
messages.indexWhere((x) => x.id == message.relatedEventId);
final idx = messages.indexWhere((x) => x.id == message.relatedEventId);
if (idx != -1) {
final newBody = Map<String, dynamic>.from(message.body);
final newBody = message.body;
newBody.remove('related_event');
messages[idx] = messages[idx].copyWith(
body: newBody,
updatedAt: message.updatedAt,
);
}
if (message.relatedEventId != null) {
await (_dt.db.snLocalChatMessage.update()
..where((e) => e.id.equals(message.relatedEventId!)))
.write(
SnLocalChatMessageCompanion.custom(
content: Constant(jsonEncode(messages[idx].toJson())),
),
);
if (_box!.containsKey(message.relatedEventId)) {
await _box!.put(message.relatedEventId, messages[idx]);
}
}
}
case 'messages.delete':
if (message.relatedEventId != null) {
messages.removeWhere((x) => x.id == message.relatedEventId);
if (message.relatedEventId != null) {
await (_dt.db.snLocalChatMessage.delete()
..where((e) => e.id.equals(message.relatedEventId!)))
.go();
if (_box!.containsKey(message.relatedEventId)) {
await _box!.delete(message.relatedEventId);
}
}
}
}
Future<Map<String, dynamic>> _encodeMessageBody(
String text,
bool isEncrypted,
) async {
if (!isEncrypted || _kp.activeKp == null) {
return {
'text': text,
'algorithm': 'plain',
};
} else {
return {
'text': await _kp.encryptText(text),
'algorithm': 'rsa',
'keypair_id': _kp.activeKp!.id,
};
}
}
Future<void> sendMessage(
String type,
String content, {
@ -273,40 +223,36 @@ class ChatMessageController extends ChangeNotifier {
int? relatedId,
List<String>? attachments,
SnChatMessage? editingMessage,
bool isEncrypted = false,
}) async {
if (channel == null) return;
const uuid = Uuid();
final nonce = uuid.v4();
final body = {
...(await _encodeMessageBody(content, isEncrypted)),
'text': content,
'algorithm': 'plain',
if (quoteId != null) 'quote_event': quoteId,
if (relatedId != null) 'related_event': relatedId,
if (attachments != null && attachments.isNotEmpty)
'attachments': attachments,
if (attachments != null && attachments.isNotEmpty) 'attachments': attachments,
};
// Mock the message locally
// Do not mock the editing message
if (editingMessage == null) {
final createdAt = DateTime.now();
final message = SnChatMessage(
id: 0,
createdAt: createdAt,
updatedAt: createdAt,
deletedAt: null,
uuid: nonce,
body: body,
type: type,
channel: channel!,
channelId: channel!.id,
sender: profile!,
senderId: profile!.id,
quoteEventId: quoteId,
relatedEventId: relatedId,
);
_addUnconfirmedMessage(message);
}
final createdAt = DateTime.now();
final message = SnChatMessage(
id: 0,
createdAt: createdAt,
updatedAt: createdAt,
deletedAt: null,
uuid: nonce,
body: body,
type: type,
channel: channel!,
channelId: channel!.id,
sender: profile!,
senderId: profile!.id,
quoteEventId: quoteId,
relatedEventId: relatedId,
);
_addUnconfirmedMessage(message);
// Send to server
try {
@ -340,36 +286,20 @@ class ChatMessageController extends ChangeNotifier {
}
}
bool isCheckedUpdate = false;
List<SnChatMessage> incomeStrandedQueue = List.empty(growable: true);
/// Check the local storage is up to date with the server.
/// If the local storage is not up to date, it will be updated.
Future<void> checkUpdate() async {
isAggressiveLoading = true;
notifyListeners();
if (_box == null) return;
if (_box!.isEmpty) return;
final mostRecentMessage = await (_dt.db.snLocalChatMessage.select()
..where((e) => e.channelId.equals(channel!.id))
..limit(1)
..orderBy([
(e) =>
OrderingTerm(expression: e.createdAt, mode: OrderingMode.desc)
]))
.getSingleOrNull();
if (mostRecentMessage == null) {
// Initial load
await loadMessages(take: 20);
isAggressiveLoading = false;
isCheckedUpdate = true;
return;
}
isLoading = true;
notifyListeners();
try {
final resp = await _sn.client.get(
'/cgi/im/channels/${channel!.keyPath}/events/update',
queryParameters: {
'pivot': mostRecentMessage.content.id,
'pivot': _box!.values.last.id,
},
);
if (resp.data['up_to_date'] == true) return;
@ -378,25 +308,13 @@ class ChatMessageController extends ChangeNotifier {
final countToFetch = math.min(resp.data['count'] as int, 100);
for (int idx = 0; idx < countToFetch; idx += kSingleBatchLoadLimit) {
final out = await getMessages(
kSingleBatchLoadLimit,
idx,
forceRemote: true,
);
messages.insertAll(0, out);
notifyListeners();
await getMessages(kSingleBatchLoadLimit, idx, forceRemote: true);
}
} catch (err) {
rethrow;
} finally {
await loadMessages();
isAggressiveLoading = false;
isCheckedUpdate = true;
_saveMessageToLocal(incomeStrandedQueue).then((_) {
incomeStrandedQueue.clear();
});
isLoading = false;
notifyListeners();
}
}
@ -405,18 +323,13 @@ class ChatMessageController extends ChangeNotifier {
/// If it was not found in local storage we will look it up in remote
Future<SnChatMessage?> getMessage(int id) async {
SnChatMessage? out;
final local = await (_dt.db.snLocalChatMessage.select()
..limit(1)
..where((e) => e.id.equals(id)))
.getSingleOrNull();
if (local != null) {
out = local.content;
if (_box != null && _box!.containsKey(id)) {
out = _box!.get(id);
}
if (out == null) {
try {
final resp = await _sn.client
.get('/cgi/im/channels/${channel!.keyPath}/events/$id');
final resp = await _sn.client.get('/cgi/im/channels/${channel!.keyPath}/events/$id');
out = SnChatMessage.fromJson(resp.data);
_saveMessageToLocal([out]);
} catch (_) {
@ -450,21 +363,16 @@ class ChatMessageController extends ChangeNotifier {
bool forceLocal = false,
bool forceRemote = false,
}) async {
final localTotal = await _dt.db.snLocalChatMessage
.count(where: (e) => e.channelId.equals(channel!.id))
.getSingle();
late List<SnChatMessage> out;
if ((localTotal >= take + offset || forceLocal) && !forceRemote) {
final result = await (_dt.db.snLocalChatMessage.select()
..where((e) => e.channelId.equals(channel!.id))
..orderBy([
(e) =>
OrderingTerm(expression: e.createdAt, mode: OrderingMode.desc)
])
..limit(take, offset: offset))
.get();
out = result.map((e) => e.content).toList();
if (_box != null && (_box!.length >= take + offset || forceLocal) && !forceRemote) {
out = _box!.keys
.toList()
.cast<int>()
.sorted((a, b) => b.compareTo(a))
.skip(offset)
.take(take)
.map((key) => _box!.get(key)!)
.toList();
} else {
final resp = await _sn.client.get(
'/cgi/im/channels/${channel!.keyPath}/events',
@ -499,8 +407,7 @@ class ChatMessageController extends ChangeNotifier {
quoteEvent: quoteEvent,
attachments: attachments
.where(
(ele) =>
out[i].body['attachments']?.contains(ele?.rid) ?? false,
(ele) => out[i].body['attachments']?.contains(ele?.rid) ?? false,
)
.toList(),
),
@ -508,10 +415,7 @@ class ChatMessageController extends ChangeNotifier {
}
// Preload sender accounts
final accountId = out
.where((ele) => ele.sender.accountId >= 0)
.map((ele) => ele.sender.accountId)
.toSet();
final accountId = out.where((ele) => ele.sender.accountId >= 0).map((ele) => ele.sender.accountId).toSet();
await _ud.listAccount(accountId);
return out;
@ -536,45 +440,10 @@ class ChatMessageController extends ChangeNotifier {
}
}
Timer? _readEventDebounce;
int? _readEventAnchor;
void readEvent(int id) {
if (_readEventAnchor != null) {
_readEventAnchor = math.max(_readEventAnchor!, id);
} else {
_readEventAnchor = id;
}
if (_readEventDebounce?.isActive ?? false) {
_readEventDebounce?.cancel();
}
_readEventDebounce = Timer(const Duration(milliseconds: 500), () {
_sendReadEvent();
});
}
void _sendReadEvent() {
_ws.conn?.sink.add(jsonEncode(
WebSocketPackage(
method: 'events.read',
endpoint: 'im',
payload: {
'channel_member_id': profile!.id,
'event_id': _readEventAnchor,
},
).toJson(),
));
logging.debug('[Messaging] Send read event request: $_readEventAnchor');
}
@override
void dispose() {
_box?.close();
_wsSubscription?.cancel();
if (_readEventDebounce?.isActive ?? false) {
_sendReadEvent();
}
_readEventDebounce?.cancel();
super.dispose();
}
}

View File

@ -16,9 +16,7 @@ import 'package:surface/providers/post.dart';
import 'package:surface/providers/sn_attachment.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/types/attachment.dart';
import 'package:surface/types/poll.dart';
import 'package:surface/types/post.dart';
import 'package:surface/types/realm.dart';
import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/universal_image.dart';
import 'package:video_compress/video_compress.dart';
@ -71,8 +69,7 @@ class PostWriteMedia {
}
}
PostWriteMedia.fromBytes(this.raw, this.name, this.type,
{this.attachment, this.file});
PostWriteMedia.fromBytes(this.raw, this.name, this.type, {this.attachment, this.file});
bool get isEmpty => attachment == null && file == null && raw == null;
@ -106,8 +103,7 @@ class PostWriteMedia {
}) {
if (attachment != null) {
final sn = context.read<SnNetworkProvider>();
final ImageProvider provider =
UniversalImage.provider(sn.getAttachmentUrl(attachment!.rid));
final ImageProvider provider = UniversalImage.provider(sn.getAttachmentUrl(attachment!.rid));
if (width != null && height != null && !kIsWeb) {
return ResizeImage(
provider,
@ -118,8 +114,7 @@ class PostWriteMedia {
}
return provider;
} else if (file != null) {
final ImageProvider provider =
kIsWeb ? NetworkImage(file!.path) : FileImage(File(file!.path));
final ImageProvider provider = kIsWeb ? NetworkImage(file!.path) : FileImage(File(file!.path));
if (width != null && height != null) {
return ResizeImage(
provider,
@ -149,8 +144,6 @@ class PostWriteController extends ChangeNotifier {
static const Map<String, String> kTitleMap = {
'stories': 'writePostTypeStory',
'articles': 'writePostTypeArticle',
'questions': 'writePostTypeQuestion',
'videos': 'writePostTypeVideo',
};
static const kAttachmentProgressWeight = 0.9;
@ -160,24 +153,8 @@ class PostWriteController extends ChangeNotifier {
final TextEditingController titleController = TextEditingController();
final TextEditingController descriptionController = TextEditingController();
final TextEditingController aliasController = TextEditingController();
final TextEditingController rewardController = TextEditingController();
ContentInsertionConfiguration get contentInsertionConfiguration =>
ContentInsertionConfiguration(
onContentInserted: (KeyboardInsertedContent content) {
if (content.hasData) {
addAttachments([
PostWriteMedia.fromBytes(content.data!,
'attachmentInsertedImage'.tr(), SnMediaType.image)
]);
}
},
);
bool _temporarySaveActive = false;
PostWriteController({bool doLoadFromTemporary = true}) {
_temporarySaveActive = doLoadFromTemporary;
PostWriteController() {
titleController.addListener(() {
_temporaryPlanSave();
notifyListeners();
@ -188,9 +165,8 @@ class PostWriteController extends ChangeNotifier {
});
contentController.addListener(() {
_temporaryPlanSave();
notifyListeners();
});
if (doLoadFromTemporary) _temporaryLoad();
_temporaryLoad();
}
String mode = kTitleMap.keys.first;
@ -199,16 +175,13 @@ class PostWriteController extends ChangeNotifier {
String get description => descriptionController.text;
bool get isRelatedNull =>
![editingPost, repostingPost, replyingPost].any((ele) => ele != null);
bool get isRelatedNull => ![editingPost, repostingPost, replyingPost].any((ele) => ele != null);
bool isLoading = false, isBusy = false;
double? progress;
SnRealm? realm;
SnPublisher? publisher;
SnPost? editingPost, repostingPost, replyingPost;
bool editingDraft = false;
int visibility = 0;
List<int> visibleUsers = List.empty();
@ -218,10 +191,6 @@ class PostWriteController extends ChangeNotifier {
PostWriteMedia? thumbnail;
List<PostWriteMedia> attachments = List.empty(growable: true);
DateTime? publishedAt, publishedUntil;
SnAttachment? videoAttachment;
String videoUrl = '';
bool videoLive = false;
SnPoll? poll;
Future<void> fetchRelatedPost(
BuildContext context, {
@ -242,41 +211,17 @@ class PostWriteController extends ChangeNotifier {
descriptionController.text = post.body['description'] ?? '';
contentController.text = post.body['content'] ?? '';
aliasController.text = post.alias ?? '';
rewardController.text = post.body['reward']?.toString() ?? '';
if (post.body['video'] != null) {
if (post.body['video'] is String) {
videoUrl = post.body['video'];
} else {
videoAttachment = SnAttachment.fromJson(post.body['video']);
}
}
publishedAt = post.publishedAt;
publishedUntil = post.publishedUntil;
visibleUsers = List.from(post.visibleUsersList ?? [], growable: true);
invisibleUsers =
List.from(post.invisibleUsersList ?? [], growable: true);
invisibleUsers = List.from(post.invisibleUsersList ?? [], growable: true);
visibility = post.visibility;
tags = List.from(post.tags.map((ele) => ele.alias), growable: true);
categories =
List.from(post.categories.map((ele) => ele.alias), growable: true);
attachments.addAll(
post.body['attachments']
?.map((ele) => SnAttachment.fromJson(ele))
?.map((ele) => PostWriteMedia(ele))
?.cast<PostWriteMedia>() ??
[],
);
poll = post.poll;
categories = List.from(post.categories.map((ele) => ele.alias), growable: true);
attachments.addAll(post.preload?.attachments?.map((ele) => PostWriteMedia(ele)) ?? []);
videoLive = post.body['is_live'] ?? false;
editingDraft = post.isDraft;
if (post.body['thumbnail'] != null) {
thumbnail =
PostWriteMedia(SnAttachment.fromJson(post.body['thumbnail']));
}
if (post.realm != null) {
realm = post.realm!;
if (post.preload?.thumbnail != null && (post.preload?.thumbnail?.rid.isNotEmpty ?? false)) {
thumbnail = PostWriteMedia(post.preload!.thumbnail);
}
editingPost = post;
@ -300,8 +245,7 @@ class PostWriteController extends ChangeNotifier {
}
}
Future<SnAttachment> _uploadAttachment(
BuildContext context, PostWriteMedia media,
Future<SnAttachment> _uploadAttachment(BuildContext context, PostWriteMedia media,
{bool isCompressed = false}) async {
final attach = context.read<SnAttachmentProvider>();
@ -310,9 +254,7 @@ class PostWriteController extends ChangeNotifier {
media.name,
'interactive',
null,
mimetype: media.raw != null && media.type == SnMediaType.image
? 'image/png'
: null,
mimetype: media.raw != null && media.type == SnMediaType.image ? 'image/png' : null,
);
var item = await attach.chunkedUploadParts(
@ -328,11 +270,9 @@ class PostWriteController extends ChangeNotifier {
if (media.type == SnMediaType.video && !isCompressed && context.mounted) {
try {
final compressedAttachment =
await _tryCompressVideoCopy(context, media);
final compressedAttachment = await _tryCompressVideoCopy(context, media);
if (compressedAttachment != null) {
item = await attach.updateOne(item,
compressedId: compressedAttachment.id);
item = await attach.updateOne(item, compressedId: compressedAttachment.id);
}
} catch (err) {
if (context.mounted) context.showErrorDialog(err);
@ -342,10 +282,8 @@ class PostWriteController extends ChangeNotifier {
return item;
}
Future<SnAttachment?> _tryCompressVideoCopy(
BuildContext context, PostWriteMedia media) async {
if (kIsWeb || !(Platform.isAndroid || Platform.isIOS || Platform.isMacOS))
return null;
Future<SnAttachment?> _tryCompressVideoCopy(BuildContext context, PostWriteMedia media) async {
if (kIsWeb || !(Platform.isAndroid || Platform.isIOS || Platform.isMacOS)) return null;
if (media.type != SnMediaType.video) return null;
if (media.file == null) return null;
if (VideoCompress.isCompressing) return null;
@ -369,8 +307,7 @@ class PostWriteController extends ChangeNotifier {
if (!context.mounted) return null;
final compressedMedia = PostWriteMedia.fromFile(XFile(mediaInfo.path!));
final compressedAttachment =
await _uploadAttachment(context, compressedMedia, isCompressed: true);
final compressedAttachment = await _uploadAttachment(context, compressedMedia, isCompressed: true);
return compressedAttachment;
}
@ -380,7 +317,6 @@ class PostWriteController extends ChangeNotifier {
Timer? _temporarySaveTimer;
void _temporaryPlanSave() {
if (!_temporarySaveActive) return;
_temporarySaveTimer?.cancel();
_temporarySaveTimer = Timer(const Duration(seconds: 1), () {
_temporarySave();
@ -406,40 +342,24 @@ class PostWriteController extends ChangeNotifier {
'content': contentController.text,
if (aliasController.text.isNotEmpty) 'alias': aliasController.text,
if (titleController.text.isNotEmpty) 'title': titleController.text,
if (descriptionController.text.isNotEmpty)
'description': descriptionController.text,
if (rewardController.text.isNotEmpty) 'reward': rewardController.text,
if (thumbnail != null && thumbnail!.attachment != null)
'thumbnail': thumbnail!.attachment!.toJson(),
'attachments': attachments
.where((e) => e.attachment != null)
.map((e) => e.attachment!.toJson())
.toList(growable: true),
if (descriptionController.text.isNotEmpty) 'description': descriptionController.text,
if (thumbnail != null && thumbnail!.attachment != null) 'thumbnail': thumbnail!.attachment!.toJson(),
'attachments':
attachments.where((e) => e.attachment != null).map((e) => e.attachment!.toJson()).toList(growable: true),
'tags': tags.map((ele) => {'alias': ele}).toList(growable: true),
'categories':
categories.map((ele) => {'alias': ele}).toList(growable: true),
'categories': categories.map((ele) => {'alias': ele}).toList(growable: true),
'visibility': visibility,
'visible_users_list': visibleUsers,
'invisible_users_list': invisibleUsers,
if (publishedAt != null)
'published_at': publishedAt!.toUtc().toIso8601String(),
if (publishedUntil != null)
'published_until': publishedAt!.toUtc().toIso8601String(),
if (publishedAt != null) 'published_at': publishedAt!.toUtc().toIso8601String(),
if (publishedUntil != null) 'published_until': publishedAt!.toUtc().toIso8601String(),
if (replyingPost != null) 'reply_to': replyingPost!.toJson(),
if (repostingPost != null) 'repost_to': repostingPost!.toJson(),
if (poll != null) 'poll': poll!.toJson(),
if (realm != null) 'realm': realm!.toJson(),
}),
);
});
}
bool get isNotEmpty =>
title.isNotEmpty ||
description.isNotEmpty ||
contentController.text.isNotEmpty ||
attachments.isNotEmpty;
bool temporaryRestored = false;
void _temporaryLoad() {
@ -451,30 +371,18 @@ class PostWriteController extends ChangeNotifier {
aliasController.text = data['alias'] ?? '';
titleController.text = data['title'] ?? '';
descriptionController.text = data['description'] ?? '';
rewardController.text = data['reward']?.toString() ?? '';
if (data['thumbnail'] != null) {
thumbnail = PostWriteMedia(SnAttachment.fromJson(data['thumbnail']));
}
attachments.addAll(data['attachments']
.map((ele) => PostWriteMedia(SnAttachment.fromJson(ele)))
.cast<PostWriteMedia>());
if (data['thumbnail'] != null) thumbnail = PostWriteMedia(SnAttachment.fromJson(data['thumbnail']));
attachments
.addAll(data['attachments'].map((ele) => PostWriteMedia(SnAttachment.fromJson(ele))).cast<PostWriteMedia>());
tags = List.from(data['tags'].map((ele) => ele['alias']));
categories = List.from(data['categories'].map((ele) => ele['alias']));
visibility = data['visibility'];
visibleUsers = List.from(data['visible_users_list'] ?? []);
invisibleUsers = List.from(data['invisible_users_list'] ?? []);
if (data['published_at'] != null) {
publishedAt = DateTime.tryParse(data['published_at'])?.toLocal();
}
if (data['published_until'] != null) {
publishedUntil = DateTime.tryParse(data['published_until'])?.toLocal();
}
replyingPost =
data['reply_to'] != null ? SnPost.fromJson(data['reply_to']) : null;
repostingPost =
data['repost_to'] != null ? SnPost.fromJson(data['repost_to']) : null;
poll = data['poll'] != null ? SnPoll.fromJson(data['poll']) : null;
realm = data['realm'] != null ? SnRealm.fromJson(data['realm']) : null;
if (data['published_at'] != null) publishedAt = DateTime.tryParse(data['published_at'])?.toLocal();
if (data['published_until'] != null) publishedUntil = DateTime.tryParse(data['published_until'])?.toLocal();
replyingPost = data['reply_to'] != null ? SnPost.fromJson(data['reply_to']) : null;
repostingPost = data['repost_to'] != null ? SnPost.fromJson(data['repost_to']) : null;
temporaryRestored = true;
notifyListeners();
});
@ -494,10 +402,7 @@ class PostWriteController extends ChangeNotifier {
notifyListeners();
}
Future<void> sendPost(
BuildContext context, {
bool saveAsDraft = false,
}) async {
Future<void> sendPost(BuildContext context) async {
if (isBusy || publisher == null) return;
final sn = context.read<SnNetworkProvider>();
@ -524,9 +429,7 @@ class PostWriteController extends ChangeNotifier {
media.name,
'interactive',
null,
mimetype: media.raw != null && media.type == SnMediaType.image
? 'image/png'
: null,
mimetype: media.raw != null && media.type == SnMediaType.image ? 'image/png' : null,
);
var item = await attach.chunkedUploadParts(
@ -535,20 +438,16 @@ class PostWriteController extends ChangeNotifier {
place.$2,
onProgress: (value) {
// Calculate overall progress for attachments
progress = math.max(
((i + value) / attachments.length) * kAttachmentProgressWeight,
value);
progress = math.max(((i + value) / attachments.length) * kAttachmentProgressWeight, value);
notifyListeners();
},
);
try {
if (context.mounted) {
final compressedAttachment =
await _tryCompressVideoCopy(context, media);
final compressedAttachment = await _tryCompressVideoCopy(context, media);
if (compressedAttachment != null) {
item = await attach.updateOne(item,
compressedId: compressedAttachment.id);
item = await attach.updateOne(item, compressedId: compressedAttachment.id);
}
}
} catch (err) {
@ -570,12 +469,10 @@ class PostWriteController extends ChangeNotifier {
progress = kAttachmentProgressWeight;
notifyListeners();
final reward = double.tryParse(rewardController.text);
// Posting the content
try {
final baseProgressVal = progress!;
final resp = await sn.client.request(
await sn.client.request(
[
'/cgi/co/$mode',
if (editingPost != null) '${editingPost!.id}',
@ -585,58 +482,32 @@ class PostWriteController extends ChangeNotifier {
'content': contentController.text,
if (aliasController.text.isNotEmpty) 'alias': aliasController.text,
if (titleController.text.isNotEmpty) 'title': titleController.text,
if (descriptionController.text.isNotEmpty)
'description': descriptionController.text,
if (thumbnail != null && thumbnail!.attachment != null)
'thumbnail': thumbnail!.attachment!.rid,
'attachments': attachments
.where((e) => e.attachment != null)
.map((e) => e.attachment!.rid)
.toList(),
if (descriptionController.text.isNotEmpty) 'description': descriptionController.text,
if (thumbnail != null && thumbnail!.attachment != null) 'thumbnail': thumbnail!.attachment!.rid,
'attachments': attachments.where((e) => e.attachment != null).map((e) => e.attachment!.rid).toList(),
'tags': tags.map((ele) => {'alias': ele}).toList(),
'categories': categories.map((ele) => {'alias': ele}).toList(),
'visibility': visibility,
'visible_users_list': visibleUsers,
'invisible_users_list': invisibleUsers,
if (publishedAt != null)
'published_at': publishedAt!.toUtc().toIso8601String(),
if (publishedUntil != null)
'published_until': publishedAt!.toUtc().toIso8601String(),
if (publishedAt != null) 'published_at': publishedAt!.toUtc().toIso8601String(),
if (publishedUntil != null) 'published_until': publishedAt!.toUtc().toIso8601String(),
if (replyingPost != null) 'reply_to': replyingPost!.id,
if (repostingPost != null) 'repost_to': repostingPost!.id,
if (reward != null) 'reward': reward,
if (videoAttachment != null || videoUrl.isNotEmpty)
'video': videoUrl.isNotEmpty ? videoUrl : videoAttachment!.rid,
if (poll != null) 'poll': poll!.id,
if (realm != null) 'realm': realm!.id,
if (videoLive) 'is_live': videoLive,
'is_draft': saveAsDraft,
},
onSendProgress: (count, total) {
progress =
baseProgressVal + (count / total) * (kPostingProgressWeight / 2);
progress = baseProgressVal + (count / total) * (kPostingProgressWeight / 2);
notifyListeners();
},
onReceiveProgress: (count, total) {
progress = baseProgressVal +
(kPostingProgressWeight / 2) +
(count / total) * (kPostingProgressWeight / 2);
progress = baseProgressVal + (kPostingProgressWeight / 2) + (count / total) * (kPostingProgressWeight / 2);
notifyListeners();
},
options: Options(
method: editingPost != null ? 'PUT' : 'POST',
),
);
if (saveAsDraft) {
if (!context.mounted) return;
editingDraft = true;
final out = SnPost.fromJson(resp.data);
final pt = context.read<SnPostContentProvider>();
editingPost = await pt.completePostData(out);
notifyListeners();
} else {
reset();
}
reset();
} catch (err) {
if (!context.mounted) return;
context.showErrorDialog(err);
@ -669,8 +540,17 @@ class PostWriteController extends ChangeNotifier {
notifyListeners();
}
void setThumbnail(SnAttachment? value) {
thumbnail = value == null ? null : PostWriteMedia(value);
void setThumbnail(int? idx) {
if (idx == null) {
attachments.add(thumbnail!);
thumbnail = null;
} else {
if (thumbnail != null) {
attachments.add(thumbnail!);
}
thumbnail = attachments[idx];
attachments.removeAt(idx);
}
notifyListeners();
}
@ -722,11 +602,6 @@ class PostWriteController extends ChangeNotifier {
notifyListeners();
}
void setRealm(SnRealm? value) {
realm = value;
notifyListeners();
}
void setProgress(double? value) {
progress = value;
_temporaryPlanSave();
@ -745,26 +620,6 @@ class PostWriteController extends ChangeNotifier {
notifyListeners();
}
void setVideoAttachment(SnAttachment? value) {
videoAttachment = value;
notifyListeners();
}
void setVideoUrl(String value) {
videoUrl = value;
notifyListeners();
}
void setVideoLive(bool value) {
videoLive = value;
notifyListeners();
}
void setPoll(SnPoll? value) {
poll = value;
notifyListeners();
}
void reset() {
publishedAt = null;
publishedUntil = null;
@ -782,8 +637,7 @@ class PostWriteController extends ChangeNotifier {
repostingPost = null;
mode = kTitleMap.keys.first;
temporaryRestored = false;
SharedPreferences.getInstance()
.then((prefs) => prefs.remove(kTemporaryStorageKey));
SharedPreferences.getInstance().then((prefs) => prefs.remove(kTemporaryStorageKey));
notifyListeners();
}

View File

@ -1,42 +0,0 @@
import 'dart:convert';
import 'package:drift/drift.dart';
import 'package:surface/types/account.dart';
class SnAccountConverter extends TypeConverter<SnAccount, String>
with JsonTypeConverter2<SnAccount, String, Map<String, Object?>> {
const SnAccountConverter();
@override
SnAccount fromSql(String fromDb) {
return fromJson(jsonDecode(fromDb) as Map<String, dynamic>);
}
@override
String toSql(SnAccount value) {
return jsonEncode(toJson(value));
}
@override
SnAccount fromJson(Map<String, Object?> json) {
return SnAccount.fromJson(json);
}
@override
Map<String, Object?> toJson(SnAccount value) {
return value.toJson();
}
}
@TableIndex(name: 'idx_account_name', columns: {#name})
class SnLocalAccount extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get name => text()();
TextColumn get content => text().map(const SnAccountConverter())();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
DateTimeColumn get cacheExpiredAt => dateTime()();
}

View File

@ -1,47 +0,0 @@
import 'dart:convert';
import 'package:drift/drift.dart';
import 'package:surface/types/attachment.dart';
class SnAttachmentConverter extends TypeConverter<SnAttachment, String>
with JsonTypeConverter2<SnAttachment, String, Map<String, Object?>> {
const SnAttachmentConverter();
@override
SnAttachment fromSql(String fromDb) {
return fromJson(jsonDecode(fromDb) as Map<String, dynamic>);
}
@override
String toSql(SnAttachment value) {
return jsonEncode(toJson(value));
}
@override
SnAttachment fromJson(Map<String, Object?> json) {
return SnAttachment.fromJson(json);
}
@override
Map<String, Object?> toJson(SnAttachment value) {
return value.toJson();
}
}
@TableIndex(name: 'idx_attachment_rid', columns: {#rid})
@TableIndex(name: 'idx_attachment_account', columns: {#accountId})
class SnLocalAttachment extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get rid => text().unique()();
TextColumn get uuid => text().unique()();
TextColumn get content => text().map(const SnAttachmentConverter())();
IntColumn get accountId => integer()();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
DateTimeColumn get cacheExpiredAt => dateTime()();
}

View File

@ -1,117 +0,0 @@
import 'dart:convert';
import 'package:drift/drift.dart';
import 'package:surface/types/chat.dart';
class SnChannelConverter extends TypeConverter<SnChannel, String>
with JsonTypeConverter2<SnChannel, String, Map<String, Object?>> {
const SnChannelConverter();
@override
SnChannel fromSql(String fromDb) {
return fromJson(jsonDecode(fromDb) as Map<String, dynamic>);
}
@override
String toSql(SnChannel value) {
return jsonEncode(toJson(value));
}
@override
SnChannel fromJson(Map<String, Object?> json) {
return SnChannel.fromJson(json);
}
@override
Map<String, Object?> toJson(SnChannel value) {
return value.toJson();
}
}
@TableIndex(name: 'idx_channel_alias', columns: {#alias})
class SnLocalChatChannel extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get alias => text()();
TextColumn get content => text().map(const SnChannelConverter())();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
}
class SnMessageConverter extends TypeConverter<SnChatMessage, String>
with JsonTypeConverter2<SnChatMessage, String, Map<String, Object?>> {
const SnMessageConverter();
@override
SnChatMessage fromSql(String fromDb) {
return fromJson(jsonDecode(fromDb) as Map<String, dynamic>);
}
@override
String toSql(SnChatMessage value) {
return jsonEncode(toJson(value));
}
@override
SnChatMessage fromJson(Map<String, Object?> json) {
return SnChatMessage.fromJson(json);
}
@override
Map<String, Object?> toJson(SnChatMessage value) {
return value.toJson();
}
}
@TableIndex(name: 'idx_chat_channel', columns: {#channelId})
class SnLocalChatMessage extends Table {
IntColumn get id => integer().autoIncrement()();
IntColumn get channelId => integer()();
IntColumn get senderId => integer().nullable()();
TextColumn get content => text().map(const SnMessageConverter())();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
}
class SnChannelMemberConverter extends TypeConverter<SnChannelMember, String>
with JsonTypeConverter2<SnChannelMember, String, Map<String, Object?>> {
const SnChannelMemberConverter();
@override
SnChannelMember fromSql(String fromDb) {
return fromJson(jsonDecode(fromDb) as Map<String, dynamic>);
}
@override
String toSql(SnChannelMember value) {
return jsonEncode(toJson(value));
}
@override
SnChannelMember fromJson(Map<String, Object?> json) {
return SnChannelMember.fromJson(json);
}
@override
Map<String, Object?> toJson(SnChannelMember value) {
return value.toJson();
}
}
class SnLocalChannelMember extends Table {
IntColumn get id => integer().autoIncrement()();
IntColumn get channelId => integer()();
IntColumn get accountId => integer()();
TextColumn get content => text().map(SnChannelMemberConverter())();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
DateTimeColumn get cacheExpiredAt => dateTime()();
}

View File

@ -1,62 +0,0 @@
import 'package:drift/drift.dart';
import 'package:drift_flutter/drift_flutter.dart';
import 'package:path_provider/path_provider.dart';
import 'package:surface/database/account.dart';
import 'package:surface/database/attachment.dart';
import 'package:surface/database/chat.dart';
import 'package:surface/database/database.steps.dart';
import 'package:surface/database/keypair.dart';
import 'package:surface/database/realm.dart';
import 'package:surface/database/sticker.dart';
import 'package:surface/types/chat.dart';
import 'package:surface/types/attachment.dart';
import 'package:surface/types/account.dart';
import 'package:surface/types/realm.dart';
part 'database.g.dart';
@DriftDatabase(tables: [
SnLocalChatChannel,
SnLocalChatMessage,
SnLocalChannelMember,
SnLocalKeyPair,
SnLocalAccount,
SnLocalAttachment,
SnLocalSticker,
SnLocalStickerPack,
SnLocalRealm,
])
class AppDatabase extends _$AppDatabase {
AppDatabase([QueryExecutor? e]) : super(e ?? _openConnection());
@override
int get schemaVersion => 4;
static QueryExecutor _openConnection() {
return driftDatabase(
name: 'solar_network_data',
native: const DriftNativeOptions(
databaseDirectory: getApplicationSupportDirectory,
),
web: DriftWebOptions(
sqlite3Wasm: Uri.parse('sqlite3.wasm'),
driftWorker: Uri.parse('drift_worker.dart.js'),
),
);
}
@override
MigrationStrategy get migration {
return MigrationStrategy(
onUpgrade: stepByStep(from1To2: (m, schema) async {
// Nothing else to do here
}, from2To3: (m, schema) async {
// Nothing else to do here, too
}, from3To4: (m, schema) async {
m.createTable(schema.snLocalRealm);
m.createIndex(schema.idxRealmAccount);
m.createIndex(schema.idxRealmAlias);
}),
);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,657 +0,0 @@
// dart format width=80
import 'package:drift/internal/versioned_schema.dart' as i0;
import 'package:drift/drift.dart' as i1;
import 'package:drift/drift.dart'; // ignore_for_file: type=lint,unused_import
// GENERATED BY drift_dev, DO NOT MODIFY.
final class Schema2 extends i0.VersionedSchema {
Schema2({required super.database}) : super(version: 2);
@override
late final List<i1.DatabaseSchemaEntity> entities = [
snLocalChatChannel,
snLocalChatMessage,
snLocalKeyPair,
];
late final Shape0 snLocalChatChannel = Shape0(
source: i0.VersionedTable(
entityName: 'sn_local_chat_channel',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_1,
_column_2,
_column_3,
],
attachedDatabase: database,
),
alias: null);
late final Shape1 snLocalChatMessage = Shape1(
source: i0.VersionedTable(
entityName: 'sn_local_chat_message',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_4,
_column_2,
_column_3,
],
attachedDatabase: database,
),
alias: null);
late final Shape2 snLocalKeyPair = Shape2(
source: i0.VersionedTable(
entityName: 'sn_local_key_pair',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(id)',
],
columns: [
_column_5,
_column_6,
_column_7,
_column_8,
_column_9,
],
attachedDatabase: database,
),
alias: null);
}
class Shape0 extends i0.VersionedTable {
Shape0({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<int> get id =>
columnsByName['id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get alias =>
columnsByName['alias']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get content =>
columnsByName['content']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<DateTime> get createdAt =>
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
}
i1.GeneratedColumn<int> _column_0(String aliasedName) =>
i1.GeneratedColumn<int>('id', aliasedName, false,
hasAutoIncrement: true,
type: i1.DriftSqlType.int,
defaultConstraints:
i1.GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'));
i1.GeneratedColumn<String> _column_1(String aliasedName) =>
i1.GeneratedColumn<String>('alias', aliasedName, false,
type: i1.DriftSqlType.string);
i1.GeneratedColumn<String> _column_2(String aliasedName) =>
i1.GeneratedColumn<String>('content', aliasedName, false,
type: i1.DriftSqlType.string);
i1.GeneratedColumn<DateTime> _column_3(String aliasedName) =>
i1.GeneratedColumn<DateTime>('created_at', aliasedName, false,
type: i1.DriftSqlType.dateTime,
defaultValue: const CustomExpression(
'CAST(strftime(\'%s\', CURRENT_TIMESTAMP) AS INTEGER)'));
class Shape1 extends i0.VersionedTable {
Shape1({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<int> get id =>
columnsByName['id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get channelId =>
columnsByName['channel_id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get content =>
columnsByName['content']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<DateTime> get createdAt =>
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
}
i1.GeneratedColumn<int> _column_4(String aliasedName) =>
i1.GeneratedColumn<int>('channel_id', aliasedName, false,
type: i1.DriftSqlType.int);
class Shape2 extends i0.VersionedTable {
Shape2({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<String> get id =>
columnsByName['id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<int> get accountId =>
columnsByName['account_id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get publicKey =>
columnsByName['public_key']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get privateKey =>
columnsByName['private_key']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<bool> get isActive =>
columnsByName['is_active']! as i1.GeneratedColumn<bool>;
}
i1.GeneratedColumn<String> _column_5(String aliasedName) =>
i1.GeneratedColumn<String>('id', aliasedName, false,
type: i1.DriftSqlType.string);
i1.GeneratedColumn<int> _column_6(String aliasedName) =>
i1.GeneratedColumn<int>('account_id', aliasedName, false,
type: i1.DriftSqlType.int);
i1.GeneratedColumn<String> _column_7(String aliasedName) =>
i1.GeneratedColumn<String>('public_key', aliasedName, false,
type: i1.DriftSqlType.string);
i1.GeneratedColumn<String> _column_8(String aliasedName) =>
i1.GeneratedColumn<String>('private_key', aliasedName, true,
type: i1.DriftSqlType.string);
i1.GeneratedColumn<bool> _column_9(String aliasedName) =>
i1.GeneratedColumn<bool>('is_active', aliasedName, false,
type: i1.DriftSqlType.bool,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
'CHECK ("is_active" IN (0, 1))'),
defaultValue: const CustomExpression('0'));
final class Schema3 extends i0.VersionedSchema {
Schema3({required super.database}) : super(version: 3);
@override
late final List<i1.DatabaseSchemaEntity> entities = [
snLocalChatChannel,
snLocalChatMessage,
snLocalChannelMember,
snLocalKeyPair,
snLocalAccount,
snLocalAttachment,
snLocalSticker,
snLocalStickerPack,
idxChannelAlias,
idxChatChannel,
idxAccountName,
idxAttachmentRid,
idxAttachmentAccount,
];
late final Shape0 snLocalChatChannel = Shape0(
source: i0.VersionedTable(
entityName: 'sn_local_chat_channel',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_1,
_column_2,
_column_3,
],
attachedDatabase: database,
),
alias: null);
late final Shape3 snLocalChatMessage = Shape3(
source: i0.VersionedTable(
entityName: 'sn_local_chat_message',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_4,
_column_10,
_column_2,
_column_3,
],
attachedDatabase: database,
),
alias: null);
late final Shape4 snLocalChannelMember = Shape4(
source: i0.VersionedTable(
entityName: 'sn_local_channel_member',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_4,
_column_6,
_column_2,
_column_3,
_column_11,
],
attachedDatabase: database,
),
alias: null);
late final Shape2 snLocalKeyPair = Shape2(
source: i0.VersionedTable(
entityName: 'sn_local_key_pair',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(id)',
],
columns: [
_column_5,
_column_6,
_column_7,
_column_8,
_column_9,
],
attachedDatabase: database,
),
alias: null);
late final Shape5 snLocalAccount = Shape5(
source: i0.VersionedTable(
entityName: 'sn_local_account',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_12,
_column_2,
_column_3,
_column_11,
],
attachedDatabase: database,
),
alias: null);
late final Shape6 snLocalAttachment = Shape6(
source: i0.VersionedTable(
entityName: 'sn_local_attachment',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_13,
_column_14,
_column_2,
_column_6,
_column_3,
_column_11,
],
attachedDatabase: database,
),
alias: null);
late final Shape7 snLocalSticker = Shape7(
source: i0.VersionedTable(
entityName: 'sn_local_sticker',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_1,
_column_15,
_column_2,
_column_3,
],
attachedDatabase: database,
),
alias: null);
late final Shape8 snLocalStickerPack = Shape8(
source: i0.VersionedTable(
entityName: 'sn_local_sticker_pack',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_2,
_column_3,
],
attachedDatabase: database,
),
alias: null);
final i1.Index idxChannelAlias = i1.Index('idx_channel_alias',
'CREATE INDEX idx_channel_alias ON sn_local_chat_channel (alias)');
final i1.Index idxChatChannel = i1.Index('idx_chat_channel',
'CREATE INDEX idx_chat_channel ON sn_local_chat_message (channel_id)');
final i1.Index idxAccountName = i1.Index('idx_account_name',
'CREATE INDEX idx_account_name ON sn_local_account (name)');
final i1.Index idxAttachmentRid = i1.Index('idx_attachment_rid',
'CREATE INDEX idx_attachment_rid ON sn_local_attachment (rid)');
final i1.Index idxAttachmentAccount = i1.Index('idx_attachment_account',
'CREATE INDEX idx_attachment_account ON sn_local_attachment (account_id)');
}
class Shape3 extends i0.VersionedTable {
Shape3({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<int> get id =>
columnsByName['id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get channelId =>
columnsByName['channel_id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get senderId =>
columnsByName['sender_id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get content =>
columnsByName['content']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<DateTime> get createdAt =>
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
}
i1.GeneratedColumn<int> _column_10(String aliasedName) =>
i1.GeneratedColumn<int>('sender_id', aliasedName, true,
type: i1.DriftSqlType.int);
class Shape4 extends i0.VersionedTable {
Shape4({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<int> get id =>
columnsByName['id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get channelId =>
columnsByName['channel_id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get accountId =>
columnsByName['account_id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get content =>
columnsByName['content']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<DateTime> get createdAt =>
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<DateTime> get cacheExpiredAt =>
columnsByName['cache_expired_at']! as i1.GeneratedColumn<DateTime>;
}
i1.GeneratedColumn<DateTime> _column_11(String aliasedName) =>
i1.GeneratedColumn<DateTime>('cache_expired_at', aliasedName, false,
type: i1.DriftSqlType.dateTime);
class Shape5 extends i0.VersionedTable {
Shape5({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<int> get id =>
columnsByName['id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get name =>
columnsByName['name']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get content =>
columnsByName['content']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<DateTime> get createdAt =>
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<DateTime> get cacheExpiredAt =>
columnsByName['cache_expired_at']! as i1.GeneratedColumn<DateTime>;
}
i1.GeneratedColumn<String> _column_12(String aliasedName) =>
i1.GeneratedColumn<String>('name', aliasedName, false,
type: i1.DriftSqlType.string);
class Shape6 extends i0.VersionedTable {
Shape6({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<int> get id =>
columnsByName['id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get rid =>
columnsByName['rid']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get uuid =>
columnsByName['uuid']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get content =>
columnsByName['content']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<int> get accountId =>
columnsByName['account_id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<DateTime> get createdAt =>
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<DateTime> get cacheExpiredAt =>
columnsByName['cache_expired_at']! as i1.GeneratedColumn<DateTime>;
}
i1.GeneratedColumn<String> _column_13(String aliasedName) =>
i1.GeneratedColumn<String>('rid', aliasedName, false,
type: i1.DriftSqlType.string,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways('UNIQUE'));
i1.GeneratedColumn<String> _column_14(String aliasedName) =>
i1.GeneratedColumn<String>('uuid', aliasedName, false,
type: i1.DriftSqlType.string,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways('UNIQUE'));
class Shape7 extends i0.VersionedTable {
Shape7({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<int> get id =>
columnsByName['id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get alias =>
columnsByName['alias']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get fullAlias =>
columnsByName['full_alias']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get content =>
columnsByName['content']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<DateTime> get createdAt =>
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
}
i1.GeneratedColumn<String> _column_15(String aliasedName) =>
i1.GeneratedColumn<String>('full_alias', aliasedName, false,
type: i1.DriftSqlType.string);
class Shape8 extends i0.VersionedTable {
Shape8({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<int> get id =>
columnsByName['id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get content =>
columnsByName['content']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<DateTime> get createdAt =>
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
}
final class Schema4 extends i0.VersionedSchema {
Schema4({required super.database}) : super(version: 4);
@override
late final List<i1.DatabaseSchemaEntity> entities = [
snLocalChatChannel,
snLocalChatMessage,
snLocalChannelMember,
snLocalKeyPair,
snLocalAccount,
snLocalAttachment,
snLocalSticker,
snLocalStickerPack,
snLocalRealm,
idxChannelAlias,
idxChatChannel,
idxAccountName,
idxAttachmentRid,
idxAttachmentAccount,
idxRealmAlias,
idxRealmAccount,
];
late final Shape0 snLocalChatChannel = Shape0(
source: i0.VersionedTable(
entityName: 'sn_local_chat_channel',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_1,
_column_2,
_column_3,
],
attachedDatabase: database,
),
alias: null);
late final Shape3 snLocalChatMessage = Shape3(
source: i0.VersionedTable(
entityName: 'sn_local_chat_message',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_4,
_column_10,
_column_2,
_column_3,
],
attachedDatabase: database,
),
alias: null);
late final Shape4 snLocalChannelMember = Shape4(
source: i0.VersionedTable(
entityName: 'sn_local_channel_member',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_4,
_column_6,
_column_2,
_column_3,
_column_11,
],
attachedDatabase: database,
),
alias: null);
late final Shape2 snLocalKeyPair = Shape2(
source: i0.VersionedTable(
entityName: 'sn_local_key_pair',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'PRIMARY KEY(id)',
],
columns: [
_column_5,
_column_6,
_column_7,
_column_8,
_column_9,
],
attachedDatabase: database,
),
alias: null);
late final Shape5 snLocalAccount = Shape5(
source: i0.VersionedTable(
entityName: 'sn_local_account',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_12,
_column_2,
_column_3,
_column_11,
],
attachedDatabase: database,
),
alias: null);
late final Shape6 snLocalAttachment = Shape6(
source: i0.VersionedTable(
entityName: 'sn_local_attachment',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_13,
_column_14,
_column_2,
_column_6,
_column_3,
_column_11,
],
attachedDatabase: database,
),
alias: null);
late final Shape7 snLocalSticker = Shape7(
source: i0.VersionedTable(
entityName: 'sn_local_sticker',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_1,
_column_15,
_column_2,
_column_3,
],
attachedDatabase: database,
),
alias: null);
late final Shape8 snLocalStickerPack = Shape8(
source: i0.VersionedTable(
entityName: 'sn_local_sticker_pack',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_2,
_column_3,
],
attachedDatabase: database,
),
alias: null);
late final Shape9 snLocalRealm = Shape9(
source: i0.VersionedTable(
entityName: 'sn_local_realm',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_16,
_column_2,
_column_6,
_column_3,
_column_11,
],
attachedDatabase: database,
),
alias: null);
final i1.Index idxChannelAlias = i1.Index('idx_channel_alias',
'CREATE INDEX idx_channel_alias ON sn_local_chat_channel (alias)');
final i1.Index idxChatChannel = i1.Index('idx_chat_channel',
'CREATE INDEX idx_chat_channel ON sn_local_chat_message (channel_id)');
final i1.Index idxAccountName = i1.Index('idx_account_name',
'CREATE INDEX idx_account_name ON sn_local_account (name)');
final i1.Index idxAttachmentRid = i1.Index('idx_attachment_rid',
'CREATE INDEX idx_attachment_rid ON sn_local_attachment (rid)');
final i1.Index idxAttachmentAccount = i1.Index('idx_attachment_account',
'CREATE INDEX idx_attachment_account ON sn_local_attachment (account_id)');
final i1.Index idxRealmAlias = i1.Index('idx_realm_alias',
'CREATE INDEX idx_realm_alias ON sn_local_realm (alias)');
final i1.Index idxRealmAccount = i1.Index('idx_realm_account',
'CREATE INDEX idx_realm_account ON sn_local_realm (account_id)');
}
class Shape9 extends i0.VersionedTable {
Shape9({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<int> get id =>
columnsByName['id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get alias =>
columnsByName['alias']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get content =>
columnsByName['content']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<int> get accountId =>
columnsByName['account_id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<DateTime> get createdAt =>
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<DateTime> get cacheExpiredAt =>
columnsByName['cache_expired_at']! as i1.GeneratedColumn<DateTime>;
}
i1.GeneratedColumn<String> _column_16(String aliasedName) =>
i1.GeneratedColumn<String>('alias', aliasedName, false,
type: i1.DriftSqlType.string,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways('UNIQUE'));
i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
required Future<void> Function(i1.Migrator m, Schema4 schema) from3To4,
}) {
return (currentVersion, database) async {
switch (currentVersion) {
case 1:
final schema = Schema2(database: database);
final migrator = i1.Migrator(database, schema);
await from1To2(migrator, schema);
return 2;
case 2:
final schema = Schema3(database: database);
final migrator = i1.Migrator(database, schema);
await from2To3(migrator, schema);
return 3;
case 3:
final schema = Schema4(database: database);
final migrator = i1.Migrator(database, schema);
await from3To4(migrator, schema);
return 4;
default:
throw ArgumentError.value('Unknown migration from $currentVersion');
}
};
}
i1.OnUpgrade stepByStep({
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
required Future<void> Function(i1.Migrator m, Schema4 schema) from3To4,
}) =>
i0.VersionedSchema.stepByStepHelper(
step: migrationSteps(
from1To2: from1To2,
from2To3: from2To3,
from3To4: from3To4,
));

View File

@ -1,8 +0,0 @@
import 'package:drift/wasm.dart';
// Use `dart compile js -O4 ./drift_worker.dart` to compile this file.
// And place it in the web/ directory.
// When compiled with dart2js, this file defines a dedicated or shared web
// worker used by drift.
void main() => WasmDatabase.workerMainForOpen();

View File

@ -1,16 +0,0 @@
import 'package:drift/drift.dart';
class SnLocalKeyPair extends Table {
TextColumn get id => text()();
IntColumn get accountId => integer()();
TextColumn get publicKey => text()();
TextColumn get privateKey => text().nullable()();
BoolColumn get isActive => boolean().withDefault(Constant(false))();
@override
Set<Column<Object>> get primaryKey => {id};
}

View File

@ -1,45 +0,0 @@
import 'dart:convert';
import 'package:drift/drift.dart';
import 'package:surface/types/realm.dart';
class SnRealmConverter extends TypeConverter<SnRealm, String>
with JsonTypeConverter2<SnRealm, String, Map<String, Object?>> {
const SnRealmConverter();
@override
SnRealm fromSql(String fromDb) {
return fromJson(jsonDecode(fromDb) as Map<String, dynamic>);
}
@override
String toSql(SnRealm value) {
return jsonEncode(toJson(value));
}
@override
SnRealm fromJson(Map<String, Object?> json) {
return SnRealm.fromJson(json);
}
@override
Map<String, Object?> toJson(SnRealm value) {
return value.toJson();
}
}
@TableIndex(name: 'idx_realm_alias', columns: {#alias})
@TableIndex(name: 'idx_realm_account', columns: {#accountId})
class SnLocalRealm extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get alias => text().unique()();
TextColumn get content => text().map(const SnRealmConverter())();
IntColumn get accountId => integer()();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
DateTimeColumn get cacheExpiredAt => dateTime()();
}

View File

@ -1,74 +0,0 @@
import 'dart:convert';
import 'package:drift/drift.dart';
import 'package:surface/types/attachment.dart';
class SnStickerConverter extends TypeConverter<SnSticker, String>
with JsonTypeConverter2<SnSticker, String, Map<String, Object?>> {
const SnStickerConverter();
@override
SnSticker fromSql(String fromDb) {
return fromJson(jsonDecode(fromDb) as Map<String, dynamic>);
}
@override
String toSql(SnSticker value) {
return jsonEncode(toJson(value));
}
@override
SnSticker fromJson(Map<String, Object?> json) {
return SnSticker.fromJson(json);
}
@override
Map<String, Object?> toJson(SnSticker value) {
return value.toJson();
}
}
class SnLocalSticker extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get alias => text()();
TextColumn get fullAlias => text()();
TextColumn get content => text().map(const SnStickerConverter())();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
}
class SnStickerPackConverter extends TypeConverter<SnStickerPack, String>
with JsonTypeConverter2<SnStickerPack, String, Map<String, Object?>> {
const SnStickerPackConverter();
@override
SnStickerPack fromSql(String fromDb) {
return fromJson(jsonDecode(fromDb) as Map<String, dynamic>);
}
@override
String toSql(SnStickerPack value) {
return jsonEncode(toJson(value));
}
@override
SnStickerPack fromJson(Map<String, Object?> json) {
return SnStickerPack.fromJson(json);
}
@override
Map<String, Object?> toJson(SnStickerPack value) {
return value.toJson();
}
}
class SnLocalStickerPack extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get content => text().map(const SnStickerPackConverter())();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
}

View File

@ -1,10 +0,0 @@
import 'package:talker/talker.dart';
final logging = Talker(
settings: TalkerSettings(
enabled: true,
useHistory: true,
maxHistoryItems: 1000,
useConsoleLogs: true,
),
);

View File

@ -1,9 +1,7 @@
import 'dart:async';
import 'dart:developer';
import 'dart:io';
import 'dart:ui';
import 'package:audioplayers/audioplayers.dart';
import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:croppy/croppy.dart';
import 'package:dio/dio.dart';
@ -12,23 +10,17 @@ import 'package:easy_localization_loader/easy_localization_loader.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:hotkey_manager/hotkey_manager.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:provider/provider.dart';
import 'package:relative_time/relative_time.dart';
import 'package:responsive_framework/responsive_framework.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/firebase_options.dart';
import 'package:surface/logger.dart';
import 'package:surface/providers/channel.dart';
import 'package:surface/providers/chat_call.dart';
import 'package:surface/providers/config.dart';
import 'package:surface/providers/database.dart';
import 'package:surface/providers/keypair.dart';
import 'package:surface/providers/link_preview.dart';
import 'package:surface/providers/navigation.dart';
import 'package:surface/providers/notification.dart';
@ -36,29 +28,21 @@ import 'package:surface/providers/post.dart';
import 'package:surface/providers/relationship.dart';
import 'package:surface/providers/sn_attachment.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/sn_realm.dart';
import 'package:surface/providers/sn_sticker.dart';
import 'package:surface/providers/special_day.dart';
import 'package:surface/providers/theme.dart';
import 'package:surface/providers/translation.dart';
import 'package:surface/providers/user_directory.dart';
import 'package:surface/providers/userinfo.dart';
import 'package:surface/providers/websocket.dart';
import 'package:surface/providers/widget.dart';
import 'package:surface/router.dart';
import 'package:surface/types/chat.dart';
import 'package:surface/types/realm.dart';
import 'package:flutter_web_plugins/url_strategy.dart' show usePathUrlStrategy;
import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/menu_bar.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:surface/widgets/version_label.dart';
import 'package:tray_manager/tray_manager.dart';
import 'package:version/version.dart';
import 'package:workmanager/workmanager.dart';
import 'package:in_app_review/in_app_review.dart';
import 'package:image_picker_android/image_picker_android.dart';
import 'package:image_picker_platform_interface/image_picker_platform_interface.dart';
import 'package:local_notifier/local_notifier.dart';
import 'package:flutter_animate/flutter_animate.dart';
@pragma('vm:entry-point')
void appBackgroundDispatcher() {
@ -77,58 +61,37 @@ void appBackgroundDispatcher() {
});
}
// Desktop size tools
Future<Size> _getSavedWindowSize() async {
final prefs = await SharedPreferences.getInstance();
String? sizeString = prefs.getString(kAppWindowSize);
if (sizeString != null) {
List<String> parts = sizeString.split('x');
if (parts.length == 2) {
double? width = double.tryParse(parts[0]);
double? height = double.tryParse(parts[1]);
if (width != null && height != null) {
return Size(width, height);
}
}
}
return const Size(1280, 720); // Default size
}
Future<void> _saveWindowSize() async {
final prefs = await SharedPreferences.getInstance();
final size = appWindow.size;
await prefs.setString(kAppWindowSize, '${size.width}x${size.height}');
}
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await EasyLocalization.ensureInitialized();
await Hive.initFlutter();
Hive.registerAdapter(SnChannelImplAdapter());
Hive.registerAdapter(SnRealmImplAdapter());
Hive.registerAdapter(SnChannelMemberImplAdapter());
Hive.registerAdapter(SnChatMessageImplAdapter());
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
GoRouter.optionURLReflectsImperativeAPIs = true;
usePathUrlStrategy();
if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS)) {
final Size savedSize = await _getSavedWindowSize();
doWhenWindowReady(() {
appWindow.minSize = Size(480, 640);
appWindow.size = savedSize;
appWindow.size = Size(1280, 720);
appWindow.alignment = Alignment.center;
appWindow.show();
});
}
await EasyLocalization.ensureInitialized();
if (!kIsWeb && !Platform.isLinux) {
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform);
}
GoRouter.optionURLReflectsImperativeAPIs = true;
usePathUrlStrategy();
if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
Workmanager()
.initialize(appBackgroundDispatcher, isInDebugMode: kDebugMode);
Workmanager().initialize(
appBackgroundDispatcher,
isInDebugMode: kDebugMode,
);
if (Platform.isAndroid) {
Workmanager().registerPeriodicTask(
"widget-update-random-post",
@ -140,14 +103,6 @@ void main() async {
}
}
if (!kIsWeb && Platform.isAndroid) {
final ImagePickerPlatform imagePickerImplementation =
ImagePickerPlatform.instance;
if (imagePickerImplementation is ImagePickerAndroid) {
imagePickerImplementation.useAndroidPhotoPicker = true;
}
}
runApp(const SolianApp());
}
@ -163,16 +118,13 @@ class SolianApp extends StatelessWidget {
Locale('en', 'US'),
Locale('zh', 'CN'),
Locale('zh', 'TW'),
Locale('zh', 'HK')
Locale('zh', 'HK'),
],
fallbackLocale: Locale('en', 'US'),
useFallbackTranslations: true,
assetLoader: JsonAssetLoader(),
child: MultiProvider(
providers: [
// Infrastructure layer
Provider(create: (ctx) => DatabaseProvider(ctx)),
// System extensions layer
Provider(create: (ctx) => HomeWidgetProvider(ctx)),
@ -187,17 +139,15 @@ class SolianApp extends StatelessWidget {
Provider(create: (ctx) => SnNetworkProvider(ctx)),
Provider(create: (ctx) => UserDirectoryProvider(ctx)),
Provider(create: (ctx) => SnAttachmentProvider(ctx)),
ChangeNotifierProvider(create: (ctx) => SnRealmProvider(ctx)),
Provider(create: (ctx) => SnPostContentProvider(ctx)),
Provider(create: (ctx) => SnRelationshipProvider(ctx)),
Provider(create: (ctx) => SnLinkPreviewProvider(ctx)),
Provider(create: (ctx) => SnStickerProvider(ctx)),
ChangeNotifierProvider(create: (ctx) => UserProvider(ctx)),
ChangeNotifierProvider(create: (ctx) => WebSocketProvider(ctx)),
Provider(create: (ctx) => KeyPairProvider(ctx)),
ChangeNotifierProvider(create: (ctx) => NotificationProvider(ctx)),
ChangeNotifierProvider(create: (ctx) => ChatChannelProvider(ctx)),
Provider(create: (ctx) => SnTranslator()),
ChangeNotifierProvider(create: (ctx) => ChatCallProvider(ctx)),
// Additional helper layer
Provider(create: (ctx) => SpecialDayProvider(ctx)),
@ -206,8 +156,8 @@ class SolianApp extends StatelessWidget {
),
),
breakpoints: [
const Breakpoint(start: 0, end: 600, name: MOBILE),
const Breakpoint(start: 601, end: 800, name: TABLET),
const Breakpoint(start: 0, end: 450, name: MOBILE),
const Breakpoint(start: 451, end: 800, name: TABLET),
const Breakpoint(start: 801, end: 1920, name: DESKTOP),
],
);
@ -256,25 +206,20 @@ class _AppSplashScreen extends StatefulWidget {
State<_AppSplashScreen> createState() => _AppSplashScreenState();
}
class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
bool _isBusy = false;
double _initPercentage = 0;
String _phaseText = 'appInitStarting';
class _AppSplashScreenState extends State<_AppSplashScreen> {
void _tryRequestRating() async {
final prefs = await SharedPreferences.getInstance();
if (prefs.containsKey('first_boot_time')) {
final rawTime = prefs.getString('first_boot_time');
final time = DateTime.tryParse(rawTime ?? '');
if (time != null &&
time.isBefore(DateTime.now().subtract(const Duration(days: 3)))) {
if (time != null && time.isBefore(DateTime.now().subtract(const Duration(days: 3)))) {
final inAppReview = InAppReview.instance;
if (prefs.getBool('rating_requested') == true) return;
if (await inAppReview.isAvailable()) {
await inAppReview.requestReview();
prefs.setBool('rating_requested', true);
} else {
logging.error('Unable request app review, unavailable');
log('Unable request app review, unavailable');
}
}
} else {
@ -289,101 +234,48 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
final localVersionString = '${info.version}+${info.buildNumber}';
final resp = await Dio(
BaseOptions(
sendTimeout: const Duration(seconds: 60),
receiveTimeout: const Duration(seconds: 60)),
sendTimeout: const Duration(seconds: 60),
receiveTimeout: const Duration(seconds: 60),
),
).get(
'https://api.github.com/repos/Solsynth/HyperNet.Surface/releases/latest');
final remoteVersionString = resp.data?['tag_name'] ?? '0.0.0+0';
'https://git.solsynth.dev/api/v1/repos/HyperNet/Surface/tags?page=1&limit=1',
);
final remoteVersionString = (resp.data as List).firstOrNull?['name'] ?? '0.0.0+0';
final remoteVersion = Version.parse(remoteVersionString.split('+').first);
final localVersion = Version.parse(localVersionString.split('+').first);
final remoteBuildNumber =
int.tryParse(remoteVersionString.split('+').last) ?? 0;
final localBuildNumber =
int.tryParse(localVersionString.split('+').last) ?? 0;
logging.info(
"[Update] Local: $localVersionString, Remote: $remoteVersionString");
if ((remoteVersion > localVersion ||
remoteBuildNumber > localBuildNumber) &&
mounted) {
final remoteBuildNumber = int.tryParse(remoteVersionString.split('+').last) ?? 0;
final localBuildNumber = int.tryParse(localVersionString.split('+').last) ?? 0;
log("[Update] Local: $localVersionString, Remote: $remoteVersionString");
if ((remoteVersion > localVersion || remoteBuildNumber > localBuildNumber) && mounted) {
final config = context.read<ConfigProvider>();
config.setUpdate(
remoteVersionString, resp.data?['body'] ?? 'No changelog');
logging.info("[Update] Update available: $remoteVersionString");
config.setUpdate(remoteVersionString);
log("[Update] Update available: $remoteVersionString");
}
} catch (e) {
logging.error('[Error] Unable to check update...', e);
if (mounted) context.showErrorDialog('Unable to check update: $e');
}
}
void _setPhaseText(String text) {
_phaseText = 'appInit${text.capitalize()}'.tr();
if (mounted) setState(() {});
}
Future<void> _initialize() async {
try {
final cfg = context.read<ConfigProvider>();
WidgetsBinding.instance.addPostFrameCallback((_) {
cfg.calcDrawerSize(context);
});
final home = context.read<HomeWidgetProvider>();
await home.initialize();
if (!mounted) return;
// The Network initialization must be done after the HomeWidget initialization
// The Network initialization will save the server url to the HomeWidget
// The Network initialization will also save initialize the Config, so it not need to be initialized again
_initPercentage = 0.1;
_setPhaseText('network');
final sn = context.read<SnNetworkProvider>();
await sn.initializeUserAgent();
await sn.setConfigWithNative();
if (!mounted) return;
_initPercentage = 0.2;
_setPhaseText('userdata');
final ua = context.read<UserProvider>();
await ua.initialize();
if (!mounted) return;
_initPercentage = 0.3;
_setPhaseText('websocket');
final ws = context.read<WebSocketProvider>();
await ws.tryConnect();
try {
if (!mounted) return;
_initPercentage = 0.9;
_setPhaseText('keyPair');
final kp = context.read<KeyPairProvider>();
kp.reloadActive();
kp.listen();
} catch (_) {}
if (ua.isAuthorized) {
if (!mounted) return;
_setPhaseText('notification');
final notify = context.read<NotificationProvider>();
notify.listen();
try {
notify.registerPushNotifications();
if (!mounted) return;
_setPhaseText('stickers');
final sticker = context.read<SnStickerProvider>();
await sticker.listSticker();
if (!mounted) return;
_setPhaseText('userDirectory');
final ud = context.read<UserDirectoryProvider>();
await ud.loadAccountCache();
if (!mounted) return;
_setPhaseText('realm');
final rm = context.read<SnRealmProvider>();
await rm.refreshAvailableRealms();
if (!mounted) return;
_setPhaseText('chat');
final ct = context.read<ChatChannelProvider>();
await ct.refreshAvailableChannels();
_initPercentage = 1;
_setPhaseText('done');
} catch (_) {}
_playIntro();
}
if (!mounted) return;
final notify = context.read<NotificationProvider>();
await notify.registerPushNotifications();
} catch (err) {
if (!mounted) return;
await context.showErrorDialog(err);
@ -394,436 +286,18 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
await widgetUpdateRandomPost();
}
Future<void> _hotkeyInitialization() async {
if (kIsWeb) return;
// The quit key has been removed, and the logic of the quit key is moved to system menu bar activator.
}
void _playIntro() async {
final cfg = context.read<ConfigProvider>();
if (!cfg.soundEffects) return;
final date = DateTime.now();
final player = AudioPlayer(playerId: 'launch-done-player');
await player.play(
(cfg.aprilFoolFeatures && date.month == 4 && date.day == 1)
? AssetSource('audio/sfx/launch-intro.mp3')
: AssetSource('audio/sfx/launch-done.mp3'),
volume: 0.8,
ctx: AudioContext(
android: AudioContextAndroid(
contentType: AndroidContentType.sonification,
usageType: AndroidUsageType.notificationEvent,
),
iOS: AudioContextIOS(category: AVAudioSessionCategory.ambient),
),
mode: PlayerMode.lowLatency,
);
player.onPlayerComplete.listen((_) {
player.dispose();
});
}
final Menu _appTrayMenu = Menu(
items: [
MenuItem(key: 'version_label', label: 'Solian', disabled: true),
MenuItem.separator(),
MenuItem.checkbox(
checked: false,
key: 'mute_notification',
label: 'trayMenuMuteNotification'.tr()),
MenuItem.separator(),
MenuItem(key: 'window_show', label: 'trayMenuShow'.tr()),
MenuItem(key: 'exit', label: 'trayMenuExit'.tr()),
],
);
Future<void> _trayInitialization() async {
if (kIsWeb || Platform.isAndroid || Platform.isIOS) return;
final icon = Platform.isWindows
? 'assets/icon/tray-icon.ico'
: 'assets/icon/tray-icon.png';
final appVersion = await PackageInfo.fromPlatform();
trayManager.addListener(this);
await trayManager.setIcon(icon);
_appTrayMenu.items![0] = MenuItem(
key: 'version_label',
label: 'Solian ${appVersion.version}+${appVersion.buildNumber}',
disabled: true,
);
await trayManager.setContextMenu(_appTrayMenu);
}
Future<void> _notifyInitialization() async {
if (kIsWeb || Platform.isAndroid || Platform.isIOS) return;
await localNotifier.setup(
appName: 'Solian', shortcutPolicy: ShortcutPolicy.requireCreate);
}
AppLifecycleListener? _appLifecycleListener;
@override
void initState() {
super.initState();
_isBusy = true;
if (!kIsWeb && !(Platform.isIOS || Platform.isAndroid)) {
_appLifecycleListener =
AppLifecycleListener(onExitRequested: _onExitRequested);
}
try {
_trayInitialization();
_hotkeyInitialization();
_notifyInitialization();
_initialize().then((_) {
_postInitialization();
_tryRequestRating();
_checkForUpdate();
setState(() => _isBusy = false);
}).catchError((err) {
logging.error('[Bootstrap] Unable to initialize app', err);
setState(() => _isBusy = false);
});
} catch (err) {
logging.error('[Bootstrap] Unable to initialize (pre-stage) app', err);
}
}
Future<AppExitResponse> _onExitRequested() async {
appWindow.hide();
return AppExitResponse.cancel;
}
void _quitApp() {
_saveWindowSize();
_appLifecycleListener?.dispose();
if (Platform.isWindows) {
appWindow.close();
} else {
SystemChannels.platform.invokeMethod('SystemNavigator.pop');
}
}
@override
void onTrayIconMouseDown() {
if (Platform.isWindows) {
context.read<NotificationProvider>().clearTray();
appWindow.show();
} else {
trayManager.popUpContextMenu();
}
}
@override
void onTrayIconRightMouseDown() {
if (Platform.isWindows) {
trayManager.popUpContextMenu();
} else {
context.read<NotificationProvider>().clearTray();
appWindow.show();
}
}
@override
void onTrayMenuItemClick(MenuItem menuItem) {
switch (menuItem.key) {
case 'mute_notification':
final nty = context.read<NotificationProvider>();
nty.isMuted = !nty.isMuted;
_appTrayMenu.items![2].checked = nty.isMuted;
trayManager.setContextMenu(_appTrayMenu);
break;
case 'window_show':
// To prevent the window from being hide after just show on macOS
Timer(const Duration(milliseconds: 100), () => appWindow.show());
break;
case 'exit':
_quitApp();
break;
}
}
@override
void dispose() {
if (!kIsWeb && !(Platform.isAndroid || Platform.isIOS)) {
trayManager.removeListener(this);
hotKeyManager.unregisterAll();
}
super.dispose();
_initialize().then((_) {
_postInitialization();
_tryRequestRating();
_checkForUpdate();
});
}
@override
Widget build(BuildContext context) {
final cfg = context.read<ConfigProvider>();
return AppSystemMenuBar(
onQuit: _quitApp,
child: NotificationListener<SizeChangedLayoutNotification>(
onNotification: (notification) {
WidgetsBinding.instance.addPostFrameCallback((_) {
cfg.calcDrawerSize(context);
});
return false;
},
child: OrientationBuilder(
builder: (context, orientation) {
final cfg = context.read<ConfigProvider>();
WidgetsBinding.instance.addPostFrameCallback((_) {
cfg.calcDrawerSize(context);
});
Future.delayed(const Duration(milliseconds: 300), () {
if (context.mounted) {
cfg.calcDrawerSize(context);
}
});
return SizeChangedLayoutNotifier(
child: _isBusy
? _AppLoadingScreen(
isBusy: _isBusy,
initPercentage: _initPercentage,
phaseText: _phaseText,
)
: widget.child,
);
},
),
),
);
}
}
class _AppLoadingScreen extends StatelessWidget {
const _AppLoadingScreen({
required this.isBusy,
required this.initPercentage,
required this.phaseText,
});
final bool isBusy;
final double initPercentage;
final String phaseText;
@override
Widget build(BuildContext context) {
if (ResponsiveScaffold.getIsExpand(context)) {
return Material(
key: Key('app-splash-screen-$isBusy'),
child: Stack(
children: [
Container(
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage('assets/icon/kanban-1st.jpg'),
fit: BoxFit.cover,
opacity: 0.1,
),
color: Theme.of(context).colorScheme.surface,
backgroundBlendMode: BlendMode.darken,
),
),
Center(
child: Row(
children: [
Expanded(
child: TweenAnimationBuilder<double>(
tween: Tween(begin: 0, end: initPercentage),
duration: Duration(milliseconds: 300),
builder: (context, value, _) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('${(value * 100).toStringAsFixed(0)}%')
.padding(left: 32, bottom: 4),
LinearProgressIndicator(
value: value,
borderRadius: const BorderRadius.all(
Radius.circular(0),
),
stopIndicatorColor: Colors.transparent,
backgroundColor: Colors.transparent,
),
const Gap(24),
],
),
),
),
Expanded(
child: TweenAnimationBuilder<double>(
tween: Tween(begin: 0, end: initPercentage),
duration: Duration(milliseconds: 300),
builder: (context, value, _) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text('${(value * 100).toStringAsFixed(0)}%')
.padding(right: 32, bottom: 4),
Transform.flip(
flipX: true,
child: LinearProgressIndicator(
value: value,
borderRadius: const BorderRadius.all(
Radius.circular(0),
),
stopIndicatorColor: Colors.transparent,
backgroundColor: Colors.transparent,
),
),
const Gap(24),
],
),
),
),
],
),
),
Center(
child: Container(
constraints: const BoxConstraints(maxWidth: 240, minWidth: 160),
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 24),
decoration: BoxDecoration(
color:
Theme.of(context).colorScheme.surface.withOpacity(0.85),
border: Border.all(
color: Theme.of(context).dividerColor,
width: 3,
),
borderRadius: const BorderRadius.all(Radius.circular(12)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'splashScreenServer',
style: GoogleFonts.notoSerifHk(height: 1, fontSize: 11),
textAlign: TextAlign.center,
).tr().opacity(0.85),
Text(
'splashScreenServerName',
style: GoogleFonts.notoSerifHk(
fontSize: 24,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
).tr().opacity(0.85),
Text.rich(
TextSpan(
text: '#',
style: GoogleFonts.notoSerifHk(),
children: [
TextSpan(
text: '0',
style: GoogleFonts.notoSerifHk(
fontSize: 80,
fontWeight: FontWeight.bold,
),
),
],
),
textAlign: TextAlign.center,
).padding(vertical: 16),
],
),
),
),
Positioned(
left: 0,
right: 0,
bottom: MediaQuery.of(context).size.height * 0.2,
child: Column(
children: [
Text(
phaseText,
textAlign: TextAlign.center,
),
AnimateWidgetExtensions(Text(
'splashScreenCaption',
textAlign: TextAlign.center,
).tr())
.animate(onPlay: (e) => e.repeat())
.fadeIn(duration: 500.ms, curve: Curves.easeOut)
.then()
.fadeOut(
duration: 500.ms,
delay: 1000.ms,
curve: Curves.easeIn,
),
],
),
),
Positioned(
bottom: 8,
left: 16,
right: 16,
child: Row(
children: [
Image.asset(
'assets/icon/icon.png',
width: 40,
height: 40,
color: Theme.of(context).colorScheme.onSurface,
).padding(all: 4),
const Gap(4),
Text('Solar Network').bold(),
Expanded(child: const SizedBox()),
AppVersionLabel(),
const Gap(12),
],
),
),
],
),
);
}
return Material(
key: Key('app-splash-screen-$isBusy'),
child: Stack(
children: [
Container(
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage('assets/icon/kanban-1st.jpg'),
fit: BoxFit.cover,
opacity: 0.1,
),
color: Theme.of(context).colorScheme.surface,
backgroundBlendMode: BlendMode.darken,
),
),
Center(
child: Container(
constraints: const BoxConstraints(maxWidth: 240),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Image.asset(
'assets/icon/icon.png',
width: 64,
height: 64,
color: Theme.of(context).colorScheme.onSurface,
),
Text('Solar Network').bold(),
AppVersionLabel(),
Gap(8),
Text(phaseText, textAlign: TextAlign.center),
Gap(16),
TweenAnimationBuilder<double>(
tween: Tween(begin: 0, end: initPercentage),
duration: Duration(milliseconds: 300),
builder: (context, value, _) =>
LinearProgressIndicator(value: value),
),
],
),
),
),
],
),
);
return widget.child;
}
}

View File

@ -1,75 +1,48 @@
import 'dart:convert';
import 'package:drift/drift.dart';
import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:provider/provider.dart';
import 'package:surface/database/database.dart';
import 'package:surface/providers/database.dart';
import 'package:surface/controllers/chat_message_controller.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/sn_realm.dart';
import 'package:surface/providers/user_directory.dart';
import 'package:surface/providers/userinfo.dart';
import 'package:surface/types/chat.dart';
import 'package:surface/types/realm.dart';
class ChatChannelProvider extends ChangeNotifier {
static const kChatChannelBoxName = 'nex_chat_channels';
late final SnNetworkProvider _sn;
late final UserDirectoryProvider _ud;
late final UserProvider _ua;
late final DatabaseProvider _dt;
late final SnRealmProvider _rels;
Box<SnChannel>? get _channelBox => Hive.box<SnChannel>(kChatChannelBoxName);
ChatChannelProvider(BuildContext context) {
_sn = context.read<SnNetworkProvider>();
_ud = context.read<UserDirectoryProvider>();
_ua = context.read<UserProvider>();
_dt = context.read<DatabaseProvider>();
_rels = context.read<SnRealmProvider>();
_initializeLocalData();
}
final List<SnChannel> _availableChannels = List.empty(growable: true);
List<SnChannel> get availableChannels => _availableChannels;
Future<void> refreshAvailableChannels() async {
final stream = fetchChannels();
stream.listen((ele) {
_availableChannels.clear();
_availableChannels.addAll(ele);
notifyListeners();
});
}
void addAvailableChannel(SnChannel channel) {
_availableChannels.add(channel);
notifyListeners();
Future<void> _initializeLocalData() async {
await Hive.openBox<SnChannel>(kChatChannelBoxName);
}
Future<void> _saveChannelToLocal(Iterable<SnChannel> channels) async {
await Future.wait(
channels.map(
(ele) => _dt.db.snLocalChatChannel.insertOne(
SnLocalChatChannelCompanion.insert(
id: Value(ele.id),
alias: ele.key,
content: ele,
createdAt: Value(ele.createdAt),
),
onConflict: DoUpdate(
(_) => SnLocalChatChannelCompanion.custom(
content: Constant(jsonEncode(ele.toJson())),
),
),
),
),
);
if (_channelBox == null) return;
await _channelBox!.putAll({
for (final channel in channels) channel.key: channel,
});
}
Future<List<SnChannel>> _fetchChannelsFromServer({
String scope = 'global',
bool direct = false,
bool doNotSave = false,
}) async {
final resp = await _sn.client.get('/cgi/im/channels/me/available');
final resp = await _sn.client.get(
'/cgi/im/channels/$scope/me/available',
queryParameters: {
'direct': direct,
},
);
final out = List<SnChannel>.from(
resp.data?.map((e) => SnChannel.fromJson(e)) ?? [],
);
@ -81,25 +54,18 @@ class ChatChannelProvider extends ChangeNotifier {
/// It will use the local storage as much as possible.
/// The alias should include the scope, formatted as `scope:alias`.
Future<SnChannel> getChannel(String key) async {
final local = await (_dt.db.snLocalChatChannel.select()
..where((e) => e.alias.equals(key)))
.getSingleOrNull();
if (local != null) {
final out = local.content;
if (out.realmId != null) {
return out.copyWith(realm: await _rels.getRealm(out.realmId!));
} else {
return out;
}
if (_channelBox != null) {
final local = _channelBox!.get(key);
if (local != null) return local;
}
var resp =
await _sn.client.get('/cgi/im/channels/${key.replaceAll(':', '/')}');
var resp = await _sn.client.get('/cgi/im/channels/$key');
var out = SnChannel.fromJson(resp.data);
// Preload realm of the channel
if (out.realmId != null) {
out = out.copyWith(realm: await _rels.getRealm(out.realmId!));
resp = await _sn.client.get('/cgi/id/realms/${out.realmId}');
out = out.copyWith(realm: SnRealm.fromJson(resp.data));
}
_saveChannelToLocal([out]);
@ -111,39 +77,42 @@ class ChatChannelProvider extends ChangeNotifier {
/// And the second time is when the data was fetched from the server.
/// But there is some exception that will only cause one of them to be emitted.
/// Like the local storage is broken or the server is down.
Stream<List<SnChannel>> fetchChannels(
{bool noRemote = false, bool noLocal = false}) async* {
if (!noLocal) {
final local = await (_dt.db.snLocalChatChannel.select()
..orderBy([
(e) =>
OrderingTerm(expression: e.createdAt, mode: OrderingMode.desc)
]))
.get();
final out = local.map((e) => e.content).toList();
for (var idx = 0; idx < out.length; idx++) {
final channel = out[idx];
if (channel.realmId != null) {
out[idx] = out[idx].copyWith(
realm: await _rels.getRealm(channel.realmId!),
);
}
}
yield out;
}
Stream<List<SnChannel>> fetchChannels() async* {
if (_channelBox != null) yield _channelBox!.values.toList();
var resp = await _sn.client.get('/cgi/id/realms/me/available');
final realms = List<SnRealm>.from(
resp.data?.map((e) => SnRealm.fromJson(e)) ?? [],
);
final realmMap = {
for (final realm in realms) realm.alias: realm,
};
final scopeToFetch = {'global', ...realms.map((e) => e.alias)};
if (noRemote) return;
final List<SnChannel> result = List.empty(growable: true);
final channels = await _fetchChannelsFromServer();
for (var idx = 0; idx < channels.length; idx++) {
final channel = channels[idx];
if (channel.realmId != null) {
channels[idx] = channels[idx].copyWith(
realm: await _rels.getRealm(channel.realmId!),
);
}
final directMessages = await _fetchChannelsFromServer(
scope: scopeToFetch.first,
direct: true,
);
result.addAll(directMessages);
final nonBelongsChannels = await _fetchChannelsFromServer(
scope: scopeToFetch.first,
direct: false,
);
result.addAll(nonBelongsChannels);
for (final scope in scopeToFetch.skip(1)) {
final channel = await _fetchChannelsFromServer(
scope: scope,
direct: false,
doNotSave: true,
);
final out = channel.map((ele) => ele.copyWith(realm: realmMap[scope]));
_saveChannelToLocal(out);
result.addAll(out);
}
result.addAll(channels);
yield result;
}
@ -151,79 +120,23 @@ class ChatChannelProvider extends ChangeNotifier {
Future<List<SnChatMessage>> getLastMessages(
Iterable<SnChannel> channels,
) async {
final result = List<Future<SnLocalChatMessageData?>>.empty(growable: true);
final result = List<SnChatMessage>.empty(growable: true);
for (final channel in channels) {
final out = (_dt.db.snLocalChatMessage.select()
..where((e) => e.channelId.equals(channel.id))
..orderBy([
(e) =>
OrderingTerm(expression: e.createdAt, mode: OrderingMode.desc)
])
..limit(1))
.getSingleOrNull();
result.add(out);
}
final out = (await Future.wait(result))
.where((e) => e != null)
.map((e) => e!.content)
.toList();
await _ud.listAccount(out.map((ele) => ele.sender.accountId).toSet());
return out;
}
Future<void> _saveMemberToLocal(Iterable<SnChannelMember> members) async {
final queries = members.map((ele) {
return _dt.db.snLocalChannelMember.insertOne(
SnLocalChannelMemberCompanion.insert(
id: Value(ele.id),
channelId: ele.channelId,
accountId: ele.accountId,
content: ele,
cacheExpiredAt: DateTime.now().add(const Duration(days: 7)),
),
onConflict: DoUpdate(
(_) => SnLocalChannelMemberCompanion.custom(
content: Constant(jsonEncode(ele.toJson())),
cacheExpiredAt:
Constant(DateTime.now().add(const Duration(days: 7))),
),
),
final channelBox = await Hive.openBox<SnChatMessage>(
'${ChatMessageController.kChatMessageBoxPrefix}${channel.id}',
);
});
await Future.wait(queries);
}
Future<void> removeLocalChannel(SnChannel channel) async {
await _dt.db.transaction(() async {
await (_dt.db.snLocalChannelMember.delete()
..where((e) => e.channelId.equals(channel.id)))
.go();
await (_dt.db.snLocalChatChannel.delete()
..where((e) => e.id.equals(channel.id)))
.go();
await (_dt.db.snLocalChatMessage.delete()
..where((e) => e.channelId.equals(channel.id)))
.go();
});
}
Future<void> updateChannelProfile(SnChannelMember member) {
return _saveMemberToLocal([member]);
}
Future<SnChannelMember> getChannelProfile(SnChannel channel) async {
if (_ua.user == null) throw Exception('User not logged in');
final local = await (_dt.db.snLocalChannelMember.select()
..where((e) => e.channelId.equals(channel.id))
..where((e) => e.accountId.equals(_ua.user!.id)))
.getSingleOrNull();
if (local != null) {
return local.content;
final lastMessage =
channelBox.isNotEmpty ? channelBox.values.reduce((a, b) => a.createdAt.isAfter(b.createdAt) ? a : b) : null;
if (lastMessage != null) result.add(lastMessage);
channelBox.close();
}
await _ud.listAccount(result.map((ele) => ele.sender.accountId).toSet());
return result;
}
final resp = await _sn.client.get('/cgi/im/channels/${channel.keyPath}/me');
final out = SnChannelMember.fromJson(resp.data);
_saveMemberToLocal([out]);
return out;
@override
void dispose() {
_channelBox?.close();
super.dispose();
}
}

View File

@ -0,0 +1,459 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:livekit_client/livekit_client.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:provider/provider.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/types/chat.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
class ChatCallProvider extends ChangeNotifier {
late final SnNetworkProvider _sn;
ChatCallProvider(BuildContext context) {
_sn = context.read<SnNetworkProvider>();
}
SnChatCall? _current;
SnChannel? _channel;
bool _isReady = false;
bool _isMounted = false;
bool _isInitialized = false;
bool _isBusy = false;
String _lastDuration = '00:00:00';
Timer? _lastDurationUpdateTimer;
String? token;
String? endpoint;
StreamSubscription? hwSubscription;
List<MediaDevice> _audioInputs = [];
List<MediaDevice> _videoInputs = [];
bool _enableAudio = true;
bool _enableVideo = false;
LocalAudioTrack? _audioTrack;
LocalVideoTrack? _videoTrack;
MediaDevice? _videoDevice;
MediaDevice? _audioDevice;
late Room _room;
late EventsListener<RoomEvent> _listener;
List<ParticipantTrack> _participantTracks = [];
ParticipantTrack? _focusTrack;
// Getters for private fields
SnChatCall? get current => _current;
SnChannel? get channel => _channel;
bool get isReady => _isReady;
bool get isMounted => _isMounted;
bool get isInitialized => _isInitialized;
bool get isBusy => _isBusy;
String get lastDuration => _lastDuration;
List<MediaDevice> get audioInputs => _audioInputs;
List<MediaDevice> get videoInputs => _videoInputs;
bool get enableAudio => _enableAudio;
bool get enableVideo => _enableVideo;
LocalAudioTrack? get audioTrack => _audioTrack;
LocalVideoTrack? get videoTrack => _videoTrack;
MediaDevice? get videoDevice => _videoDevice;
MediaDevice? get audioDevice => _audioDevice;
List<ParticipantTrack> get participantTracks => _participantTracks;
ParticipantTrack? get focusTrack => _focusTrack;
Room get room => _room;
void _updateDuration() {
if (_current == null) {
_lastDuration = '00:00:00';
} else {
Duration duration = DateTime.now().difference(_current!.createdAt);
String twoDigits(int n) => n.toString().padLeft(2, '0');
_lastDuration = '${twoDigits(duration.inHours)}:'
'${twoDigits(duration.inMinutes.remainder(60))}:'
'${twoDigits(duration.inSeconds.remainder(60))}';
}
notifyListeners();
}
void enableDurationUpdater() {
_updateDuration();
_lastDurationUpdateTimer = Timer.periodic(
const Duration(seconds: 1),
(_) => _updateDuration(),
);
}
void disableDurationUpdater() {
_lastDurationUpdateTimer?.cancel();
_lastDurationUpdateTimer = null;
}
Future<void> checkPermissions() async {
if (lkPlatformIs(PlatformType.macOS) || lkPlatformIs(PlatformType.linux)) {
return;
}
await Permission.camera.request();
await Permission.microphone.request();
await Permission.bluetooth.request();
await Permission.bluetoothConnect.request();
}
void setCall(SnChatCall call, SnChannel related) {
_current = call;
_channel = related;
notifyListeners();
}
Future<(String, String)> getRoomToken() async {
final resp = await _sn.client.post(
'/cgi/im/channels/${_channel!.keyPath}/calls/ongoing/token',
);
token = resp.data['token'];
endpoint = 'wss://${resp.data['endpoint']}';
return (token!, endpoint!);
}
void initHardware() {
if (_isReady) return;
_isReady = true;
hwSubscription = Hardware.instance.onDeviceChange.stream.listen(
_revertDevices,
);
Hardware.instance.enumerateDevices().then(_revertDevices);
notifyListeners();
}
void initRoom() {
initHardware();
_room = Room(
roomOptions: const RoomOptions(
dynacast: true,
adaptiveStream: true,
defaultAudioPublishOptions: AudioPublishOptions(
name: 'call_voice',
stream: 'call_stream',
),
defaultVideoPublishOptions: VideoPublishOptions(
name: 'call_video',
stream: 'call_stream',
simulcast: true,
backupVideoCodec: BackupVideoCodec(enabled: true),
),
defaultScreenShareCaptureOptions: ScreenShareCaptureOptions(
useiOSBroadcastExtension: true,
params: VideoParametersPresets.screenShareH1080FPS30,
),
defaultCameraCaptureOptions: CameraCaptureOptions(
maxFrameRate: 30,
params: VideoParametersPresets.h1080_169,
),
),
);
_listener = _room.createListener();
WakelockPlus.enable();
}
Future<void> joinRoom(String url, String token) async {
if (_isMounted) return;
try {
await _room.connect(
url,
token,
fastConnectOptions: FastConnectOptions(
microphone: TrackOption(track: _audioTrack),
camera: TrackOption(track: _videoTrack),
),
);
} finally {
_isMounted = true;
notifyListeners();
}
}
void setupRoom() {
if (isInitialized) return;
sortParticipants();
_room.addListener(_onRoomDidUpdate);
WidgetsBindingCompatible.instance?.addPostFrameCallback(
(_) => autoPublish(),
);
if (lkPlatformIsMobile()) {
Hardware.instance.setSpeakerphoneOn(true);
}
_isBusy = false;
_isInitialized = true;
notifyListeners();
}
void autoPublish() async {
try {
if (enableVideo) {
await _room.localParticipant?.setCameraEnabled(true);
}
if (enableAudio) {
await _room.localParticipant?.setMicrophoneEnabled(true);
}
} catch (error) {
rethrow;
}
}
Future<void> setEnableAudio(bool value) async {
_enableAudio = value;
if (!_enableAudio) {
await _audioTrack?.stop();
_audioTrack = null;
} else {
await _changeLocalAudioTrack();
}
notifyListeners();
}
Future<void> setEnableVideo(bool value) async {
_enableVideo = value;
if (!_enableVideo) {
await _videoTrack?.stop();
_videoTrack = null;
} else {
await _changeLocalVideoTrack();
}
notifyListeners();
}
void setupRoomListeners({
required Function(DisconnectReason?) onDisconnected,
}) {
_listener
..on<RoomDisconnectedEvent>((event) async {
onDisconnected(event.reason);
})
..on<ParticipantEvent>((event) => sortParticipants())
..on<LocalTrackPublishedEvent>((_) => sortParticipants())
..on<LocalTrackUnpublishedEvent>((_) => sortParticipants())
..on<TrackSubscribedEvent>((_) => sortParticipants())
..on<TrackUnsubscribedEvent>((_) => sortParticipants())
..on<ParticipantNameUpdatedEvent>((event) {
sortParticipants();
});
}
void sortParticipants() {
Map<String, ParticipantTrack> mediaTracks = {};
for (var participant in _room.remoteParticipants.values) {
mediaTracks[participant.sid] = ParticipantTrack(
participant: participant,
videoTrack: null,
isScreenShare: false,
);
for (var t in participant.videoTrackPublications) {
mediaTracks[participant.sid]?.videoTrack = t.track;
mediaTracks[participant.sid]?.isScreenShare = t.isScreenShare;
}
}
final newTracks = List<ParticipantTrack>.empty(growable: true);
final mediaTrackList = mediaTracks.values.toList();
mediaTrackList.sort((a, b) {
// Loudest people first
if (a.participant.isSpeaking && b.participant.isSpeaking) {
if (a.participant.audioLevel > b.participant.audioLevel) {
return -1;
} else {
return 1;
}
}
// Last spoke first
final aSpokeAt = a.participant.lastSpokeAt?.millisecondsSinceEpoch ?? 0;
final bSpokeAt = b.participant.lastSpokeAt?.millisecondsSinceEpoch ?? 0;
if (aSpokeAt != bSpokeAt) {
return aSpokeAt > bSpokeAt ? -1 : 1;
}
// Has video first
if (a.participant.hasVideo != b.participant.hasVideo) {
return a.participant.hasVideo ? -1 : 1;
}
// First joined people first
return a.participant.joinedAt.millisecondsSinceEpoch -
b.participant.joinedAt.millisecondsSinceEpoch;
});
newTracks.addAll(mediaTrackList);
if (_room.localParticipant != null) {
ParticipantTrack localTrack = ParticipantTrack(
participant: _room.localParticipant!,
videoTrack: null,
isScreenShare: false,
);
final localParticipantTracks =
_room.localParticipant?.videoTrackPublications;
if (localParticipantTracks != null) {
for (var t in localParticipantTracks) {
localTrack.videoTrack = t.track;
localTrack.isScreenShare = t.isScreenShare;
}
}
newTracks.add(localTrack);
}
_participantTracks = newTracks;
if (focusTrack != null) {
final idx = participantTracks
.indexWhere((x) => x.participant.sid == _focusTrack!.participant.sid);
if (idx == -1) {
_focusTrack = null;
}
}
if (focusTrack == null) {
_focusTrack = participantTracks.firstOrNull;
} else {
final idx = participantTracks.indexWhere(
(x) => _focusTrack!.participant.sid == x.participant.sid,
);
if (idx > -1) {
_focusTrack = participantTracks[idx];
}
}
notifyListeners();
}
Future<void> _changeLocalAudioTrack() async {
if (_audioTrack != null) {
await _audioTrack!.stop();
_audioTrack = null;
}
if (_audioDevice != null) {
_audioTrack = await LocalAudioTrack.create(
AudioCaptureOptions(deviceId: _audioDevice!.deviceId),
);
await _audioTrack!.start();
}
notifyListeners();
}
Future<void> _changeLocalVideoTrack() async {
if (_videoTrack != null) {
await _videoTrack!.stop();
_videoTrack = null;
}
if (_videoDevice != null) {
_videoTrack = await LocalVideoTrack.createCameraTrack(
CameraCaptureOptions(
deviceId: _videoDevice!.deviceId,
params: VideoParametersPresets.h1080_169,
),
);
await _videoTrack!.start();
}
notifyListeners();
}
void _revertDevices(List<MediaDevice> devices) {
_audioInputs = devices.where((d) => d.kind == 'audioinput').toList();
_videoInputs = devices.where((d) => d.kind == 'videoinput').toList();
notifyListeners();
}
void _onRoomDidUpdate() => sortParticipants();
Future<void> changeLocalAudioTrack() async {
if (audioTrack != null) {
await audioTrack!.stop();
_audioTrack = null;
}
if (audioDevice != null) {
_audioTrack = await LocalAudioTrack.create(
AudioCaptureOptions(
deviceId: audioDevice!.deviceId,
),
);
await audioTrack!.start();
}
}
Future<void> changeLocalVideoTrack() async {
if (videoTrack != null) {
await _videoTrack!.stop();
_videoTrack = null;
}
if (videoDevice != null) {
_videoTrack = await LocalVideoTrack.createCameraTrack(
CameraCaptureOptions(
deviceId: videoDevice!.deviceId,
params: VideoParametersPresets.h1080_169,
),
);
await videoTrack!.start();
}
}
void deactivateHardware() {
hwSubscription?.cancel();
}
void disposeRoom() {
_isBusy = false;
_isMounted = false;
_isInitialized = false;
_current = null;
_channel = null;
_room.removeListener(_onRoomDidUpdate);
_room.disconnect();
_room.dispose();
_listener.dispose();
WakelockPlus.disable();
}
void disposeHardware() {
_isReady = false;
_audioTrack?.stop();
_audioTrack = null;
_videoTrack?.stop();
_videoTrack = null;
}
void setVideoDevice(MediaDevice? value) {
_videoDevice = value;
notifyListeners();
}
void setAudioDevice(MediaDevice? value) {
_audioDevice = value;
notifyListeners();
}
void setFocusTrack(ParticipantTrack? value) {
_focusTrack = value;
notifyListeners();
}
void setIsBusy(bool value) {
_isBusy = value;
notifyListeners();
}
}

View File

@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:responsive_framework/responsive_framework.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:surface/providers/widget.dart';
@ -13,17 +12,6 @@ const kNetworkServerStoreKey = 'app_server_url';
const kAppbarTransparentStoreKey = 'app_bar_transparent';
const kAppBackgroundStoreKey = 'app_has_background';
const kAppColorSchemeStoreKey = 'app_color_scheme';
const kAppNotifyWithHaptic = 'app_notify_with_haptic';
const kAppExpandPostLink = 'app_expand_post_link';
const kAppExpandChatLink = 'app_expand_chat_link';
const kAppRealmCompactView = 'app_realm_compact_view';
const kAppCustomFonts = 'app_custom_fonts';
const kAppMixedFeed = 'app_mixed_feed';
const kAppAutoTranslate = 'app_auto_translate';
const kAppHideBottomNav = 'app_hide_bottom_nav';
const kAppSoundEffects = 'app_sound_effects';
const kAppAprilFoolFeatures = 'app_april_fool_features';
const kAppWindowSize = 'app_window_size';
const Map<String, FilterQuality> kImageQualityLevel = {
'settingsImageQualityLowest': FilterQuality.none,
@ -45,98 +33,23 @@ class ConfigProvider extends ChangeNotifier {
prefs = await SharedPreferences.getInstance();
}
bool drawerIsCollapsed = false;
void calcDrawerSize(BuildContext context, {bool withMediaQuery = false}) {
bool newDrawerIsCollapsed = false;
if (withMediaQuery) {
newDrawerIsCollapsed = MediaQuery.of(context).size.width < 600;
} else {
final rpb = ResponsiveBreakpoints.of(context);
newDrawerIsCollapsed = rpb.smallerOrEqualTo(MOBILE);
}
if (newDrawerIsCollapsed != drawerIsCollapsed) {
drawerIsCollapsed = newDrawerIsCollapsed;
notifyListeners();
}
}
FilterQuality get imageQuality {
return kImageQualityLevel.values
.elementAtOrNull(prefs.getInt('app_image_quality') ?? 3) ??
FilterQuality.high;
return kImageQualityLevel.values.elementAtOrNull(prefs.getInt('app_image_quality') ?? 3) ?? FilterQuality.high;
}
String get serverUrl {
return prefs.getString(kNetworkServerStoreKey) ?? kNetworkServerDefault;
}
bool get realmCompactView {
return prefs.getBool(kAppRealmCompactView) ?? false;
}
bool get mixedFeed {
return prefs.getBool(kAppMixedFeed) ?? true;
}
bool get autoTranslate {
return prefs.getBool(kAppAutoTranslate) ?? false;
}
bool get hideBottomNav {
return prefs.getBool(kAppHideBottomNav) ?? false;
}
bool get aprilFoolFeatures {
return prefs.getBool(kAppAprilFoolFeatures) ?? true;
}
bool get soundEffects {
return prefs.getBool(kAppSoundEffects) ?? true;
}
set soundEffects(bool value) {
prefs.setBool(kAppSoundEffects, value);
notifyListeners();
}
set aprilFoolFeatures(bool value) {
prefs.setBool(kAppAprilFoolFeatures, value);
notifyListeners();
}
set hideBottomNav(bool value) {
prefs.setBool(kAppHideBottomNav, value);
notifyListeners();
}
set autoTranslate(bool value) {
prefs.setBool(kAppAutoTranslate, value);
notifyListeners();
}
set mixedFeed(bool value) {
prefs.setBool(kAppMixedFeed, value);
notifyListeners();
}
set realmCompactView(bool value) {
prefs.setBool(kAppRealmCompactView, value);
notifyListeners();
}
set serverUrl(String url) {
prefs.setString(kNetworkServerStoreKey, url);
_home.saveWidgetData("nex_server_url", url);
}
String? updatableVersion;
String? updatableChangelog;
void setUpdate(String newVersion, String newChangelog) {
void setUpdate(String newVersion) {
updatableVersion = newVersion;
updatableChangelog = newChangelog;
notifyListeners();
}
}

View File

@ -1,31 +0,0 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:path/path.dart' show join;
import 'package:path_provider/path_provider.dart';
import 'package:surface/database/database.dart';
class DatabaseProvider {
late AppDatabase db;
DatabaseProvider(BuildContext context) {
db = AppDatabase();
}
Future<int> getDatabaseSize() async {
if (kIsWeb) return 0;
final basepath = await getApplicationSupportDirectory();
return await File(join(basepath.path, 'solar_network_data.sqlite'))
.length();
}
Future<void> removeDatabase() async {
if (kIsWeb) return;
final basepath = await getApplicationSupportDirectory();
final file = File(join(basepath.path, 'solar_network_data.sqlite'));
db.close();
await file.delete();
db = AppDatabase();
}
}

View File

@ -1,245 +0,0 @@
import 'dart:async';
import 'dart:convert';
import 'package:drift/drift.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:surface/database/database.dart';
import 'package:surface/logger.dart';
import 'package:surface/providers/database.dart';
import 'package:surface/providers/userinfo.dart';
import 'package:surface/providers/websocket.dart';
import 'package:surface/types/keypair.dart';
import 'package:fast_rsa/fast_rsa.dart';
import 'package:surface/types/websocket.dart';
import 'package:uuid/uuid.dart';
// Currently the keypair only provide RSA encryption
// Supported by the `fast_rsa` package
class KeyPairProvider {
late final DatabaseProvider _dt;
late final UserProvider _ua;
late final WebSocketProvider _ws;
SnKeyPair? activeKp;
KeyPairProvider(BuildContext context) {
_dt = context.read<DatabaseProvider>();
_ua = context.read<UserProvider>();
_ws = context.read<WebSocketProvider>();
}
void listen() {
_ws.pk.stream.listen((event) {
switch (event.method) {
case 'kex.ack':
ackKeyExchange(event);
break;
case 'kex.ask':
replyAskKeyExchange(event);
break;
}
});
}
Future<String> decryptText(String text, String kpId, {int? kpOwner}) async {
String? publicKey;
final kp = await (_dt.db.snLocalKeyPair.select()
..where((e) => e.id.equals(kpId)))
.getSingleOrNull();
if (kp == null) {
if (kpOwner != null) {
final out = await askKeyExchange(kpOwner, kpId);
publicKey = out.publicKey;
}
} else {
publicKey = kp.publicKey;
}
if (publicKey == null) {
throw Exception('Key pair not found');
}
return await RSA.decryptPKCS1v15(text, publicKey);
}
Future<String> encryptText(String text) async {
if (activeKp == null) throw Exception('No active key pair');
return await RSA.encryptPKCS1v15(text, activeKp!.privateKey!);
}
final Map<String, Completer<SnKeyPair>> _requests = {};
Future<SnKeyPair> askKeyExchange(int kpOwner, String kpId) async {
if (_requests.containsKey(kpId)) return await _requests[kpId]!.future;
final completer = Completer<SnKeyPair>();
_requests[kpId] = completer;
_ws.conn?.sink.add(
jsonEncode(WebSocketPackage(
method: 'kex.ask',
endpoint: 'id',
payload: {
'keypair_id': kpId,
'user_id': kpOwner,
},
)),
);
return Future.any([
_requests[kpId]!.future,
Future.delayed(const Duration(seconds: 60), () {
_requests.remove(kpId);
throw TimeoutException("Key exchange timed out");
}),
]);
}
Future<void> ackKeyExchange(WebSocketPackage pkt) async {
if (pkt.payload == null) return;
final kpMeta = SnKeyPair(
id: pkt.payload!['keypair_id'] as String,
accountId: pkt.payload!['user_id'] as int,
publicKey: pkt.payload!['public_key'] as String,
privateKey: pkt.payload?['private_key'] as String?,
);
if (_requests.containsKey(kpMeta.id)) {
_requests[kpMeta.id]!.complete(kpMeta);
_requests.remove(kpMeta.id);
}
// Save the keypair to the local database
await _dt.db.snLocalKeyPair.insertOne(
SnLocalKeyPairCompanion.insert(
id: kpMeta.id,
accountId: kpMeta.accountId,
publicKey: kpMeta.publicKey,
privateKey: Value(kpMeta.privateKey),
),
onConflict: DoNothing(),
);
}
Future<void> replyAskKeyExchange(WebSocketPackage pkt) async {
final kpId = pkt.payload!['keypair_id'] as String;
final userId = pkt.payload!['user_id'] as int;
final clientId = pkt.payload!['client_id'] as String;
final localKp = await (_dt.db.snLocalKeyPair.select()
..where((e) => e.id.equals(kpId))
..limit(1))
.getSingleOrNull();
if (localKp == null) return;
logging.info(
'[Kex] Reply to key exchange request of $kpId from user $userId',
);
// We do not give the private key to the client
_ws.conn?.sink.add(jsonEncode(
WebSocketPackage(
method: 'kex.ack',
endpoint: 'id',
payload: {
'keypair_id': localKp.id,
'user_id': localKp.accountId,
'public_key': localKp.publicKey,
'client_id': clientId,
},
).toJson(),
));
}
Future<SnKeyPair?> reloadActive({bool autoEnroll = true}) async {
final kp = await (_dt.db.snLocalKeyPair.select()
..where((e) => e.accountId.equals(_ua.user?.id ?? 0))
..where((e) => e.privateKey.isNotNull())
..where((e) => e.isActive.equals(true))
..limit(1))
.getSingleOrNull();
if (kp != null) {
activeKp = SnKeyPair(
id: kp.id,
accountId: kp.accountId,
publicKey: kp.publicKey,
privateKey: kp.privateKey,
);
}
if (kp == null && autoEnroll) {
return await enrollNew();
}
return activeKp;
}
Future<List<SnKeyPair>> listKeyPair() async {
final kps = await (_dt.db.snLocalKeyPair.select()).get();
return kps
.map((e) => SnKeyPair(
id: e.id,
accountId: e.accountId,
publicKey: e.publicKey,
privateKey: e.privateKey,
isActive: e.isActive,
))
.toList();
}
Future<void> activeKeyPair(String kpId) async {
final kp = await (_dt.db.snLocalKeyPair.select()
..where((e) => e.id.equals(kpId))
..where((e) => e.privateKey.isNotNull())
..limit(1))
.getSingleOrNull();
if (kp == null) return;
await _dt.db.transaction(() async {
await (_dt.db.update(_dt.db.snLocalKeyPair)
..where((e) => e.isActive.equals(true)))
.write(SnLocalKeyPairCompanion(isActive: Value(false)));
await (_dt.db.update(_dt.db.snLocalKeyPair)
..where((e) => e.id.equals(kp.id)))
.write(SnLocalKeyPairCompanion(isActive: Value(true)));
});
}
Future<SnKeyPair> enrollNew() async {
if (!_ua.isAuthorized) throw Exception('Unauthorized');
final id = const Uuid().v4();
final kp = await RSA.generate(2048);
final kpMeta = SnKeyPair(
id: id,
accountId: _ua.user!.id,
// This is work as expected
// We need to share private key to let everyone can decode the message
publicKey: kp.privateKey,
privateKey: kp.publicKey,
);
// Save the keypair to the local database
// If there is already one with private key, it will be overwritten
await _dt.db.transaction(() async {
await (_dt.db.update(_dt.db.snLocalKeyPair)
..where((e) => e.isActive.equals(true)))
.write(SnLocalKeyPairCompanion(isActive: Value(false)));
await _dt.db.snLocalKeyPair.insertOne(
SnLocalKeyPairCompanion.insert(
id: kpMeta.id,
accountId: kpMeta.accountId,
publicKey: kpMeta.publicKey,
privateKey: Value(kpMeta.privateKey),
isActive: Value(true),
),
);
});
await reloadActive(autoEnroll: false);
return kpMeta;
}
}

View File

@ -1,8 +1,8 @@
import 'dart:convert';
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:surface/logger.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/types/link.dart';
@ -20,7 +20,7 @@ class SnLinkPreviewProvider {
final target = b64.encode(url);
if (_cache.containsKey(target)) return _cache[target];
logging.debug('[LinkPreview] Fetching $url ($target)');
log('[LinkPreview] Fetching $url ($target)');
try {
final resp = await _sn.client.get('/cgi/re/link/$target');
@ -28,7 +28,7 @@ class SnLinkPreviewProvider {
_cache[url] = meta;
return meta;
} catch (err) {
logging.warning('[LinkPreview] Failed to fetch $url ($target)...', err);
log('[LinkPreview] Failed to fetch $url ($target)...');
return null;
}
}

View File

@ -5,20 +5,6 @@ import 'package:go_router/go_router.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:shared_preferences/shared_preferences.dart';
class AppNavListItem {
final String title;
final String subtitle;
final String screen;
final IconData icon;
const AppNavListItem({
required this.title,
required this.subtitle,
required this.screen,
required this.icon,
});
}
class AppNavDestination {
final String label;
final String screen;
@ -38,10 +24,13 @@ class NavigationProvider extends ChangeNotifier {
int? get currentIndex => _currentIndex;
List<String> get showBottomNavScreen => destinations
.where((ele) => ele.isPinned)
.map((ele) => ele.screen)
.toList();
static const List<String> kShowBottomNavScreen = [
'home',
'explore',
'account',
'album',
'chat',
];
static const List<AppNavDestination> kAllDestination = [
AppNavDestination(
@ -59,22 +48,37 @@ class NavigationProvider extends ChangeNotifier {
screen: 'chat',
label: 'screenChat',
),
AppNavDestination(
icon: Icon(Symbols.account_circle, weight: 400, opticalSize: 20),
screen: 'account',
label: 'screenAccount',
),
AppNavDestination(
icon: Icon(Symbols.group, weight: 400, opticalSize: 20),
screen: 'realm',
label: 'screenRealm',
),
AppNavDestination(
icon: Icon(Symbols.settings, weight: 400, opticalSize: 20),
screen: 'settings',
label: 'screenSettings',
icon: Icon(Symbols.photo_library, weight: 400, opticalSize: 20),
screen: 'album',
label: 'screenAlbum',
),
AppNavDestination(
icon: Icon(Symbols.diversity_4, weight: 400, opticalSize: 20),
screen: 'friend',
label: 'screenFriend',
),
AppNavDestination(
icon: Icon(Symbols.notifications, weight: 400, opticalSize: 20),
screen: 'notification',
label: 'screenNotification',
),
];
static const List<String> kDefaultPinnedDestination = [
'home',
'explore',
'chat',
'realm',
'account',
];
List<AppNavDestination> destinations = [];

View File

@ -1,35 +1,21 @@
import 'dart:developer';
import 'dart:io';
import 'package:audioplayers/audioplayers.dart';
import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_udid/flutter_udid.dart';
import 'package:local_notifier/local_notifier.dart';
import 'package:provider/provider.dart';
import 'package:surface/logger.dart';
import 'package:surface/providers/config.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/userinfo.dart';
import 'package:surface/providers/websocket.dart';
import 'package:surface/types/notification.dart';
import 'package:tray_manager/tray_manager.dart';
class NotificationProvider extends ChangeNotifier {
late final SnNetworkProvider _sn;
late final UserProvider _ua;
late final WebSocketProvider _ws;
late final ConfigProvider _cfg;
final AudioPlayer _notifySoundPlayer = AudioPlayer(playerId: 'notify-sound');
NotificationProvider(BuildContext context) {
_sn = context.read<SnNetworkProvider>();
_ua = context.read<UserProvider>();
_ws = context.read<WebSocketProvider>();
_cfg = context.read<ConfigProvider>();
}
Future<void> registerPushNotifications() async {
@ -51,13 +37,11 @@ class NotificationProvider extends ChangeNotifier {
var deviceUuid = await FlutterUdid.consistentUdid;
if (deviceUuid.isEmpty) {
logging.warning(
'[Push Notification] Unable to active push notifications, couldn\'t get device uuid');
log("Unable to active push notifications, couldn't get device uuid");
return;
} else {
logging.info('[Push Notification] Device UUID is $deviceUuid');
logging
.info('[Push Notification] Registering device push notifications...');
log('Device UUID is $deviceUuid');
log('Registering device push notifications...');
}
if (Platform.isIOS || Platform.isMacOS) {
@ -67,111 +51,15 @@ class NotificationProvider extends ChangeNotifier {
provider = 'fcm';
token = await FirebaseMessaging.instance.getToken();
}
logging.info('[Push Notification] Device Push Token is $token');
log('Device Push Token is $token');
try {
await _sn.client.post(
'/cgi/id/notifications/subscription',
data: {
'provider': provider,
'device_token': token,
'device_id': deviceUuid
},
);
} catch (err) {
logging.error(
'[Push Notification] Unable to register push notifications: $err');
}
}
int showingCount = 0;
int showingTrayCount = 0;
List<SnNotification> notifications = List.empty(growable: true);
int? skippableNotifyChannel;
bool isMuted = false;
void listen() {
_ws.pk.stream.listen((event) {
if (event.method == 'notifications.new') {
final notification = SnNotification.fromJson(event.payload!);
final doHaptic = _cfg.prefs.getBool(kAppNotifyWithHaptic) ?? true;
if (doHaptic) HapticFeedback.mediumImpact();
// April fool notification sfx
if (_cfg.prefs.getBool(kAppAprilFoolFeatures) ?? true) {
final now = DateTime.now();
if (now.day == 1 && now.month == 4) {
_notifySoundPlayer.play(
AssetSource('audio/notify/metal-pipe.mp3'),
volume: 0.6,
ctx: AudioContext(
android: AudioContextAndroid(
contentType: AndroidContentType.sonification,
usageType: AndroidUsageType.notificationEvent,
),
iOS: AudioContextIOS(category: AVAudioSessionCategory.ambient),
),
mode: PlayerMode.lowLatency,
);
}
}
if (notification.topic == 'messaging.message' &&
skippableNotifyChannel != null) {
if (notification.metadata['channel_id'] != null &&
notification.metadata['channel_id'] == skippableNotifyChannel) {
return;
}
}
if (showingCount < 0) showingCount = 0;
showingCount++;
showingTrayCount++;
notifications.add(notification);
Future.delayed(const Duration(seconds: 5), () {
if (showingCount >= 0) showingCount--;
notifyListeners();
});
notifyListeners();
updateTray();
if (!kIsWeb && !isMuted) {
if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) {
LocalNotification notify = LocalNotification(
title: notification.title,
subtitle: notification.subtitle,
body: notification.body,
);
notify.onClick = () {
appWindow.show();
};
notify.show();
}
}
}
});
}
void clearTray() {
showingTrayCount = 0;
updateTray();
}
void updateTray() {
if (kIsWeb || Platform.isAndroid || Platform.isIOS) return;
if (showingTrayCount == 0) {
trayManager.setTitle('');
} else {
trayManager.setTitle(' $showingTrayCount');
}
}
void clear() {
showingCount = 0;
notifications.clear();
updateTray();
notifyListeners();
await _sn.client.post(
'/cgi/id/notifications/subscription',
data: {
'provider': provider,
'device_token': token,
'device_id': deviceUuid,
},
);
}
}

View File

@ -1,70 +1,83 @@
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:surface/providers/sn_attachment.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/user_directory.dart';
import 'package:surface/types/post.dart';
class SnPostContentProvider {
late final SnNetworkProvider _sn;
late final UserDirectoryProvider _ud;
late final SnAttachmentProvider _attach;
SnPostContentProvider(BuildContext context) {
_sn = context.read<SnNetworkProvider>();
_ud = context.read<UserDirectoryProvider>();
_attach = context.read<SnAttachmentProvider>();
}
Future<List<SnPost>> _preloadRelatedDataInBatch(List<SnPost> out) async {
Set<String> rids = {};
for (var i = 0; i < out.length; i++) {
rids.addAll(out[i].body['attachments']?.cast<String>() ?? []);
if (out[i].body['thumbnail'] != null) {
rids.add(out[i].body['thumbnail']);
}
if (out[i].repostTo != null) {
out[i] = out[i].copyWith(
repostTo: await _preloadRelatedDataSingle(out[i].repostTo!),
);
}
}
final attachments = await _attach.getMultiple(rids.toList());
for (var i = 0; i < out.length; i++) {
out[i] = out[i].copyWith(
preload: SnPostPreload(
thumbnail: attachments.where((ele) => ele?.rid == out[i].body['thumbnail']).firstOrNull,
attachments: attachments.where((ele) => out[i].body['attachments']?.contains(ele?.rid) ?? false).toList(),
),
);
}
await _ud.listAccount(
attachments.where((ele) => ele != null).map((ele) => ele!.accountId).toSet(),
);
return out;
}
Future<SnPost> _preloadRelatedDataSingle(SnPost out) async {
Set<String> rids = {};
rids.addAll(out.body['attachments']?.cast<String>() ?? []);
if (out.body['thumbnail'] != null) {
rids.add(out.body['thumbnail']);
}
if (out.repostTo != null) {
out = out.copyWith(
repostTo: await _preloadRelatedDataSingle(out.repostTo!),
);
}
final attachments = await _attach.getMultiple(rids.toList());
out = out.copyWith(
preload: SnPostPreload(
thumbnail: attachments.where((ele) => ele?.rid == out.body['thumbnail']).firstOrNull,
attachments: attachments.where((ele) => out.body['attachments']?.contains(ele?.rid) ?? false).toList(),
),
);
return out;
}
Future<List<SnPost>> listRecommendations() async {
final resp = await _sn.client.get(
'/cgi/co/recommendations',
options: Options(headers: {
'X-API-Version': '2',
}),
);
final resp = await _sn.client.get('/cgi/co/recommendations');
final out = _preloadRelatedDataInBatch(
List.from(resp.data.map((ele) => SnPost.fromJson(ele))),
);
return out;
}
Future<List<SnFeedEntry>> getFeed({int take = 20, DateTime? cursor}) async {
final resp = await _sn.client.get(
'/cgi/co/recommendations/feed',
queryParameters: {
'take': take,
if (cursor != null) 'cursor': cursor.toUtc().millisecondsSinceEpoch,
},
options: Options(headers: {'X-API-Version': '2'}),
);
final List<SnFeedEntry> out =
List.from(resp.data.map((ele) => SnFeedEntry.fromJson(ele)));
List<SnPost> posts = List.empty(growable: true);
for (var idx = 0; idx < out.length; idx++) {
final ele = out[idx];
if (ele.type == 'interactive.post') {
posts.add(SnPost.fromJson(ele.data));
}
}
posts = await _preloadRelatedDataInBatch(posts);
var postsIdx = 0;
for (var idx = 0; idx < out.length; idx++) {
final ele = out[idx];
if (ele.type == 'interactive.post') {
out[idx] = ele.copyWith(data: posts[postsIdx].toJson());
postsIdx++;
}
}
return out;
}
Future<(List<SnPost>, int)> listPosts({
int take = 10,
int offset = 0,
@ -72,30 +85,15 @@ class SnPostContentProvider {
String? author,
Iterable<String>? categories,
Iterable<String>? tags,
String? realm,
String? channel,
bool isDraft = false,
bool isShuffle = false,
}) async {
final resp = await _sn.client.get(
isShuffle
? '/cgi/co/recommendations/shuffle'
: '/cgi/co/posts${isDraft ? '/drafts' : ''}',
queryParameters: {
'take': take,
'offset': offset,
if (type != null) 'type': type,
if (author != null) 'author': author,
if (tags?.isNotEmpty ?? false) 'tags': tags!.join(','),
if (categories?.isNotEmpty ?? false)
'categories': categories!.join(','),
if (realm != null) 'realm': realm,
if (channel != null) 'channel': channel,
},
options: Options(headers: {
'X-API-Version': '2',
}),
);
final resp = await _sn.client.get('/cgi/co/posts', queryParameters: {
'take': take,
'offset': offset,
if (type != null) 'type': type,
if (author != null) 'author': author,
if (tags?.isNotEmpty ?? false) 'tags': tags!.join(','),
if (categories?.isNotEmpty ?? false) 'categories': categories!.join(','),
});
final List<SnPost> out = await _preloadRelatedDataInBatch(
List.from(resp.data['data']?.map((e) => SnPost.fromJson(e)) ?? []),
);
@ -108,16 +106,10 @@ class SnPostContentProvider {
int take = 10,
int offset = 0,
}) async {
final resp = await _sn.client.get(
'/cgi/co/posts/$parentId/replies',
queryParameters: {
'take': take,
'offset': offset,
},
options: Options(headers: {
'X-API-Version': '2',
}),
);
final resp = await _sn.client.get('/cgi/co/posts/$parentId/replies', queryParameters: {
'take': take,
'offset': offset,
});
final List<SnPost> out = await _preloadRelatedDataInBatch(
List.from(resp.data['data']?.map((e) => SnPost.fromJson(e)) ?? []),
);
@ -132,20 +124,13 @@ class SnPostContentProvider {
Iterable<String>? tags,
Iterable<String>? categories,
}) async {
final resp = await _sn.client.get(
'/cgi/co/posts/search',
queryParameters: {
'take': take,
'offset': offset,
'probe': searchTerm,
if (tags?.isNotEmpty ?? false) 'tags': tags!.join(','),
if (categories?.isNotEmpty ?? false)
'categories': categories!.join(','),
},
options: Options(headers: {
'X-API-Version': '2',
}),
);
final resp = await _sn.client.get('/cgi/co/posts/search', queryParameters: {
'take': take,
'offset': offset,
'probe': searchTerm,
if (tags?.isNotEmpty ?? false) 'tags': tags!.join(','),
if (categories?.isNotEmpty ?? false) 'categories': categories!.join(','),
});
final List<SnPost> out = await _preloadRelatedDataInBatch(
List.from(resp.data['data']?.map((e) => SnPost.fromJson(e)) ?? []),
);
@ -154,20 +139,10 @@ class SnPostContentProvider {
}
Future<SnPost> getPost(dynamic id) async {
final resp = await _sn.client.get(
'/cgi/co/posts/$id',
options: Options(headers: {
'X-API-Version': '2',
}),
);
final resp = await _sn.client.get('/cgi/co/posts/$id');
final out = _preloadRelatedDataSingle(
SnPost.fromJson(resp.data),
);
return out;
}
Future<SnPost> completePostData(SnPost post) async {
final out = await _preloadRelatedDataSingle(post);
return out;
}
}

View File

@ -1,14 +1,11 @@
import 'dart:collection';
import 'dart:convert';
import 'dart:math' as math;
import 'dart:typed_data';
import 'package:dio/dio.dart';
import 'package:drift/drift.dart';
import 'package:flutter/widgets.dart';
import 'package:cross_file/cross_file.dart';
import 'package:provider/provider.dart';
import 'package:surface/database/database.dart';
import 'package:surface/providers/database.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/types/attachment.dart';
@ -16,12 +13,10 @@ const kConcurrentUploadChunks = 5;
class SnAttachmentProvider {
late final SnNetworkProvider _sn;
late final DatabaseProvider _dt;
final Map<String, SnAttachment> _cache = {};
SnAttachmentProvider(BuildContext context) {
_sn = context.read<SnNetworkProvider>();
_dt = context.read<DatabaseProvider>();
}
void putCache(Iterable<SnAttachment> items, {bool noCheck = false}) {
@ -33,33 +28,20 @@ class SnAttachmentProvider {
}
Future<SnAttachment> getOne(String rid, {noCache = false}) async {
// In-memory cache
if (!noCache && _cache.containsKey(rid)) {
return _cache[rid]!;
}
// On-disk cache
final dbResp = await (_dt.db.snLocalAttachment.select()
..where((e) => e.rid.equals(rid))
..where((e) => e.cacheExpiredAt.isBiggerThanValue(DateTime.now())))
.getSingleOrNull();
if (dbResp != null) {
_cache[rid] = dbResp.content;
return dbResp.content;
}
// Remote server
final resp = await _sn.client.get('/cgi/uc/attachments/$rid/meta');
final out = SnAttachment.fromJson(resp.data);
if (out.isAnalyzed) {
_cache[rid] = out;
}
_saveToLocal([out]);
return out;
}
Future<List<SnAttachment?>> getMultiple(List<String> rids,
{bool noCache = false}) async {
// In-memory cache
Future<List<SnAttachment?>> getMultiple(List<String> rids, {noCache = false}) async {
final result = List<SnAttachment?>.filled(rids.length, null);
final Map<String, int> randomMapping = {};
for (int i = 0; i < rids.length; i++) {
@ -70,74 +52,32 @@ class SnAttachmentProvider {
result[i] = _cache[rid]!;
}
}
var pendingFetch = randomMapping.keys;
// On-disk cache
if (pendingFetch.isEmpty) return result;
if (!noCache) {
final dbResp = await (_dt.db.snLocalAttachment.select()
..where((e) => e.rid.isIn(pendingFetch))
..where((e) => e.cacheExpiredAt.isBiggerThanValue(DateTime.now())))
.get();
for (final item in dbResp) {
if (item.content.isAnalyzed) {
_cache[item.rid] = item.content;
final pendingFetch = randomMapping.keys;
if (pendingFetch.isNotEmpty) {
final resp = await _sn.client.get(
'/cgi/uc/attachments',
queryParameters: {
'take': pendingFetch.length,
'id': pendingFetch.join(','),
},
);
final List<SnAttachment?> out =
resp.data['data'].map((e) => e['id'] == 0 ? null : SnAttachment.fromJson(e)).cast<SnAttachment?>().toList();
for (final item in out) {
if (item == null) continue;
if (item.isAnalyzed) {
_cache[item.rid] = item;
}
result[randomMapping[item.rid]!] = item.content;
randomMapping.remove(item.rid);
result[randomMapping[item.rid]!] = item;
}
pendingFetch = randomMapping.keys;
}
// Remote server
if (pendingFetch.isEmpty) return result;
final resp = await _sn.client.get(
'/cgi/uc/attachments',
queryParameters: {
'take': pendingFetch.length,
'id': pendingFetch.join(','),
},
);
final List<SnAttachment?> out = resp.data['data']
.map((e) => e['id'] == 0 ? null : SnAttachment.fromJson(e))
.cast<SnAttachment?>()
.toList();
for (final item in out) {
if (item == null) continue;
if (item.isAnalyzed) {
_cache[item.rid] = item;
}
result[randomMapping[item.rid]!] = item;
}
_saveToLocal(out.where((ele) => ele != null).cast());
return result;
}
static Map<String, String> mimetypeOverrides = {
'mov': 'video/quicktime',
'mp4': 'video/mp4',
'm4a': 'audio/mp4',
'apng': 'image/apng',
'webp': 'image/webp',
};
Future<SnAttachment> createWithReferenceLink(
String url,
String pool,
Map<String, dynamic>? metadata, {
String? mimetype,
}) async {
final resp = await _sn.client.post(
'/cgi/uc/attachments/referenced',
data: {
'url': url,
'pool': pool,
'metadata': metadata,
if (mimetype != null) 'mimetype': mimetype,
},
);
return SnAttachment.fromJson(resp.data);
}
static Map<String, String> mimetypeOverrides = {'mov': 'video/quicktime', 'mp4': 'video/mp4'};
Future<SnAttachment> directUploadOne(
Uint8List data,
@ -149,11 +89,8 @@ class SnAttachmentProvider {
bool analyzeNow = false,
}) async {
final filePayload = MultipartFile.fromBytes(data, filename: filename);
final fileAlt = filename.contains('.')
? filename.substring(0, filename.lastIndexOf('.'))
: filename;
final fileExt =
filename.substring(filename.lastIndexOf('.') + 1).toLowerCase();
final fileAlt = filename.contains('.') ? filename.substring(0, filename.lastIndexOf('.')) : filename;
final fileExt = filename.substring(filename.lastIndexOf('.') + 1).toLowerCase();
String? mimetypeOverride;
if (mimetype != null) {
@ -190,11 +127,8 @@ class SnAttachmentProvider {
Map<String, dynamic>? metadata, {
String? mimetype,
}) async {
final fileAlt = filename.contains('.')
? filename.substring(0, filename.lastIndexOf('.'))
: filename;
final fileExt =
filename.substring(filename.lastIndexOf('.') + 1).toLowerCase();
final fileAlt = filename.contains('.') ? filename.substring(0, filename.lastIndexOf('.')) : filename;
final fileExt = filename.substring(filename.lastIndexOf('.') + 1).toLowerCase();
String? mimetypeOverride;
if (mimetype == null && mimetypeOverrides.keys.contains(fileExt)) {
@ -212,10 +146,7 @@ class SnAttachmentProvider {
if (mimetypeOverride != null) 'mimetype': mimetypeOverride,
});
return (
SnAttachmentFragment.fromJson(resp.data['meta']),
resp.data['chunk_size'] as int
);
return (SnAttachmentFragment.fromJson(resp.data['meta']), resp.data['chunk_size'] as int);
}
Future<dynamic> _chunkedUploadOnePart(
@ -266,10 +197,7 @@ class SnAttachmentProvider {
(entry.value + 1) * chunkSize,
await file.length(),
);
final data = Uint8List.fromList(await file
.openRead(beginCursor, endCursor)
.expand((chunk) => chunk)
.toList());
final data = Uint8List.fromList(await file.openRead(beginCursor, endCursor).expand((chunk) => chunk).toList());
final result = await _chunkedUploadOnePart(
data,
@ -325,48 +253,6 @@ class SnAttachmentProvider {
'metadata': metadata ?? item.usermeta,
'is_indexable': isIndexable ?? item.isIndexable,
});
final out = SnAttachment.fromJson(resp.data);
_saveToLocal([out]);
return out;
}
Future<SnAttachment> rateOne(
SnAttachment item, {
int? content,
int? quality,
}) async {
final resp = await _sn.client.put(
'/cgi/uc/attachments/${item.id}/rating',
data: {
'content_rating': content ?? item.contentRating,
'quality_rating': quality ?? item.qualityRating,
},
);
final out = SnAttachment.fromJson(resp.data);
_saveToLocal([out]);
return out;
}
Future<void> _saveToLocal(Iterable<SnAttachment> out) async {
for (final ele in out) {
if (!ele.isAnalyzed || ele.destination == 0) continue;
await _dt.db.snLocalAttachment.insertOne(
SnLocalAttachmentCompanion.insert(
id: Value(ele.id),
rid: ele.rid,
uuid: ele.uuid,
content: ele,
accountId: ele.accountId,
cacheExpiredAt: DateTime.now().add(const Duration(hours: 1)),
),
onConflict: DoUpdate(
(_) => SnLocalAttachmentCompanion.custom(
content: Constant(jsonEncode(ele.toJson())),
cacheExpiredAt:
Constant(DateTime.now().add(const Duration(hours: 1))),
),
),
);
}
return SnAttachment.fromJson(resp.data);
}
}

View File

@ -1,5 +1,6 @@
import 'dart:async';
import 'dart:convert';
import 'dart:developer';
import 'dart:io';
import 'package:dio/dio.dart';
@ -10,26 +11,9 @@ import 'package:package_info_plus/package_info_plus.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:surface/logger.dart';
import 'package:surface/providers/config.dart';
import 'package:surface/providers/widget.dart';
import 'package:synchronized/synchronized.dart';
import 'package:talker_dio_logger/talker_dio_logger_interceptor.dart';
import 'package:talker_dio_logger/talker_dio_logger_settings.dart';
enum ServiceStatus { operational, downgraded, failed }
const Map<String, String> kServicesName = {
'ai': 'Insights',
'co': 'Interactive',
're': 'Reader',
'im': 'Messaging',
'ma': 'Matrix',
'uc': 'Paperclip',
'wa': 'Wallet',
'id': 'Passport',
'pusher': 'Pusher',
};
const kNetworkServerDirectory = [
('Solar Network', 'https://api.sn.solsynth.dev'),
@ -52,19 +36,6 @@ class SnNetworkProvider {
client = Dio();
client.interceptors.add(
TalkerDioLogger(
talker: logging,
settings: const TalkerDioLoggerSettings(
printRequestHeaders: false,
printResponseHeaders: false,
printResponseMessage: false,
printResponseData: false,
printRequestData: false,
),
),
);
client.interceptors.add(RetryInterceptor(
dio: client,
retries: 3,
@ -98,6 +69,7 @@ class SnNetworkProvider {
_prefs = _config.prefs;
client.options.baseUrl = _config.serverUrl;
});
}
static Future<Dio> createOffContextClient() async {
@ -119,8 +91,7 @@ class SnNetworkProvider {
RequestOptions options,
RequestInterceptorHandler handler,
) async {
final atk = await _getFreshAtk(client, prefs.getString(kAtkStoreKey),
prefs.getString(kRtkStoreKey), (atk, rtk) {
final atk = await _getFreshAtk(client, prefs.getString(kAtkStoreKey), prefs.getString(kRtkStoreKey), (atk, rtk) {
prefs.setString(kAtkStoreKey, atk);
prefs.setString(kRtkStoreKey, rtk);
});
@ -132,8 +103,7 @@ class SnNetworkProvider {
},
),
);
client.options.baseUrl =
prefs.getString(kNetworkServerStoreKey) ?? kNetworkServerDefault;
client.options.baseUrl = prefs.getString(kNetworkServerStoreKey) ?? kNetworkServerDefault;
return client;
}
@ -149,8 +119,7 @@ class SnNetworkProvider {
platformInfo = 'Web; ${deviceInfo.vendor}';
} else if (Platform.isAndroid) {
final deviceInfo = await DeviceInfoPlugin().androidInfo;
platformInfo =
'Android; ${deviceInfo.brand} ${deviceInfo.model}; ${deviceInfo.id}';
platformInfo = 'Android; ${deviceInfo.brand} ${deviceInfo.model}; ${deviceInfo.id}';
} else if (Platform.isIOS) {
final deviceInfo = await DeviceInfoPlugin().iosInfo;
platformInfo = 'iOS; ${deviceInfo.model}; ${deviceInfo.name}';
@ -159,8 +128,7 @@ class SnNetworkProvider {
platformInfo = 'MacOS; ${deviceInfo.model}; ${deviceInfo.hostName}';
} else if (Platform.isWindows) {
final deviceInfo = await DeviceInfoPlugin().windowsInfo;
platformInfo =
'Windows NT; ${deviceInfo.productName}; ${deviceInfo.computerName}';
platformInfo = 'Windows NT; ${deviceInfo.productName}; ${deviceInfo.computerName}';
} else if (Platform.isLinux) {
final deviceInfo = await DeviceInfoPlugin().linuxInfo;
platformInfo = 'Linux; ${deviceInfo.prettyName}';
@ -180,15 +148,12 @@ class SnNetworkProvider {
final tkLock = Lock();
Future<String?> getFreshAtk() async {
return await _getFreshAtk(
client, _prefs.getString(kAtkStoreKey), _prefs.getString(kRtkStoreKey),
(atk, rtk) {
return await _getFreshAtk(client, _prefs.getString(kAtkStoreKey), _prefs.getString(kRtkStoreKey), (atk, rtk) {
setTokenPair(atk, rtk);
});
}
static Future<String?> _getFreshAtk(Dio client, String? atk, String? rtk,
Function(String atk, String rtk)? onRefresh) async {
static Future<String?> _getFreshAtk(Dio client, String? atk, String? rtk, Function(String atk, String rtk)? onRefresh) async {
if (_refreshCompleter != null) {
return await _refreshCompleter!.future;
} else {
@ -220,8 +185,7 @@ class SnNetworkProvider {
final payload = b64.decode(rawPayload);
final exp = jsonDecode(payload)['exp'];
if (exp <= DateTime.now().millisecondsSinceEpoch ~/ 1000) {
logging.debug(
'[Auth] Access token need refresh, doing it at ${DateTime.now()}');
log('Access token need refresh, doing it at ${DateTime.now()}');
final result = await _refreshToken(client.options.baseUrl, rtk);
if (result == null) {
atk = null;
@ -235,12 +199,12 @@ class SnNetworkProvider {
_refreshCompleter!.complete(atk);
return atk;
} else {
logging.error('[Auth] Access token refresh failed...');
log('Access token refresh failed...');
_refreshCompleter!.complete(null);
}
}
} catch (err) {
logging.error('[Auth] Failed to authenticate user...', err);
log('Failed to authenticate user: $err');
_refreshCompleter!.completeError(err);
} finally {
_refreshCompleter = null;
@ -249,11 +213,8 @@ class SnNetworkProvider {
return null;
}
String getAttachmentUrl(String ky, {bool preview = true}) {
String getAttachmentUrl(String ky) {
if (ky.startsWith("http")) return ky;
if (!preview) {
return '${client.options.baseUrl}/cgi/uc/attachments/$ky?preview=false';
}
return '${client.options.baseUrl}/cgi/uc/attachments/$ky';
}
@ -276,8 +237,7 @@ class SnNetworkProvider {
return result.$1;
}
static Future<(String, String)?> _refreshToken(
String baseUrl, String? rtk) async {
static Future<(String, String)?> _refreshToken(String baseUrl, String? rtk) async {
if (rtk == null) return null;
final dio = Dio();

View File

@ -1,90 +0,0 @@
import 'dart:convert';
import 'package:drift/drift.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:surface/database/database.dart';
import 'package:surface/providers/database.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/types/realm.dart';
class SnRealmProvider extends ChangeNotifier {
late final SnNetworkProvider _sn;
late final DatabaseProvider _dt;
SnRealmProvider(BuildContext context) {
_sn = context.read<SnNetworkProvider>();
_dt = context.read<DatabaseProvider>();
}
final Map<String, SnRealm> _cache = {};
List<SnRealm> _availableRealms = List.empty(growable: true);
Future<void> refreshAvailableRealms() async {
_availableRealms = await listAvailableRealms();
}
List<SnRealm> get availableRealms => _availableRealms;
Future<List<SnRealm>> listAvailableRealms() async {
final resp = await _sn.client.get('/cgi/id/realms/me/available');
final out = List<SnRealm>.from(
resp.data?.map((e) => SnRealm.fromJson(e)) ?? [],
);
for (final realm in out) {
_cache[realm.alias] = realm;
_cache[realm.id.toString()] = realm;
}
_saveToLocal(out);
return out;
}
void addAvailableRealm(SnRealm realm) {
_availableRealms.add(realm);
notifyListeners();
}
Future<SnRealm> getRealm(dynamic aliasOrId) async {
if (_cache.containsKey(aliasOrId.toString())) {
return _cache[aliasOrId.toString()]!;
}
final localResp = await (_dt.db.snLocalRealm.select()
..where((e) =>
e.id.equals(aliasOrId is int ? aliasOrId : 0) |
e.alias.equals(aliasOrId.toString()))
..where((e) => e.cacheExpiredAt.isBiggerThanValue(DateTime.now())))
.getSingleOrNull();
if (localResp != null) {
_cache[localResp.content.id.toString()] = localResp.content;
_cache[localResp.content.alias] = localResp.content;
return localResp.content;
}
final resp = await _sn.client.get('/cgi/id/realms/$aliasOrId');
final out = SnRealm.fromJson(resp.data);
_cache[out.alias] = out;
_cache[out.id.toString()] = out;
_saveToLocal([out]);
return out;
}
Future<void> _saveToLocal(Iterable<SnRealm> out) async {
for (final ele in out) {
await _dt.db.snLocalRealm.insertOne(
SnLocalRealmCompanion.insert(
id: Value(ele.id),
alias: ele.alias,
content: ele,
accountId: ele.accountId,
cacheExpiredAt: DateTime.now().add(const Duration(hours: 1)),
),
onConflict: DoUpdate(
(_) => SnLocalRealmCompanion.custom(
content: Constant(jsonEncode(ele.toJson())),
cacheExpiredAt:
Constant(DateTime.now().add(const Duration(hours: 1))),
),
),
);
}
}
}

View File

@ -1,132 +1,38 @@
import 'dart:convert';
import 'dart:developer';
import 'package:drift/drift.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:surface/database/database.dart';
import 'package:surface/logger.dart';
import 'package:surface/providers/database.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/types/attachment.dart';
class SnStickerProvider {
late final SnNetworkProvider _sn;
late final DatabaseProvider _dt;
final Map<String, SnSticker?> _cache = {};
final Map<int, List<SnSticker>> stickersByPack = {};
List<SnSticker> get stickers =>
_cache.values.where((ele) => ele != null).cast<SnSticker>().toList();
SnStickerProvider(BuildContext context) {
_sn = context.read<SnNetworkProvider>();
_dt = context.read<DatabaseProvider>();
}
bool hasNotSticker(String alias) {
return _cache.containsKey(alias) && _cache[alias] == null;
}
void _cacheSticker(SnSticker sticker) {
_cache['${sticker.pack.prefix}:${sticker.alias}'] = sticker;
if (stickersByPack[sticker.pack.id] == null) {
stickersByPack[sticker.pack.id] = List.empty(growable: true);
}
if (!stickersByPack[sticker.pack.id]!.contains(sticker)) {
stickersByPack[sticker.pack.id]!.add(sticker);
}
}
void putSticker(Iterable<SnSticker> stickers) {
for (final ele in stickers) {
_cacheSticker(ele);
}
_saveStickerToLocal(stickers);
_saveStickerPackToLocal(stickers.map((ele) => ele.pack).toSet());
}
Future<SnSticker?> lookupSticker(String alias) async {
// In-memory cache
if (_cache.containsKey(alias)) {
return _cache[alias];
}
// On-disk cache
final localStickers = await (_dt.db.snLocalSticker.select()
..where((e) => e.fullAlias.equals(alias)))
.getSingleOrNull();
if (localStickers != null) {
_cache[alias] = localStickers.content;
return localStickers.content;
}
// Remote server
try {
final resp = await _sn.client.get('/cgi/uc/stickers/lookup/$alias');
final sticker = SnSticker.fromJson(resp.data);
putSticker([sticker]);
_cache[alias] = sticker;
return sticker;
} catch (err) {
_cache[alias] = null;
logging.warning('[Sticker] Failed to lookup sticker $alias', err);
log('[Sticker] Failed to lookup sticker $alias: $err');
}
return null;
}
Future<void> listSticker() async {
final localPacks = await _dt.db.snLocalStickerPack.select().get();
final localStickers = await _dt.db.snLocalSticker.select().get();
final local = localStickers.map((ele) {
return ele.content.copyWith(
pack: localPacks
.firstWhere((pk) => pk.content.id == ele.content.packId)
.content,
);
});
for (final sticker in local) {
_cacheSticker(sticker);
}
try {
final resp = await _sn.client.get('/cgi/uc/stickers');
final data = resp.data;
final stickers = List.from(data).map((ele) => SnSticker.fromJson(ele));
for (final sticker in stickers) {
_cacheSticker(sticker);
}
} catch (err) {
logging.error('[Sticker] Failed to list stickers...', err);
rethrow;
}
}
Future<void> _saveStickerToLocal(Iterable<SnSticker> stickers) async {
await _dt.db.snLocalSticker.insertAll(
stickers.map(
(ele) => SnLocalStickerCompanion.insert(
id: Value(ele.id),
alias: ele.alias,
fullAlias: '${ele.pack.prefix}${ele.alias}',
content: ele,
createdAt: Value(ele.createdAt),
),
),
onConflict: DoNothing(),
);
}
Future<void> _saveStickerPackToLocal(Iterable<SnStickerPack> packs) async {
final queries = packs
.map(
(ele) => _dt.db.snLocalStickerPack.insertOne(
SnLocalStickerPackCompanion.insert(
id: Value(ele.id),
content: ele,
createdAt: Value(ele.createdAt),
),
onConflict: DoUpdate((_) => SnLocalStickerPackCompanion.custom(
content: Constant(jsonEncode(ele.toJson()))))),
)
.toList();
await Future.wait(queries);
}
}

View File

@ -13,16 +13,8 @@ class ThemeProvider extends ChangeNotifier {
});
}
void reloadTheme({
Color? seedColorOverride,
bool? useMaterial3,
String? customFonts,
}) {
createAppThemeSet(
seedColorOverride: seedColorOverride,
useMaterial3: useMaterial3,
customFonts: customFonts,
).then((value) {
void reloadTheme({Color? seedColorOverride, bool? useMaterial3}) {
createAppThemeSet(seedColorOverride: seedColorOverride, useMaterial3: useMaterial3).then((value) {
theme = value;
notifyListeners();
});

View File

@ -1,55 +0,0 @@
import 'dart:convert';
import 'package:crypto/crypto.dart';
import 'package:dio/dio.dart';
import 'package:surface/logger.dart';
const kTranslateApiBaseUrl = 'https://translate.solsynth.dev';
class SnTranslator {
final Dio client = Dio(
BaseOptions(
baseUrl: kTranslateApiBaseUrl,
connectTimeout: Duration(seconds: 3),
sendTimeout: Duration(seconds: 3),
receiveTimeout: Duration(seconds: 3),
),
);
final Map<String, String> _cache = {};
Future<String> translate(
String text, {
required String to,
String from = 'auto',
bool skipCache = false,
}) async {
if (text.isEmpty) return text;
final cacheKey = md5.convert(utf8.encode('$text$from$to')).toString();
if (!skipCache && _cache.containsKey(cacheKey)) {
return _cache[cacheKey]!;
}
logging.info('[Translator] Translate $text from $from to $to');
final resp = await client.post(
'/translate',
data: {
'q': text,
'source': from,
'target': to,
'format': 'text',
},
);
if (resp.statusCode == 200) {
final out = resp.data['translatedText'];
if (out.isNotEmpty) {
logging.info('[Translator] Translated $text from $from to $to');
_cache[cacheKey] = out;
return out;
}
}
throw Exception('translate failed: $resp');
}
}

View File

@ -1,115 +1,33 @@
import 'dart:convert';
import 'package:drift/drift.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:surface/database/database.dart';
import 'package:surface/providers/database.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/types/account.dart';
class UserDirectoryProvider {
late final SnNetworkProvider _sn;
late final DatabaseProvider _dt;
UserDirectoryProvider(BuildContext context) {
_sn = context.read<SnNetworkProvider>();
_dt = context.read<DatabaseProvider>();
}
final Map<String, int> _idCache = {};
final Map<int, SnAccount> _cache = {};
DateTime? _cacheExpiredAt;
Future<int> loadAccountCache({int max = 100}) async {
final out = await (_dt.db.snLocalAccount.select()..limit(max)).get();
for (final ele in out) {
_cache[ele.id] = ele.content;
_idCache[ele.name] = ele.id;
}
_cacheExpiredAt = DateTime.now().add(const Duration(hours: 1));
return out.length;
}
Future<List<SnAccount?>> listAccount(Iterable<dynamic> id) async {
// In-memory cache
if (_cacheExpiredAt != null && _cacheExpiredAt!.isBefore(DateTime.now())) {
_cache.clear();
_cacheExpiredAt = DateTime.now().add(const Duration(hours: 1));
} else {
_cacheExpiredAt ??= DateTime.now().add(const Duration(hours: 1));
}
final out = List<SnAccount?>.generate(id.length, (e) => null);
final plannedQuery = <int>{};
for (var idx = 0; idx < out.length; idx++) {
var item = id.elementAt(idx);
if (item is String && _idCache.containsKey(item)) {
item = _idCache[item];
}
if (_cache.containsKey(item)) {
out[idx] = _cache[item];
} else {
plannedQuery.add(item);
}
}
// On-disk cache
if (plannedQuery.isEmpty) return out;
final dbResp = await (_dt.db.snLocalAccount.select()
..where((e) => e.id.isIn(plannedQuery))
..where((e) => e.cacheExpiredAt.isBiggerThanValue(DateTime.now()))
..limit(plannedQuery.length))
.get();
for (var idx = 0; idx < out.length; idx++) {
if (out[idx] != null) continue;
if (dbResp.length <= idx) {
break;
}
out[idx] = dbResp[idx].content;
_cache[dbResp[idx].id] = dbResp[idx].content;
_idCache[dbResp[idx].name] = dbResp[idx].id;
plannedQuery.remove(dbResp[idx].id);
}
// Remote server
_saveToLocal(out.where((ele) => ele != null).cast());
if (plannedQuery.isEmpty) return out;
final resp = await _sn.client
.get('/cgi/id/users', queryParameters: {'id': plannedQuery.join(',')});
final respDecoded =
resp.data.map((e) => SnAccount.fromJson(e)).cast<SnAccount>().toList();
var sideIdx = 0;
for (var idx = 0; idx < out.length; idx++) {
if (out[idx] != null) continue;
if (respDecoded.length <= sideIdx) {
break;
}
out[idx] = respDecoded[sideIdx];
_cache[respDecoded[sideIdx].id] = out[idx]!;
_idCache[respDecoded[sideIdx].name] = respDecoded[sideIdx].id;
sideIdx++;
}
if (respDecoded.isNotEmpty) _saveToLocal(respDecoded);
final out = await Future.wait(
id.map((e) => getAccount(e)),
);
return out;
}
Future<SnAccount?> getAccount(dynamic id) async {
// In-memory cache
if (id is String && _idCache.containsKey(id)) {
id = _idCache[id];
}
if (_cache.containsKey(id)) {
return _cache[id];
}
// On-disk cache
final dbResp = await (_dt.db.snLocalAccount.select()
..where((e) => e.id.equals(id))
..where((e) => e.cacheExpiredAt.isBiggerThanValue(DateTime.now())))
.getSingleOrNull();
if (dbResp != null) {
_cache[dbResp.id] = dbResp.content;
_idCache[dbResp.name] = dbResp.id;
return dbResp.content;
}
// Remote server
try {
final resp = await _sn.client.get('/cgi/id/users/$id');
final account = SnAccount.fromJson(
@ -117,42 +35,16 @@ class UserDirectoryProvider {
);
_cache[account.id] = account;
if (id is String) _idCache[id] = account.id;
_saveToLocal([account]);
return account;
} catch (err) {
return null;
}
}
SnAccount? getFromCache(dynamic id) {
SnAccount? getAccountFromCache(dynamic id) {
if (id is String && _idCache.containsKey(id)) {
id = _idCache[id];
}
return _cache[id];
}
Future<void> _saveToLocal(Iterable<SnAccount> out) async {
// For better on conflict resolution
// And consider the method usually called with usually small amount of data
// Use for to insert each record instead of bulk insert
List<Future<int>> queries = out.map((ele) {
return _dt.db.snLocalAccount.insertOne(
SnLocalAccountCompanion.insert(
id: Value(ele.id),
name: ele.name,
content: ele,
cacheExpiredAt: DateTime.now().add(const Duration(hours: 1)),
),
onConflict: DoUpdate(
(_) => SnLocalAccountCompanion.custom(
name: Constant(ele.name),
content: Constant(jsonEncode(ele.toJson())),
cacheExpiredAt:
Constant(DateTime.now().add(const Duration(hours: 1))),
),
),
);
}).toList();
await Future.wait(queries);
}
}

View File

@ -1,9 +1,8 @@
import 'dart:convert';
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:surface/logger.dart';
import 'package:surface/providers/config.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/types/account.dart';
@ -31,40 +30,13 @@ class UserProvider extends ChangeNotifier {
notifyListeners();
refreshUser().then((value) async {
if (value != null) {
logging.info('[Auth] Logged in as @${value.name}');
logging.debug('[Auth] Access token: ${await atk}');
log('Logged in as @${value.name}');
log('Atk: ${await atk}');
}
});
}
Future<Map<String, dynamic>?> get atkClaims async {
final tk = (await atk);
if (tk == null) return null;
final atkParts = tk.split('.');
if (atkParts.length != 3) {
throw Exception('invalid format of access token');
}
var rawPayload = atkParts[1].replaceAll('-', '+').replaceAll('_', '/');
switch (rawPayload.length % 4) {
case 0:
break;
case 2:
rawPayload += '==';
break;
case 3:
rawPayload += '=';
break;
default:
throw Exception('illegal format of access token payload');
}
final b64 = utf8.fuse(base64Url);
return jsonDecode(b64.decode(rawPayload));
}
Future<SnAccount?> refreshUser() async {
if (!isAuthorized) return null;
final resp = await _sn.client.get('/cgi/id/users/me');
final out = SnAccount.fromJson(resp.data);
@ -76,22 +48,9 @@ class UserProvider extends ChangeNotifier {
}
void logoutUser() async {
atkClaims.then((value) async {
if (value != null) {
await _sn.client.delete('/cgi/id/users/me/tickets/${value['sed']}');
logging.info('[Auth] Current session has been destroyed.');
}
_sn.clearTokenPair();
});
_sn.clearTokenPair();
isAuthorized = false;
user = null;
notifyListeners();
}
void setLanguage(String? value) {
if (value == null) return;
if (user == null) return;
user = user!.copyWith(language: value);
notifyListeners();
}
}

View File

@ -1,15 +1,12 @@
import 'dart:async';
import 'dart:convert';
import 'dart:developer';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_udid/flutter_udid.dart';
import 'package:provider/provider.dart';
import 'package:surface/logger.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/userinfo.dart';
import 'package:surface/types/websocket.dart';
import 'package:web_socket_channel/io.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
class WebSocketProvider extends ChangeNotifier {
@ -21,8 +18,7 @@ class WebSocketProvider extends ChangeNotifier {
late final SnNetworkProvider _sn;
late final UserProvider _ua;
StreamController<WebSocketPackage> pk = StreamController.broadcast();
Stream<dynamic>? _wsStream;
StreamController<WebSocketPackage> stream = StreamController.broadcast();
WebSocketProvider(BuildContext context) {
_sn = context.read<SnNetworkProvider>();
@ -33,61 +29,39 @@ class WebSocketProvider extends ChangeNotifier {
if (isConnected) return;
if (!_ua.isAuthorized) return;
logging.debug('[WebSocket] Connecting to the server...');
log('[WebSocket] Connecting to the server...');
await connect();
}
Completer<void>? _connectCompleter;
Future<void> connect({noRetry = false}) async {
if (_connectCompleter != null) {
await _connectCompleter!.future;
return;
}
if (!_ua.isAuthorized) return;
if (isConnected || conn != null) {
disconnect();
}
final atk = await _sn.getFreshAtk();
final uri = Uri.parse(
'${_sn.client.options.baseUrl.replaceFirst('http', 'ws')}/ws?tk=$atk',
);
isBusy = true;
notifyListeners();
try {
_connectCompleter = Completer<void>();
final atk = await _sn.getFreshAtk();
final uri = Uri.parse(
kIsWeb
? '${_sn.client.options.baseUrl.replaceFirst('http', 'ws')}/ws?tk=$atk'
: '${_sn.client.options.baseUrl.replaceFirst('http', 'ws')}/ws?clientId=${await FlutterUdid.consistentUdid}tk=$atk',
);
isBusy = true;
notifyListeners();
conn = kIsWeb
? WebSocketChannel.connect(uri)
: IOWebSocketChannel.connect(
uri,
headers: {'Authorization': 'Bearer $atk'},
);
conn = WebSocketChannel.connect(uri);
await conn!.ready;
_wsStream = conn!.stream.asBroadcastStream();
listen();
logging.info('[WebSocket] Connected to server!');
log('[WebSocket] Connected to server!');
isConnected = true;
} catch (err) {
if (err is WebSocketChannelException) {
logging.error(
'[WebSocket] Failed to connect to websocket...',
err.inner,
);
log('Failed to connect to websocket: ${(err.inner as dynamic).message}');
} else {
logging.error('[WebSocket] Failed to connect to websocket...', err);
log('Failed to connect to websocket: $err');
}
if (!noRetry) {
logging.warning(
'[WebSocket] Retry connecting to websocket in 3 seconds...',
);
log('Retry connecting to websocket in 3 seconds...');
return Future.delayed(
const Duration(seconds: 3),
() => connect(noRetry: true),
@ -96,8 +70,6 @@ class WebSocketProvider extends ChangeNotifier {
} finally {
isBusy = false;
notifyListeners();
_connectCompleter!.complete();
_connectCompleter = null;
}
}
@ -111,14 +83,11 @@ class WebSocketProvider extends ChangeNotifier {
}
void listen() {
if (_wsStream == null) return;
_wsStream!.listen(
conn?.stream.listen(
(event) {
final packet = WebSocketPackage.fromJson(jsonDecode(event));
logging.debug(
'[Websocket] Incoming message: ${packet.method} ${packet.message}',
);
pk.sink.add(packet);
log('Websocket incoming message: ${packet.method} ${packet.message}');
stream.sink.add(packet);
},
onDone: () {
isConnected = false;

View File

@ -47,7 +47,6 @@ class HomeWidgetProvider {
}
Future<void> widgetUpdateRandomPost() async {
if (kIsWeb || (!Platform.isAndroid && !Platform.isIOS)) return;
final snc = await SnNetworkProvider.createOffContextClient();
final resp = await snc.get('/cgi/co/recommendations/shuffle?take=1');
final post = SnPost.fromJson(resp.data['data'][0]);

View File

@ -1,383 +1,318 @@
import 'package:animations/animations.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:surface/screens/abuse_report.dart';
import 'package:surface/screens/account.dart';
import 'package:surface/screens/account/punishments.dart';
import 'package:surface/screens/account/settings.dart';
import 'package:surface/screens/account/action_events.dart';
import 'package:surface/screens/account/badges.dart';
import 'package:surface/screens/account/contact_methods.dart';
import 'package:surface/screens/account/factor_settings.dart';
import 'package:surface/screens/account/keypairs.dart';
import 'package:surface/screens/account/prefs/notify.dart';
import 'package:surface/screens/account/prefs/security.dart';
import 'package:surface/screens/account/profile_page.dart';
import 'package:surface/screens/account/pfp.dart';
import 'package:surface/screens/account/profile_edit.dart';
import 'package:surface/screens/account/programs.dart';
import 'package:surface/screens/account/publishers/publisher_edit.dart';
import 'package:surface/screens/account/publishers/publisher_new.dart';
import 'package:surface/screens/account/publishers/publishers.dart';
import 'package:surface/screens/account/auth_tickets.dart';
import 'package:surface/screens/album.dart';
import 'package:surface/screens/auth/login.dart';
import 'package:surface/screens/auth/register.dart';
import 'package:surface/screens/chat.dart';
import 'package:surface/screens/chat/call_room.dart';
import 'package:surface/screens/chat/channel_detail.dart';
import 'package:surface/screens/chat/manage.dart';
import 'package:surface/screens/chat/room.dart';
import 'package:surface/screens/explore.dart';
import 'package:surface/screens/friend.dart';
import 'package:surface/screens/home.dart';
import 'package:surface/screens/logging.dart';
import 'package:surface/screens/feed/feed_detail.dart';
import 'package:surface/screens/notification.dart';
import 'package:surface/screens/post/post_detail.dart';
import 'package:surface/screens/post/post_draft.dart';
import 'package:surface/screens/post/post_editor.dart';
import 'package:surface/screens/post/post_shuffle.dart';
import 'package:surface/screens/post/publisher_page.dart';
import 'package:surface/screens/post/post_search.dart';
import 'package:surface/screens/realm.dart';
import 'package:surface/screens/realm/community.dart';
import 'package:surface/screens/realm/manage.dart';
import 'package:surface/screens/realm/realm_detail.dart';
import 'package:surface/screens/realm/realm_discovery.dart';
import 'package:surface/screens/settings.dart';
import 'package:surface/screens/sharing.dart';
import 'package:surface/screens/stickers.dart';
import 'package:surface/screens/stickers/pack_detail.dart';
import 'package:surface/screens/wallet.dart';
import 'package:surface/types/post.dart';
import 'package:surface/widgets/about.dart';
import 'package:surface/widgets/navigation/app_background.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
final _appRoutes = [
GoRoute(
path: '/',
name: 'home',
builder: (context, state) => const HomeScreen(),
),
GoRoute(
path: '/posts',
name: 'posts',
builder: (_, __) => const SizedBox.shrink(),
routes: [
GoRoute(
path: '/draft',
name: 'postDraftBox',
builder: (context, state) => const PostDraftBox(),
),
GoRoute(
path: '/write',
name: 'postEditor',
builder: (context, state) => PostEditorScreen(
mode: state.uri.queryParameters['mode'],
postEditId: int.tryParse(
state.uri.queryParameters['editing'] ?? '',
),
postReplyId: int.tryParse(
state.uri.queryParameters['replying'] ?? '',
),
postRepostId: int.tryParse(
state.uri.queryParameters['reposting'] ?? '',
),
extraProps: state.extra as PostEditorExtra?,
),
),
GoRoute(
path: '/shuffle',
name: 'postShuffle',
builder: (context, state) => const PostShuffleScreen(),
),
GoRoute(
path: '/search',
name: 'postSearch',
builder: (context, state) => PostSearchScreen(
initialTags: state.uri.queryParameters['tags']?.split(','),
initialCategories:
state.uri.queryParameters['categories']?.split(','),
),
),
],
),
ShellRoute(
builder: (context, state, child) => ResponsiveScaffold(
asideFlex: 2,
contentFlex: 3,
aside: const ExploreScreen(),
child: child,
builder: (context, state, child) => AppPageScaffold(
body: child,
showAppBar: false,
),
routes: [
GoRoute(
path: '/explore',
path: '/',
name: 'home',
pageBuilder: (context, state) => NoTransitionPage(
child: const HomeScreen(),
),
),
GoRoute(
path: '/posts',
name: 'explore',
builder: (context, state) => const ResponsiveScaffoldLanding(
child: ExploreScreen(),
pageBuilder: (context, state) => NoTransitionPage(
child: const ExploreScreen(),
),
),
GoRoute(
path: '/posts/:slug',
name: 'postDetail',
builder: (context, state) => PostDetailScreen(
key: ValueKey(state.pathParameters['slug']!),
slug: state.pathParameters['slug']!,
preload: state.extra as SnPost?,
),
),
GoRoute(
path: '/pages/:id',
name: 'readerFeedDetail',
builder: (context, state) => ReaderPageScreen(
id: state.pathParameters['id']!,
),
),
GoRoute(
path: '/publishers/:name',
name: 'postPublisher',
builder: (context, state) =>
PostPublisherScreen(name: state.pathParameters['name']!),
),
],
),
ShellRoute(
builder: (context, state, child) => ResponsiveScaffold(
aside: const AccountScreen(),
child: child,
),
routes: [
GoRoute(
path: '/account',
name: 'account',
builder: (context, state) =>
const ResponsiveScaffoldLanding(child: AccountScreen()),
routes: [
GoRoute(
path: '/punishments',
name: 'accountPunishments',
builder: (context, state) => const PunishmentsScreen(),
),
GoRoute(
path: '/programs',
name: 'accountProgram',
builder: (context, state) => const AccountProgramScreen(),
),
GoRoute(
path: '/contacts',
name: 'accountContactMethods',
builder: (context, state) => const AccountContactMethod(),
),
GoRoute(
path: '/events',
name: 'accountActionEvents',
builder: (context, state) => const ActionEventScreen(),
),
GoRoute(
path: '/tickets',
name: 'accountAuthTickets',
builder: (context, state) => const AccountAuthTicket(),
),
GoRoute(
path: '/badges',
name: 'accountBadges',
builder: (context, state) => const AccountBadgesScreen(),
),
GoRoute(
path: '/wallet',
name: 'accountWallet',
builder: (context, state) => const WalletScreen(),
),
GoRoute(
path: '/keypairs',
name: 'accountKeyPairs',
builder: (context, state) => const KeyPairScreen(),
),
GoRoute(
path: '/settings',
name: 'accountSettings',
builder: (context, state) => AccountSettingsScreen(),
routes: [
GoRoute(
path: '/notify',
name: 'accountSettingsNotify',
builder: (context, state) => const AccountNotifyPrefsScreen(),
path: '/write/:mode',
name: 'postEditor',
builder: (context, state) => AppBackground(
child: PostEditorScreen(
mode: state.pathParameters['mode']!,
postEditId: int.tryParse(
state.uri.queryParameters['editing'] ?? '',
),
postReplyId: int.tryParse(
state.uri.queryParameters['replying'] ?? '',
),
postRepostId: int.tryParse(
state.uri.queryParameters['reposting'] ?? '',
),
extraProps: state.extra as PostEditorExtraProps?,
),
GoRoute(
path: '/auth',
name: 'accountSettingsSecurity',
builder: (context, state) => const AccountSecurityPrefsScreen(),
),
),
GoRoute(
path: '/search',
name: 'postSearch',
builder: (context, state) => AppBackground(
child: PostSearchScreen(
initialTags: state.uri.queryParameters['tags']?.split(','),
initialCategories: state.uri.queryParameters['categories']?.split(','),
),
],
),
),
GoRoute(
path: '/settings/factors',
name: 'factorSettings',
builder: (context, state) => FactorSettingsScreen(),
path: '/publishers/:name',
name: 'postPublisher',
builder: (context, state) => AppBackground(
child: PostPublisherScreen(name: state.pathParameters['name']!),
),
),
GoRoute(
path: '/profile/edit',
name: 'accountProfileEdit',
builder: (context, state) => ProfileEditScreen(),
),
GoRoute(
path: '/publishers',
name: 'accountPublishers',
builder: (context, state) => PublisherScreen(),
),
GoRoute(
path: '/publishers/new',
name: 'accountPublisherNew',
builder: (context, state) => AccountPublisherNewScreen(),
),
GoRoute(
path: '/publishers/edit/:name',
name: 'accountPublisherEdit',
builder: (context, state) => AccountPublisherEditScreen(
name: state.pathParameters['name']!,
path: '/:slug',
name: 'postDetail',
builder: (context, state) => AppBackground(
child: PostDetailScreen(
slug: state.pathParameters['slug']!,
preload: state.extra as SnPost?,
),
),
),
],
),
GoRoute(
path: '/account',
name: 'account',
pageBuilder: (context, state) => NoTransitionPage(
child: const AccountScreen(),
),
routes: [],
),
GoRoute(
path: '/chat',
name: 'chat',
pageBuilder: (context, state) => NoTransitionPage(
child: const ChatScreen(),
),
routes: [
GoRoute(
path: '/:scope/:alias',
name: 'chatRoom',
builder: (context, state) => AppBackground(
child: ChatRoomScreen(
scope: state.pathParameters['scope']!,
alias: state.pathParameters['alias']!,
),
),
),
GoRoute(
path: '/:scope/:alias/call',
name: 'chatCallRoom',
builder: (context, state) => AppBackground(
child: CallRoomScreen(
scope: state.pathParameters['scope']!,
alias: state.pathParameters['alias']!,
),
),
),
GoRoute(
path: '/:scope/:alias/detail',
name: 'channelDetail',
builder: (context, state) => AppBackground(
child: ChannelDetailScreen(
scope: state.pathParameters['scope']!,
alias: state.pathParameters['alias']!,
),
),
),
GoRoute(
path: '/manage',
name: 'chatManage',
pageBuilder: (context, state) => CustomTransitionPage(
child: ChatManageScreen(
editingChannelAlias: state.uri.queryParameters['editing'],
),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeThroughTransition(
animation: animation,
secondaryAnimation: secondaryAnimation,
fillColor: Colors.transparent,
child: AppBackground(
child: child,
),
);
},
),
),
GoRoute(
path: '/:alias',
name: 'realmDetail',
builder: (context, state) => AppBackground(
child: RealmDetailScreen(alias: state.pathParameters['alias']!),
),
),
],
),
GoRoute(
path: '/realm',
name: 'realm',
pageBuilder: (context, state) => NoTransitionPage(
child: const RealmScreen(),
),
routes: [
GoRoute(
path: '/manage',
name: 'realmManage',
pageBuilder: (context, state) => CustomTransitionPage(
child: RealmManageScreen(
editingRealmAlias: state.uri.queryParameters['editing'],
),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeThroughTransition(
animation: animation,
secondaryAnimation: secondaryAnimation,
fillColor: Colors.transparent,
child: AppBackground(
child: child,
),
);
},
),
),
],
),
GoRoute(
path: '/album',
name: 'album',
pageBuilder: (context, state) => NoTransitionPage(
child: const AlbumScreen(),
),
),
GoRoute(
path: '/friend',
name: 'friend',
pageBuilder: (context, state) => NoTransitionPage(
child: const FriendScreen(),
),
),
GoRoute(
path: '/notification',
name: 'notification',
pageBuilder: (context, state) => NoTransitionPage(
child: const NotificationScreen(),
),
),
],
),
ShellRoute(
builder: (context, state, child) => AppPageScaffold(body: child),
routes: [
GoRoute(
path: '/auth/login',
name: 'authLogin',
builder: (context, state) => const AppBackground(
child: LoginScreen(),
),
),
GoRoute(
path: '/auth/register',
name: 'authRegister',
builder: (context, state) => const AppBackground(
child: RegisterScreen(),
),
),
GoRoute(
path: '/reports',
name: 'abuseReport',
builder: (context, state) => const AppBackground(
child: AbuseReportScreen(),
),
),
GoRoute(
path: '/account/profile/edit',
name: 'accountProfileEdit',
builder: (context, state) => const AppBackground(
child: ProfileEditScreen(),
),
),
GoRoute(
path: '/account/publishers',
name: 'accountPublishers',
builder: (context, state) => const AppBackground(
child: PublisherScreen(),
),
),
GoRoute(
path: '/account/publishers/new',
name: 'accountPublisherNew',
builder: (context, state) => const AppBackground(
child: AccountPublisherNewScreen(),
),
),
GoRoute(
path: '/account/publishers/edit/:name',
name: 'accountPublisherEdit',
builder: (context, state) => AppBackground(
child: AccountPublisherEditScreen(
name: state.pathParameters['name']!,
),
),
),
],
),
GoRoute(
path: '/accounts/:name',
path: '/account/:name',
name: 'accountProfilePage',
pageBuilder: (context, state) => NoTransitionPage(
child: UserScreen(name: state.pathParameters['name']!),
),
),
ShellRoute(
builder: (context, state, child) =>
ResponsiveScaffold(aside: const ChatScreen(), child: child),
builder: (context, state, child) => AppPageScaffold(body: child),
routes: [
GoRoute(
path: '/chat',
name: 'chat',
builder: (context, state) => const ResponsiveScaffoldLanding(
child: ChatScreen(),
),
routes: [
GoRoute(
path: '/:scope/:alias',
name: 'chatRoom',
builder: (context, state) => ChatRoomScreen(
key: ValueKey(
'${state.pathParameters['scope']!}:${state.pathParameters['alias']!}',
),
scope: state.pathParameters['scope']!,
alias: state.pathParameters['alias']!,
extra: state.extra as ChatRoomScreenExtra?,
),
),
GoRoute(
path: '/:scope/:alias/detail',
name: 'channelDetail',
builder: (context, state) => ChannelDetailScreen(
scope: state.pathParameters['scope']!,
alias: state.pathParameters['alias']!,
),
),
GoRoute(
path: '/manage',
name: 'chatManage',
builder: (context, state) => ChatManageScreen(
editingChannelAlias: state.uri.queryParameters['editing'],
),
),
],
),
],
),
GoRoute(
path: '/realm',
name: 'realm',
builder: (context, state) => const RealmScreen(),
routes: [
GoRoute(
path: '/:alias/community',
name: 'realmCommunity',
builder: (context, state) => RealmCommunityScreen(
alias: state.pathParameters['alias']!,
),
),
GoRoute(
path: '/manage',
name: 'realmManage',
builder: (context, state) => RealmManageScreen(
editingRealmAlias: state.uri.queryParameters['editing'],
),
),
GoRoute(
path: '/discovery',
name: 'realmDiscovery',
builder: (context, state) => const RealmDiscoveryScreen(),
),
GoRoute(
path: '/:alias',
name: 'realmDetail',
builder: (context, state) =>
RealmDetailScreen(alias: state.pathParameters['alias']!),
),
],
),
GoRoute(
path: '/stickers',
name: 'stickers',
builder: (context, state) => const StickerScreen(),
routes: [
GoRoute(
path: '/packs/:id',
name: 'stickerPack',
builder: (context, state) => StickerPackScreen(
id: int.tryParse(state.pathParameters['id']!)!,
path: '/settings',
name: 'settings',
builder: (context, state) => const AppBackground(
child: SettingsScreen(),
),
),
],
),
GoRoute(
path: '/debug/logging',
name: 'debugLogging',
builder: (context, state) => const DebugLoggingScreen(),
),
GoRoute(
path: '/album',
name: 'album',
builder: (context, state) => const AlbumScreen(),
),
GoRoute(
path: '/friend',
name: 'friend',
builder: (context, state) => const FriendScreen(),
),
GoRoute(
path: '/notification',
name: 'notification',
builder: (context, state) => const NotificationScreen(),
),
GoRoute(
path: '/auth/login',
name: 'authLogin',
builder: (context, state) => LoginScreen(),
),
GoRoute(
path: '/auth/register',
name: 'authRegister',
builder: (context, state) => RegisterScreen(),
),
GoRoute(
path: '/reports',
name: 'abuseReport',
builder: (context, state) => AbuseReportScreen(),
),
GoRoute(
path: '/settings',
name: 'settings',
builder: (context, state) => SettingsScreen(),
),
GoRoute(
path: '/about',
name: 'about',
builder: (context, state) => AboutScreen(),
ShellRoute(
builder: (context, state, child) => AppPageScaffold(body: child),
routes: [
GoRoute(
path: '/about',
name: 'about',
builder: (context, state) => const AppBackground(
child: AboutScreen(),
),
),
],
),
];
@ -390,10 +325,4 @@ final appRouter = GoRouter(
),
),
],
onException: (context, state, router) {
if (state.error is GoException) {
router.goNamed('/');
}
},
navigatorKey: GlobalKey(),
);

Some files were not shown because too many files have changed in this diff Show More