Compare commits
133 Commits
Author | SHA1 | Date | |
---|---|---|---|
107379d9fe | |||
0d807b8708 | |||
ac1b3fe15c | |||
5853de32a2 | |||
eac1be365e | |||
3fb1d7a6d4 | |||
0480b5244f | |||
56fb92c6b9 | |||
b3267f0026 | |||
88587c10da | |||
9012566dbf | |||
6e00a99803 | |||
aa17a5d52a | |||
ebeffbe1aa | |||
d22eac5c10 | |||
e5381dd5e0 | |||
1c26944a05 | |||
df787f02a1 | |||
db43b7dca5 | |||
59c4d667f6 | |||
063c087089 | |||
48e3b510cf | |||
77288713e1 | |||
1abc65f8fa | |||
a6b17f2c05 | |||
d8dd4060c0 | |||
c8e131c1ab | |||
f4621dd2b4 | |||
6e442c144e | |||
8bbd964026 | |||
0b8a5a3303 | |||
65c6083640 | |||
ad7a34ec18 | |||
6c32d76f78 | |||
2aa699547c | |||
1f4aa8916d | |||
e2c2e41f89 | |||
0f2b854e45 | |||
c21ca5573c | |||
1809f2557d | |||
1fc84099fe | |||
f8755f5220 | |||
4041d6dc4e | |||
cc1071d86e | |||
e334b862df | |||
32c33a963a | |||
a04bfe4cf9 | |||
7b7988e6cb | |||
81a616157e | |||
52312662fb | |||
ca18d6ade4 | |||
af7cc8dab0 | |||
382e3c4a4c | |||
1e37c6ddae | |||
442ef06147 | |||
606a0d708a | |||
558828f3e0 | |||
09dc7d2a0d | |||
6876d2e7c0 | |||
3a5964730c | |||
271c722df3 | |||
97656249f2 | |||
d7e6fe2d8f | |||
2e9c4d166e | |||
c5258cb9ca | |||
47c535910d | |||
66f2f33394 | |||
f5fbe1f483 | |||
fcf4dc7a2d | |||
43b7059957 | |||
11c913af60 | |||
db8f0d63e1 | |||
4036a79995 | |||
859bbd09e0 | |||
60033fdef3 | |||
9c3d181deb | |||
9e6829bd5a | |||
f50461a7f7 | |||
147879e4d8 | |||
f353c05cb5 | |||
ac60043ca7 | |||
8d79274b0c | |||
ad4e4071fa | |||
c59f77c877 | |||
16047a7d57 | |||
fdc68fc5e1 | |||
bbee825cf4 | |||
2673c11046 | |||
3ac6822ab6 | |||
7a5fd2e468 | |||
e1ddd22e4e | |||
22b2ae32e9 | |||
9d5c452eae | |||
0fdb1e4ead | |||
724bd6592e | |||
2d347e0d41 | |||
de39799301 | |||
4b921602a2 | |||
6cde218393 | |||
c896185af0 | |||
4cbeafd447 | |||
91a32e6736 | |||
befc647b03 | |||
16b2e3a0c7 | |||
0cc842c030 | |||
fb370a484d | |||
153c15e5c9 | |||
6a0f42cdc9 | |||
01aaa5455e | |||
f3ceb5f967 | |||
b5e2fa4c25 | |||
8378024490 | |||
6d40d6bba3 | |||
77075c8dab | |||
dec34e297d | |||
358677ade0 | |||
d2f37ae45d | |||
e4b741ff0c | |||
e69abb7f9d | |||
565a8e41cc | |||
c9fbe47337 | |||
01db63e297 | |||
d87e67bd17 | |||
06aa1fb359 | |||
62733bf29f | |||
ce16de9c71 | |||
47eb6cbc66 | |||
029e72fb0b | |||
152efd97a0 | |||
ad1dc064e6 | |||
675b5dea5d | |||
5941cb9fd5 | |||
e11bf204af |
13
.roadsignrc
Normal file
13
.roadsignrc
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"sync": {
|
||||
"region": "solian",
|
||||
"configPath": "roadsign.toml"
|
||||
},
|
||||
"deployments": [
|
||||
{
|
||||
"region": "solian",
|
||||
"site": "solian-web",
|
||||
"path": "build/web"
|
||||
}
|
||||
]
|
||||
}
|
5
.vscode/settings.json
vendored
Normal file
5
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"cSpell.words": [
|
||||
"annvisery"
|
||||
]
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="dev.solsynth.solian">
|
||||
<uses-feature android:name="android.hardware.camera" />
|
||||
<uses-feature android:name="android.hardware.camera.autofocus" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
|
@ -4,3 +4,4 @@ android.enableJetifier=true
|
||||
android.defaults.buildfeatures.buildconfig=true
|
||||
android.nonTransitiveRClass=false
|
||||
android.nonFinalResIds=false
|
||||
kotlin.jvm.target.validation.mode = IGNORE
|
||||
|
@ -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.6-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip
|
||||
|
@ -18,7 +18,7 @@ pluginManagement {
|
||||
|
||||
plugins {
|
||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||
id "com.android.application" version '8.4.0' apply false
|
||||
id "com.android.application" version '8.6.0' 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
|
||||
id "org.jetbrains.kotlin.android" version '2.0.0' apply false
|
||||
|
358
assets/highlighting/cpp.json
Normal file
358
assets/highlighting/cpp.json
Normal file
@ -0,0 +1,358 @@
|
||||
{
|
||||
"name": "C++",
|
||||
"version": "1.0.0",
|
||||
"fileTypes": ["cpp", "hpp", "cc", "h"],
|
||||
"scopeName": "source.cpp",
|
||||
|
||||
"foldingStartMarker": "\\{\\s*$",
|
||||
"foldingStopMarker": "^\\s*\\}",
|
||||
|
||||
"patterns": [
|
||||
{
|
||||
"name": "meta.preprocessor.script.cpp",
|
||||
"match": "^\\s*#\\s*(include|define|if|ifdef|ifndef|else|endif|pragma)\\b"
|
||||
},
|
||||
{
|
||||
"name": "meta.declaration.cpp",
|
||||
"begin": "^\\w*\\b(namespace|class|struct|enum|typedef|template)\\b",
|
||||
"beginCaptures": {
|
||||
"0": {
|
||||
"name": "keyword.other.declaration.cpp"
|
||||
}
|
||||
},
|
||||
"end": "(\\{|;)",
|
||||
"endCaptures": {
|
||||
"0": {
|
||||
"name": "punctuation.terminator.cpp"
|
||||
}
|
||||
},
|
||||
"patterns": [
|
||||
{
|
||||
"include": "#strings"
|
||||
},
|
||||
{
|
||||
"include": "#comments"
|
||||
},
|
||||
{
|
||||
"name": "keyword.other.cpp",
|
||||
"match": "\\b(public|private|protected|virtual|override|final)\\b"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"include": "#comments"
|
||||
},
|
||||
{
|
||||
"include": "#punctuation"
|
||||
},
|
||||
{
|
||||
"include": "#annotations"
|
||||
},
|
||||
{
|
||||
"include": "#keywords"
|
||||
},
|
||||
{
|
||||
"include": "#constants-and-special-vars"
|
||||
},
|
||||
{
|
||||
"include": "#operators"
|
||||
},
|
||||
{
|
||||
"include": "#strings"
|
||||
}
|
||||
],
|
||||
|
||||
"repository": {
|
||||
"comments": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "comment.block.empty.cpp",
|
||||
"match": "/\\*\\*/",
|
||||
"captures": {
|
||||
"0": {
|
||||
"name": "punctuation.definition.comment.cpp"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"include": "#comments-doc-oldschool"
|
||||
},
|
||||
{
|
||||
"include": "#comments-doc"
|
||||
},
|
||||
{
|
||||
"include": "#comments-inline"
|
||||
}
|
||||
]
|
||||
},
|
||||
"comments-doc-oldschool": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "comment.block.documentation.cpp",
|
||||
"begin": "/\\*\\*",
|
||||
"end": "\\*/",
|
||||
"patterns": [
|
||||
{
|
||||
"include": "#comments-doc-oldschool"
|
||||
},
|
||||
{
|
||||
"include": "#comments-block"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"comments-doc": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "comment.block.documentation.cpp",
|
||||
"begin": "///",
|
||||
"while": "^\\s*///",
|
||||
"patterns": [
|
||||
{
|
||||
"include": "#comments-inline"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"comments-inline": {
|
||||
"patterns": [
|
||||
{
|
||||
"include": "#comments-block"
|
||||
},
|
||||
{
|
||||
"match": "(//.*)$",
|
||||
"captures": {
|
||||
"1": {
|
||||
"name": "comment.line.double-slash.cpp"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"comments-block": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "comment.block.cpp",
|
||||
"begin": "/\\*",
|
||||
"end": "\\*/",
|
||||
"patterns": [
|
||||
{
|
||||
"include": "#comments-block"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"annotations": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "storage.type.annotation.cpp",
|
||||
"match": "__attribute__\\(\\w+\\)"
|
||||
}
|
||||
]
|
||||
},
|
||||
"constants-and-special-vars": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "constant.language.cpp",
|
||||
"match": "\\b(true|false|nullptr)\\b"
|
||||
},
|
||||
{
|
||||
"name": "variable.language.cpp",
|
||||
"match": "\\b(this|super)\\b"
|
||||
},
|
||||
{
|
||||
"name": "constant.numeric.cpp",
|
||||
"match": "\\b((0(x|X)[0-9a-fA-F]+)|(([0-9]+\\.?[0-9]*)|(\\.[0-9]+))((e|E)(\\+|-)?[0-9]+)?)\\b"
|
||||
},
|
||||
{
|
||||
"include": "#class-identifier"
|
||||
},
|
||||
{
|
||||
"include": "#function-identifier"
|
||||
}
|
||||
]
|
||||
},
|
||||
"class-identifier": {
|
||||
"patterns": [
|
||||
{
|
||||
"match": "\\b(bool|int|char|double|float|long|short|signed|unsigned|void)\\b",
|
||||
"name": "storage.type.primitive.cpp"
|
||||
},
|
||||
{
|
||||
"begin": "(\\b[A-Z]\\w*\\b)",
|
||||
"end": "(?!<)",
|
||||
"beginCaptures": {
|
||||
"1": {
|
||||
"name": "support.class.cpp"
|
||||
}
|
||||
},
|
||||
"patterns": [
|
||||
{
|
||||
"include": "#type-args"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"function-identifier": {
|
||||
"patterns": [
|
||||
{
|
||||
"match": "\\b([a-z_][a-zA-Z0-9_]*)\\s*\\(",
|
||||
"captures": {
|
||||
"1": {
|
||||
"name": "entity.name.function.cpp"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"type-args": {
|
||||
"begin": "(<)",
|
||||
"end": "(>)",
|
||||
"beginCaptures": {
|
||||
"1": {
|
||||
"name": "other.source.cpp"
|
||||
}
|
||||
},
|
||||
"endCaptures": {
|
||||
"1": {
|
||||
"name": "other.source.cpp"
|
||||
}
|
||||
},
|
||||
"patterns": [
|
||||
{
|
||||
"include": "#class-identifier"
|
||||
},
|
||||
{
|
||||
"match": ","
|
||||
},
|
||||
{
|
||||
"name": "keyword.declaration.cpp",
|
||||
"match": "extends"
|
||||
},
|
||||
{
|
||||
"include": "#comments"
|
||||
}
|
||||
]
|
||||
},
|
||||
"keywords": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "keyword.control.cpp",
|
||||
"match": "\\b(if|else|for|while|do|switch|case|break|continue|goto|return)\\b"
|
||||
},
|
||||
{
|
||||
"name": "keyword.operator.cpp",
|
||||
"match": "\\b(sizeof|typeid|decltype|new|delete)\\b"
|
||||
},
|
||||
{
|
||||
"name": "keyword.control.try.cpp",
|
||||
"match": "\\b(try|catch|throw)\\b"
|
||||
},
|
||||
{
|
||||
"name": "keyword.control.cpp",
|
||||
"match": "\\b(static|inline|virtual|override|const|volatile|explicit|friend|constexpr)\\b"
|
||||
}
|
||||
]
|
||||
},
|
||||
"operators": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "keyword.operator.comparison.cpp",
|
||||
"match": "(==|!=|<=?|>=?)"
|
||||
},
|
||||
{
|
||||
"name": "keyword.operator.arithmetic.cpp",
|
||||
"match": "(\\+|\\-|\\*|\\/|%)"
|
||||
},
|
||||
{
|
||||
"name": "keyword.operator.assignment.cpp",
|
||||
"match": "(=|\\+=|-=|\\*=|/=|%=)"
|
||||
},
|
||||
{
|
||||
"name": "keyword.operator.logical.cpp",
|
||||
"match": "(\\&\\&|\\|\\||!)"
|
||||
},
|
||||
{
|
||||
"name": "keyword.operator.bitwise.cpp",
|
||||
"match": "(<<|>>|\\&|\\||\\^|~)"
|
||||
}
|
||||
]
|
||||
},
|
||||
"string-interp": {
|
||||
"patterns": [
|
||||
{
|
||||
"match": "\\$([a-zA-Z0-9_]+)",
|
||||
"captures": {
|
||||
"1": {
|
||||
"name": "variable.parameter.cpp"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "string.interpolated.expression.cpp",
|
||||
"begin": "\\$\\{",
|
||||
"end": "\\}",
|
||||
"patterns": [
|
||||
{
|
||||
"include": "#constants-and-special-vars",
|
||||
"name": "variable.parameter.cpp"
|
||||
},
|
||||
{
|
||||
"include": "#strings"
|
||||
},
|
||||
{
|
||||
"name": "variable.parameter.cpp",
|
||||
"match": "[a-zA-Z0-9_]+"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "constant.character.escape.cpp",
|
||||
"match": "\\\\."
|
||||
}
|
||||
]
|
||||
},
|
||||
"strings": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "string.quoted.double.cpp",
|
||||
"begin": "\"",
|
||||
"end": "\"",
|
||||
"patterns": [
|
||||
{
|
||||
"name": "constant.character.escape.cpp",
|
||||
"match": "\\\\."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "string.quoted.single.cpp",
|
||||
"begin": "'",
|
||||
"end": "'",
|
||||
"patterns": [
|
||||
{
|
||||
"name": "constant.character.escape.cpp",
|
||||
"match": "\\\\."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"punctuation": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "punctuation.comma.cpp",
|
||||
"match": ","
|
||||
},
|
||||
{
|
||||
"name": "punctuation.terminator.cpp",
|
||||
"match": ";"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
531
assets/highlighting/dart.json
Normal file
531
assets/highlighting/dart.json
Normal file
@ -0,0 +1,531 @@
|
||||
{
|
||||
"name": "Dart",
|
||||
"version": "1.2.3",
|
||||
"fileTypes": ["dart"],
|
||||
"scopeName": "source.dart",
|
||||
|
||||
"foldingStartMarker": "\\{\\s*$",
|
||||
"foldingStopMarker": "^\\s*\\}",
|
||||
|
||||
"patterns": [
|
||||
{
|
||||
"name": "meta.preprocessor.script.dart",
|
||||
"match": "^(#!.*)$"
|
||||
},
|
||||
{
|
||||
"name": "meta.declaration.dart",
|
||||
"begin": "^\\w*\\b(library|import|part of|part|export)\\b",
|
||||
"beginCaptures": {
|
||||
"0": {
|
||||
"name": "keyword.other.import.dart"
|
||||
}
|
||||
},
|
||||
"end": ";",
|
||||
"endCaptures": {
|
||||
"0": {
|
||||
"name": "punctuation.terminator.dart"
|
||||
}
|
||||
},
|
||||
"patterns": [
|
||||
{
|
||||
"include": "#strings"
|
||||
},
|
||||
{
|
||||
"include": "#comments"
|
||||
},
|
||||
{
|
||||
"name": "keyword.other.import.dart",
|
||||
"match": "\\b(as|show|hide)\\b"
|
||||
},
|
||||
{
|
||||
"name": "keyword.control.dart",
|
||||
"match": "\\b(if)\\b"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"include": "#comments"
|
||||
},
|
||||
{
|
||||
"include": "#punctuation"
|
||||
},
|
||||
{
|
||||
"include": "#annotations"
|
||||
},
|
||||
{
|
||||
"include": "#keywords"
|
||||
},
|
||||
{
|
||||
"include": "#constants-and-special-vars"
|
||||
},
|
||||
{
|
||||
"include": "#operators"
|
||||
},
|
||||
{
|
||||
"include": "#strings"
|
||||
}
|
||||
],
|
||||
|
||||
"repository": {
|
||||
"dartdoc": {
|
||||
"patterns": [
|
||||
{
|
||||
"match": "(\\[.*?\\])",
|
||||
"captures": {
|
||||
"0": {
|
||||
"name": "variable.name.source.dart"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"match": "^ {4,}(?![ \\*]).*",
|
||||
"captures": {
|
||||
"0": {
|
||||
"name": "variable.name.source.dart"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"contentName": "variable.other.source.dart",
|
||||
"begin": "```.*?$",
|
||||
"end": "```"
|
||||
},
|
||||
{
|
||||
"match": "(`.*?`)",
|
||||
"captures": {
|
||||
"0": {
|
||||
"name": "variable.other.source.dart"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"match": "(`.*?`)",
|
||||
"captures": {
|
||||
"0": {
|
||||
"name": "variable.other.source.dart"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"match": "(\\* (( ).*))$",
|
||||
"captures": {
|
||||
"2": {
|
||||
"name": "variable.other.source.dart"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"comments": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "comment.block.empty.dart",
|
||||
"match": "/\\*\\*/",
|
||||
"captures": {
|
||||
"0": {
|
||||
"name": "punctuation.definition.comment.dart"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"include": "#comments-doc-oldschool"
|
||||
},
|
||||
{
|
||||
"include": "#comments-doc"
|
||||
},
|
||||
{
|
||||
"include": "#comments-inline"
|
||||
}
|
||||
]
|
||||
},
|
||||
"comments-doc-oldschool": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "comment.block.documentation.dart",
|
||||
"begin": "/\\*\\*",
|
||||
"end": "\\*/",
|
||||
"patterns": [
|
||||
{
|
||||
"include": "#comments-doc-oldschool"
|
||||
},
|
||||
{
|
||||
"include": "#comments-block"
|
||||
},
|
||||
{
|
||||
"include": "#dartdoc"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"comments-doc": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "comment.block.documentation.dart",
|
||||
"begin": "///",
|
||||
"while": "^\\s*///",
|
||||
"patterns": [
|
||||
{
|
||||
"include": "#dartdoc"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"comments-inline": {
|
||||
"patterns": [
|
||||
{
|
||||
"include": "#comments-block"
|
||||
},
|
||||
{
|
||||
"match": "((//).*)$",
|
||||
"captures": {
|
||||
"1": {
|
||||
"name": "comment.line.double-slash.dart"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"comments-block": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "comment.block.dart",
|
||||
"begin": "/\\*",
|
||||
"end": "\\*/",
|
||||
"patterns": [
|
||||
{
|
||||
"include": "#comments-block"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"annotations": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "storage.type.annotation.dart",
|
||||
"match": "@[a-zA-Z]+"
|
||||
}
|
||||
]
|
||||
},
|
||||
"constants-and-special-vars": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "constant.language.dart",
|
||||
"match": "(?<!\\$)\\b(true|false|null)\\b(?!\\$)"
|
||||
},
|
||||
{
|
||||
"name": "variable.language.dart",
|
||||
"match": "(?<!\\$)\\b(this|super)\\b(?!\\$)"
|
||||
},
|
||||
{
|
||||
"name": "constant.numeric.dart",
|
||||
"match": "(?<!\\$)\\b((0(x|X)[0-9a-fA-F]*)|(([0-9]+\\.?[0-9]*)|(\\.[0-9]+))((e|E)(\\+|-)?[0-9]+)?)\\b(?!\\$)"
|
||||
},
|
||||
{
|
||||
"include": "#class-identifier"
|
||||
},
|
||||
{
|
||||
"include": "#function-identifier"
|
||||
}
|
||||
]
|
||||
},
|
||||
"class-identifier": {
|
||||
"patterns": [
|
||||
{
|
||||
"match": "(?<!\\$)\\b(bool|num|int|double|dynamic)\\b(?!\\$)",
|
||||
"name": "support.class.dart"
|
||||
},
|
||||
{
|
||||
"match": "(?<!\\$)\\bvoid\\b(?!\\$)",
|
||||
"name": "storage.type.primitive.dart"
|
||||
},
|
||||
{
|
||||
"begin": "(?<![a-zA-Z0-9_$])([_$]*[A-Z][a-zA-Z0-9_$]*)\\b",
|
||||
"end": "(?!<)",
|
||||
"beginCaptures": {
|
||||
"1": {
|
||||
"name": "support.class.dart"
|
||||
}
|
||||
},
|
||||
"patterns": [
|
||||
{
|
||||
"include": "#type-args"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"function-identifier": {
|
||||
"patterns": [
|
||||
{
|
||||
"match": "([_$]*[a-z][a-zA-Z0-9_$]*)(<(?:[a-zA-Z0-9_$<>?]|,\\s*|\\s+extends\\s+)+>)?[!?]?\\(",
|
||||
"captures": {
|
||||
"1": {
|
||||
"name": "entity.name.function.dart"
|
||||
},
|
||||
"2": {
|
||||
"patterns": [
|
||||
{
|
||||
"include": "#type-args"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"type-args": {
|
||||
"begin": "(<)",
|
||||
"end": "(>)",
|
||||
"beginCaptures": {
|
||||
"1": {
|
||||
"name": "other.source.dart"
|
||||
}
|
||||
},
|
||||
"endCaptures": {
|
||||
"1": {
|
||||
"name": "other.source.dart"
|
||||
}
|
||||
},
|
||||
"patterns": [
|
||||
{
|
||||
"include": "#class-identifier"
|
||||
},
|
||||
{
|
||||
"match": ","
|
||||
},
|
||||
{
|
||||
"name": "keyword.declaration.dart",
|
||||
"match": "extends"
|
||||
},
|
||||
{
|
||||
"include": "#comments"
|
||||
}
|
||||
]
|
||||
},
|
||||
"keywords": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "keyword.cast.dart",
|
||||
"match": "(?<!\\$)\\bas\\b(?!\\$)"
|
||||
},
|
||||
{
|
||||
"name": "keyword.control.catch-exception.dart",
|
||||
"match": "(?<!\\$)\\b(try|on|catch|finally|throw|rethrow)\\b(?!\\$)"
|
||||
},
|
||||
{
|
||||
"name": "keyword.control.dart",
|
||||
"match": "(?<!\\$)\\b(break|case|continue|default|do|else|for|if|in|return|switch|while|when)\\b(?!\\$)"
|
||||
},
|
||||
{
|
||||
"name": "keyword.control.dart",
|
||||
"match": "(?<!\\$)\\b(sync(\\*)?|async(\\*)?|await|yield(\\*)?)\\b(?!\\$)"
|
||||
},
|
||||
{
|
||||
"name": "keyword.control.dart",
|
||||
"match": "(?<!\\$)\\bassert\\b(?!\\$)"
|
||||
},
|
||||
{
|
||||
"name": "keyword.control.new.dart",
|
||||
"match": "(?<!\\$)\\b(new)\\b(?!\\$)"
|
||||
},
|
||||
{
|
||||
"name": "keyword.declaration.dart",
|
||||
"match": "(?<!\\$)\\b(abstract|sealed|base|interface|class|enum|extends|extension type|extension|external|factory|implements|get(?!\\()|mixin|native|operator|set(?!\\()|typedef|with|covariant)\\b(?!\\$)"
|
||||
},
|
||||
{
|
||||
"name": "storage.modifier.dart",
|
||||
"match": "(?<!\\$)\\b(static|final|const|required|late)\\b(?!\\$)"
|
||||
},
|
||||
{
|
||||
"name": "storage.type.primitive.dart",
|
||||
"match": "(?<!\\$)\\b(?:void|var)\\b(?!\\$)"
|
||||
}
|
||||
]
|
||||
},
|
||||
"operators": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "keyword.operator.dart",
|
||||
"match": "(?<!\\$)\\b(is\\!?)\\b(?!\\$)"
|
||||
},
|
||||
{
|
||||
"name": "keyword.operator.ternary.dart",
|
||||
"match": "\\?|:"
|
||||
},
|
||||
{
|
||||
"name": "keyword.operator.bitwise.dart",
|
||||
"match": "(<<|>>>?|~|\\^|\\||&)"
|
||||
},
|
||||
{
|
||||
"name": "keyword.operator.assignment.bitwise.dart",
|
||||
"match": "((&|\\^|\\||<<|>>>?)=)"
|
||||
},
|
||||
{
|
||||
"name": "keyword.operator.closure.dart",
|
||||
"match": "(=>)"
|
||||
},
|
||||
{
|
||||
"name": "keyword.operator.comparison.dart",
|
||||
"match": "(==|!=|<=?|>=?)"
|
||||
},
|
||||
{
|
||||
"name": "keyword.operator.assignment.arithmetic.dart",
|
||||
"match": "(([+*/%-]|\\~)=)"
|
||||
},
|
||||
{
|
||||
"name": "keyword.operator.assignment.dart",
|
||||
"match": "(=)"
|
||||
},
|
||||
{
|
||||
"name": "keyword.operator.increment-decrement.dart",
|
||||
"match": "(\\-\\-|\\+\\+)"
|
||||
},
|
||||
{
|
||||
"name": "keyword.operator.arithmetic.dart",
|
||||
"match": "(\\-|\\+|\\*|\\/|\\~\\/|%)"
|
||||
},
|
||||
{
|
||||
"name": "keyword.operator.logical.dart",
|
||||
"match": "(!|&&|\\|\\|)"
|
||||
}
|
||||
]
|
||||
},
|
||||
"string-interp": {
|
||||
"patterns": [
|
||||
{
|
||||
"match": "\\$([a-zA-Z0-9_]+)",
|
||||
"captures": {
|
||||
"1": {
|
||||
"name": "variable.parameter.dart"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "string.interpolated.expression.dart",
|
||||
"begin": "\\$\\{",
|
||||
"end": "\\}",
|
||||
"patterns": [
|
||||
{
|
||||
"include": "#constants-and-special-vars",
|
||||
"name": "variable.parameter.dart"
|
||||
},
|
||||
{
|
||||
"include": "#strings"
|
||||
},
|
||||
{
|
||||
"name": "variable.parameter.dart",
|
||||
"match": "[a-zA-Z0-9_]+"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "constant.character.escape.dart",
|
||||
"match": "\\\\."
|
||||
}
|
||||
]
|
||||
},
|
||||
"strings": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "string.interpolated.triple.double.dart",
|
||||
"begin": "(?<!r)\"\"\"",
|
||||
"end": "\"\"\"(?!\")",
|
||||
"patterns": [
|
||||
{
|
||||
"include": "#string-interp"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "string.interpolated.triple.single.dart",
|
||||
"begin": "(?<!r)'''",
|
||||
"end": "'''(?!')",
|
||||
"patterns": [
|
||||
{
|
||||
"include": "#string-interp"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "string.quoted.triple.double.dart",
|
||||
"begin": "r\"\"\"",
|
||||
"end": "\"\"\"(?!\")"
|
||||
},
|
||||
{
|
||||
"name": "string.quoted.triple.single.dart",
|
||||
"begin": "r'''",
|
||||
"end": "'''(?!')"
|
||||
},
|
||||
{
|
||||
"name": "string.interpolated.double.dart",
|
||||
"begin": "(?<!\\|r)\"",
|
||||
"end": "\"",
|
||||
"patterns": [
|
||||
{
|
||||
"name": "invalid.string.newline",
|
||||
"match": "\\n"
|
||||
},
|
||||
{
|
||||
"include": "#string-interp"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "string.quoted.double.dart",
|
||||
"begin": "r\"",
|
||||
"end": "\"",
|
||||
"patterns": [
|
||||
{
|
||||
"name": "invalid.string.newline",
|
||||
"match": "\\n"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "string.interpolated.single.dart",
|
||||
"begin": "(?<!\\|r)'",
|
||||
"end": "'",
|
||||
"patterns": [
|
||||
{
|
||||
"name": "invalid.string.newline",
|
||||
"match": "\\n"
|
||||
},
|
||||
{
|
||||
"include": "#string-interp"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "string.quoted.single.dart",
|
||||
"begin": "r'",
|
||||
"end": "'",
|
||||
"patterns": [
|
||||
{
|
||||
"name": "invalid.string.newline",
|
||||
"match": "\\n"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"punctuation": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "punctuation.comma.dart",
|
||||
"match": ","
|
||||
},
|
||||
{
|
||||
"name": "punctuation.terminator.dart",
|
||||
"match": ";"
|
||||
},
|
||||
{
|
||||
"name": "punctuation.dot.dart",
|
||||
"match": "\\."
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
212
assets/highlighting/json.json
Normal file
212
assets/highlighting/json.json
Normal file
@ -0,0 +1,212 @@
|
||||
{
|
||||
"fileTypes": ["json"],
|
||||
"foldingStartMarker": "^\\s*[{\\[](?!.*[}\\]],?\\s*$)|[{\\[]\\s*$",
|
||||
"foldingStopMarker": "^\\s*[}\\]]",
|
||||
"keyEquivalent": "^~J",
|
||||
"name": "JSON (Javascript Next)",
|
||||
"patterns": [
|
||||
{
|
||||
"include": "#value"
|
||||
}
|
||||
],
|
||||
"repository": {
|
||||
"array": {
|
||||
"begin": "\\[",
|
||||
"beginCaptures": {
|
||||
"0": {
|
||||
"name": "punctuation.definition.array.begin.json"
|
||||
}
|
||||
},
|
||||
"end": "\\]",
|
||||
"endCaptures": {
|
||||
"0": {
|
||||
"name": "punctuation.definition.array.end.json"
|
||||
}
|
||||
},
|
||||
"name": "meta.structure.array.json",
|
||||
"patterns": [
|
||||
{
|
||||
"include": "#value"
|
||||
},
|
||||
{
|
||||
"match": ",",
|
||||
"name": "punctuation.separator.array.json"
|
||||
},
|
||||
{
|
||||
"match": "[^\\s\\]]",
|
||||
"name": "invalid.illegal.expected-array-separator.json"
|
||||
}
|
||||
]
|
||||
},
|
||||
"comments": {
|
||||
"patterns": [
|
||||
{
|
||||
"begin": "/\\*\\*",
|
||||
"captures": {
|
||||
"0": {
|
||||
"name": "punctuation.definition.comment.json"
|
||||
}
|
||||
},
|
||||
"end": "\\*/",
|
||||
"name": "comment.block.documentation.json"
|
||||
},
|
||||
{
|
||||
"begin": "/\\*",
|
||||
"captures": {
|
||||
"0": {
|
||||
"name": "punctuation.definition.comment.json"
|
||||
}
|
||||
},
|
||||
"end": "\\*/",
|
||||
"name": "comment.block.json"
|
||||
},
|
||||
{
|
||||
"captures": {
|
||||
"1": {
|
||||
"name": "punctuation.definition.comment.json"
|
||||
}
|
||||
},
|
||||
"match": "(//).*$\\n?",
|
||||
"name": "comment.line.double-slash.js"
|
||||
}
|
||||
]
|
||||
},
|
||||
"constant": {
|
||||
"match": "\\b(?:true|false|null)\\b",
|
||||
"name": "constant.language.json"
|
||||
},
|
||||
"number": {
|
||||
"match": "-?(?:0|[1-9]\\d*)\n(?:\n(?:\n\\.\\d+)?\n(?:\n[eE][+-]?\\d+)?)?",
|
||||
"name": "constant.numeric.json"
|
||||
},
|
||||
"object": {
|
||||
"begin": "\\{",
|
||||
"beginCaptures": {
|
||||
"0": {
|
||||
"name": "punctuation.definition.dictionary.begin.json"
|
||||
}
|
||||
},
|
||||
"end": "\\}",
|
||||
"endCaptures": {
|
||||
"0": {
|
||||
"name": "punctuation.definition.dictionary.end.json"
|
||||
}
|
||||
},
|
||||
"name": "meta.structure.dictionary.json",
|
||||
"patterns": [
|
||||
{
|
||||
"comment": "the JSON object key",
|
||||
"include": "#objectkey"
|
||||
},
|
||||
{
|
||||
"include": "#comments"
|
||||
},
|
||||
{
|
||||
"begin": ":",
|
||||
"beginCaptures": {
|
||||
"0": {
|
||||
"name": "punctuation.separator.dictionary.key-value.json"
|
||||
}
|
||||
},
|
||||
"end": "(,)|(?=\\})",
|
||||
"endCaptures": {
|
||||
"1": {
|
||||
"name": "punctuation.separator.dictionary.pair.json"
|
||||
}
|
||||
},
|
||||
"name": "meta.structure.dictionary.value.json",
|
||||
"patterns": [
|
||||
{
|
||||
"comment": "the JSON object value",
|
||||
"include": "#value"
|
||||
},
|
||||
{
|
||||
"match": "[^\\s,]",
|
||||
"name": "invalid.illegal.expected-dictionary-separator.json"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"match": "[^\\s\\}]",
|
||||
"name": "invalid.illegal.expected-dictionary-separator.json"
|
||||
}
|
||||
]
|
||||
},
|
||||
"string": {
|
||||
"begin": "\"",
|
||||
"beginCaptures": {
|
||||
"0": {
|
||||
"name": "punctuation.definition.string.begin.json"
|
||||
}
|
||||
},
|
||||
"end": "\"",
|
||||
"endCaptures": {
|
||||
"0": {
|
||||
"name": "punctuation.definition.string.end.json"
|
||||
}
|
||||
},
|
||||
"name": "string.quoted.double.json",
|
||||
"patterns": [
|
||||
{
|
||||
"include": "#stringcontent"
|
||||
}
|
||||
]
|
||||
},
|
||||
"objectkey": {
|
||||
"begin": "\"",
|
||||
"beginCaptures": {
|
||||
"0": {
|
||||
"name": "punctuation.support.type.property-name.begin.json"
|
||||
}
|
||||
},
|
||||
"end": "\"",
|
||||
"endCaptures": {
|
||||
"0": {
|
||||
"name": "punctuation.support.type.property-name.end.json"
|
||||
}
|
||||
},
|
||||
"name": "support.type.property-name.json",
|
||||
"patterns": [
|
||||
{
|
||||
"include": "#stringcontent"
|
||||
}
|
||||
]
|
||||
},
|
||||
"stringcontent": {
|
||||
"patterns": [
|
||||
{
|
||||
"match": "\\\\(?:[\"\\\\/bfnrt]|u[0-9a-fA-F]{4})",
|
||||
"name": "constant.character.escape.json"
|
||||
},
|
||||
{
|
||||
"match": "\\\\.",
|
||||
"name": "invalid.illegal.unrecognized-string-escape.json"
|
||||
}
|
||||
]
|
||||
},
|
||||
"value": {
|
||||
"patterns": [
|
||||
{
|
||||
"include": "#constant"
|
||||
},
|
||||
{
|
||||
"include": "#number"
|
||||
},
|
||||
{
|
||||
"include": "#string"
|
||||
},
|
||||
{
|
||||
"include": "#array"
|
||||
},
|
||||
{
|
||||
"include": "#object"
|
||||
},
|
||||
{
|
||||
"include": "#comments"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"scopeName": "source.json",
|
||||
"uuid": "8f97457b-516e-48ce-83c7-08ae12fb327a"
|
||||
}
|
98
assets/highlighting/python.json
Normal file
98
assets/highlighting/python.json
Normal file
@ -0,0 +1,98 @@
|
||||
{
|
||||
"name": "Python",
|
||||
"version": "1.0.0",
|
||||
"fileTypes": ["py"],
|
||||
"scopeName": "source.python",
|
||||
"foldingStartMarker": "\\b(?:def|class)\\s*[^:]*:\\s*$",
|
||||
"foldingStopMarker": "^\\s*\\}",
|
||||
"patterns": [
|
||||
{ "include": "#comments" },
|
||||
{ "include": "#keywords" },
|
||||
{ "include": "#constants-and-special-vars" },
|
||||
{ "include": "#operators" },
|
||||
{ "include": "#strings" }
|
||||
],
|
||||
"repository": {
|
||||
"comments": {
|
||||
"patterns": [
|
||||
{ "name": "comment.line.hash.python", "match": "#.*$" },
|
||||
{ "name": "comment.block.python", "begin": "'''", "end": "'''" },
|
||||
{ "name": "comment.block.python", "begin": "\"\"\"", "end": "\"\"\"" }
|
||||
]
|
||||
},
|
||||
"keywords": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "keyword.control.python",
|
||||
"match": "\\b(?:if|else|while|for|in|break|continue|return)\\b"
|
||||
},
|
||||
{
|
||||
"name": "keyword.operator.logical.python",
|
||||
"match": "\\b(?:and|or|not)\\b"
|
||||
},
|
||||
{ "name": "keyword.operator.assignment.python", "match": "=" },
|
||||
{ "name": "storage.modifier.python", "match": "\\b(?:def|class)\\b" }
|
||||
]
|
||||
},
|
||||
"constants-and-special-vars": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "constant.language.python",
|
||||
"match": "\\b(?:True|False|None)\\b"
|
||||
},
|
||||
{ "name": "variable.language.python", "match": "\\b(?:self)\\b" },
|
||||
{
|
||||
"name": "constant.numeric.python",
|
||||
"match": "\\b(?:\\d+\\.?\\d*|\\.\\d+)\\b"
|
||||
}
|
||||
]
|
||||
},
|
||||
"operators": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "keyword.operator.arithmetic.python",
|
||||
"match": "\\b(?:\\+|-|\\*|/|%|//)\\b"
|
||||
},
|
||||
{
|
||||
"name": "keyword.operator.comparison.python",
|
||||
"match": "\\b(?:==|!=|<|<=|>|>=)\\b"
|
||||
},
|
||||
{
|
||||
"name": "keyword.operator.logical.python",
|
||||
"match": "\\b(?:and|or|not)\\b"
|
||||
}
|
||||
]
|
||||
},
|
||||
"strings": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "string.quoted.triple.double.python",
|
||||
"begin": "\"\"\"",
|
||||
"end": "\"\"\""
|
||||
},
|
||||
{
|
||||
"name": "string.quoted.triple.single.python",
|
||||
"begin": "'''",
|
||||
"end": "'''"
|
||||
},
|
||||
{
|
||||
"name": "string.quoted.double.python",
|
||||
"begin": "\"",
|
||||
"end": "\"",
|
||||
"patterns": [{ "include": "#string-escape" }]
|
||||
},
|
||||
{
|
||||
"name": "string.quoted.single.python",
|
||||
"begin": "'",
|
||||
"end": "'",
|
||||
"patterns": [{ "include": "#string-escape" }]
|
||||
}
|
||||
]
|
||||
},
|
||||
"string-escape": {
|
||||
"patterns": [
|
||||
{ "name": "constant.character.escape.python", "match": "\\\\[\"']" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
145
assets/highlighting/sql.json
Normal file
145
assets/highlighting/sql.json
Normal file
@ -0,0 +1,145 @@
|
||||
{
|
||||
"fileTypes": ["sql", "ddl", "dml"],
|
||||
"foldingStartMarker": "(?i)^\\s*(begin|if|loop)\\b",
|
||||
"foldingStopMarker": "(?i)^\\s*(end)\\b",
|
||||
"keyEquivalent": "^~S",
|
||||
"name": "PL/pgSQL (Postgres)",
|
||||
"patterns": [
|
||||
{
|
||||
"begin": "/\\*",
|
||||
"end": "\\*/",
|
||||
"name": "comment.block.postgres"
|
||||
},
|
||||
{
|
||||
"match": "--.*$",
|
||||
"name": "comment.line.double-dash.postgres"
|
||||
},
|
||||
{
|
||||
"captures": {
|
||||
"1": {
|
||||
"name": "keyword.other.postgres"
|
||||
},
|
||||
"2": {
|
||||
"name": "keyword.other.postgres"
|
||||
}
|
||||
},
|
||||
"match": "(?i)^\\s*(create)(\\s+or\\s+replace)?\\s+",
|
||||
"name": "meta.create.postgres"
|
||||
},
|
||||
{
|
||||
"captures": {
|
||||
"1": {
|
||||
"name": "keyword.other.postgres"
|
||||
},
|
||||
"2": {
|
||||
"name": "keyword.other.postgres"
|
||||
},
|
||||
"3": {
|
||||
"name": "entity.name.type.postgres"
|
||||
}
|
||||
},
|
||||
"match": "(?i)\\b(package)(\\s+body)?\\s+(\\S+)",
|
||||
"name": "meta.package.postgres"
|
||||
},
|
||||
{
|
||||
"captures": {
|
||||
"1": {
|
||||
"name": "keyword.other.postgres"
|
||||
},
|
||||
"2": {
|
||||
"name": "entity.name.type.postgres"
|
||||
}
|
||||
},
|
||||
"match": "(?i)\\b(type)\\s+\"([^\"]+)\"",
|
||||
"name": "meta.type.postgres"
|
||||
},
|
||||
{
|
||||
"captures": {
|
||||
"1": {
|
||||
"name": "keyword.other.postgres"
|
||||
},
|
||||
"2": {
|
||||
"name": "entity.name.function.postgres"
|
||||
}
|
||||
},
|
||||
"match": "(?i)\\s*(function|procedure)\\s+([-a-z0-9_.]+)",
|
||||
"name": "meta.procedure.postgres"
|
||||
},
|
||||
{
|
||||
"match": "[!<>:]?=|<>|<|>|\\+|(?<!\\.)\\*|-|(?<!^)/|@@|\\|\\|",
|
||||
"name": "keyword.operator.postgres"
|
||||
},
|
||||
{
|
||||
"match": "(?i)\\b(true|false|null|found)\\b",
|
||||
"name": "constant.language.postgres"
|
||||
},
|
||||
{
|
||||
"match": "\\b\\d+(\\.\\d+)?\\b",
|
||||
"name": "constant.numeric.postgres"
|
||||
},
|
||||
{
|
||||
"match": "(?i)\\b(if|elsif|else|end\\s+if|loop|end\\s+loop|for|foreach|array|case|end\\s+case|continue|return|goto|alias)\\b",
|
||||
"name": "keyword.control.postgres"
|
||||
},
|
||||
{
|
||||
"match": "(?i)\\b(or|and|not|like)\\b",
|
||||
"name": "keyword.operator.postgres"
|
||||
},
|
||||
{
|
||||
"match": "(?i)\\b(sysdate|%(isopen|found|notfound|rowcount)|commit|rollback|sqlerrm|substr|cast|decode|length|lower|upper|coalesce)\\b",
|
||||
"name": "support.function.postgres"
|
||||
},
|
||||
{
|
||||
"match": "(?i)\\b(avg|count|sum|max|min|nvl|trim|to_date|to_char|lpad|ltrim|rpad|rtrim|trunc|to_number|regexp_split_to_array|regexp_replace)\\b",
|
||||
"name": "support.function.builtin.postgres"
|
||||
},
|
||||
{
|
||||
"match": "(?i)\\b(sql|sqlcode)\\b",
|
||||
"name": "variable.language.postgres"
|
||||
},
|
||||
{
|
||||
"match": "(?i)\\b(p(i|o|io)_[-a-z0-9_]+)\\b",
|
||||
"name": "variable.parameter.postgres"
|
||||
},
|
||||
{
|
||||
"match": "(?i)\\b(l_[-a-z0-9_]+)\\b",
|
||||
"name": "variable.other.postgres"
|
||||
},
|
||||
{
|
||||
"match": "(?i)\\b(immutable|volatile|stable|serial|primary|key|references|comment|column|schema|authorization|get|diagnostics|returning|drop|all|raise|notice|warning|exception|external|security|definer|language|grant|execute|on|to|function|procedure|returns|end|then|deterministic|exception|when|others|subtype|constant|range|binary_integer|declare|begin|in|out|is|as|exit|open|fetch|into|close|type|rowtype|default|\\.(extend|count|first|last|next|nextval|currval)|cost|alter|owner)\\b",
|
||||
"name": "keyword.other.postgres"
|
||||
},
|
||||
{
|
||||
"match": "(?i)\\b(select|perform|from|where|order\\s+by|group\\s+by|asc|desc|update|set|insert|into|values|delete|from|distinct|union|having|limit|table|of|prepare|(inner|left|outer) join)\\b",
|
||||
"name": "keyword.other.sql.postgres"
|
||||
},
|
||||
{
|
||||
"match": "[$][0-9]+",
|
||||
"name": "storage.type.postgres"
|
||||
},
|
||||
{
|
||||
"match": "(?i)\\b(dbms_lock|dbms_output)\\b",
|
||||
"name": "support.class.postgres"
|
||||
},
|
||||
{
|
||||
"match": "(?i)\\b(put_line)\\b",
|
||||
"name": "support.function.postgres"
|
||||
},
|
||||
{
|
||||
"begin": "'",
|
||||
"end": "'",
|
||||
"name": "string.quoted.single.postgres"
|
||||
},
|
||||
{
|
||||
"begin": "\"",
|
||||
"end": "\"",
|
||||
"name": "string.quoted.double.postgres"
|
||||
},
|
||||
{
|
||||
"match": "(?i)\\b(number|integer|bigint|varchar2|varchar|boolean|date|setof|record|query|numeric|void|character varying|text|([-a-z0-9_.]+%(row)?type))\\b",
|
||||
"name": "storage.type.postgres"
|
||||
}
|
||||
],
|
||||
"scopeName": "source.plpgsql.postgres",
|
||||
"uuid": "28DCE4DD-F5E1-4ED3-8847-64DA6B1F9163"
|
||||
}
|
66
assets/highlighting/yaml.json
Normal file
66
assets/highlighting/yaml.json
Normal file
@ -0,0 +1,66 @@
|
||||
{
|
||||
"name": "YAML",
|
||||
"fileTypes": ["yaml", "yml"],
|
||||
"scopeName": "source.yaml",
|
||||
"patterns": [
|
||||
{
|
||||
"name": "comment.line.number-sign.yaml",
|
||||
"match": "#.*",
|
||||
"captures": {
|
||||
"0": {
|
||||
"name": "punctuation.definition.comment.yaml"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "entity.name.tag.yaml",
|
||||
"match": "^\\s*\\w+",
|
||||
"captures": {
|
||||
"0": {
|
||||
"name": "punctuation.definition.tag.yaml"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "punctuation.separator.key-value.yaml",
|
||||
"match": ":",
|
||||
"captures": {
|
||||
"0": {
|
||||
"name": "punctuation.separator.key-value.yaml"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "string.quoted.double.yaml",
|
||||
"begin": "\"",
|
||||
"end": "\"",
|
||||
"patterns": [
|
||||
{
|
||||
"name": "constant.character.escape.yaml",
|
||||
"match": "\\\\(x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{6}|.)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "string.quoted.single.yaml",
|
||||
"begin": "'",
|
||||
"end": "'",
|
||||
"patterns": [
|
||||
{
|
||||
"name": "constant.character.escape.yaml",
|
||||
"match": "''"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"repository": {
|
||||
"scalar-plain": {
|
||||
"patterns": [
|
||||
{
|
||||
"match": "\\b(\\w+)\\b",
|
||||
"name": "scalar.plain.yaml"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
@ -22,9 +22,9 @@
|
||||
"explore": "Explore",
|
||||
"posts": "Posts",
|
||||
"unlink": "Unlink",
|
||||
"feedSearch": "Search Feed",
|
||||
"feedSearchWithTag": "Searching with tag #@key",
|
||||
"feedSearchWithCategory": "Searching in category @category",
|
||||
"postSearch": "Search Post",
|
||||
"postSearchWithTag": "Searching with tag #@key",
|
||||
"postSearchWithCategory": "Searching in category @category",
|
||||
"feedUnreadCount": "@count posts you may missed",
|
||||
"messages": "Messages",
|
||||
"messagesUnreadCount": "@count messages unread",
|
||||
@ -54,8 +54,9 @@
|
||||
"about": "About",
|
||||
"edit": "Edit",
|
||||
"delete": "Delete",
|
||||
"insert": "Insert",
|
||||
"settings": "Settings",
|
||||
"settingsNotificationBgService": "Background Notification Service",
|
||||
"settingsNotificationBgService": "Background notification service",
|
||||
"settingsNotificationBgServiceDesc": "A notification service is always installed on the device, so that some devices that do not support push notifications can receive notifications in the background. When this feature is enabled, push notifications will not be registered with the server, and you will always appear to be online in the eyes of others (except for invisible). You may need to turn off power and traffic optimization in the settings.",
|
||||
"search": "Search",
|
||||
"post": "Post",
|
||||
@ -68,6 +69,11 @@
|
||||
"notificationUnreadCount": "@count unread notifications",
|
||||
"errorHappened": "An error occurred",
|
||||
"errorHappenedUnauthorized": "Unauthorized request, please sign in or try resign in.",
|
||||
"errorHappenedRequestBad": "Request error, the server refused to process the request. Please check your request data.",
|
||||
"errorHappenedRequestForbidden": "Request error, insufficient permissions.",
|
||||
"errorHappenedRequestNotFound": "Request error, the requested data does not exist.",
|
||||
"errorHappenedRequestConnection": "Network request failed. Please check the connection status and service status, then try again.",
|
||||
"errorHappenedRequestUnknown": "Request error, unknown type. Please take a full screenshot of this message and submit feedback.",
|
||||
"forgotPassword": "Forgot password",
|
||||
"email": "Email",
|
||||
"username": "Username",
|
||||
@ -93,6 +99,8 @@
|
||||
"accountFriendBlocked": "Friend blocklist",
|
||||
"accountFriendListHint": "Swipe left to decline, right to approve",
|
||||
"accountFriendRequestSent": "Friend request sent, waiting for processing...",
|
||||
"accountBlocked": "Account has been blocked",
|
||||
"accountUnblocked": "Account has been unblocked",
|
||||
"accountSuspended": "Account was suspended",
|
||||
"accountSuspendedAt": "Account was suspended since @date",
|
||||
"aspectRatio": "Aspect Ratio",
|
||||
@ -133,7 +141,7 @@
|
||||
"clear": "Clear",
|
||||
"pinPost": "Pin this post",
|
||||
"unpinPost": "Unpin this post",
|
||||
"postRestoreFromLocal": "Restore from local",
|
||||
"postRestoreFromLocal": "Restored",
|
||||
"postAutoSaveAt": "Auto saved at @date",
|
||||
"postCategoriesAndTags": "Categories n' Tags",
|
||||
"postPublishDate": "Publish Date",
|
||||
@ -157,6 +165,9 @@
|
||||
"postListNews": "News",
|
||||
"postListFriends": "Friends",
|
||||
"postListShuffle": "Random",
|
||||
"attachmentThumbnail": "Thumbnail",
|
||||
"attachmentThumbnailAttachmentNew": "Upload thumbnail",
|
||||
"attachmentThumbnailAttachment": "Attachment serial number",
|
||||
"postEditorModeStory": "Post a post",
|
||||
"postEditorModeArticle": "Post an article",
|
||||
"postEditor": "Post editor",
|
||||
@ -225,6 +236,8 @@
|
||||
"realmDescription": "Description",
|
||||
"realmPublic": "Public Realm",
|
||||
"realmCommunity": "Community Realm",
|
||||
"realmAvatar": "Realm avatar",
|
||||
"realmBanner": "Realm banner",
|
||||
"realmDetail": "Realm detail",
|
||||
"realmMember": "Realm member",
|
||||
"realmMembers": "Realm members",
|
||||
@ -250,7 +263,8 @@
|
||||
"channelName": "Name",
|
||||
"channelDescription": "Description",
|
||||
"channelDirectDescription": "Direct message with @username",
|
||||
"channelEncrypted": "Encrypted Channel",
|
||||
"channelPublic": "Public channel",
|
||||
"channelCommunity": "Community channel",
|
||||
"channelMember": "Channel member",
|
||||
"channelMembers": "Channel members",
|
||||
"channelMembersAdd": "Add channel members",
|
||||
@ -344,8 +358,7 @@
|
||||
"bsCheckForUpdate": "Checking For Updates",
|
||||
"bsCheckForUpdateFailed": "Unable to Check Updates",
|
||||
"bsCheckForUpdateNew": "Found New Version",
|
||||
"bsCheckForUpdateDescApple": "Please head to TestFlight and update your app to latest version to prevent error happens and get latest functions.",
|
||||
"bsCheckForUpdateDescCommon": "Please head to our website download and install latest version of application to prevent error happens and get latest functions.",
|
||||
"bsCheckForUpdateDesc": "Please head to app store and update your app to latest version to prevent error happens and get latest functions.",
|
||||
"bsCheckingServer": "Checking Server Status",
|
||||
"bsCheckingServerFail": "Unable connect to server, check your network connection",
|
||||
"bsCheckingServerDown": "Server currently unavailable, please retry later",
|
||||
@ -354,8 +367,9 @@
|
||||
"bsPreparingData": "Preparing User Data",
|
||||
"bsRegisteringPushNotify": "Enabling Push Notifications",
|
||||
"bsDismissibleErrorHint": "Click anywhere to ignore this error",
|
||||
"bsContinuable": "Click anywhere to continue",
|
||||
"postShareContent": "@content\n\n@username on the Solar Network\nCheck it out: @link",
|
||||
"postShareSubject": "@username posted a post on the Solar Network",
|
||||
"postShareSubject": "@title by @username on Solar Network",
|
||||
"themeColor": "Global Theme Color",
|
||||
"themeColorRed": "Modern Red",
|
||||
"themeColorBlue": "Classic Blue",
|
||||
@ -407,5 +421,78 @@
|
||||
"userLevel13": "Immortal",
|
||||
"postBrowsingIn": "Browsing in @region",
|
||||
"needRestartToApply": "Restart the application to take effect",
|
||||
"holdToSeeDetail": "Long press / Mouse hover to see detail"
|
||||
"holdToSeeDetail": "Long press / Mouse hover to see detail",
|
||||
"subscribe": "Subscribe",
|
||||
"subscribed": "Subscribed",
|
||||
"unsubscribe": "Unsubscribe",
|
||||
"preferences": "Preferences",
|
||||
"notificationPreferences": "Notification preferences",
|
||||
"notificationTopicPostFeedback": "Post feedbacks",
|
||||
"notificationTopicPostSubscription": "Post subscriptions",
|
||||
"preferencesApplied": "Preferences has been applied.",
|
||||
"save": "Save",
|
||||
"updateAvailable": "Update available",
|
||||
"updateAvailableDesc": "There is an update available (@from to @to). Do you want to download and install it now? You can still use the app normally while waiting for the download to complete.",
|
||||
"update": "Update",
|
||||
"updateCheckStrictly": "Strict mode",
|
||||
"updateCheckStrictlyDesc": "If enabled, the app will ask for updating once the local version is different from remote one.",
|
||||
"updateMayAvailable": "App version @version is available, you can update from app store or our website.",
|
||||
"updateNow": "Update now",
|
||||
"termAccept": "I've read and agree to Solar Network's Terms",
|
||||
"termAcceptDesc": "Including but not limited to \"User Agreement\" and \"Privacy Policy\"",
|
||||
"termAcceptLink": "View terms",
|
||||
"termAcceptNextWithAgree": "By clicking the \"Next\", it means you agree to our terms and its updates. You should already agreed with them while you sign up.",
|
||||
"termRelated": "Related Terms",
|
||||
"appDetails": "App Details",
|
||||
"projectWebsite": "Project Website",
|
||||
"iAmNotRobot": "I'm not a Robot",
|
||||
"report": "Report",
|
||||
"reportAbuse": "Report abuse",
|
||||
"reportAbuseDesc": "Report any violation of service terms",
|
||||
"reportAbuseResource": "Resource identifier",
|
||||
"reportAbuseReason": "Report reason",
|
||||
"reportSubmitted": "Report submitted, thank you for your contribution. We will send a notification about the result of the report within 24 hours for you.",
|
||||
"accountDeletion": "Request account deletion",
|
||||
"accountDeletionDesc": "Delete the current account and all its data. Note that this action is irreversible!",
|
||||
"accountDeletionConfirm": "Confirm request account deletion",
|
||||
"accountDeletionConfirmDesc": "Are you sure to delete account @account? You will receive a confirmation email with a link to confirm the deletion of the account within 24 hours. Note that this action is irreversible, and all data associated with the account will be deleted, and you should be careful about it.",
|
||||
"accountDeletionRequested": "Account deletion requested, check your inbox to confirm the request.",
|
||||
"slideToConfirm": "Slide to confirm",
|
||||
"serviceStatus": "Status of Service",
|
||||
"firstBootTime": "First boot at @time",
|
||||
"rateTheApp": "Rate the app",
|
||||
"rateTheAppDesc": "Rate Solar Network on the App Store to let us serve you better!",
|
||||
"friendAdd": "Add as friend",
|
||||
"blockUser": "Block user",
|
||||
"unblockUser": "Unblock user",
|
||||
"learnMoreAboutPerson": "Learn more about that person",
|
||||
"global": "Global",
|
||||
"all": "All",
|
||||
"unablePreview": "Unable to preview",
|
||||
"dashboardNav": "Dash",
|
||||
"accountNav": "You",
|
||||
"performance": "Performance",
|
||||
"animatedMessageList": "Non-animated message list",
|
||||
"animatedMessageListDesc": "Remove animation effects in message list, to reduce cause lag",
|
||||
"theme": "Theme",
|
||||
"globalTheme": "Global theme",
|
||||
"agedTheme": "Old school style theme",
|
||||
"agedThemeDesc": "Downgrade the global theme to Material Design 2. Unexpected issues may occur. For experimental use only.",
|
||||
"appBackgroundImage": "Global background image",
|
||||
"appBackgroundImageDesc": "The global background image will be displayed on all pages",
|
||||
"authPreferences": "Auth preferences",
|
||||
"authPreferencesDesc": "Set the security behavior of your account",
|
||||
"authMaximumAuthSteps": "Maximum authentication steps",
|
||||
"authMaximumAuthStepsDesc": "The maximum number of authentication steps when logging in, higher value is more secure, lower value is more convenient; default is 2",
|
||||
"auditLog": "Audit log",
|
||||
"shareImage": "Share as image",
|
||||
"shareImageFooter": "Only on the Solar Network",
|
||||
"fileSavedAt": "File saved at @path",
|
||||
"showIp": "Show IP Address",
|
||||
"shotOn": "Shot on @device",
|
||||
"unread": "Unread",
|
||||
"searchTook": "Took @time",
|
||||
"searchResult": "@count Matches",
|
||||
"happyBirthday": "Happy birthday @name!",
|
||||
"happyBirthdayDesc": "Today is your @count birthday"
|
||||
}
|
||||
|
@ -14,6 +14,7 @@
|
||||
"about": "关于",
|
||||
"edit": "编辑",
|
||||
"delete": "删除",
|
||||
"insert": "插入",
|
||||
"settings": "设置",
|
||||
"settingsNotificationBgService": "常驻通知服务",
|
||||
"settingsNotificationBgServiceDesc": "在设备常驻一个通知服务,使得部分不支持推送通知的设备可以在后台收到通知;启用该功能的情况下不会向服务器注册推送通知,并且你会始终在他人眼中成为在线(隐身除外);可能需要在设置中关闭电量与流量优化。",
|
||||
@ -32,9 +33,9 @@
|
||||
"dashboard": "仪表盘",
|
||||
"today": "今日",
|
||||
"yesterday": "昨日",
|
||||
"feedSearch": "搜索资讯",
|
||||
"feedSearchWithTag": "检索带有 #@key 标签的资讯",
|
||||
"feedSearchWithCategory": "检索位于分类 @category 的资讯",
|
||||
"postSearch": "搜索帖子",
|
||||
"postSearchWithTag": "检索带有 #@key 标签的资讯",
|
||||
"postSearchWithCategory": "检索位于分类 @category 的资讯",
|
||||
"feedUnreadCount": "@count 条你可能错过的帖子",
|
||||
"messages": "消息",
|
||||
"messagesUnreadCount": "@count 条未读的消息",
|
||||
@ -98,6 +99,8 @@
|
||||
"accountFriendBlocked": "好友黑名单",
|
||||
"accountFriendListHint": "左滑来拒绝,右滑来接受",
|
||||
"accountFriendRequestSent": "好友请求已发送,等待处理对方中……",
|
||||
"accountBlocked": "已屏蔽账号",
|
||||
"accountUnblocked": "已解除屏蔽账号",
|
||||
"accountSuspended": "帐号被停用",
|
||||
"accountSuspendedAt": "该帐号自 @date 起被停用",
|
||||
"aspectRatio": "纵横比",
|
||||
@ -168,6 +171,9 @@
|
||||
"postListNews": "新鲜事",
|
||||
"postListFriends": "好友圈",
|
||||
"postListShuffle": "打乱看",
|
||||
"attachmentThumbnail": "附件缩略图",
|
||||
"attachmentThumbnailAttachmentNew": "上传附件作为缩略图",
|
||||
"attachmentThumbnailAttachment": "附件序列号",
|
||||
"postNew": "创建新帖子",
|
||||
"postNewInRealmHint": "在领域 @realm 里发表新帖子",
|
||||
"postAction": "发表",
|
||||
@ -226,6 +232,8 @@
|
||||
"realmDescription": "领域简介",
|
||||
"realmPublic": "公开领域",
|
||||
"realmCommunity": "社区领域",
|
||||
"realmAvatar": "领域头像",
|
||||
"realmBanner": "领域横幅",
|
||||
"realmDetail": "领域详情",
|
||||
"realmMember": "领域成员",
|
||||
"realmMembers": "领域成员",
|
||||
@ -251,14 +259,15 @@
|
||||
"channelName": "显示名称",
|
||||
"channelDescription": "频道简介",
|
||||
"channelDirectDescription": "与 @username 的私聊",
|
||||
"channelEncrypted": "加密频道",
|
||||
"channelPublic": "公开频道",
|
||||
"channelCommunity": "社区频道",
|
||||
"channelMember": "频道成员",
|
||||
"channelMembers": "频道成员",
|
||||
"channelMembersAdd": "添加频道成员",
|
||||
"channelMembersAddHint": "到 @channel",
|
||||
"channelType": "频道类型",
|
||||
"channelTypeCommon": "普通频道",
|
||||
"channelTypeDirect": "私信聊天",
|
||||
"channelTypeDirect": "私信",
|
||||
"channelAdjust": "调整频道",
|
||||
"channelDetail": "频道详情",
|
||||
"channelSettings": "频道设置",
|
||||
@ -345,8 +354,7 @@
|
||||
"bsCheckForUpdate": "正在检查更新",
|
||||
"bsCheckForUpdateFailed": "无法检查更新",
|
||||
"bsCheckForUpdateNew": "发现新版本",
|
||||
"bsCheckForUpdateDescApple": "请前往 TestFlight 并将您的应用程序更新到最新版本,以防止出现错误并获取最新功能。",
|
||||
"bsCheckForUpdateDescCommon": "请前往我们的网站下载并安装最新版本的应用程序,以防止出现错误并获取最新功能。",
|
||||
"bsCheckForUpdateDesc": "请前往应用商店并将您的应用程序更新到最新版本,以防止出现错误并获取最新功能。",
|
||||
"bsCheckingServer": "检查服务器状态中",
|
||||
"bsCheckingServerFail": "无法连接至服务器,请检查你的网络连接状态",
|
||||
"bsCheckingServerDown": "当前服务器不可用,请稍后重试",
|
||||
@ -355,8 +363,9 @@
|
||||
"bsPreparingData": "正在准备用户资料",
|
||||
"bsRegisteringPushNotify": "正在启用推送通知",
|
||||
"bsDismissibleErrorHint": "点击任意地方忽略此错误",
|
||||
"bsContinuable": "点击任意处继续",
|
||||
"postShareContent": "@content\n\n@username 在 Solar Network\n原帖地址:@link",
|
||||
"postShareSubject": "@username 在 Solar Network 上发布了一篇帖子",
|
||||
"postShareSubject": "@username 在 Solar Network 发表的 @title",
|
||||
"themeColor": "全局主题色",
|
||||
"themeColorRed": "现代红",
|
||||
"themeColorBlue": "经典蓝",
|
||||
@ -408,5 +417,78 @@
|
||||
"userLevel13": "万古流芳",
|
||||
"postBrowsingIn": "浏览 @region 内的帖子中",
|
||||
"needRestartToApply": "需要重启应用来生效",
|
||||
"holdToSeeDetail": "长按 / 鼠标悬浮来查看详情"
|
||||
"holdToSeeDetail": "长按 / 鼠标悬浮来查看详情",
|
||||
"subscribe": "订阅",
|
||||
"subscribed": "已订阅",
|
||||
"unsubscribe": "取消订阅",
|
||||
"preferences": "偏好设置",
|
||||
"notificationPreferences": "通知偏好设置",
|
||||
"notificationTopicPostFeedback": "帖子反馈",
|
||||
"notificationTopicPostSubscription": "订阅源",
|
||||
"preferencesApplied": "偏好设置已应用",
|
||||
"save": "保存",
|
||||
"updateAvailable": "有可用更新",
|
||||
"updateAvailableDesc": "有可用更新 (@from 到 @to) 你想现在下载安装吗?在等待下载期间你仍可以正常使用。",
|
||||
"update": "更新",
|
||||
"updateCheckStrictly": "严格模式",
|
||||
"updateCheckStrictlyDesc": "如果启用,应用程序将会在本地版本与远程版本不同时询问更新,而不会检查版本号大小。",
|
||||
"updateNow": "立即更新",
|
||||
"updateMayAvailable": "版本 @version 现已可用,你可以前往应用商店或是我们的官网下载更新。",
|
||||
"termAccept": "我已阅读并同意 Solar Network 各项条款",
|
||||
"termAcceptDesc": "包括但不限于《用户守则》和《隐私政策》",
|
||||
"termAcceptLink": "浏览条款",
|
||||
"termAcceptNextWithAgree": "点击 “下一步”,即表示你同意我们的各项条款,包括其之后的更新。你应该在注册时已经同意过了。",
|
||||
"termRelated": "相关条款",
|
||||
"projectWebsite": "项目网站",
|
||||
"appDetails": "应用详情",
|
||||
"iAmNotRobot": "我不是机器人",
|
||||
"report": "举报",
|
||||
"reportAbuse": "举报滥用",
|
||||
"reportAbuseDesc": "举报任何违反服务条款的行为",
|
||||
"reportAbuseResource": "举报的资源",
|
||||
"reportAbuseReason": "举报的原因",
|
||||
"reportSubmitted": "举报已提交,感谢你的贡献。我们将通过通知在 24 小时内通知该举报的处理结果。",
|
||||
"accountDeletion": "请求删除账号",
|
||||
"accountDeletionDesc": "删除目前登陆的账号,及其所有的数据。注意,该操作不可撤销!",
|
||||
"accountDeletionConfirm": "确认账号删除请求",
|
||||
"accountDeletionConfirmDesc": "你确定要删除账号 @account 吗?你将会在其绑定的主要邮件地址收到一封包含着确认删除账号连接的邮件,在二十四小时内使用该连接即可完成删除账号。注意,本操作不可撤销,并且账号创建或关联的所有数据都将被删除,请三思而后行。",
|
||||
"accountDeletionRequested": "已请求删除账号,检查你的收件箱来确认请求。",
|
||||
"slideToConfirm": "滑动来确认",
|
||||
"serviceStatus": "服务状态",
|
||||
"firstBootTime": "首次启动于 @time",
|
||||
"rateTheApp": "给应用评分",
|
||||
"rateTheAppDesc": "在 App Store 上给 Solar Network 评分,让我们更好地为您服务吧!",
|
||||
"friendAdd": "添加好友",
|
||||
"blockUser": "屏蔽用户",
|
||||
"unblockUser": "解除屏蔽用户",
|
||||
"learnMoreAboutPerson": "了解关于 TA 的更多",
|
||||
"global": "全局",
|
||||
"all": "全部",
|
||||
"unablePreview": "无法预览",
|
||||
"dashboardNav": "仪表盘",
|
||||
"accountNav": "您",
|
||||
"performance": "性能",
|
||||
"animatedMessageList": "无动画消息列表",
|
||||
"animatedMessageListDesc": "在消息列表中禁用动画效果",
|
||||
"theme": "主题",
|
||||
"globalTheme": "全局应用主题",
|
||||
"agedTheme": "过时主题",
|
||||
"agedThemeDesc": "将全局主题降级为 Material Design 2,可能发生意料之外的问题,仅供实验使用",
|
||||
"appBackgroundImage": "全局背景图片",
|
||||
"appBackgroundImageDesc": "全局背景图片将会在所有页面中展示",
|
||||
"authPreferences": "安全偏好设置",
|
||||
"authPreferencesDesc": "调整账号的安全行为模式",
|
||||
"authMaximumAuthSteps": "最大认证步数",
|
||||
"authMaximumAuthStepsDesc": "登陆时最多的验证步数,值越高则越安全,反之则会相对方便;默认设置为 2",
|
||||
"auditLog": "活动日志",
|
||||
"shareImage": "分享图片",
|
||||
"shareImageFooter": "上 Solar Network 看更多有趣帖子",
|
||||
"fileSavedAt": "文件保存于 @path",
|
||||
"showIp": "显示 IP 地址",
|
||||
"shotOn": "由 @device 拍摄",
|
||||
"unread": "未读",
|
||||
"searchTook": "耗时 @time",
|
||||
"searchResult": "匹配到 @count 条结果",
|
||||
"happyBirthday": "生日快乐,@name!",
|
||||
"happyBirthdayDesc": "今天是你的第 @count 个生日"
|
||||
}
|
||||
|
174
ios/Podfile.lock
174
ios/Podfile.lock
@ -38,45 +38,47 @@ PODS:
|
||||
- file_picker (0.0.1):
|
||||
- DKImagePickerController/PhotoGallery
|
||||
- Flutter
|
||||
- Firebase/Analytics (11.0.0):
|
||||
- file_saver (0.0.1):
|
||||
- Flutter
|
||||
- Firebase/Analytics (11.2.0):
|
||||
- Firebase/Core
|
||||
- Firebase/Core (11.0.0):
|
||||
- Firebase/Core (11.2.0):
|
||||
- Firebase/CoreOnly
|
||||
- FirebaseAnalytics (~> 11.0.0)
|
||||
- Firebase/CoreOnly (11.0.0):
|
||||
- FirebaseCore (= 11.0.0)
|
||||
- Firebase/Crashlytics (11.0.0):
|
||||
- FirebaseAnalytics (~> 11.2.0)
|
||||
- Firebase/CoreOnly (11.2.0):
|
||||
- FirebaseCore (= 11.2.0)
|
||||
- Firebase/Crashlytics (11.2.0):
|
||||
- Firebase/CoreOnly
|
||||
- FirebaseCrashlytics (~> 11.0.0)
|
||||
- Firebase/Messaging (11.0.0):
|
||||
- FirebaseCrashlytics (~> 11.2.0)
|
||||
- Firebase/Messaging (11.2.0):
|
||||
- Firebase/CoreOnly
|
||||
- FirebaseMessaging (~> 11.0.0)
|
||||
- Firebase/Performance (11.0.0):
|
||||
- FirebaseMessaging (~> 11.2.0)
|
||||
- Firebase/Performance (11.2.0):
|
||||
- Firebase/CoreOnly
|
||||
- FirebasePerformance (~> 11.0.0)
|
||||
- firebase_analytics (11.3.1):
|
||||
- Firebase/Analytics (= 11.0.0)
|
||||
- FirebasePerformance (~> 11.2.0)
|
||||
- firebase_analytics (11.3.3):
|
||||
- Firebase/Analytics (= 11.2.0)
|
||||
- firebase_core
|
||||
- Flutter
|
||||
- firebase_core (3.4.1):
|
||||
- Firebase/CoreOnly (= 11.0.0)
|
||||
- firebase_core (3.6.0):
|
||||
- Firebase/CoreOnly (= 11.2.0)
|
||||
- Flutter
|
||||
- firebase_crashlytics (4.1.1):
|
||||
- Firebase/Crashlytics (= 11.0.0)
|
||||
- firebase_crashlytics (4.1.3):
|
||||
- Firebase/Crashlytics (= 11.2.0)
|
||||
- firebase_core
|
||||
- Flutter
|
||||
- firebase_messaging (15.1.1):
|
||||
- Firebase/Messaging (= 11.0.0)
|
||||
- firebase_messaging (15.1.3):
|
||||
- Firebase/Messaging (= 11.2.0)
|
||||
- firebase_core
|
||||
- Flutter
|
||||
- firebase_performance (0.10.0-6):
|
||||
- Firebase/Performance (= 11.0.0)
|
||||
- firebase_performance (0.10.0-8):
|
||||
- Firebase/Performance (= 11.2.0)
|
||||
- firebase_core
|
||||
- Flutter
|
||||
- FirebaseABTesting (11.1.0):
|
||||
- FirebaseABTesting (11.3.0):
|
||||
- FirebaseCore (~> 11.0)
|
||||
- FirebaseAnalytics (11.0.0):
|
||||
- FirebaseAnalytics/AdIdSupport (= 11.0.0)
|
||||
- FirebaseAnalytics (11.2.0):
|
||||
- FirebaseAnalytics/AdIdSupport (= 11.2.0)
|
||||
- FirebaseCore (~> 11.0)
|
||||
- FirebaseInstallations (~> 11.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||
@ -84,24 +86,24 @@ PODS:
|
||||
- GoogleUtilities/Network (~> 8.0)
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||
- nanopb (~> 3.30910.0)
|
||||
- FirebaseAnalytics/AdIdSupport (11.0.0):
|
||||
- FirebaseAnalytics/AdIdSupport (11.2.0):
|
||||
- FirebaseCore (~> 11.0)
|
||||
- FirebaseInstallations (~> 11.0)
|
||||
- GoogleAppMeasurement (= 11.0.0)
|
||||
- GoogleAppMeasurement (= 11.2.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||
- GoogleUtilities/Network (~> 8.0)
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||
- nanopb (~> 3.30910.0)
|
||||
- FirebaseCore (11.0.0):
|
||||
- FirebaseCore (11.2.0):
|
||||
- FirebaseCoreInternal (~> 11.0)
|
||||
- GoogleUtilities/Environment (~> 8.0)
|
||||
- GoogleUtilities/Logger (~> 8.0)
|
||||
- FirebaseCoreExtension (11.1.0):
|
||||
- FirebaseCoreExtension (11.3.0):
|
||||
- FirebaseCore (~> 11.0)
|
||||
- FirebaseCoreInternal (11.1.0):
|
||||
- FirebaseCoreInternal (11.3.0):
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||
- FirebaseCrashlytics (11.0.0):
|
||||
- FirebaseCrashlytics (11.2.0):
|
||||
- FirebaseCore (~> 11.0)
|
||||
- FirebaseInstallations (~> 11.0)
|
||||
- FirebaseRemoteConfigInterop (~> 11.0)
|
||||
@ -110,12 +112,12 @@ PODS:
|
||||
- GoogleUtilities/Environment (~> 8.0)
|
||||
- nanopb (~> 3.30910.0)
|
||||
- PromisesObjC (~> 2.4)
|
||||
- FirebaseInstallations (11.1.0):
|
||||
- FirebaseInstallations (11.3.0):
|
||||
- FirebaseCore (~> 11.0)
|
||||
- GoogleUtilities/Environment (~> 8.0)
|
||||
- GoogleUtilities/UserDefaults (~> 8.0)
|
||||
- PromisesObjC (~> 2.4)
|
||||
- FirebaseMessaging (11.0.0):
|
||||
- FirebaseMessaging (11.2.0):
|
||||
- FirebaseCore (~> 11.0)
|
||||
- FirebaseInstallations (~> 11.0)
|
||||
- GoogleDataTransport (~> 10.0)
|
||||
@ -124,7 +126,7 @@ PODS:
|
||||
- GoogleUtilities/Reachability (~> 8.0)
|
||||
- GoogleUtilities/UserDefaults (~> 8.0)
|
||||
- nanopb (~> 3.30910.0)
|
||||
- FirebasePerformance (11.0.0):
|
||||
- FirebasePerformance (11.2.0):
|
||||
- FirebaseCore (~> 11.0)
|
||||
- FirebaseInstallations (~> 11.0)
|
||||
- FirebaseRemoteConfig (~> 11.0)
|
||||
@ -134,7 +136,7 @@ PODS:
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||
- GoogleUtilities/UserDefaults (~> 8.0)
|
||||
- nanopb (~> 3.30910.0)
|
||||
- FirebaseRemoteConfig (11.1.0):
|
||||
- FirebaseRemoteConfig (11.3.0):
|
||||
- FirebaseABTesting (~> 11.0)
|
||||
- FirebaseCore (~> 11.0)
|
||||
- FirebaseInstallations (~> 11.0)
|
||||
@ -142,8 +144,8 @@ PODS:
|
||||
- FirebaseSharedSwift (~> 11.0)
|
||||
- GoogleUtilities/Environment (~> 8.0)
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||
- FirebaseRemoteConfigInterop (11.1.0)
|
||||
- FirebaseSessions (11.1.0):
|
||||
- FirebaseRemoteConfigInterop (11.3.0)
|
||||
- FirebaseSessions (11.3.0):
|
||||
- FirebaseCore (~> 11.0)
|
||||
- FirebaseCoreExtension (~> 11.0)
|
||||
- FirebaseInstallations (~> 11.0)
|
||||
@ -152,8 +154,10 @@ PODS:
|
||||
- GoogleUtilities/UserDefaults (~> 8.0)
|
||||
- nanopb (~> 3.30910.0)
|
||||
- PromisesSwift (~> 2.1)
|
||||
- FirebaseSharedSwift (11.1.0)
|
||||
- FirebaseSharedSwift (11.3.0)
|
||||
- Flutter (1.0.0)
|
||||
- flutter_app_update (0.0.1):
|
||||
- Flutter
|
||||
- flutter_background_service_ios (0.0.3):
|
||||
- Flutter
|
||||
- flutter_keyboard_visibility (0.0.1):
|
||||
@ -164,27 +168,30 @@ PODS:
|
||||
- Flutter
|
||||
- flutter_secure_storage (6.0.0):
|
||||
- Flutter
|
||||
- flutter_udid (0.0.1):
|
||||
- Flutter
|
||||
- SAMKeychain
|
||||
- flutter_webrtc (0.11.3):
|
||||
- Flutter
|
||||
- WebRTC-SDK (= 125.6422.04)
|
||||
- gal (1.0.0):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- GoogleAppMeasurement (11.0.0):
|
||||
- GoogleAppMeasurement/AdIdSupport (= 11.0.0)
|
||||
- GoogleAppMeasurement (11.2.0):
|
||||
- GoogleAppMeasurement/AdIdSupport (= 11.2.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.0.0):
|
||||
- GoogleAppMeasurement/WithoutAdIdSupport (= 11.0.0)
|
||||
- GoogleAppMeasurement/AdIdSupport (11.2.0):
|
||||
- GoogleAppMeasurement/WithoutAdIdSupport (= 11.2.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.0.0):
|
||||
- GoogleAppMeasurement/WithoutAdIdSupport (11.2.0):
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||
- GoogleUtilities/Network (~> 8.0)
|
||||
@ -225,7 +232,9 @@ PODS:
|
||||
- TOCropViewController (~> 2.7.4)
|
||||
- image_picker_ios (0.0.1):
|
||||
- Flutter
|
||||
- livekit_client (2.2.5):
|
||||
- in_app_review (0.2.0):
|
||||
- Flutter
|
||||
- livekit_client (2.2.6):
|
||||
- Flutter
|
||||
- WebRTC-SDK (= 125.6422.04)
|
||||
- media_kit_libs_ios_video (1.0.4):
|
||||
@ -255,6 +264,7 @@ PODS:
|
||||
- PromisesObjC (= 2.4.0)
|
||||
- protocol_handler_ios (0.0.1):
|
||||
- Flutter
|
||||
- SAMKeychain (1.5.3)
|
||||
- screen_brightness_ios (0.1.0):
|
||||
- Flutter
|
||||
- SDWebImage (5.19.7):
|
||||
@ -265,7 +275,7 @@ PODS:
|
||||
- shared_preferences_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- sqflite (0.0.3):
|
||||
- sqflite_darwin (0.0.4):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- "sqlite3 (3.46.1+1)":
|
||||
@ -300,21 +310,25 @@ DEPENDENCIES:
|
||||
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/darwin`)
|
||||
- device_info_plus (from `.symlinks/plugins/device_info_plus/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`)
|
||||
- firebase_core (from `.symlinks/plugins/firebase_core/ios`)
|
||||
- firebase_crashlytics (from `.symlinks/plugins/firebase_crashlytics/ios`)
|
||||
- firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
|
||||
- firebase_performance (from `.symlinks/plugins/firebase_performance/ios`)
|
||||
- Flutter (from `Flutter`)
|
||||
- flutter_app_update (from `.symlinks/plugins/flutter_app_update/ios`)
|
||||
- flutter_background_service_ios (from `.symlinks/plugins/flutter_background_service_ios/ios`)
|
||||
- flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`)
|
||||
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
|
||||
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
||||
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
|
||||
- flutter_udid (from `.symlinks/plugins/flutter_udid/ios`)
|
||||
- flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`)
|
||||
- gal (from `.symlinks/plugins/gal/darwin`)
|
||||
- image_cropper (from `.symlinks/plugins/image_cropper/ios`)
|
||||
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
||||
- in_app_review (from `.symlinks/plugins/in_app_review/ios`)
|
||||
- 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`)
|
||||
@ -328,7 +342,7 @@ DEPENDENCIES:
|
||||
- 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 (from `.symlinks/plugins/sqflite/darwin`)
|
||||
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
|
||||
- sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/ios`)
|
||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||
- volume_controller (from `.symlinks/plugins/volume_controller/ios`)
|
||||
@ -358,6 +372,7 @@ SPEC REPOS:
|
||||
- nanopb
|
||||
- PromisesObjC
|
||||
- PromisesSwift
|
||||
- SAMKeychain
|
||||
- SDWebImage
|
||||
- sqlite3
|
||||
- SwiftyGif
|
||||
@ -371,6 +386,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/device_info_plus/ios"
|
||||
file_picker:
|
||||
:path: ".symlinks/plugins/file_picker/ios"
|
||||
file_saver:
|
||||
:path: ".symlinks/plugins/file_saver/ios"
|
||||
firebase_analytics:
|
||||
:path: ".symlinks/plugins/firebase_analytics/ios"
|
||||
firebase_core:
|
||||
@ -383,6 +400,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/firebase_performance/ios"
|
||||
Flutter:
|
||||
:path: Flutter
|
||||
flutter_app_update:
|
||||
:path: ".symlinks/plugins/flutter_app_update/ios"
|
||||
flutter_background_service_ios:
|
||||
:path: ".symlinks/plugins/flutter_background_service_ios/ios"
|
||||
flutter_keyboard_visibility:
|
||||
@ -393,6 +412,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
||||
flutter_secure_storage:
|
||||
:path: ".symlinks/plugins/flutter_secure_storage/ios"
|
||||
flutter_udid:
|
||||
:path: ".symlinks/plugins/flutter_udid/ios"
|
||||
flutter_webrtc:
|
||||
:path: ".symlinks/plugins/flutter_webrtc/ios"
|
||||
gal:
|
||||
@ -401,6 +422,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/image_cropper/ios"
|
||||
image_picker_ios:
|
||||
:path: ".symlinks/plugins/image_picker_ios/ios"
|
||||
in_app_review:
|
||||
:path: ".symlinks/plugins/in_app_review/ios"
|
||||
livekit_client:
|
||||
:path: ".symlinks/plugins/livekit_client/ios"
|
||||
media_kit_libs_ios_video:
|
||||
@ -427,8 +450,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/share_plus/ios"
|
||||
shared_preferences_foundation:
|
||||
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
||||
sqflite:
|
||||
:path: ".symlinks/plugins/sqflite/darwin"
|
||||
sqflite_darwin:
|
||||
:path: ".symlinks/plugins/sqflite_darwin/darwin"
|
||||
sqlite3_flutter_libs:
|
||||
:path: ".symlinks/plugins/sqlite3_flutter_libs/ios"
|
||||
url_launcher_ios:
|
||||
@ -439,49 +462,53 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/wakelock_plus/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db
|
||||
device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d
|
||||
connectivity_plus: 4c41c08fc6d7c91f63bc7aec70ffe3730b04f563
|
||||
device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342
|
||||
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
|
||||
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
||||
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
|
||||
Firebase: 9f574c08c2396885b5e7e100ed4293d956218af9
|
||||
firebase_analytics: b8ce6c2c4b245d3c3bb3a147965d09da0f455959
|
||||
firebase_core: ba84e940cf5cbbc601095f86556560937419195c
|
||||
firebase_crashlytics: 4111f8198b78c99471c955af488cecd8224967e6
|
||||
firebase_messaging: c40f84e7a98da956d5262fada373b5c458edcf13
|
||||
firebase_performance: 8b7b9ca5adf3a9b3afa12b4eb96b9cabefc2c248
|
||||
FirebaseABTesting: c2e22c3aab99afa81d0561708b2c1c356c556976
|
||||
FirebaseAnalytics: 27eb78b97880ea4a004839b9bac0b58880f5a92a
|
||||
FirebaseCore: 3cf438f431f18c12cdf2aaf64434648b63f7e383
|
||||
FirebaseCoreExtension: aa5c9779c2d0d39d83f1ceb3fdbafe80c4feecfa
|
||||
FirebaseCoreInternal: adefedc9a88dbe393c4884640a73ec9e8e790f8c
|
||||
FirebaseCrashlytics: 745d8f0221fe49c62865391d1bf56f5a12eeec0b
|
||||
FirebaseInstallations: d0a8fea5a6fa91abc661591cf57c0f0d70863e57
|
||||
FirebaseMessaging: d2d1d9c62c46dd2db49a952f7deb5b16ad2c9742
|
||||
FirebasePerformance: efdc02bacb1b4710588c9f867011605c081cdf79
|
||||
FirebaseRemoteConfig: 05521e937b72e01847a7128da5a492327364c705
|
||||
FirebaseRemoteConfigInterop: abf8b1bbc0bf1b84abd22b66746926410bf91a87
|
||||
FirebaseSessions: 78f137e68dc01ca71606169ba4ac73b98c13752a
|
||||
FirebaseSharedSwift: 260a35e08943ec810d820a70bc0359136351d0c5
|
||||
file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808
|
||||
Firebase: 98e6bf5278170668a7983e12971a66b2cd57fc8c
|
||||
firebase_analytics: fbc57838bdb94eef1e0ff504f127d974ff2981ad
|
||||
firebase_core: 2bedc3136ec7c7b8561c6123ed0239387b53f2af
|
||||
firebase_crashlytics: 37d104d457b51760b48504a93a12b3bf70995d77
|
||||
firebase_messaging: 15d114e1a41fc31e4fbabcd48d765a19eec94a38
|
||||
firebase_performance: 26ad47755d3e8d7b04b9bb36bdfbf1cec8d8dfcc
|
||||
FirebaseABTesting: c4559fcd2eba9f6bdaf0599e2c37ded01c343e4c
|
||||
FirebaseAnalytics: c36efd5710c60c17558650fa58c2066eca7e9265
|
||||
FirebaseCore: a282032ae9295c795714ded2ec9c522fc237f8da
|
||||
FirebaseCoreExtension: 30bb063476ef66cd46925243d64ad8b2c8ac3264
|
||||
FirebaseCoreInternal: ac26d09a70c730e497936430af4e60fb0c68ec4e
|
||||
FirebaseCrashlytics: cfc69af5b53565dc6a5e563788809b5778ac4eac
|
||||
FirebaseInstallations: 58cf94dabf1e2bb2fa87725a9be5c2249171cda0
|
||||
FirebaseMessaging: c9ec7b90c399c7a6100297e9d16f8a27fc7f7152
|
||||
FirebasePerformance: c39138c0700b8ef6040f0b80b5707320808e2862
|
||||
FirebaseRemoteConfig: 5be2ca4f9870d475b39214210955fdaeecf7e5ca
|
||||
FirebaseRemoteConfigInterop: c3a5c31b3c22079f41ba1dc645df889d9ce38cb9
|
||||
FirebaseSessions: 655ff17f3cc1a635cbdc2d69b953878001f9e25b
|
||||
FirebaseSharedSwift: d39c2ad64a11a8d936ce25a42b00df47078bb59c
|
||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||
flutter_app_update: 65f61da626cb111d1b24674abc4b01728d7723bc
|
||||
flutter_background_service_ios: e30e0d3ee69e4cee66272d0c78eacd48c2e94aac
|
||||
flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069
|
||||
flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086
|
||||
flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778
|
||||
flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
|
||||
flutter_udid: a2482c67a61b9c806ef59dd82ed8d007f1b7ac04
|
||||
flutter_webrtc: 75b868e4f9e817c7a9a42ca4b6169063de4eec9f
|
||||
gal: 61e868295d28fe67ffa297fae6dacebf56fd53e1
|
||||
GoogleAppMeasurement: 6e49ffac7d3f2c3ded9cc663f912a13b67bbd0de
|
||||
GoogleAppMeasurement: 76d4f8b36b03bd8381fa9a7fe2cc7f99c0a2e93a
|
||||
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
|
||||
image_cropper: 37d40f62177c101ff4c164906d259ea2c3aa70cf
|
||||
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
|
||||
livekit_client: 9c8080879256a0fb16da13c9be4845248209d896
|
||||
in_app_review: 318597b3a06c22bb46dc454d56828c85f444f99d
|
||||
livekit_client: 20e01637431bc108dad451c8a11c1d206e1dd2cd
|
||||
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
|
||||
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
|
||||
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
|
||||
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
||||
package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c
|
||||
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
|
||||
pasteboard: 982969ebaa7c78af3e6cc7761e8f5e77565d9ce0
|
||||
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
||||
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
|
||||
@ -489,11 +516,12 @@ SPEC CHECKSUMS:
|
||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
|
||||
protocol_handler_ios: a5db8abc38526ee326988b808be621e5fd568990
|
||||
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
||||
screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625
|
||||
SDWebImage: 8a6b7b160b4d710e2a22b6900e25301075c34cb3
|
||||
share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad
|
||||
share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
|
||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
|
||||
sqflite_darwin: a553b1fd6fe66f53bbb0fe5b4f5bab93f08d7a13
|
||||
sqlite3: 0bb0e6389d824e40296f531b858a2a0b71c0d2fb
|
||||
sqlite3_flutter_libs: c00457ebd31e59fa6bb830380ddba24d44fbcd3b
|
||||
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
||||
|
@ -616,6 +616,7 @@
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
@ -920,6 +921,7 @@
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
@ -947,6 +949,7 @@
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
|
@ -59,6 +59,7 @@
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
showGraphicsOverview = "Yes"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
|
@ -1,87 +1,91 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleURLName</key>
|
||||
<string></string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>solink</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>FirebaseMessagingAutoInitEnabled</key>
|
||||
<false/>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Solian</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>solian</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>Allow you take photo/video for your message or post</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>Allow you record audio for your message or post</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>Allow you add photo to your message or post</string>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>audio</string>
|
||||
<string>fetch</string>
|
||||
<string>remote-notification</string>
|
||||
<string>voip</string>
|
||||
</array>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>FlutterDeepLinkingEnabled</key>
|
||||
<true/>
|
||||
<key>NSUserActivityTypes</key>
|
||||
<array>
|
||||
<string>INSendMessageIntent</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIStatusBarHidden</key>
|
||||
<false/>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleURLName</key>
|
||||
<string></string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>solink</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>FirebaseMessagingAutoInitEnabled</key>
|
||||
<false/>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Solian</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>solian</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>Allow you take photo/video for your message or post</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>Allow you record audio for your message or post</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>Allow you add photo to your message or post</string>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>audio</string>
|
||||
<string>fetch</string>
|
||||
<string>remote-notification</string>
|
||||
<string>voip</string>
|
||||
</array>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>FlutterDeepLinkingEnabled</key>
|
||||
<true/>
|
||||
<key>NSUserActivityTypes</key>
|
||||
<array>
|
||||
<string>INSendMessageIntent</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>CFBundleLocalizations</key>
|
||||
<array>
|
||||
<string>en</string>
|
||||
</array>
|
||||
<key>UIStatusBarHidden</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
@ -1,19 +1,34 @@
|
||||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:confetti/confetti.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:in_app_review/in_app_review.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:solian/exceptions/request.dart';
|
||||
import 'package:solian/exts.dart';
|
||||
import 'package:solian/models/account.dart';
|
||||
import 'package:solian/platform.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/providers/content/channel.dart';
|
||||
import 'package:solian/providers/content/realm.dart';
|
||||
import 'package:solian/providers/notifications.dart';
|
||||
import 'package:solian/providers/relation.dart';
|
||||
import 'package:solian/providers/stickers.dart';
|
||||
import 'package:solian/providers/theme_switcher.dart';
|
||||
import 'package:solian/providers/websocket.dart';
|
||||
import 'package:solian/services.dart';
|
||||
import 'package:solian/widgets/root_container.dart';
|
||||
import 'package:solian/widgets/sized_container.dart';
|
||||
import 'package:flutter_app_update/flutter_app_update.dart';
|
||||
import 'package:version/version.dart';
|
||||
|
||||
enum BootstrapperSpecialState {
|
||||
userBirthday,
|
||||
appAnniversary,
|
||||
}
|
||||
|
||||
class BootstrapperShell extends StatefulWidget {
|
||||
final Widget child;
|
||||
@ -35,6 +50,111 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
|
||||
|
||||
int _periodCursor = 0;
|
||||
|
||||
// Special state is some special event triggered after bootstrapping
|
||||
BootstrapperSpecialState? _specialState;
|
||||
|
||||
final Completer _bootCompleter = Completer();
|
||||
|
||||
void _requestRating() 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)))) {
|
||||
final inAppReview = InAppReview.instance;
|
||||
if (prefs.getBool('rating_requested') == true) return;
|
||||
if (await inAppReview.isAvailable()) {
|
||||
await inAppReview.requestReview();
|
||||
prefs.setBool('rating_requested', true);
|
||||
} else {
|
||||
log('Unable request app review, unavailable');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
prefs.setString('first_boot_time', DateTime.now().toIso8601String());
|
||||
}
|
||||
}
|
||||
|
||||
void _updateNow(String localVersionString, String remoteVersionString) {
|
||||
context
|
||||
.showConfirmDialog(
|
||||
'updateAvailable'.tr,
|
||||
'updateAvailableDesc'.trParams({
|
||||
'from': localVersionString,
|
||||
'to': remoteVersionString,
|
||||
}),
|
||||
)
|
||||
.then((result) {
|
||||
if (result) {
|
||||
final model = UpdateModel(
|
||||
'https://files.solsynth.dev/d/production01/solian/app-arm64-v8a-release.apk',
|
||||
'solian-app-arm64-v8a-release.apk',
|
||||
'ic_launcher',
|
||||
'https://testflight.apple.com/join/YJ0lmN6O',
|
||||
);
|
||||
AzhonAppUpdate.update(model);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _checkForUpdate() async {
|
||||
if (PlatformInfo.isWeb) return;
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final info = await PackageInfo.fromPlatform();
|
||||
final localVersionString = '${info.version}+${info.buildNumber}';
|
||||
final resp = await GetConnect(
|
||||
timeout: const Duration(seconds: 60),
|
||||
).get(
|
||||
'https://git.solsynth.dev/api/v1/repos/hydrogen/solian/tags?page=1&limit=1',
|
||||
);
|
||||
if (resp.statusCode != 200) {
|
||||
throw RequestException(resp);
|
||||
}
|
||||
final remoteVersionString =
|
||||
(resp.body 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;
|
||||
final strictUpdate = prefs.getBool('check_update_strictly') ?? false;
|
||||
if (remoteVersion > localVersion ||
|
||||
(remoteVersion == localVersion &&
|
||||
remoteBuildNumber > localBuildNumber) ||
|
||||
(remoteVersionString != localVersionString && strictUpdate)) {
|
||||
if (PlatformInfo.isAndroid) {
|
||||
_updateNow(localVersionString, remoteVersionString);
|
||||
} else {
|
||||
context.showInfoDialog(
|
||||
'updateAvailable'.tr,
|
||||
'bsCheckForUpdateDesc'.tr,
|
||||
);
|
||||
}
|
||||
} else if (remoteVersionString != localVersionString) {
|
||||
_bootCompleter.future.then((_) {
|
||||
context.showSnackbar(
|
||||
'updateMayAvailable'.trParams({
|
||||
'version': remoteVersionString,
|
||||
}),
|
||||
action: PlatformInfo.isAndroid
|
||||
? SnackBarAction(
|
||||
label: 'updateNow'.tr,
|
||||
onPressed: () {
|
||||
_updateNow(localVersionString, remoteVersionString);
|
||||
},
|
||||
)
|
||||
: null,
|
||||
);
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
context.showErrorDialog('Unable to check update: $e');
|
||||
}
|
||||
}
|
||||
|
||||
late final List<({String label, Future<void> Function() action})> _periods = [
|
||||
(
|
||||
label: 'bsLoadingTheme',
|
||||
@ -42,32 +162,6 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
|
||||
await context.read<ThemeSwitcher>().restoreTheme();
|
||||
},
|
||||
),
|
||||
(
|
||||
label: 'bsCheckForUpdate',
|
||||
action: () async {
|
||||
if (PlatformInfo.isWeb) return;
|
||||
try {
|
||||
final info = await PackageInfo.fromPlatform();
|
||||
final localVersionString = '${info.version}+${info.buildNumber}';
|
||||
final resp = await GetConnect().get(
|
||||
'https://git.solsynth.dev/api/v1/repos/hydrogen/solian/tags?limit=1',
|
||||
);
|
||||
if (resp.body[0]['name'] != localVersionString) {
|
||||
setState(() {
|
||||
_isErrored = true;
|
||||
_subtitle = PlatformInfo.isIOS || PlatformInfo.isMacOS
|
||||
? 'bsCheckForUpdateDescApple'.tr
|
||||
: 'bsCheckForUpdateDescCommon'.tr;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_isErrored = true;
|
||||
_subtitle = 'bsCheckForUpdateFailed'.tr;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
(
|
||||
label: 'bsCheckingServer',
|
||||
action: () async {
|
||||
@ -115,14 +209,26 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
|
||||
final AuthProvider auth = Get.find();
|
||||
try {
|
||||
await Future.wait([
|
||||
Get.find<StickerProvider>().refreshAvailableStickers(),
|
||||
if (auth.isAuthorized.isTrue)
|
||||
Get.find<ChannelProvider>().refreshAvailableChannel(),
|
||||
Get.find<NotificationProvider>().fetchNotification(),
|
||||
if (auth.isAuthorized.isTrue)
|
||||
Get.find<RelationshipProvider>().refreshRelativeList(),
|
||||
if (auth.isAuthorized.isTrue)
|
||||
Get.find<RealmProvider>().refreshAvailableRealms(),
|
||||
]);
|
||||
|
||||
if (auth.isAuthorized.isTrue && auth.userProfile.value != null) {
|
||||
final account = Account.fromJson(auth.userProfile.value!);
|
||||
if (account.profile?.birthday != null) {
|
||||
final birthDate = account.profile!.birthday!.toLocal();
|
||||
final isBirthday = birthDate.day == DateTime.now().day;
|
||||
if (isBirthday) {
|
||||
setState(
|
||||
() => _specialState = BootstrapperSpecialState.userBirthday,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
context.showErrorDialog(e);
|
||||
}
|
||||
@ -134,7 +240,7 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isTrue) {
|
||||
try {
|
||||
Get.find<WebSocketProvider>().registerPushNotifications();
|
||||
Get.find<NotificationProvider>().registerPushNotifications();
|
||||
} catch (err) {
|
||||
context.showSnackbar(
|
||||
'pushNotifyRegisterFailed'.trParams({'reason': err.toString()}),
|
||||
@ -156,6 +262,9 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
|
||||
}
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
Future.delayed(const Duration(milliseconds: 100), () {
|
||||
_bootCompleter.complete();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -163,14 +272,17 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
_runPeriods();
|
||||
_checkForUpdate();
|
||||
_bootCompleter.future.then((_) {
|
||||
_requestRating();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_isBusy || _isErrored) {
|
||||
return GestureDetector(
|
||||
child: Material(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: RootContainer(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
@ -253,6 +365,9 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
|
||||
_isBusy = false;
|
||||
_isErrored = false;
|
||||
});
|
||||
Future.delayed(const Duration(milliseconds: 100), () {
|
||||
_bootCompleter.complete();
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
_isBusy = true;
|
||||
@ -263,8 +378,142 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
|
||||
}
|
||||
},
|
||||
);
|
||||
} else if (_specialState != null) {
|
||||
return GestureDetector(
|
||||
child: RootContainer(
|
||||
child: switch (_specialState) {
|
||||
BootstrapperSpecialState.appAnniversary => const Placeholder(),
|
||||
_ => _BirthdaySpecialScreen(),
|
||||
},
|
||||
),
|
||||
onTap: () {
|
||||
setState(() => _specialState = null);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return widget.child;
|
||||
}
|
||||
}
|
||||
|
||||
class _BirthdaySpecialScreen extends StatefulWidget {
|
||||
const _BirthdaySpecialScreen();
|
||||
|
||||
@override
|
||||
State<_BirthdaySpecialScreen> createState() => _BirthdaySpecialScreenState();
|
||||
}
|
||||
|
||||
class _BirthdaySpecialScreenState extends State<_BirthdaySpecialScreen> {
|
||||
late final ConfettiController _confettiController =
|
||||
ConfettiController(duration: const Duration(seconds: 10));
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_confettiController.play();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_confettiController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Color get _unFocusColor =>
|
||||
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
|
||||
|
||||
String _toOrdinal(int num) {
|
||||
if (num >= 11 && num <= 13) {
|
||||
return '${num}th';
|
||||
}
|
||||
|
||||
switch (num % 10) {
|
||||
case 1:
|
||||
return '${num}st';
|
||||
case 2:
|
||||
return '${num}nd';
|
||||
case 3:
|
||||
return '${num}rd';
|
||||
default:
|
||||
return '${num}th';
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final AuthProvider auth = Get.find();
|
||||
final account = Account.fromJson(auth.userProfile.value!);
|
||||
|
||||
final birthDate = account.profile!.birthday!.toLocal();
|
||||
final birthdayCount = DateTime.now().difference(birthDate).inDays ~/ 365;
|
||||
|
||||
return Stack(
|
||||
children: <Widget>[
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: ConfettiWidget(
|
||||
confettiController: _confettiController,
|
||||
blastDirectionality: BlastDirectionality.explosive,
|
||||
shouldLoop: true,
|
||||
colors: const [
|
||||
Colors.green,
|
||||
Colors.blue,
|
||||
Colors.pink,
|
||||
Colors.orange,
|
||||
Colors.purple
|
||||
],
|
||||
maxBlastForce: 30,
|
||||
minBlastForce: 15,
|
||||
emissionFrequency: 0.05,
|
||||
numberOfParticles: 20,
|
||||
gravity: 0.2,
|
||||
),
|
||||
),
|
||||
Align(
|
||||
child: CenteredContainer(
|
||||
maxWidth: 320,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'🎂',
|
||||
style: TextStyle(fontSize: 60),
|
||||
),
|
||||
const Gap(8),
|
||||
Text(
|
||||
'happyBirthday'.trParams({
|
||||
'name': account.profile?.firstName != null
|
||||
? [
|
||||
account.profile?.firstName,
|
||||
account.profile?.lastName
|
||||
].join(' ')
|
||||
: '@${account.name}',
|
||||
}),
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
Text(
|
||||
'happyBirthdayDesc'.trParams({
|
||||
'count': _toOrdinal(birthdayCount),
|
||||
}),
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
const Gap(8),
|
||||
Text(
|
||||
'bsContinuable'.tr,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: _unFocusColor,
|
||||
),
|
||||
).paddingOnly(bottom: 5),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/models/channel.dart';
|
||||
import 'package:solian/models/event.dart';
|
||||
@ -30,14 +32,31 @@ class ChatEventController {
|
||||
this.channel = channel;
|
||||
this.scope = scope;
|
||||
|
||||
isLoading.value = true;
|
||||
await syncLocal(channel, take: 10);
|
||||
const firstTake = 20;
|
||||
const furtherTake = 100;
|
||||
|
||||
src.pullRemoteEvents(channel, scope: scope, take: 10).then((result) {
|
||||
totalEvents.value = result?.$2 ?? 0;
|
||||
syncLocal(channel, take: 10);
|
||||
});
|
||||
isLoading.value = true;
|
||||
await syncLocal(channel, take: firstTake);
|
||||
isLoading.value = false;
|
||||
|
||||
// Take a small range of messages to check is local database up to date
|
||||
var isUpToDate = true;
|
||||
final result =
|
||||
await src.pullRemoteEvents(channel, scope: scope, take: firstTake);
|
||||
totalEvents.value = result?.$2 ?? 0;
|
||||
if ((result?.$1.length ?? 0) > 0) {
|
||||
final minId = result!.$1.map((x) => x.id).reduce(math.min);
|
||||
isUpToDate = await src.getEventFromLocal(minId) != null;
|
||||
}
|
||||
syncLocal(channel, take: firstTake);
|
||||
|
||||
if (!isUpToDate) {
|
||||
// Loading more content due to isn't up to date
|
||||
final result =
|
||||
await src.pullRemoteEvents(channel, scope: scope, take: furtherTake);
|
||||
totalEvents.value = result?.$2 ?? 0;
|
||||
syncLocal(channel, take: furtherTake);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadEvents(Channel channel, String scope) async {
|
||||
@ -46,7 +65,9 @@ class ChatEventController {
|
||||
|
||||
isLoading.value = true;
|
||||
await syncLocal(channel, take: take, offset: offset);
|
||||
src.pullRemoteEvents(channel, scope: scope, offset: offset).then((result) {
|
||||
src
|
||||
.pullRemoteEvents(channel, scope: scope, take: take, offset: offset)
|
||||
.then((result) {
|
||||
totalEvents.value = result?.$2 ?? 0;
|
||||
syncLocal(channel, take: take, offset: offset);
|
||||
});
|
||||
@ -56,7 +77,11 @@ class ChatEventController {
|
||||
Future<bool> syncLocal(Channel channel,
|
||||
{required int take, int offset = 0}) async {
|
||||
final data = await src.listEvents(channel, take: take, offset: offset);
|
||||
currentEvents.replaceRange(0, currentEvents.length, data);
|
||||
if (currentEvents.length >= offset + take) {
|
||||
currentEvents.replaceRange(offset, offset + take, data);
|
||||
} else {
|
||||
currentEvents.insertAll(currentEvents.length, data);
|
||||
}
|
||||
for (final x in data.reversed) {
|
||||
applyEvent(x);
|
||||
}
|
||||
|
@ -43,14 +43,17 @@ class PostEditorController extends GetxController {
|
||||
|
||||
RxBool isRestoreFromLocal = false.obs;
|
||||
Rx<DateTime?> lastSaveTime = Rx(null);
|
||||
Timer? _saveTimer;
|
||||
Future? _saveFuture;
|
||||
|
||||
PostEditorController() {
|
||||
SharedPreferences.getInstance().then((inst) {
|
||||
_prefs = inst;
|
||||
_saveTimer = Timer.periodic(
|
||||
const Duration(seconds: 3),
|
||||
(Timer t) {
|
||||
});
|
||||
contentController.addListener(() {
|
||||
contentLength.value = contentController.text.length;
|
||||
_saveFuture ??= Future.delayed(
|
||||
const Duration(seconds: 1),
|
||||
() {
|
||||
if (isNotEmpty) {
|
||||
localSave();
|
||||
lastSaveTime.value = DateTime.now();
|
||||
@ -59,12 +62,10 @@ class PostEditorController extends GetxController {
|
||||
localClear();
|
||||
lastSaveTime.value = null;
|
||||
}
|
||||
_saveFuture = null;
|
||||
},
|
||||
);
|
||||
});
|
||||
contentController.addListener(() {
|
||||
contentLength.value = contentController.text.length;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> editOverview(BuildContext context) {
|
||||
@ -124,6 +125,21 @@ class PostEditorController extends GetxController {
|
||||
onRemove: (String value) {
|
||||
attachments.remove(value);
|
||||
},
|
||||
onInsert: (String str) {
|
||||
final text = contentController.text;
|
||||
final selection = contentController.selection;
|
||||
final newText = text.replaceRange(
|
||||
selection.start,
|
||||
selection.end,
|
||||
str,
|
||||
);
|
||||
contentController.value = TextEditingValue(
|
||||
text: newText,
|
||||
selection: TextSelection.collapsed(
|
||||
offset: selection.baseOffset + str.length,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -355,8 +371,6 @@ class PostEditorController extends GetxController {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_saveTimer?.cancel();
|
||||
|
||||
titleController.dispose();
|
||||
descriptionController.dispose();
|
||||
contentController.dispose();
|
||||
|
@ -1,9 +1,12 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:get/get.dart';
|
||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
import 'package:solian/models/attachment.dart';
|
||||
import 'package:solian/models/pagination.dart';
|
||||
import 'package:solian/models/post.dart';
|
||||
import 'package:solian/providers/content/attachment.dart';
|
||||
import 'package:solian/providers/content/posts.dart';
|
||||
import 'package:solian/providers/last_read.dart';
|
||||
|
||||
@ -31,9 +34,18 @@ class PostListController extends GetxController {
|
||||
pagingController.addPageRequestListener(_onPagingControllerRequest);
|
||||
}
|
||||
|
||||
Completer<void>? _pagingLoadCompleter;
|
||||
|
||||
Future<void> _onPagingControllerRequest(int pageKey) async {
|
||||
try {
|
||||
if (_pagingLoadCompleter != null) {
|
||||
await _pagingLoadCompleter!.future;
|
||||
return;
|
||||
}
|
||||
_pagingLoadCompleter = Completer();
|
||||
final result = await loadMore();
|
||||
_pagingLoadCompleter!.complete();
|
||||
_pagingLoadCompleter = null;
|
||||
|
||||
if (result != null && hasMore.value) {
|
||||
pagingController.appendPage(result, nextPageKey.value);
|
||||
@ -97,9 +109,6 @@ class PostListController extends GetxController {
|
||||
hasMore.value = false;
|
||||
}
|
||||
|
||||
final idx = <dynamic>{};
|
||||
postList.retainWhere((x) => idx.add(x.id));
|
||||
|
||||
if (postList.isNotEmpty) {
|
||||
var lastId = postList.map((x) => x.id).reduce(max);
|
||||
Get.find<LastReadProvider>().feedLastReadAt = lastId;
|
||||
@ -111,35 +120,39 @@ class PostListController extends GetxController {
|
||||
Future<List<Post>?> _loadPosts(int pageKey) async {
|
||||
isBusy.value = true;
|
||||
|
||||
final PostProvider provider = Get.find();
|
||||
final PostProvider posts = Get.find();
|
||||
|
||||
Response resp;
|
||||
try {
|
||||
if (author != null) {
|
||||
resp = await provider.listPost(
|
||||
resp = await posts.listPost(
|
||||
pageKey,
|
||||
author: author,
|
||||
take: 10,
|
||||
);
|
||||
} else {
|
||||
switch (mode.value) {
|
||||
case 2:
|
||||
resp = await provider.listRecommendations(
|
||||
resp = await posts.listRecommendations(
|
||||
pageKey,
|
||||
channel: 'shuffle',
|
||||
realm: realm,
|
||||
take: 10,
|
||||
);
|
||||
break;
|
||||
case 1:
|
||||
resp = await provider.listRecommendations(
|
||||
resp = await posts.listRecommendations(
|
||||
pageKey,
|
||||
channel: 'friends',
|
||||
realm: realm,
|
||||
take: 10,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
resp = await provider.listRecommendations(
|
||||
resp = await posts.listRecommendations(
|
||||
pageKey,
|
||||
realm: realm,
|
||||
take: 10,
|
||||
);
|
||||
break;
|
||||
}
|
||||
@ -153,6 +166,27 @@ class PostListController extends GetxController {
|
||||
final result = PaginationResult.fromJson(resp.body);
|
||||
final out = result.data?.map((e) => Post.fromJson(e)).toList();
|
||||
|
||||
final AttachmentProvider attach = Get.find();
|
||||
|
||||
if (out != null) {
|
||||
final attachmentIds = out
|
||||
.mapMany((x) => x.body['attachments'] ?? [])
|
||||
.cast<String>()
|
||||
.toSet()
|
||||
.toList();
|
||||
final attachmentOut = await attach.listMetadata(attachmentIds);
|
||||
|
||||
for (var idx = 0; idx < out.length; idx++) {
|
||||
final rids = List<String>.from(out[idx].body['attachments'] ?? []);
|
||||
out[idx].preload = PostPreload(
|
||||
attachments: attachmentOut
|
||||
.where((x) => x != null && rids.contains(x.rid))
|
||||
.cast<Attachment>()
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
postTotal.value = result.count;
|
||||
|
||||
return out;
|
||||
|
@ -1,6 +1,8 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:action_slider/action_slider.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/exceptions/request.dart';
|
||||
import 'package:solian/exceptions/unauthorized.dart';
|
||||
@ -51,6 +53,69 @@ extension AppExtensions on BuildContext {
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> showConfirmDialog(String title, body) async {
|
||||
return await showDialog<bool>(
|
||||
useRootNavigator: true,
|
||||
context: this,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: Text(title),
|
||||
content: Text(body),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, false),
|
||||
child: Text('cancel'.tr),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, true),
|
||||
child: Text('okay'.tr),
|
||||
)
|
||||
],
|
||||
),
|
||||
) ??
|
||||
false;
|
||||
}
|
||||
|
||||
Future<bool> showSlideToConfirmDialog(String title, body) async {
|
||||
return await showDialog<bool>(
|
||||
useRootNavigator: true,
|
||||
context: this,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: Text(title, textAlign: TextAlign.center),
|
||||
content: SizedBox(
|
||||
width: double.maxFinite,
|
||||
child: ListView(
|
||||
shrinkWrap: true,
|
||||
children: [
|
||||
Text(body, textAlign: TextAlign.center),
|
||||
const Gap(28),
|
||||
ActionSlider.standard(
|
||||
icon: const Icon(Icons.send),
|
||||
iconAlignment: Alignment.center,
|
||||
sliderBehavior: SliderBehavior.move,
|
||||
actionThresholdType: ThresholdType.release,
|
||||
toggleColor: Colors.red,
|
||||
action: (controller) async {
|
||||
controller.success();
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
Navigator.pop(ctx, true);
|
||||
},
|
||||
child: Text('slideToConfirm'.tr),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actionsAlignment: MainAxisAlignment.center,
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, false),
|
||||
child: Text('cancel'.tr),
|
||||
)
|
||||
],
|
||||
),
|
||||
) ??
|
||||
false;
|
||||
}
|
||||
|
||||
Future<void> showErrorDialog(dynamic exception) {
|
||||
Widget content = Text(exception.toString().capitalize!);
|
||||
if (exception is UnauthorizedException) {
|
||||
|
@ -9,7 +9,6 @@ import 'package:go_router/go_router.dart';
|
||||
import 'package:protocol_handler/protocol_handler.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:solian/background.dart';
|
||||
import 'package:solian/bootstrapper.dart';
|
||||
import 'package:solian/firebase_options.dart';
|
||||
import 'package:solian/platform.dart';
|
||||
import 'package:solian/providers/attachment_uploader.dart';
|
||||
@ -19,7 +18,9 @@ import 'package:solian/providers/database/services/messages.dart';
|
||||
import 'package:solian/providers/last_read.dart';
|
||||
import 'package:solian/providers/link_expander.dart';
|
||||
import 'package:solian/providers/navigation.dart';
|
||||
import 'package:solian/providers/notifications.dart';
|
||||
import 'package:solian/providers/stickers.dart';
|
||||
import 'package:solian/providers/subscription.dart';
|
||||
import 'package:solian/providers/theme_switcher.dart';
|
||||
import 'package:solian/providers/websocket.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
@ -57,13 +58,16 @@ void main() async {
|
||||
|
||||
Future<void> _initializeFirebase() async {
|
||||
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
|
||||
FlutterError.onError = (errorDetails) {
|
||||
FirebaseCrashlytics.instance.recordFlutterFatalError(errorDetails);
|
||||
};
|
||||
PlatformDispatcher.instance.onError = (error, stack) {
|
||||
FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
|
||||
return true;
|
||||
};
|
||||
if (PlatformInfo.isIOS || PlatformInfo.isAndroid || PlatformInfo.isMacOS) {
|
||||
// Initialize firebase crashlytics for the platform that supported
|
||||
FlutterError.onError = (errorDetails) {
|
||||
FirebaseCrashlytics.instance.recordFlutterFatalError(errorDetails);
|
||||
};
|
||||
PlatformDispatcher.instance.onError = (error, stack) {
|
||||
FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
|
||||
return true;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _initializeBackgroundNotificationService() async {
|
||||
@ -122,9 +126,7 @@ class SolianApp extends StatelessWidget {
|
||||
builder: (context, child) {
|
||||
return SystemShell(
|
||||
child: ScaffoldMessenger(
|
||||
child: BootstrapperShell(
|
||||
child: child ?? const SizedBox.shrink(),
|
||||
),
|
||||
child: child ?? const SizedBox.shrink(),
|
||||
),
|
||||
);
|
||||
},
|
||||
@ -137,11 +139,12 @@ class SolianApp extends StatelessWidget {
|
||||
Get.put(NavigationStateProvider());
|
||||
|
||||
Get.lazyPut(() => AuthProvider());
|
||||
Get.lazyPut(() => WebSocketProvider());
|
||||
Get.lazyPut(() => RelationshipProvider());
|
||||
Get.lazyPut(() => PostProvider());
|
||||
Get.lazyPut(() => StickerProvider());
|
||||
Get.lazyPut(() => AttachmentProvider());
|
||||
Get.lazyPut(() => WebSocketProvider());
|
||||
Get.lazyPut(() => NotificationProvider());
|
||||
Get.lazyPut(() => StatusProvider());
|
||||
Get.lazyPut(() => ChannelProvider());
|
||||
Get.lazyPut(() => RealmProvider());
|
||||
@ -151,7 +154,8 @@ class SolianApp extends StatelessWidget {
|
||||
Get.lazyPut(() => LinkExpandProvider());
|
||||
Get.lazyPut(() => DailySignProvider());
|
||||
Get.lazyPut(() => LastReadProvider());
|
||||
Get.lazyPut(() => SubscriptionProvider());
|
||||
|
||||
Get.find<WebSocketProvider>().requestPermissions();
|
||||
Get.find<NotificationProvider>().requestPermissions();
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'account.g.dart';
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'account_status.g.dart';
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:solian/models/account.dart';
|
||||
|
||||
part 'attachment.g.dart';
|
||||
|
38
lib/models/audit_log.dart
Normal file
38
lib/models/audit_log.dart
Normal file
@ -0,0 +1,38 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:solian/models/account.dart';
|
||||
|
||||
part 'audit_log.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class AuditEvent {
|
||||
int id;
|
||||
DateTime createdAt;
|
||||
DateTime updatedAt;
|
||||
DateTime? deletedAt;
|
||||
String type;
|
||||
String target;
|
||||
String location;
|
||||
String ipAddress;
|
||||
String userAgent;
|
||||
Account account;
|
||||
int accountId;
|
||||
|
||||
AuditEvent({
|
||||
required this.id,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
required this.deletedAt,
|
||||
required this.type,
|
||||
required this.target,
|
||||
required this.location,
|
||||
required this.ipAddress,
|
||||
required this.userAgent,
|
||||
required this.account,
|
||||
required this.accountId,
|
||||
});
|
||||
|
||||
static AuditEvent fromJson(Map<String, dynamic> json) =>
|
||||
_$AuditEventFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$AuditEventToJson(this);
|
||||
}
|
38
lib/models/audit_log.g.dart
Normal file
38
lib/models/audit_log.g.dart
Normal file
@ -0,0 +1,38 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'audit_log.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
AuditEvent _$AuditEventFromJson(Map<String, dynamic> json) => AuditEvent(
|
||||
id: (json['id'] as num).toInt(),
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
deletedAt: json['deleted_at'] == null
|
||||
? null
|
||||
: DateTime.parse(json['deleted_at'] as String),
|
||||
type: json['type'] as String,
|
||||
target: json['target'] as String,
|
||||
location: json['location'] as String,
|
||||
ipAddress: json['ip_address'] as String,
|
||||
userAgent: json['user_agent'] as String,
|
||||
account: Account.fromJson(json['account'] as Map<String, dynamic>),
|
||||
accountId: (json['account_id'] as num).toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$AuditEventToJson(AuditEvent instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
'updated_at': instance.updatedAt.toIso8601String(),
|
||||
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||
'type': instance.type,
|
||||
'target': instance.target,
|
||||
'location': instance.location,
|
||||
'ip_address': instance.ipAddress,
|
||||
'user_agent': instance.userAgent,
|
||||
'account': instance.account.toJson(),
|
||||
'account_id': instance.accountId,
|
||||
};
|
@ -1,4 +1,4 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:solian/models/account.dart';
|
||||
|
||||
part 'auth.g.dart';
|
||||
|
@ -1,4 +1,4 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:livekit_client/livekit_client.dart';
|
||||
import 'package:solian/models/channel.dart';
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:solian/models/account.dart';
|
||||
import 'package:solian/models/realm.dart';
|
||||
|
||||
@ -19,7 +19,8 @@ class Channel {
|
||||
int accountId;
|
||||
Realm? realm;
|
||||
int? realmId;
|
||||
bool isEncrypted;
|
||||
bool isPublic;
|
||||
bool isCommunity;
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: true)
|
||||
bool isAvailable = false;
|
||||
@ -36,7 +37,8 @@ class Channel {
|
||||
required this.members,
|
||||
required this.account,
|
||||
required this.accountId,
|
||||
required this.isEncrypted,
|
||||
required this.isPublic,
|
||||
required this.isCommunity,
|
||||
required this.realm,
|
||||
required this.realmId,
|
||||
});
|
||||
|
@ -22,7 +22,8 @@ Channel _$ChannelFromJson(Map<String, dynamic> json) => Channel(
|
||||
.toList(),
|
||||
account: Account.fromJson(json['account'] as Map<String, dynamic>),
|
||||
accountId: (json['account_id'] as num).toInt(),
|
||||
isEncrypted: json['is_encrypted'] as bool,
|
||||
isPublic: json['is_public'] as bool,
|
||||
isCommunity: json['is_community'] as bool,
|
||||
realm: json['realm'] == null
|
||||
? null
|
||||
: Realm.fromJson(json['realm'] as Map<String, dynamic>),
|
||||
@ -43,7 +44,8 @@ Map<String, dynamic> _$ChannelToJson(Channel instance) => <String, dynamic>{
|
||||
'account_id': instance.accountId,
|
||||
'realm': instance.realm?.toJson(),
|
||||
'realm_id': instance.realmId,
|
||||
'is_encrypted': instance.isEncrypted,
|
||||
'is_public': instance.isPublic,
|
||||
'is_community': instance.isCommunity,
|
||||
'is_available': instance.isAvailable,
|
||||
};
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/models/account.dart';
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:solian/models/channel.dart';
|
||||
|
||||
part 'event.g.dart';
|
||||
|
@ -1,4 +1,4 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'link.g.dart';
|
||||
|
||||
|
@ -1,18 +1,29 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'notification.g.dart';
|
||||
|
||||
const Map<String, IconData> NotificationTopicIcons = {
|
||||
'passport.security.alert': Icons.gpp_maybe,
|
||||
'interactive.subscription': Icons.subscriptions,
|
||||
'interactive.feedback': Icons.add_reaction,
|
||||
'messaging.callStart': Icons.call_received,
|
||||
};
|
||||
|
||||
@JsonSerializable()
|
||||
class Notification {
|
||||
int id;
|
||||
DateTime createdAt;
|
||||
DateTime updatedAt;
|
||||
DateTime? deletedAt;
|
||||
DateTime? readAt;
|
||||
String topic;
|
||||
String title;
|
||||
String? subtitle;
|
||||
String body;
|
||||
String? avatar;
|
||||
String? picture;
|
||||
Map<String, dynamic>? metadata;
|
||||
int? senderId;
|
||||
int accountId;
|
||||
|
||||
@ -21,11 +32,14 @@ class Notification {
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
required this.deletedAt,
|
||||
required this.readAt,
|
||||
required this.topic,
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
required this.body,
|
||||
required this.avatar,
|
||||
required this.picture,
|
||||
required this.metadata,
|
||||
required this.senderId,
|
||||
required this.accountId,
|
||||
});
|
||||
|
@ -13,11 +13,16 @@ Notification _$NotificationFromJson(Map<String, dynamic> json) => Notification(
|
||||
deletedAt: json['deleted_at'] == null
|
||||
? null
|
||||
: DateTime.parse(json['deleted_at'] as String),
|
||||
readAt: json['read_at'] == null
|
||||
? null
|
||||
: DateTime.parse(json['read_at'] as String),
|
||||
topic: json['topic'] as String,
|
||||
title: json['title'] as String,
|
||||
subtitle: json['subtitle'] as String?,
|
||||
body: json['body'] as String,
|
||||
avatar: json['avatar'] as String?,
|
||||
picture: json['picture'] as String?,
|
||||
metadata: json['metadata'] as Map<String, dynamic>?,
|
||||
senderId: (json['sender_id'] as num?)?.toInt(),
|
||||
accountId: (json['account_id'] as num).toInt(),
|
||||
);
|
||||
@ -28,11 +33,14 @@ Map<String, dynamic> _$NotificationToJson(Notification instance) =>
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
'updated_at': instance.updatedAt.toIso8601String(),
|
||||
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||
'read_at': instance.readAt?.toIso8601String(),
|
||||
'topic': instance.topic,
|
||||
'title': instance.title,
|
||||
'subtitle': instance.subtitle,
|
||||
'body': instance.body,
|
||||
'avatar': instance.avatar,
|
||||
'picture': instance.picture,
|
||||
'metadata': instance.metadata,
|
||||
'sender_id': instance.senderId,
|
||||
'account_id': instance.accountId,
|
||||
};
|
||||
|
@ -1,10 +1,10 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'packet.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class NetworkPackage {
|
||||
@JsonKey(name: 'w')
|
||||
@JsonKey(name: 'w', defaultValue: 'unknown')
|
||||
String method;
|
||||
@JsonKey(name: 'e')
|
||||
String? endpoint;
|
||||
|
@ -8,7 +8,7 @@ part of 'packet.dart';
|
||||
|
||||
NetworkPackage _$NetworkPackageFromJson(Map<String, dynamic> json) =>
|
||||
NetworkPackage(
|
||||
method: json['w'] as String,
|
||||
method: json['w'] as String? ?? 'unknown',
|
||||
endpoint: json['e'] as String?,
|
||||
message: json['m'] as String?,
|
||||
payload: json['p'] as Map<String, dynamic>?,
|
||||
|
@ -1,4 +1,4 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'pagination.g.dart';
|
||||
|
||||
|
@ -1,10 +1,19 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:solian/models/account.dart';
|
||||
import 'package:solian/models/attachment.dart';
|
||||
import 'package:solian/models/post_categories.dart';
|
||||
import 'package:solian/models/realm.dart';
|
||||
|
||||
part 'post.g.dart';
|
||||
|
||||
class PostPreload {
|
||||
List<Attachment> attachments;
|
||||
|
||||
PostPreload({
|
||||
required this.attachments,
|
||||
});
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class Post {
|
||||
int id;
|
||||
@ -15,6 +24,7 @@ class Post {
|
||||
String? alias;
|
||||
String? areaAlias;
|
||||
dynamic body;
|
||||
int visibility;
|
||||
List<Tag>? tags;
|
||||
List<Category>? categories;
|
||||
List<Post>? replies;
|
||||
@ -33,6 +43,9 @@ class Post {
|
||||
Account author;
|
||||
PostMetric? metric;
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
PostPreload? preload;
|
||||
|
||||
Post({
|
||||
required this.id,
|
||||
required this.createdAt,
|
||||
@ -43,6 +56,7 @@ class Post {
|
||||
required this.areaAlias,
|
||||
required this.type,
|
||||
required this.body,
|
||||
required this.visibility,
|
||||
required this.tags,
|
||||
required this.categories,
|
||||
required this.replies,
|
||||
|
@ -20,6 +20,7 @@ Post _$PostFromJson(Map<String, dynamic> json) => Post(
|
||||
areaAlias: json['area_alias'] as String?,
|
||||
type: json['type'] as String,
|
||||
body: json['body'],
|
||||
visibility: (json['visibility'] as num).toInt(),
|
||||
tags: (json['tags'] as List<dynamic>?)
|
||||
?.map((e) => Tag.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
@ -67,6 +68,7 @@ Map<String, dynamic> _$PostToJson(Post instance) => <String, dynamic>{
|
||||
'alias': instance.alias,
|
||||
'area_alias': instance.areaAlias,
|
||||
'body': instance.body,
|
||||
'visibility': instance.visibility,
|
||||
'tags': instance.tags?.map((e) => e.toJson()).toList(),
|
||||
'categories': instance.categories?.map((e) => e.toJson()).toList(),
|
||||
'replies': instance.replies?.map((e) => e.toJson()).toList(),
|
||||
|
@ -1,4 +1,4 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'post_categories.g.dart';
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:solian/models/account.dart';
|
||||
|
||||
part 'realm.g.dart';
|
||||
|
@ -1,4 +1,4 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:solian/models/account.dart';
|
||||
|
||||
part 'relations.g.dart';
|
||||
|
@ -1,4 +1,4 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:solian/models/account.dart';
|
||||
import 'package:solian/models/attachment.dart';
|
||||
import 'package:solian/services.dart';
|
||||
|
41
lib/models/subscription.dart
Normal file
41
lib/models/subscription.dart
Normal file
@ -0,0 +1,41 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:solian/models/account.dart';
|
||||
import 'package:solian/models/post_categories.dart';
|
||||
|
||||
part 'subscription.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class Subscription {
|
||||
int id;
|
||||
DateTime createdAt;
|
||||
DateTime updatedAt;
|
||||
DateTime? deletedAt;
|
||||
int followerId;
|
||||
Account follower;
|
||||
int? accountId;
|
||||
Account? account;
|
||||
int? tagId;
|
||||
Tag? tag;
|
||||
int? categoryId;
|
||||
Category? category;
|
||||
|
||||
Subscription({
|
||||
required this.id,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
required this.deletedAt,
|
||||
required this.followerId,
|
||||
required this.follower,
|
||||
required this.accountId,
|
||||
required this.account,
|
||||
required this.tagId,
|
||||
required this.tag,
|
||||
required this.categoryId,
|
||||
required this.category,
|
||||
});
|
||||
|
||||
factory Subscription.fromJson(Map<String, dynamic> json) =>
|
||||
_$SubscriptionFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$SubscriptionToJson(this);
|
||||
}
|
46
lib/models/subscription.g.dart
Normal file
46
lib/models/subscription.g.dart
Normal file
@ -0,0 +1,46 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'subscription.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
Subscription _$SubscriptionFromJson(Map<String, dynamic> json) => Subscription(
|
||||
id: (json['id'] as num).toInt(),
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
deletedAt: json['deleted_at'] == null
|
||||
? null
|
||||
: DateTime.parse(json['deleted_at'] as String),
|
||||
followerId: (json['follower_id'] as num).toInt(),
|
||||
follower: Account.fromJson(json['follower'] as Map<String, dynamic>),
|
||||
accountId: (json['account_id'] as num?)?.toInt(),
|
||||
account: json['account'] == null
|
||||
? null
|
||||
: Account.fromJson(json['account'] as Map<String, dynamic>),
|
||||
tagId: (json['tag_id'] as num?)?.toInt(),
|
||||
tag: json['tag'] == null
|
||||
? null
|
||||
: Tag.fromJson(json['tag'] as Map<String, dynamic>),
|
||||
categoryId: (json['category_id'] as num?)?.toInt(),
|
||||
category: json['category'] == null
|
||||
? null
|
||||
: Category.fromJson(json['category'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$SubscriptionToJson(Subscription instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
'updated_at': instance.updatedAt.toIso8601String(),
|
||||
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||
'follower_id': instance.followerId,
|
||||
'follower': instance.follower.toJson(),
|
||||
'account_id': instance.accountId,
|
||||
'account': instance.account?.toJson(),
|
||||
'tag_id': instance.tagId,
|
||||
'tag': instance.tag?.toJson(),
|
||||
'category_id': instance.categoryId,
|
||||
'category': instance.category?.toJson(),
|
||||
};
|
50
lib/models/theme.dart
Normal file
50
lib/models/theme.dart
Normal file
@ -0,0 +1,50 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'theme.g.dart';
|
||||
|
||||
@JsonSerializable(converters: [ColorConverter()])
|
||||
class SolianThemeData {
|
||||
String id;
|
||||
Color seedColor;
|
||||
String? fontFamily;
|
||||
List<String>? fontFamilyFallback;
|
||||
|
||||
SolianThemeData({
|
||||
required this.id,
|
||||
required this.seedColor,
|
||||
this.fontFamily,
|
||||
this.fontFamilyFallback,
|
||||
});
|
||||
|
||||
factory SolianThemeData.fromJson(Map<String, dynamic> json) =>
|
||||
_$SolianThemeDataFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$SolianThemeDataToJson(this);
|
||||
|
||||
@override
|
||||
int get hashCode => id.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (other is SolianThemeData) {
|
||||
return id == other.id;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
class ColorConverter extends JsonConverter<Color, int> {
|
||||
const ColorConverter();
|
||||
|
||||
@override
|
||||
Color fromJson(int json) {
|
||||
return Color(json);
|
||||
}
|
||||
|
||||
@override
|
||||
int toJson(Color object) {
|
||||
return object.value;
|
||||
}
|
||||
}
|
26
lib/models/theme.g.dart
Normal file
26
lib/models/theme.g.dart
Normal file
@ -0,0 +1,26 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'theme.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
SolianThemeData _$SolianThemeDataFromJson(Map<String, dynamic> json) =>
|
||||
SolianThemeData(
|
||||
id: json['id'] as String,
|
||||
seedColor:
|
||||
const ColorConverter().fromJson((json['seed_color'] as num).toInt()),
|
||||
fontFamily: json['font_family'] as String?,
|
||||
fontFamilyFallback: (json['font_family_fallback'] as List<dynamic>?)
|
||||
?.map((e) => e as String)
|
||||
.toList(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$SolianThemeDataToJson(SolianThemeData instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'seed_color': const ColorConverter().toJson(instance.seedColor),
|
||||
'font_family': instance.fontFamily,
|
||||
'font_family_fallback': instance.fontFamilyFallback,
|
||||
};
|
@ -27,6 +27,10 @@ abstract class PlatformInfo {
|
||||
|
||||
static bool get canCacheImage => isAndroid || isIOS || isMacOS;
|
||||
|
||||
static bool get canRateTheApp => isIOS || isMacOS;
|
||||
|
||||
static bool get canCropImage => isIOS || isAndroid || isWeb;
|
||||
|
||||
static bool get canRecord => (isMobile || isMacOS);
|
||||
|
||||
static bool get canPushNotification => isAndroid || isIOS || isMacOS;
|
||||
@ -38,4 +42,4 @@ abstract class PlatformInfo {
|
||||
} catch (_) {}
|
||||
return version;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import 'package:solian/exceptions/request.dart';
|
||||
import 'package:solian/exceptions/unauthorized.dart';
|
||||
import 'package:solian/models/auth.dart';
|
||||
import 'package:solian/providers/database/database.dart';
|
||||
import 'package:solian/providers/notifications.dart';
|
||||
import 'package:solian/providers/websocket.dart';
|
||||
import 'package:solian/services.dart';
|
||||
|
||||
@ -125,7 +126,7 @@ class AuthProvider extends GetConnect {
|
||||
userAgent: await ServiceFinder.getUserAgent(),
|
||||
sendUserAgent: true,
|
||||
);
|
||||
client.httpClient.addAuthenticator(requestAuthenticator);
|
||||
client.httpClient.addRequestModifier(requestAuthenticator);
|
||||
client.httpClient.baseUrl = ServiceFinder.buildUrl(service, null);
|
||||
|
||||
return client;
|
||||
@ -174,7 +175,7 @@ class AuthProvider extends GetConnect {
|
||||
);
|
||||
|
||||
Get.find<WebSocketProvider>().connect();
|
||||
Get.find<WebSocketProvider>().notifyPrefetch();
|
||||
Get.find<NotificationProvider>().fetchNotification();
|
||||
|
||||
return credentials!;
|
||||
}
|
||||
@ -184,8 +185,8 @@ class AuthProvider extends GetConnect {
|
||||
userProfile.value = null;
|
||||
|
||||
Get.find<WebSocketProvider>().disconnect();
|
||||
Get.find<WebSocketProvider>().notifications.clear();
|
||||
Get.find<WebSocketProvider>().notificationUnread.value = 0;
|
||||
Get.find<NotificationProvider>().notifications.clear();
|
||||
Get.find<NotificationProvider>().notificationUnread.value = 0;
|
||||
|
||||
AppDatabase.removeDatabase();
|
||||
autoStopBackgroundNotificationService();
|
||||
|
@ -392,7 +392,7 @@ class ChatCallProvider extends GetxController {
|
||||
}
|
||||
|
||||
Future gotoScreen(BuildContext context) {
|
||||
return Navigator.of(context, rootNavigator: true).push(
|
||||
return Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => const CallScreen()),
|
||||
);
|
||||
}
|
||||
|
@ -23,6 +23,21 @@ class AttachmentProvider extends GetConnect {
|
||||
|
||||
final Map<String, Attachment> _cachedResponses = {};
|
||||
|
||||
List<Attachment?> listMetadataFromCache(List<String> rid) {
|
||||
if (rid.isEmpty) return List.empty();
|
||||
|
||||
List<Attachment?> result = List.filled(rid.length, null);
|
||||
for (var idx = 0; idx < rid.length; idx++) {
|
||||
if (_cachedResponses.containsKey(rid[idx])) {
|
||||
result[idx] = _cachedResponses[rid[idx]];
|
||||
} else {
|
||||
result[idx] = null;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<List<Attachment?>> listMetadata(
|
||||
List<String> rid, {
|
||||
noCache = false,
|
||||
@ -41,25 +56,27 @@ class AttachmentProvider extends GetConnect {
|
||||
}
|
||||
}
|
||||
|
||||
final resp = await get(
|
||||
'/attachments?take=${pendingQuery.length}&id=${pendingQuery.join(',')}',
|
||||
);
|
||||
if (resp.statusCode != 200) return result;
|
||||
if (pendingQuery.isNotEmpty) {
|
||||
final resp = await get(
|
||||
'/attachments?take=${pendingQuery.length}&id=${pendingQuery.join(',')}',
|
||||
);
|
||||
if (resp.statusCode != 200) return result;
|
||||
|
||||
final rawOut = PaginationResult.fromJson(resp.body);
|
||||
if (rawOut.data == null) return result;
|
||||
final rawOut = PaginationResult.fromJson(resp.body);
|
||||
if (rawOut.data == null) return result;
|
||||
|
||||
final List<Attachment> out =
|
||||
rawOut.data!.map((x) => Attachment.fromJson(x)).toList();
|
||||
for (final item in out) {
|
||||
if (item.destination != 0 && item.isAnalyzed) {
|
||||
_cachedResponses[item.rid] = item;
|
||||
final List<Attachment> out =
|
||||
rawOut.data!.map((x) => Attachment.fromJson(x)).toList();
|
||||
for (final item in out) {
|
||||
if (item.destination != 0 && item.isAnalyzed) {
|
||||
_cachedResponses[item.rid] = item;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (var i = 0; i < out.length; i++) {
|
||||
for (var j = 0; j < rid.length; j++) {
|
||||
if (out[i].rid == rid[j]) {
|
||||
result[j] = out[i];
|
||||
for (var i = 0; i < out.length; i++) {
|
||||
for (var j = 0; j < rid.length; j++) {
|
||||
if (out[i].rid == rid[j]) {
|
||||
result[j] = out[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,25 +9,6 @@ import 'package:uuid/uuid.dart';
|
||||
|
||||
class ChannelProvider extends GetxController {
|
||||
RxBool isLoading = false.obs;
|
||||
RxList<Channel> availableChannels = RxList.empty(growable: true);
|
||||
|
||||
List<Channel> get groupChannels =>
|
||||
availableChannels.where((x) => x.type == 0).toList();
|
||||
List<Channel> get directChannels =>
|
||||
availableChannels.where((x) => x.type == 1).toList();
|
||||
|
||||
Future<void> refreshAvailableChannel() async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
|
||||
|
||||
isLoading.value = true;
|
||||
final resp = await listAvailableChannel();
|
||||
isLoading.value = false;
|
||||
|
||||
availableChannels.value =
|
||||
resp.body.map((x) => Channel.fromJson(x)).toList().cast<Channel>();
|
||||
availableChannels.refresh();
|
||||
}
|
||||
|
||||
Future<Response> getChannel(String alias, {String realm = 'global'}) async {
|
||||
final AuthProvider auth = Get.find();
|
||||
@ -89,18 +70,22 @@ class ChannelProvider extends GetxController {
|
||||
return resp;
|
||||
}
|
||||
|
||||
Future<Response> listAvailableChannel({String realm = 'global'}) async {
|
||||
Future<List<Channel>> listAvailableChannel({
|
||||
String scope = 'global',
|
||||
bool isDirect = false,
|
||||
}) async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
|
||||
|
||||
final client = await auth.configureClient('messaging');
|
||||
|
||||
final resp = await client.get('/channels/$realm/me/available');
|
||||
final resp =
|
||||
await client.get('/channels/$scope/me/available?direct=$isDirect');
|
||||
if (resp.statusCode != 200) {
|
||||
throw RequestException(resp);
|
||||
}
|
||||
|
||||
return resp;
|
||||
return List.from(resp.body.map((x) => Channel.fromJson(x)));
|
||||
}
|
||||
|
||||
Future<Response> createChannel(String scope, dynamic payload) async {
|
||||
|
@ -1,23 +1,13 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/exceptions/request.dart';
|
||||
import 'package:solian/exceptions/unauthorized.dart';
|
||||
import 'package:solian/models/post.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/services.dart';
|
||||
|
||||
class PostProvider extends GetConnect {
|
||||
@override
|
||||
void onInit() {
|
||||
httpClient.baseUrl = ServiceFinder.buildUrl('interactive', null);
|
||||
}
|
||||
|
||||
class PostProvider extends GetxController {
|
||||
Future<Response> seeWhatsNew(int pivot) async {
|
||||
GetConnect client;
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.value) {
|
||||
client = await auth.configureClient('co');
|
||||
} else {
|
||||
client = await ServiceFinder.configureClient('co');
|
||||
}
|
||||
final client = await auth.configureClient('co');
|
||||
final resp = await client.get('/whats-new?pivot=$pivot');
|
||||
if (resp.statusCode != 200) {
|
||||
throw RequestException(resp);
|
||||
@ -27,19 +17,14 @@ class PostProvider extends GetConnect {
|
||||
}
|
||||
|
||||
Future<Response> listRecommendations(int page,
|
||||
{String? realm, String? channel}) async {
|
||||
GetConnect client;
|
||||
final AuthProvider auth = Get.find();
|
||||
{String? realm, String? channel, int take = 10}) async {
|
||||
final queries = [
|
||||
'take=${10}',
|
||||
'take=$take',
|
||||
'offset=$page',
|
||||
if (realm != null) 'realm=$realm',
|
||||
];
|
||||
if (auth.isAuthorized.value) {
|
||||
client = await auth.configureClient('co');
|
||||
} else {
|
||||
client = await ServiceFinder.configureClient('co');
|
||||
}
|
||||
final AuthProvider auth = Get.find();
|
||||
final client = await auth.configureClient('interactive');
|
||||
final resp = await client.get(
|
||||
channel == null
|
||||
? '/recommendations?${queries.join('&')}'
|
||||
@ -59,9 +44,33 @@ class PostProvider extends GetConnect {
|
||||
final queries = [
|
||||
'take=${10}',
|
||||
'offset=$page',
|
||||
'truncate=false',
|
||||
];
|
||||
final client = await auth.configureClient('interactive');
|
||||
final resp = await client.get('/posts/drafts?${queries.join('&')}');
|
||||
final resp = await client.get(
|
||||
'/posts/drafts?${queries.join('&')}',
|
||||
);
|
||||
if (resp.statusCode != 200) {
|
||||
throw RequestException(resp);
|
||||
}
|
||||
|
||||
return resp;
|
||||
}
|
||||
|
||||
Future<Response> searchPost(String probe, int page,
|
||||
{String? realm, String? author, tag, category, int take = 10}) async {
|
||||
final queries = [
|
||||
'probe=$probe',
|
||||
'take=$take',
|
||||
'offset=$page',
|
||||
if (tag != null) 'tag=$tag',
|
||||
if (category != null) 'category=$category',
|
||||
if (author != null) 'author=$author',
|
||||
if (realm != null) 'realm=$realm',
|
||||
];
|
||||
final AuthProvider auth = Get.find();
|
||||
final client = await auth.configureClient('co');
|
||||
final resp = await client.get('/posts/search?${queries.join('&')}');
|
||||
if (resp.statusCode != 200) {
|
||||
throw RequestException(resp);
|
||||
}
|
||||
@ -70,16 +79,18 @@ class PostProvider extends GetConnect {
|
||||
}
|
||||
|
||||
Future<Response> listPost(int page,
|
||||
{String? realm, String? author, tag, category}) async {
|
||||
{String? realm, String? author, tag, category, int take = 10}) async {
|
||||
final queries = [
|
||||
'take=${10}',
|
||||
'take=$take',
|
||||
'offset=$page',
|
||||
if (tag != null) 'tag=$tag',
|
||||
if (category != null) 'category=$category',
|
||||
if (author != null) 'author=$author',
|
||||
if (realm != null) 'realm=$realm',
|
||||
];
|
||||
final resp = await get('/posts?${queries.join('&')}');
|
||||
final AuthProvider auth = Get.find();
|
||||
final client = await auth.configureClient('co');
|
||||
final resp = await client.get('/posts?${queries.join('&')}');
|
||||
if (resp.statusCode != 200) {
|
||||
throw RequestException(resp);
|
||||
}
|
||||
@ -88,7 +99,10 @@ class PostProvider extends GetConnect {
|
||||
}
|
||||
|
||||
Future<Response> listPostReplies(String alias, int page) async {
|
||||
final resp = await get('/posts/$alias/replies?take=${10}&offset=$page');
|
||||
final AuthProvider auth = Get.find();
|
||||
final client = await auth.configureClient('co');
|
||||
final resp =
|
||||
await client.get('/posts/$alias/replies?take=${10}&offset=$page');
|
||||
if (resp.statusCode != 200) {
|
||||
throw RequestException(resp);
|
||||
}
|
||||
@ -96,17 +110,21 @@ class PostProvider extends GetConnect {
|
||||
return resp;
|
||||
}
|
||||
|
||||
Future<List<Post>> listPostFeaturedReply(String alias, {int take = 1}) async {
|
||||
final AuthProvider auth = Get.find();
|
||||
final client = await auth.configureClient('co');
|
||||
final resp = await client.get('/posts/$alias/replies/featured?take=$take');
|
||||
if (resp.statusCode != 200) {
|
||||
throw RequestException(resp);
|
||||
}
|
||||
|
||||
return List<Post>.from(resp.body.map((x) => Post.fromJson(x)));
|
||||
}
|
||||
|
||||
Future<Response> getPost(String alias) async {
|
||||
final resp = await get('/posts/$alias');
|
||||
if (resp.statusCode != 200) {
|
||||
throw RequestException(resp);
|
||||
}
|
||||
|
||||
return resp;
|
||||
}
|
||||
|
||||
Future<Response> getArticle(String alias) async {
|
||||
final resp = await get('/articles/$alias');
|
||||
final AuthProvider auth = Get.find();
|
||||
final client = await auth.configureClient('co');
|
||||
final resp = await client.get('/posts/$alias');
|
||||
if (resp.statusCode != 200) {
|
||||
throw RequestException(resp);
|
||||
}
|
||||
|
@ -299,53 +299,71 @@ typedef $$LocalMessageEventTableTableUpdateCompanionBuilder
|
||||
});
|
||||
|
||||
class $$LocalMessageEventTableTableFilterComposer
|
||||
extends FilterComposer<_$AppDatabase, $LocalMessageEventTableTable> {
|
||||
$$LocalMessageEventTableTableFilterComposer(super.$state);
|
||||
ColumnFilters<int> get id => $state.composableBuilder(
|
||||
column: $state.table.id,
|
||||
builder: (column, joinBuilders) =>
|
||||
ColumnFilters(column, joinBuilders: joinBuilders));
|
||||
extends Composer<_$AppDatabase, $LocalMessageEventTableTable> {
|
||||
$$LocalMessageEventTableTableFilterComposer({
|
||||
required super.$db,
|
||||
required super.$table,
|
||||
super.joinBuilder,
|
||||
super.$addJoinBuilderToRootComposer,
|
||||
super.$removeJoinBuilderFromRootComposer,
|
||||
});
|
||||
ColumnFilters<int> get id => $composableBuilder(
|
||||
column: $table.id, builder: (column) => ColumnFilters(column));
|
||||
|
||||
ColumnFilters<int> get channelId => $state.composableBuilder(
|
||||
column: $state.table.channelId,
|
||||
builder: (column, joinBuilders) =>
|
||||
ColumnFilters(column, joinBuilders: joinBuilders));
|
||||
ColumnFilters<int> get channelId => $composableBuilder(
|
||||
column: $table.channelId, builder: (column) => ColumnFilters(column));
|
||||
|
||||
ColumnWithTypeConverterFilters<Event?, Event, String> get data =>
|
||||
$state.composableBuilder(
|
||||
column: $state.table.data,
|
||||
builder: (column, joinBuilders) => ColumnWithTypeConverterFilters(
|
||||
column,
|
||||
joinBuilders: joinBuilders));
|
||||
$composableBuilder(
|
||||
column: $table.data,
|
||||
builder: (column) => ColumnWithTypeConverterFilters(column));
|
||||
|
||||
ColumnFilters<DateTime> get createdAt => $state.composableBuilder(
|
||||
column: $state.table.createdAt,
|
||||
builder: (column, joinBuilders) =>
|
||||
ColumnFilters(column, joinBuilders: joinBuilders));
|
||||
ColumnFilters<DateTime> get createdAt => $composableBuilder(
|
||||
column: $table.createdAt, builder: (column) => ColumnFilters(column));
|
||||
}
|
||||
|
||||
class $$LocalMessageEventTableTableOrderingComposer
|
||||
extends OrderingComposer<_$AppDatabase, $LocalMessageEventTableTable> {
|
||||
$$LocalMessageEventTableTableOrderingComposer(super.$state);
|
||||
ColumnOrderings<int> get id => $state.composableBuilder(
|
||||
column: $state.table.id,
|
||||
builder: (column, joinBuilders) =>
|
||||
ColumnOrderings(column, joinBuilders: joinBuilders));
|
||||
extends Composer<_$AppDatabase, $LocalMessageEventTableTable> {
|
||||
$$LocalMessageEventTableTableOrderingComposer({
|
||||
required super.$db,
|
||||
required super.$table,
|
||||
super.joinBuilder,
|
||||
super.$addJoinBuilderToRootComposer,
|
||||
super.$removeJoinBuilderFromRootComposer,
|
||||
});
|
||||
ColumnOrderings<int> get id => $composableBuilder(
|
||||
column: $table.id, builder: (column) => ColumnOrderings(column));
|
||||
|
||||
ColumnOrderings<int> get channelId => $state.composableBuilder(
|
||||
column: $state.table.channelId,
|
||||
builder: (column, joinBuilders) =>
|
||||
ColumnOrderings(column, joinBuilders: joinBuilders));
|
||||
ColumnOrderings<int> get channelId => $composableBuilder(
|
||||
column: $table.channelId, builder: (column) => ColumnOrderings(column));
|
||||
|
||||
ColumnOrderings<String> get data => $state.composableBuilder(
|
||||
column: $state.table.data,
|
||||
builder: (column, joinBuilders) =>
|
||||
ColumnOrderings(column, joinBuilders: joinBuilders));
|
||||
ColumnOrderings<String> get data => $composableBuilder(
|
||||
column: $table.data, builder: (column) => ColumnOrderings(column));
|
||||
|
||||
ColumnOrderings<DateTime> get createdAt => $state.composableBuilder(
|
||||
column: $state.table.createdAt,
|
||||
builder: (column, joinBuilders) =>
|
||||
ColumnOrderings(column, joinBuilders: joinBuilders));
|
||||
ColumnOrderings<DateTime> get createdAt => $composableBuilder(
|
||||
column: $table.createdAt, builder: (column) => ColumnOrderings(column));
|
||||
}
|
||||
|
||||
class $$LocalMessageEventTableTableAnnotationComposer
|
||||
extends Composer<_$AppDatabase, $LocalMessageEventTableTable> {
|
||||
$$LocalMessageEventTableTableAnnotationComposer({
|
||||
required super.$db,
|
||||
required super.$table,
|
||||
super.joinBuilder,
|
||||
super.$addJoinBuilderToRootComposer,
|
||||
super.$removeJoinBuilderFromRootComposer,
|
||||
});
|
||||
GeneratedColumn<int> get id =>
|
||||
$composableBuilder(column: $table.id, builder: (column) => column);
|
||||
|
||||
GeneratedColumn<int> get channelId =>
|
||||
$composableBuilder(column: $table.channelId, builder: (column) => column);
|
||||
|
||||
GeneratedColumnWithTypeConverter<Event?, String> get data =>
|
||||
$composableBuilder(column: $table.data, builder: (column) => column);
|
||||
|
||||
GeneratedColumn<DateTime> get createdAt =>
|
||||
$composableBuilder(column: $table.createdAt, builder: (column) => column);
|
||||
}
|
||||
|
||||
class $$LocalMessageEventTableTableTableManager extends RootTableManager<
|
||||
@ -354,6 +372,7 @@ class $$LocalMessageEventTableTableTableManager extends RootTableManager<
|
||||
LocalMessageEventTableData,
|
||||
$$LocalMessageEventTableTableFilterComposer,
|
||||
$$LocalMessageEventTableTableOrderingComposer,
|
||||
$$LocalMessageEventTableTableAnnotationComposer,
|
||||
$$LocalMessageEventTableTableCreateCompanionBuilder,
|
||||
$$LocalMessageEventTableTableUpdateCompanionBuilder,
|
||||
(
|
||||
@ -368,10 +387,15 @@ class $$LocalMessageEventTableTableTableManager extends RootTableManager<
|
||||
: super(TableManagerState(
|
||||
db: db,
|
||||
table: table,
|
||||
filteringComposer: $$LocalMessageEventTableTableFilterComposer(
|
||||
ComposerState(db, table)),
|
||||
orderingComposer: $$LocalMessageEventTableTableOrderingComposer(
|
||||
ComposerState(db, table)),
|
||||
createFilteringComposer: () =>
|
||||
$$LocalMessageEventTableTableFilterComposer(
|
||||
$db: db, $table: table),
|
||||
createOrderingComposer: () =>
|
||||
$$LocalMessageEventTableTableOrderingComposer(
|
||||
$db: db, $table: table),
|
||||
createComputedFieldComposer: () =>
|
||||
$$LocalMessageEventTableTableAnnotationComposer(
|
||||
$db: db, $table: table),
|
||||
updateCompanionCallback: ({
|
||||
Value<int> id = const Value.absent(),
|
||||
Value<int> channelId = const Value.absent(),
|
||||
@ -410,6 +434,7 @@ typedef $$LocalMessageEventTableTableProcessedTableManager
|
||||
LocalMessageEventTableData,
|
||||
$$LocalMessageEventTableTableFilterComposer,
|
||||
$$LocalMessageEventTableTableOrderingComposer,
|
||||
$$LocalMessageEventTableTableAnnotationComposer,
|
||||
$$LocalMessageEventTableTableCreateCompanionBuilder,
|
||||
$$LocalMessageEventTableTableUpdateCompanionBuilder,
|
||||
(
|
||||
|
@ -1,3 +1,6 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:get/get.dart' hide Value;
|
||||
import 'package:solian/exceptions/request.dart';
|
||||
@ -129,6 +132,14 @@ class MessagesFetchingProvider extends GetxController {
|
||||
return await receiveEvent(remoteRecord);
|
||||
}
|
||||
|
||||
Future<LocalMessageEventTableData?> getEventFromLocal(int id) async {
|
||||
final database = Get.find<DatabaseProvider>().database;
|
||||
final localRecord = await (database.select(database.localMessageEventTable)
|
||||
..where((x) => x.id.equals(id)))
|
||||
.getSingleOrNull();
|
||||
return localRecord;
|
||||
}
|
||||
|
||||
/// Pull the remote events to local database
|
||||
Future<(List<Event>, int)?> pullRemoteEvents(Channel channel,
|
||||
{String scope = 'global', take = 10, offset = 0}) async {
|
||||
@ -174,4 +185,26 @@ class MessagesFetchingProvider extends GetxController {
|
||||
..orderBy([(t) => OrderingTerm.desc(t.id)]))
|
||||
.getSingleOrNull();
|
||||
}
|
||||
|
||||
Future<Map<int, List<LocalMessageEventTableData>>>
|
||||
getLastInAllChannels() async {
|
||||
final database = Get.find<DatabaseProvider>().database;
|
||||
final rows = await database.customSelect('''
|
||||
SELECT id, channel_id, data, created_at
|
||||
FROM ${database.localMessageEventTable.actualTableName}
|
||||
WHERE (channel_id, created_at) IN (
|
||||
SELECT channel_id, MAX(created_at)
|
||||
FROM ${database.localMessageEventTable.actualTableName}
|
||||
GROUP BY channel_id
|
||||
)
|
||||
''', readsFrom: {database.localMessageEventTable}).get();
|
||||
return rows.map((row) {
|
||||
return LocalMessageEventTableData(
|
||||
id: row.read<int>('id'),
|
||||
channelId: row.read<int>('channel_id'),
|
||||
data: Event.fromJson(jsonDecode(row.read<String>('data'))),
|
||||
createdAt: row.read<DateTime>('created_at'),
|
||||
);
|
||||
}).groupListsBy((x) => x.channelId);
|
||||
}
|
||||
}
|
||||
|
@ -4,19 +4,19 @@ import 'package:intl/intl.dart';
|
||||
class ExperienceProvider extends GetxController {
|
||||
static List<int> experienceToLevelRequirements = [
|
||||
0, // Level 0
|
||||
100, // Level 1
|
||||
400, // Level 2
|
||||
900, // Level 3
|
||||
1600, // Level 4
|
||||
2500, // Level 5
|
||||
3600, // Level 6
|
||||
4900, // Level 7
|
||||
6400, // Level 8
|
||||
8100, // Level 9
|
||||
10000, // Level 10
|
||||
12100, // Level 11
|
||||
14400, // Level 12
|
||||
36800 // Level 13
|
||||
1000, // Level 1
|
||||
4000, // Level 2
|
||||
9000, // Level 3
|
||||
16000, // Level 4
|
||||
25000, // Level 5
|
||||
36000, // Level 6
|
||||
49000, // Level 7
|
||||
64000, // Level 8
|
||||
81000, // Level 9
|
||||
100000, // Level 10
|
||||
121000, // Level 11
|
||||
144000, // Level 12
|
||||
368000 // Level 13
|
||||
];
|
||||
|
||||
static List<String> levelLabelMapping =
|
||||
@ -35,7 +35,7 @@ class ExperienceProvider extends GetxController {
|
||||
final idx = experienceToLevelRequirements.indexOf(exp);
|
||||
if (idx + 1 >= experienceToLevelRequirements.length) return 1;
|
||||
final nextExp = experienceToLevelRequirements[idx + 1];
|
||||
return exp / nextExp;
|
||||
return (experience - exp).abs() / (exp - nextExp).abs();
|
||||
}
|
||||
|
||||
static String calcLevelUpProgressLevel(int experience) {
|
||||
@ -43,9 +43,9 @@ class ExperienceProvider extends GetxController {
|
||||
.firstWhere((x) => x <= experience);
|
||||
final idx = experienceToLevelRequirements.indexOf(exp);
|
||||
if (idx + 1 >= experienceToLevelRequirements.length) return 'Infinity';
|
||||
final nextExp = experienceToLevelRequirements[idx + 1];
|
||||
final nextExp = exp - experienceToLevelRequirements[idx + 1];
|
||||
final formatter =
|
||||
NumberFormat.compactCurrency(symbol: '', decimalDigits: 1);
|
||||
return '${formatter.format(exp)}/${formatter.format(nextExp)}';
|
||||
return '${formatter.format((exp - experience).abs())}/${formatter.format(nextExp.abs())}';
|
||||
}
|
||||
}
|
||||
|
175
lib/providers/notifications.dart
Normal file
175
lib/providers/notifications.dart
Normal file
@ -0,0 +1,175 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:flutter_udid/flutter_udid.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:solian/exceptions/request.dart';
|
||||
import 'package:solian/models/notification.dart';
|
||||
import 'package:solian/models/pagination.dart';
|
||||
import 'package:solian/platform.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
|
||||
class NotificationProvider extends GetxController {
|
||||
RxBool isBusy = false.obs;
|
||||
|
||||
RxInt notificationUnread = 0.obs;
|
||||
RxList<Notification> notifications =
|
||||
List<Notification>.empty(growable: true).obs;
|
||||
|
||||
Future<void> fetchNotification() async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) return;
|
||||
|
||||
final client = await auth.configureClient('auth');
|
||||
|
||||
final resp = await client.get('/notifications?skip=0&take=100');
|
||||
if (resp.statusCode == 200) {
|
||||
final result = PaginationResult.fromJson(resp.body);
|
||||
final data = result.data?.map((x) => Notification.fromJson(x)).toList();
|
||||
if (data != null) {
|
||||
notifications.addAll(data);
|
||||
notificationUnread.value = data.where((x) => x.readAt == null).length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> markAllRead() async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) return;
|
||||
|
||||
isBusy.value = true;
|
||||
|
||||
final NotificationProvider nty = Get.find();
|
||||
|
||||
List<int> markList = List.empty(growable: true);
|
||||
for (final element in nty.notifications) {
|
||||
if (element.id <= 0) continue;
|
||||
if (element.readAt != null) continue;
|
||||
markList.add(element.id);
|
||||
}
|
||||
|
||||
if (markList.isNotEmpty) {
|
||||
final client = await auth.configureClient('auth');
|
||||
await client.put('/notifications/read', {'messages': markList});
|
||||
}
|
||||
|
||||
nty.notifications.value = nty.notifications.map((x) {
|
||||
x.readAt = DateTime.now();
|
||||
return x;
|
||||
}).toList();
|
||||
nty.notifications.refresh();
|
||||
|
||||
isBusy.value = false;
|
||||
}
|
||||
|
||||
Future<void> markOneRead(Notification element, int index) async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) return;
|
||||
|
||||
final NotificationProvider nty = Get.find();
|
||||
|
||||
if (element.id <= 0) {
|
||||
nty.notifications.removeAt(index);
|
||||
return;
|
||||
} else if (element.readAt != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
isBusy.value = true;
|
||||
|
||||
final client = await auth.configureClient('auth');
|
||||
|
||||
await client.put('/notifications/read/${element.id}', {});
|
||||
|
||||
nty.notifications[0].readAt = DateTime.now();
|
||||
nty.notifications.refresh();
|
||||
|
||||
isBusy.value = false;
|
||||
}
|
||||
|
||||
void requestPermissions() {
|
||||
try {
|
||||
FirebaseMessaging.instance.requestPermission(
|
||||
alert: true,
|
||||
announcement: true,
|
||||
carPlay: true,
|
||||
badge: true,
|
||||
sound: true);
|
||||
} catch (_) {
|
||||
// When firebase isn't initialized (background service)
|
||||
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
|
||||
FlutterLocalNotificationsPlugin();
|
||||
flutterLocalNotificationsPlugin
|
||||
.resolvePlatformSpecificImplementation<
|
||||
AndroidFlutterLocalNotificationsPlugin>()
|
||||
?.requestNotificationsPermission();
|
||||
flutterLocalNotificationsPlugin
|
||||
.resolvePlatformSpecificImplementation<
|
||||
IOSFlutterLocalNotificationsPlugin>()
|
||||
?.requestPermissions(
|
||||
alert: true,
|
||||
badge: true,
|
||||
sound: true,
|
||||
);
|
||||
flutterLocalNotificationsPlugin
|
||||
.resolvePlatformSpecificImplementation<
|
||||
MacOSFlutterLocalNotificationsPlugin>()
|
||||
?.requestPermissions(
|
||||
alert: true,
|
||||
badge: true,
|
||||
sound: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> registerPushNotifications() async {
|
||||
if (PlatformInfo.isWeb) return;
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
if (prefs.getBool('service_background_notification') == true) {
|
||||
log('Background notification service has been enabled, skip register push notifications');
|
||||
return;
|
||||
}
|
||||
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) return;
|
||||
|
||||
late final String? token;
|
||||
late final String provider;
|
||||
var deviceUuid = await _getDeviceUuid();
|
||||
|
||||
if (deviceUuid == null || deviceUuid.isEmpty) {
|
||||
log("Unable to active push notifications, couldn't get device uuid");
|
||||
return;
|
||||
} else {
|
||||
log('Device UUID is $deviceUuid');
|
||||
}
|
||||
|
||||
if (PlatformInfo.isIOS || PlatformInfo.isMacOS) {
|
||||
provider = 'apple';
|
||||
token = await FirebaseMessaging.instance.getAPNSToken();
|
||||
} else {
|
||||
provider = 'firebase';
|
||||
token = await FirebaseMessaging.instance.getToken();
|
||||
}
|
||||
log('Device Push Token is $token');
|
||||
|
||||
final client = await auth.configureClient('auth');
|
||||
|
||||
final resp = await client.post('/notifications/subscribe', {
|
||||
'provider': provider,
|
||||
'device_token': token,
|
||||
'device_id': deviceUuid,
|
||||
});
|
||||
if (resp.statusCode != 200 && resp.statusCode != 400) {
|
||||
throw RequestException(resp);
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> _getDeviceUuid() async {
|
||||
if (PlatformInfo.isWeb) return null;
|
||||
return await FlutterUdid.consistentUdid;
|
||||
}
|
||||
}
|
@ -26,6 +26,19 @@ class RelationshipProvider extends GetxController {
|
||||
return _friends.any((x) => x.relatedId == account.id);
|
||||
}
|
||||
|
||||
Future<Relationship?> getRelationship(int relatedId) async {
|
||||
final AuthProvider auth = Get.find();
|
||||
final client = await auth.configureClient('auth');
|
||||
final resp = await client.get('/users/me/relations/$relatedId');
|
||||
if (resp.statusCode == 404) {
|
||||
return null;
|
||||
} else if (resp.statusCode != 200) {
|
||||
throw RequestException(resp);
|
||||
}
|
||||
|
||||
return Relationship.fromJson(resp.body);
|
||||
}
|
||||
|
||||
Future<Response> listRelation() async {
|
||||
final AuthProvider auth = Get.find();
|
||||
final client = await auth.configureClient('auth');
|
||||
@ -38,7 +51,19 @@ class RelationshipProvider extends GetxController {
|
||||
return client.get('/users/me/relations?status=$status');
|
||||
}
|
||||
|
||||
Future<Response> makeFriend(String username) async {
|
||||
Future<Relationship?> blockUser(String username) async {
|
||||
final AuthProvider auth = Get.find();
|
||||
final client = await auth.configureClient('auth');
|
||||
final resp =
|
||||
await client.post('/users/me/relations/block?related=$username', {});
|
||||
if (resp.statusCode != 200) {
|
||||
throw RequestException(resp);
|
||||
}
|
||||
|
||||
return Relationship.fromJson(resp.body);
|
||||
}
|
||||
|
||||
Future<Relationship?> makeFriend(String username) async {
|
||||
final AuthProvider auth = Get.find();
|
||||
final client = await auth.configureClient('auth');
|
||||
final resp = await client.post('/users/me/relations?related=$username', {});
|
||||
@ -46,7 +71,7 @@ class RelationshipProvider extends GetxController {
|
||||
throw RequestException(resp);
|
||||
}
|
||||
|
||||
return resp;
|
||||
return Relationship.fromJson(resp.body);
|
||||
}
|
||||
|
||||
Future<Response> handleRelation(
|
||||
@ -64,17 +89,17 @@ class RelationshipProvider extends GetxController {
|
||||
return resp;
|
||||
}
|
||||
|
||||
Future<Response> editRelation(Relationship relationship, int status) async {
|
||||
Future<Relationship?> editRelation(int relatedId, int status) async {
|
||||
final AuthProvider auth = Get.find();
|
||||
final client = await auth.configureClient('auth');
|
||||
final resp = await client.patch(
|
||||
'/users/me/relations/${relationship.relatedId}',
|
||||
final resp = await client.put(
|
||||
'/users/me/relations/$relatedId',
|
||||
{'status': status},
|
||||
);
|
||||
if (resp.statusCode != 200) {
|
||||
throw RequestException(resp);
|
||||
}
|
||||
|
||||
return resp;
|
||||
return Relationship.fromJson(resp.body);
|
||||
}
|
||||
}
|
||||
|
@ -1,34 +1,48 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/models/pagination.dart';
|
||||
import 'package:solian/exceptions/request.dart';
|
||||
import 'package:solian/models/stickers.dart';
|
||||
import 'package:solian/services.dart';
|
||||
|
||||
class StickerProvider extends GetxController {
|
||||
final RxMap<String, String> aliasImageMapping = RxMap();
|
||||
final RxList<Sticker> availableStickers = RxList.empty(growable: true);
|
||||
final RxMap<String, FutureOr<Sticker?>> stickerCache = RxMap();
|
||||
|
||||
Future<void> refreshAvailableStickers() async {
|
||||
availableStickers.clear();
|
||||
aliasImageMapping.clear();
|
||||
Future<Sticker?> getStickerByAlias(String alias) {
|
||||
if (stickerCache.containsKey(alias)) {
|
||||
return Future.value(stickerCache[alias]);
|
||||
}
|
||||
|
||||
stickerCache[alias] = Future(() async {
|
||||
final client = await ServiceFinder.configureClient('files');
|
||||
final resp = await client.get(
|
||||
'/stickers/lookup/$alias',
|
||||
);
|
||||
if (resp.statusCode != 200) {
|
||||
if (resp.statusCode == 404) {
|
||||
stickerCache[alias] = null;
|
||||
}
|
||||
throw RequestException(resp);
|
||||
}
|
||||
|
||||
return Sticker.fromJson(resp.body);
|
||||
}).then((result) {
|
||||
stickerCache[alias] = result;
|
||||
return result;
|
||||
});
|
||||
|
||||
return Future.value(stickerCache[alias]);
|
||||
}
|
||||
|
||||
Future<List<Sticker>> searchStickerByAlias(String alias) async {
|
||||
final client = await ServiceFinder.configureClient('files');
|
||||
final resp = await client.get(
|
||||
'/stickers/manifest?take=100',
|
||||
'/stickers/lookup?probe=$alias',
|
||||
);
|
||||
if (resp.statusCode == 200) {
|
||||
final result = PaginationResult.fromJson(resp.body);
|
||||
final out = result.data?.map((e) => StickerPack.fromJson(e)).toList();
|
||||
if (out == null) return;
|
||||
|
||||
for (final pack in out) {
|
||||
for (final sticker in (pack.stickers ?? List<Sticker>.empty())) {
|
||||
sticker.pack = pack;
|
||||
aliasImageMapping[sticker.textPlaceholder.toUpperCase()] =
|
||||
sticker.imageUrl;
|
||||
availableStickers.add(sticker);
|
||||
}
|
||||
}
|
||||
if (resp.statusCode != 200) {
|
||||
throw RequestException(resp);
|
||||
}
|
||||
availableStickers.refresh();
|
||||
|
||||
return List<Sticker>.from(resp.body.map((x) => Sticker.fromJson(x)));
|
||||
}
|
||||
}
|
||||
|
46
lib/providers/subscription.dart
Normal file
46
lib/providers/subscription.dart
Normal file
@ -0,0 +1,46 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/exceptions/request.dart';
|
||||
import 'package:solian/exceptions/unauthorized.dart';
|
||||
import 'package:solian/models/subscription.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
|
||||
class SubscriptionProvider extends GetxController {
|
||||
Future<Subscription?> getSubscriptionOnUser(int userId) async {
|
||||
final auth = Get.find<AuthProvider>();
|
||||
if (!auth.isAuthorized.value) throw const UnauthorizedException();
|
||||
|
||||
final client = await auth.configureClient('co');
|
||||
final resp = await client.get('/subscriptions/users/$userId');
|
||||
if (resp.statusCode == 404) {
|
||||
return null;
|
||||
} else if (resp.statusCode != 200) {
|
||||
throw RequestException(resp);
|
||||
}
|
||||
|
||||
return Subscription.fromJson(resp.body);
|
||||
}
|
||||
|
||||
Future<Subscription> subscribeToUser(int userId) async {
|
||||
final auth = Get.find<AuthProvider>();
|
||||
if (!auth.isAuthorized.value) throw const UnauthorizedException();
|
||||
|
||||
final client = await auth.configureClient('co');
|
||||
final resp = await client.post('/subscriptions/users/$userId', {});
|
||||
if (resp.statusCode != 200) {
|
||||
throw RequestException(resp);
|
||||
}
|
||||
|
||||
return Subscription.fromJson(resp.body);
|
||||
}
|
||||
|
||||
Future<void> unsubscribeFromUser(int userId) async {
|
||||
final auth = Get.find<AuthProvider>();
|
||||
if (!auth.isAuthorized.value) throw const UnauthorizedException();
|
||||
|
||||
final client = await auth.configureClient('co');
|
||||
final resp = await client.delete('/subscriptions/users/$userId');
|
||||
if (resp.statusCode != 200) {
|
||||
throw RequestException(resp);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,8 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:solian/models/theme.dart';
|
||||
import 'package:solian/theme.dart';
|
||||
|
||||
class ThemeSwitcher extends ChangeNotifier {
|
||||
@ -13,11 +16,21 @@ class ThemeSwitcher extends ChangeNotifier {
|
||||
|
||||
Future<void> restoreTheme() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
if (prefs.containsKey('global_theme_color')) {
|
||||
final value = prefs.getInt('global_theme_color')!;
|
||||
final color = Color(value);
|
||||
lightThemeData = AppTheme.build(Brightness.light, seedColor: color);
|
||||
darkThemeData = AppTheme.build(Brightness.dark, seedColor: color);
|
||||
if (prefs.containsKey('global_theme')) {
|
||||
final value = SolianThemeData.fromJson(
|
||||
jsonDecode(prefs.getString('global_theme')!),
|
||||
);
|
||||
final agedTheme = prefs.getBool('aged_theme');
|
||||
lightThemeData = AppTheme.buildFromData(
|
||||
Brightness.light,
|
||||
value,
|
||||
useMaterial3: agedTheme == null ? true : !agedTheme,
|
||||
);
|
||||
darkThemeData = AppTheme.buildFromData(
|
||||
Brightness.dark,
|
||||
value,
|
||||
useMaterial3: agedTheme == null ? true : !agedTheme,
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
@ -27,4 +40,25 @@ class ThemeSwitcher extends ChangeNotifier {
|
||||
darkThemeData = dark;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> setThemeData(SolianThemeData? data) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
if (data == null) {
|
||||
prefs.remove('global_theme');
|
||||
} else {
|
||||
prefs.setString(
|
||||
'global_theme',
|
||||
jsonEncode(data.toJson()),
|
||||
);
|
||||
lightThemeData = AppTheme.buildFromData(Brightness.light, data);
|
||||
darkThemeData = AppTheme.buildFromData(Brightness.dark, data);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setAgedTheme(bool enabled) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
prefs.setBool('aged_theme', enabled);
|
||||
await restoreTheme();
|
||||
}
|
||||
}
|
||||
|
@ -3,17 +3,11 @@ import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:solian/exceptions/request.dart';
|
||||
import 'package:solian/models/notification.dart';
|
||||
import 'package:solian/models/packet.dart';
|
||||
import 'package:solian/models/pagination.dart';
|
||||
import 'package:solian/platform.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/providers/notifications.dart';
|
||||
import 'package:solian/services.dart';
|
||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||
|
||||
@ -21,56 +15,10 @@ class WebSocketProvider extends GetxController {
|
||||
RxBool isConnected = false.obs;
|
||||
RxBool isConnecting = false.obs;
|
||||
|
||||
RxInt notificationUnread = 0.obs;
|
||||
RxList<Notification> notifications =
|
||||
List<Notification>.empty(growable: true).obs;
|
||||
|
||||
WebSocketChannel? websocket;
|
||||
|
||||
StreamController<NetworkPackage> stream = StreamController.broadcast();
|
||||
|
||||
@override
|
||||
onInit() {
|
||||
notifyPrefetch();
|
||||
|
||||
super.onInit();
|
||||
}
|
||||
|
||||
void requestPermissions() {
|
||||
try {
|
||||
FirebaseMessaging.instance.requestPermission(
|
||||
alert: true,
|
||||
announcement: true,
|
||||
carPlay: true,
|
||||
badge: true,
|
||||
sound: true);
|
||||
} catch (_) {
|
||||
// When firebase isn't initialized (background service)
|
||||
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
|
||||
FlutterLocalNotificationsPlugin();
|
||||
flutterLocalNotificationsPlugin
|
||||
.resolvePlatformSpecificImplementation<
|
||||
AndroidFlutterLocalNotificationsPlugin>()
|
||||
?.requestNotificationsPermission();
|
||||
flutterLocalNotificationsPlugin
|
||||
.resolvePlatformSpecificImplementation<
|
||||
IOSFlutterLocalNotificationsPlugin>()
|
||||
?.requestPermissions(
|
||||
alert: true,
|
||||
badge: true,
|
||||
sound: true,
|
||||
);
|
||||
flutterLocalNotificationsPlugin
|
||||
.resolvePlatformSpecificImplementation<
|
||||
MacOSFlutterLocalNotificationsPlugin>()
|
||||
?.requestPermissions(
|
||||
alert: true,
|
||||
badge: true,
|
||||
sound: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> connect({noRetry = false}) async {
|
||||
if (isConnected.value) {
|
||||
return;
|
||||
@ -119,8 +67,9 @@ class WebSocketProvider extends GetxController {
|
||||
log('Websocket incoming message: ${packet.method} ${packet.message}');
|
||||
stream.sink.add(packet);
|
||||
if (packet.method == 'notifications.new') {
|
||||
notifications.add(Notification.fromJson(packet.payload!));
|
||||
notificationUnread.value++;
|
||||
final NotificationProvider nty = Get.find();
|
||||
nty.notifications.add(Notification.fromJson(packet.payload!));
|
||||
nty.notificationUnread.value++;
|
||||
}
|
||||
},
|
||||
onDone: () {
|
||||
@ -133,95 +82,4 @@ class WebSocketProvider extends GetxController {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> notifyPrefetch() async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) return;
|
||||
|
||||
final client = await auth.configureClient('auth');
|
||||
|
||||
final resp = await client.get('/notifications?skip=0&take=100');
|
||||
if (resp.statusCode == 200) {
|
||||
final result = PaginationResult.fromJson(resp.body);
|
||||
final data = result.data?.map((x) => Notification.fromJson(x)).toList();
|
||||
if (data != null) {
|
||||
notifications.addAll(data);
|
||||
notificationUnread.value = data.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> registerPushNotifications() async {
|
||||
if (PlatformInfo.isWeb) return;
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
if (prefs.getBool('service_background_notification') == true) {
|
||||
log('Background notification service has been enabled, skip register push notifications');
|
||||
return;
|
||||
}
|
||||
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) return;
|
||||
|
||||
late final String? token;
|
||||
late final String provider;
|
||||
final deviceUuid = await _getDeviceUuid();
|
||||
|
||||
if (deviceUuid == null || deviceUuid.isEmpty) {
|
||||
log("Unable to active push notifications, couldn't get device uuid");
|
||||
} else {
|
||||
log('Device UUID is $deviceUuid');
|
||||
}
|
||||
|
||||
if (PlatformInfo.isIOS || PlatformInfo.isMacOS) {
|
||||
provider = 'apple';
|
||||
token = await FirebaseMessaging.instance.getAPNSToken();
|
||||
} else {
|
||||
provider = 'firebase';
|
||||
token = await FirebaseMessaging.instance.getToken();
|
||||
}
|
||||
log('Device Push Token is $token');
|
||||
|
||||
final client = await auth.configureClient('auth');
|
||||
|
||||
final resp = await client.post('/notifications/subscribe', {
|
||||
'provider': provider,
|
||||
'device_token': token,
|
||||
'device_id': deviceUuid,
|
||||
});
|
||||
if (resp.statusCode != 200 && resp.statusCode != 400) {
|
||||
throw RequestException(resp);
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> _getDeviceUuid() async {
|
||||
DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
|
||||
if (PlatformInfo.isWeb) {
|
||||
final webInfo = await deviceInfo.webBrowserInfo;
|
||||
return webInfo.vendor! +
|
||||
webInfo.userAgent! +
|
||||
webInfo.hardwareConcurrency.toString();
|
||||
}
|
||||
if (PlatformInfo.isAndroid) {
|
||||
final androidInfo = await deviceInfo.androidInfo;
|
||||
return androidInfo.id;
|
||||
}
|
||||
if (PlatformInfo.isIOS) {
|
||||
final iosInfo = await deviceInfo.iosInfo;
|
||||
return iosInfo.identifierForVendor!;
|
||||
}
|
||||
if (PlatformInfo.isLinux) {
|
||||
final linuxInfo = await deviceInfo.linuxInfo;
|
||||
return linuxInfo.machineId!;
|
||||
}
|
||||
if (PlatformInfo.isWindows) {
|
||||
final windowsInfo = await deviceInfo.windowsInfo;
|
||||
return windowsInfo.deviceId;
|
||||
}
|
||||
if (PlatformInfo.isMacOS) {
|
||||
final macosInfo = await deviceInfo.macOsInfo;
|
||||
return macosInfo.systemGUID;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +1,17 @@
|
||||
import 'package:animations/animations.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:solian/bootstrapper.dart';
|
||||
import 'package:solian/models/post.dart';
|
||||
import 'package:solian/models/realm.dart';
|
||||
import 'package:solian/screens/about.dart';
|
||||
import 'package:solian/screens/account.dart';
|
||||
import 'package:solian/screens/account/audit_log.dart';
|
||||
import 'package:solian/screens/account/friend.dart';
|
||||
import 'package:solian/screens/account/personalize.dart';
|
||||
import 'package:solian/screens/account/preferences/notifications.dart';
|
||||
import 'package:solian/screens/account/preferences/security.dart';
|
||||
import 'package:solian/screens/account/profile_edit.dart';
|
||||
import 'package:solian/screens/account/profile_page.dart';
|
||||
import 'package:solian/screens/account/stickers.dart';
|
||||
import 'package:solian/screens/auth/signin.dart';
|
||||
import 'package:solian/screens/auth/signup.dart';
|
||||
import 'package:solian/screens/channel/channel_chat.dart';
|
||||
@ -15,26 +19,31 @@ import 'package:solian/screens/channel/channel_detail.dart';
|
||||
import 'package:solian/screens/channel/channel_organize.dart';
|
||||
import 'package:solian/screens/chat.dart';
|
||||
import 'package:solian/screens/dashboard.dart';
|
||||
import 'package:solian/screens/feed/search.dart';
|
||||
import 'package:solian/screens/posts/post_search.dart';
|
||||
import 'package:solian/screens/posts/post_detail.dart';
|
||||
import 'package:solian/screens/feed/draft_box.dart';
|
||||
import 'package:solian/screens/posts/draft_box.dart';
|
||||
import 'package:solian/screens/realms.dart';
|
||||
import 'package:solian/screens/realms/realm_detail.dart';
|
||||
import 'package:solian/screens/realms/realm_organize.dart';
|
||||
import 'package:solian/screens/realms/realm_view.dart';
|
||||
import 'package:solian/screens/feed.dart';
|
||||
import 'package:solian/screens/explore.dart';
|
||||
import 'package:solian/screens/posts/post_editor.dart';
|
||||
import 'package:solian/screens/settings.dart';
|
||||
import 'package:solian/shells/root_shell.dart';
|
||||
import 'package:solian/shells/title_shell.dart';
|
||||
import 'package:solian/theme.dart';
|
||||
import 'package:solian/widgets/sidebar/empty_placeholder.dart';
|
||||
|
||||
abstract class AppRouter {
|
||||
static GoRouter instance = GoRouter(
|
||||
routes: [
|
||||
ShellRoute(
|
||||
builder: (context, state, child) => RootShell(
|
||||
state: state,
|
||||
child: child,
|
||||
builder: (context, state, child) => BootstrapperShell(
|
||||
key: const Key('global-bootstrapper'),
|
||||
child: RootShell(
|
||||
state: state,
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
routes: [
|
||||
GoRoute(
|
||||
@ -74,26 +83,26 @@ abstract class AppRouter {
|
||||
builder: (context, state, child) => child,
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/feed',
|
||||
name: 'feed',
|
||||
builder: (context, state) => const FeedScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/feed/search',
|
||||
name: 'feedSearch',
|
||||
builder: (context, state) => TitleShell(
|
||||
state: state,
|
||||
child: FeedSearchScreen(
|
||||
tag: state.uri.queryParameters['tag'],
|
||||
category: state.uri.queryParameters['category'],
|
||||
),
|
||||
),
|
||||
path: '/explore',
|
||||
name: 'explore',
|
||||
builder: (context, state) => const ExploreScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/drafts',
|
||||
name: 'draftBox',
|
||||
builder: (context, state) => const DraftBoxScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/posts/search',
|
||||
name: 'postSearch',
|
||||
builder: (context, state) => TitleShell(
|
||||
state: state,
|
||||
child: PostSearchScreen(
|
||||
tag: state.uri.queryParameters['tag'],
|
||||
category: state.uri.queryParameters['category'],
|
||||
),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/posts/view/:id',
|
||||
name: 'postDetail',
|
||||
@ -101,6 +110,7 @@ abstract class AppRouter {
|
||||
state: state,
|
||||
child: PostDetailScreen(
|
||||
id: state.pathParameters['id']!,
|
||||
post: state.extra as Post?,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -133,12 +143,15 @@ abstract class AppRouter {
|
||||
);
|
||||
|
||||
static final ShellRoute _chatRoute = ShellRoute(
|
||||
builder: (context, state, child) => child,
|
||||
builder: (context, state, child) =>
|
||||
AppTheme.isLargeScreen(context) ? ChatListShell(child: child) : child,
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/chat',
|
||||
name: 'chat',
|
||||
builder: (context, state) => const ChatScreen(),
|
||||
builder: (context, state) => AppTheme.isLargeScreen(context)
|
||||
? const EmptyPagePlaceholder()
|
||||
: const ChatScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/chat/organize',
|
||||
@ -169,6 +182,7 @@ abstract class AppRouter {
|
||||
final arguments = state.extra as ChannelDetailArguments;
|
||||
return TitleShell(
|
||||
state: state,
|
||||
isResponsive: true,
|
||||
child: ChannelDetailScreen(
|
||||
channel: arguments.channel,
|
||||
profile: arguments.profile,
|
||||
@ -238,14 +252,6 @@ abstract class AppRouter {
|
||||
name: 'accountFriend',
|
||||
builder: (context, state) => const FriendScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/account/stickers',
|
||||
name: 'accountStickers',
|
||||
builder: (context, state) => TitleShell(
|
||||
state: state,
|
||||
child: const StickerScreen(),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/account/personalize',
|
||||
name: 'accountProfile',
|
||||
@ -254,6 +260,30 @@ abstract class AppRouter {
|
||||
child: const PersonalizeScreen(),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/account/preferences/notifications',
|
||||
name: 'notificationPreferences',
|
||||
builder: (context, state) => TitleShell(
|
||||
state: state,
|
||||
child: const NotificationPreferencesScreen(),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/account/preferences/auth',
|
||||
name: 'authPreferences',
|
||||
builder: (context, state) => TitleShell(
|
||||
state: state,
|
||||
child: const AuthPreferencesScreen(),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/account/audit',
|
||||
name: 'auditLog',
|
||||
builder: (context, state) => TitleShell(
|
||||
state: state,
|
||||
child: const AuditLogScreen(),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/account/view/:name',
|
||||
name: 'accountProfilePage',
|
||||
|
@ -1,6 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:solian/widgets/sized_container.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
class AboutScreen extends StatelessWidget {
|
||||
@ -11,78 +15,130 @@ class AboutScreen extends StatelessWidget {
|
||||
const denseButtonStyle =
|
||||
ButtonStyle(visualDensity: VisualDensity(vertical: -4));
|
||||
|
||||
return Material(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
||||
child: Image.asset('assets/logo.png', width: 120, height: 120),
|
||||
),
|
||||
const Gap(8),
|
||||
Text(
|
||||
'Solian',
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
),
|
||||
const Text(
|
||||
'The Solar Network',
|
||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
||||
),
|
||||
const Gap(8),
|
||||
FutureBuilder(
|
||||
future: PackageInfo.fromPlatform(),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
||||
child: Image.asset('assets/logo.png', width: 120, height: 120),
|
||||
),
|
||||
const Gap(8),
|
||||
Text(
|
||||
'Solian',
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
),
|
||||
const Text(
|
||||
'The Solar Network',
|
||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
||||
),
|
||||
const Gap(8),
|
||||
FutureBuilder(
|
||||
future: PackageInfo.fromPlatform(),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Text(
|
||||
'v${snapshot.data!.version} · ${snapshot.data!.buildNumber}',
|
||||
style: const TextStyle(fontFamily: 'monospace'),
|
||||
);
|
||||
},
|
||||
),
|
||||
Text('Copyright © ${DateTime.now().year} Solsynth LLC'),
|
||||
const Gap(16),
|
||||
TextButton(
|
||||
style: denseButtonStyle,
|
||||
child: const Text('App Details'),
|
||||
onPressed: () async {
|
||||
final info = await PackageInfo.fromPlatform();
|
||||
return Text(
|
||||
'v${snapshot.data!.version} · ${snapshot.data!.buildNumber}',
|
||||
style: const TextStyle(fontFamily: 'monospace'),
|
||||
);
|
||||
},
|
||||
),
|
||||
Text('Copyright © ${DateTime.now().year} Solsynth LLC'),
|
||||
const Gap(16),
|
||||
CenteredContainer(
|
||||
maxWidth: 280,
|
||||
child: Wrap(
|
||||
spacing: 4,
|
||||
runSpacing: 4,
|
||||
alignment: WrapAlignment.center,
|
||||
children: [
|
||||
TextButton(
|
||||
style: denseButtonStyle,
|
||||
child: Text('appDetails'.tr),
|
||||
onPressed: () async {
|
||||
final info = await PackageInfo.fromPlatform();
|
||||
|
||||
showAboutDialog(
|
||||
context: context,
|
||||
applicationVersion: '${info.version} (${info.buildNumber})',
|
||||
applicationLegalese:
|
||||
'The Solar Network App is an intuitive and self-hostable social network and computing platform. Experience the freedom of a user-friendly design that empowers you to create and connect with communities on your own terms. Embrace the future of social networking with a platform that prioritizes your independence and privacy.',
|
||||
applicationIcon: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
||||
child:
|
||||
Image.asset('assets/logo.png', width: 60, height: 60),
|
||||
),
|
||||
);
|
||||
},
|
||||
showAboutDialog(
|
||||
context: context,
|
||||
applicationVersion:
|
||||
'${info.version} (${info.buildNumber})',
|
||||
applicationLegalese:
|
||||
'The Solar Network App is an intuitive and open-source social network and computing platform. Experience the freedom of a user-friendly design that empowers you to create and connect with communities on your own terms. Embrace the future of social networking with a platform that prioritizes your independence and privacy.',
|
||||
applicationIcon: ClipRRect(
|
||||
borderRadius:
|
||||
const BorderRadius.all(Radius.circular(16)),
|
||||
child: Image.asset('assets/logo.png',
|
||||
width: 60, height: 60),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
style: denseButtonStyle,
|
||||
child: Text('projectWebsite'.tr),
|
||||
onPressed: () {
|
||||
launchUrlString(
|
||||
'https://solsynth.dev/products/solar-network');
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
style: denseButtonStyle,
|
||||
child: Text('termRelated'.tr),
|
||||
onPressed: () {
|
||||
launchUrlString('https://solsynth.dev/terms');
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
style: denseButtonStyle,
|
||||
child: Text('serviceStatus'.tr),
|
||||
onPressed: () {
|
||||
launchUrlString('https://status.solsynth.dev');
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
TextButton(
|
||||
style: denseButtonStyle,
|
||||
child: const Text('Project Website'),
|
||||
onPressed: () {
|
||||
launchUrlString('https://solsynth.dev/products/solar-network');
|
||||
},
|
||||
),
|
||||
const Gap(16),
|
||||
const Text(
|
||||
'Open-sourced under AGPLv3',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w300,
|
||||
fontSize: 12,
|
||||
),
|
||||
const Gap(16),
|
||||
const Text(
|
||||
'Open-sourced under AGPLv3',
|
||||
style: TextStyle(
|
||||
),
|
||||
FutureBuilder(
|
||||
future: SharedPreferences.getInstance(),
|
||||
builder: (context, snapshot) {
|
||||
const textStyle = TextStyle(
|
||||
fontWeight: FontWeight.w300,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (!snapshot.hasData ||
|
||||
!snapshot.data!.containsKey('first_boot_time')) {
|
||||
return Text(
|
||||
'firstBootTime'.trParams({'time': 'unknown'.tr}),
|
||||
style: textStyle,
|
||||
);
|
||||
} else {
|
||||
return Text(
|
||||
'firstBootTime'.trParams({
|
||||
'time': DateFormat('yyyy-MM-dd').format(
|
||||
DateTime.tryParse(
|
||||
snapshot.data!.getString('first_boot_time')!,
|
||||
)?.toLocal() ??
|
||||
DateTime.now(),
|
||||
),
|
||||
}),
|
||||
style: textStyle,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -45,113 +45,132 @@ class _AccountScreenState extends State<AccountScreen> {
|
||||
'accountFriend'.tr,
|
||||
'accountFriend',
|
||||
),
|
||||
(
|
||||
const Icon(Icons.emoji_symbols),
|
||||
'accountStickers'.tr,
|
||||
'accountStickers',
|
||||
),
|
||||
];
|
||||
|
||||
final AuthProvider auth = Get.find();
|
||||
|
||||
return Material(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: SafeArea(
|
||||
child: Obx(() {
|
||||
if (auth.isAuthorized.isFalse) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_ActionCard(
|
||||
icon: Icon(
|
||||
Icons.login,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
title: 'signin'.tr,
|
||||
caption: 'signinCaption'.tr,
|
||||
onTap: () {
|
||||
AppRouter.instance.pushNamed('signin').then((val) async {
|
||||
if (val == true) {
|
||||
await auth.refreshUserProfile();
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
_ActionCard(
|
||||
icon: Icon(
|
||||
Icons.add,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
title: 'signup'.tr,
|
||||
caption: 'signupCaption'.tr,
|
||||
onTap: () {
|
||||
AppRouter.instance.pushNamed('signup').then((_) {
|
||||
setState(() {});
|
||||
});
|
||||
},
|
||||
),
|
||||
const Gap(4),
|
||||
TextButton(
|
||||
style: const ButtonStyle(
|
||||
visualDensity: VisualDensity(
|
||||
horizontal: -4,
|
||||
vertical: -2,
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
AppRouter.instance.pushNamed('settings');
|
||||
},
|
||||
child: Text('settings'.tr),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return CenteredContainer(
|
||||
child: ListView(
|
||||
return SafeArea(
|
||||
child: Obx(() {
|
||||
if (auth.isAuthorized.isFalse) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (auth.userProfile.value != null)
|
||||
const AccountHeading().paddingOnly(bottom: 8, top: 8),
|
||||
...(actionItems.map(
|
||||
(x) => ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 34),
|
||||
leading: x.$1,
|
||||
title: Text(x.$2),
|
||||
onTap: () {
|
||||
AppRouter.instance
|
||||
.pushNamed(x.$3)
|
||||
.then((_) => setState(() {}));
|
||||
},
|
||||
_ActionCard(
|
||||
icon: Icon(
|
||||
Icons.login,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
)),
|
||||
const Divider(thickness: 0.3, height: 1)
|
||||
.paddingSymmetric(vertical: 4),
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 34),
|
||||
leading: const Icon(Icons.settings),
|
||||
title: Text('settings'.tr),
|
||||
title: 'signin'.tr,
|
||||
caption: 'signinCaption'.tr,
|
||||
onTap: () {
|
||||
AppRouter.instance.pushNamed('settings');
|
||||
AppRouter.instance.pushNamed('signin').then((val) async {
|
||||
if (val == true) {
|
||||
await auth.refreshUserProfile();
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
const Divider(thickness: 0.3, height: 1)
|
||||
.paddingSymmetric(vertical: 4),
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 34),
|
||||
leading: const Icon(Icons.logout),
|
||||
title: Text('signout'.tr),
|
||||
_ActionCard(
|
||||
icon: Icon(
|
||||
Icons.add,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
title: 'signup'.tr,
|
||||
caption: 'signupCaption'.tr,
|
||||
onTap: () {
|
||||
auth.signout();
|
||||
setState(() {});
|
||||
AppRouter.instance.pushNamed('signup').then((_) {
|
||||
setState(() {});
|
||||
});
|
||||
},
|
||||
),
|
||||
const Gap(4),
|
||||
TextButton(
|
||||
style: const ButtonStyle(
|
||||
visualDensity: VisualDensity(
|
||||
horizontal: -4,
|
||||
vertical: -2,
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
AppRouter.instance.pushNamed('settings');
|
||||
},
|
||||
child: Text('settings'.tr),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
}
|
||||
|
||||
return CenteredContainer(
|
||||
child: ListView(
|
||||
children: [
|
||||
if (auth.userProfile.value != null)
|
||||
const AccountHeading().paddingOnly(bottom: 8, top: 16),
|
||||
...(actionItems.map(
|
||||
(x) => ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 34),
|
||||
leading: x.$1,
|
||||
title: Text(x.$2),
|
||||
onTap: () {
|
||||
AppRouter.instance
|
||||
.pushNamed(x.$3)
|
||||
.then((_) => setState(() {}));
|
||||
},
|
||||
),
|
||||
)),
|
||||
const Divider(thickness: 0.3, height: 1)
|
||||
.paddingSymmetric(vertical: 4),
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 34),
|
||||
leading: const Icon(Icons.settings),
|
||||
title: Text('settings'.tr),
|
||||
onTap: () {
|
||||
AppRouter.instance.pushNamed('settings');
|
||||
},
|
||||
),
|
||||
if (auth.isAuthorized.value)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.event_repeat),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 34),
|
||||
title: Text('auditLog'.tr),
|
||||
onTap: () {
|
||||
AppRouter.instance.pushNamed('auditLog');
|
||||
},
|
||||
),
|
||||
if (auth.isAuthorized.value)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.lock),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 34),
|
||||
title: Text('authPreferences'.tr),
|
||||
onTap: () {
|
||||
AppRouter.instance.pushNamed('authPreferences');
|
||||
},
|
||||
),
|
||||
if (auth.isAuthorized.value)
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 34),
|
||||
leading: const Icon(Icons.edit_notifications),
|
||||
title: Text('notificationPreferences'.tr),
|
||||
onTap: () {
|
||||
AppRouter.instance.pushNamed('notificationPreferences');
|
||||
},
|
||||
),
|
||||
const Divider(thickness: 0.3, height: 1)
|
||||
.paddingSymmetric(vertical: 4),
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 34),
|
||||
leading: const Icon(Icons.logout),
|
||||
title: Text('signout'.tr),
|
||||
onTap: () {
|
||||
auth.signout();
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
154
lib/screens/account/audit_log.dart
Normal file
154
lib/screens/account/audit_log.dart
Normal file
@ -0,0 +1,154 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:marquee/marquee.dart';
|
||||
import 'package:solian/exceptions/request.dart';
|
||||
import 'package:solian/exts.dart';
|
||||
import 'package:solian/models/audit_log.dart';
|
||||
import 'package:solian/models/pagination.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/widgets/relative_date.dart';
|
||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||
import 'package:timeline_tile/timeline_tile.dart';
|
||||
|
||||
class AuditLogScreen extends StatefulWidget {
|
||||
const AuditLogScreen({super.key});
|
||||
|
||||
@override
|
||||
State<AuditLogScreen> createState() => _AuditLogScreenState();
|
||||
}
|
||||
|
||||
class _AuditLogScreenState extends State<AuditLogScreen> {
|
||||
bool _isBusy = true;
|
||||
|
||||
final List<AuditEvent> _events = List.empty(growable: true);
|
||||
|
||||
Future<void> _getEvents() async {
|
||||
if (!_isBusy) setState(() => _isBusy = true);
|
||||
|
||||
final AuthProvider auth = Get.find();
|
||||
final client = await auth.configureClient('id');
|
||||
final resp =
|
||||
await client.get('/users/me/events?take=15&offset=${_events.length}');
|
||||
if (resp.statusCode != 200) {
|
||||
context.showErrorDialog(RequestException(resp));
|
||||
}
|
||||
|
||||
final result = PaginationResult.fromJson(resp.body);
|
||||
|
||||
setState(() {
|
||||
_events.addAll(
|
||||
result.data?.map((x) => AuditEvent.fromJson(x)).toList() ??
|
||||
List.empty(),
|
||||
);
|
||||
_isBusy = false;
|
||||
});
|
||||
}
|
||||
|
||||
bool _showIp = false;
|
||||
|
||||
String _censorIpAddress(String ip) {
|
||||
List<String> parts = ip.split('.');
|
||||
|
||||
if (parts.length == 4) {
|
||||
String censoredPart1 = '*' * parts[1].length;
|
||||
String censoredPart2 = '*' * parts[2].length;
|
||||
String censoredPart3 = '*' * parts[3].length;
|
||||
|
||||
return '${parts[0]}.$censoredPart1.$censoredPart2.$censoredPart3';
|
||||
} else {
|
||||
return '***.***.***.***';
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_getEvents();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
CheckboxListTile(
|
||||
value: _showIp,
|
||||
title: Text('showIp'.tr),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
secondary: const Icon(Icons.alternate_email),
|
||||
tileColor:
|
||||
Theme.of(context).colorScheme.surfaceContainer.withOpacity(0.5),
|
||||
onChanged: (val) {
|
||||
setState(() => _showIp = val ?? false);
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () {
|
||||
_events.clear();
|
||||
return _getEvents();
|
||||
},
|
||||
child: InfiniteList(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
itemCount: _events.length,
|
||||
isLoading: _isBusy,
|
||||
onFetchData: () {
|
||||
_getEvents();
|
||||
},
|
||||
itemBuilder: (context, idx) {
|
||||
final element = _events[idx];
|
||||
return TimelineTile(
|
||||
isFirst: idx == 0,
|
||||
isLast: _events.length - 1 == idx,
|
||||
alignment: TimelineAlign.start,
|
||||
indicatorStyle: IndicatorStyle(width: 15),
|
||||
endChild: Container(
|
||||
child: Card(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
element.type,
|
||||
style: GoogleFonts.robotoMono(fontSize: 15),
|
||||
),
|
||||
Text(
|
||||
_showIp
|
||||
? element.ipAddress
|
||||
: _censorIpAddress(element.ipAddress),
|
||||
style: GoogleFonts.sourceCodePro(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 20,
|
||||
width: double.maxFinite,
|
||||
child: Marquee(
|
||||
text: element.userAgent,
|
||||
velocity: 25,
|
||||
startAfter: Duration(milliseconds: 500),
|
||||
pauseAfterRound: Duration(milliseconds: 3000),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
RelativeDate(element.createdAt),
|
||||
const Gap(6),
|
||||
Text('·'),
|
||||
const Gap(6),
|
||||
RelativeDate(element.createdAt, isFull: true),
|
||||
],
|
||||
),
|
||||
],
|
||||
).paddingSymmetric(horizontal: 12, vertical: 8),
|
||||
).paddingOnly(left: 16),
|
||||
),
|
||||
).paddingSymmetric(horizontal: 18);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@ import 'package:solian/models/relations.dart';
|
||||
import 'package:solian/providers/relation.dart';
|
||||
import 'package:solian/theme.dart';
|
||||
import 'package:solian/widgets/account/relative_list.dart';
|
||||
import 'package:solian/widgets/root_container.dart';
|
||||
|
||||
class FriendScreen extends StatefulWidget {
|
||||
const FriendScreen({super.key});
|
||||
@ -117,8 +118,7 @@ class _FriendScreenState extends State<FriendScreen>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
return RootContainer(
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
centerTitle: false,
|
||||
|
@ -1,9 +1,14 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/providers/websocket.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/models/notification.dart' as notify;
|
||||
import 'package:solian/models/notification.dart';
|
||||
import 'package:solian/models/post.dart';
|
||||
import 'package:solian/providers/notifications.dart';
|
||||
import 'package:solian/router.dart';
|
||||
import 'package:solian/widgets/loading_indicator.dart';
|
||||
import 'package:solian/widgets/markdown_text_content.dart';
|
||||
import 'package:solian/widgets/posts/post_item.dart';
|
||||
import 'package:solian/widgets/relative_date.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class NotificationScreen extends StatefulWidget {
|
||||
@ -14,57 +19,9 @@ class NotificationScreen extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _NotificationScreenState extends State<NotificationScreen> {
|
||||
bool _isBusy = false;
|
||||
|
||||
Future<void> _markAllRead() async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) return;
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
final WebSocketProvider provider = Get.find();
|
||||
|
||||
List<int> markList = List.empty(growable: true);
|
||||
for (final element in provider.notifications) {
|
||||
if (element.id <= 0) continue;
|
||||
markList.add(element.id);
|
||||
}
|
||||
|
||||
if (markList.isNotEmpty) {
|
||||
final client = await auth.configureClient('auth');
|
||||
await client.put('/notifications/read', {'messages': markList});
|
||||
}
|
||||
|
||||
provider.notifications.clear();
|
||||
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
|
||||
Future<void> _markOneRead(notify.Notification element, int index) async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) return;
|
||||
|
||||
final WebSocketProvider provider = Get.find();
|
||||
|
||||
if (element.id <= 0) {
|
||||
provider.notifications.removeAt(index);
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
final client = await auth.configureClient('auth');
|
||||
|
||||
await client.put('/notifications/read/${element.id}', {});
|
||||
|
||||
provider.notifications.removeAt(index);
|
||||
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final WebSocketProvider ws = Get.find();
|
||||
final NotificationProvider nty = Get.find();
|
||||
|
||||
return SizedBox(
|
||||
height: MediaQuery.of(context).size.height * 0.85,
|
||||
@ -77,71 +34,174 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
||||
).paddingOnly(left: 24, right: 24, top: 32, bottom: 16),
|
||||
Expanded(
|
||||
child: Obx(() {
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
if (_isBusy)
|
||||
return RefreshIndicator(
|
||||
onRefresh: () => nty.fetchNotification(),
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: const LinearProgressIndicator().animate().scaleX(),
|
||||
),
|
||||
if (ws.notifications.isEmpty)
|
||||
SliverToBoxAdapter(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
color:
|
||||
Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.check),
|
||||
title: Text('notifyEmpty'.tr),
|
||||
subtitle: Text('notifyEmptyCaption'.tr),
|
||||
),
|
||||
child: LoadingIndicator(
|
||||
isActive: nty.isBusy.value,
|
||||
),
|
||||
),
|
||||
if (ws.notifications.isNotEmpty)
|
||||
SliverToBoxAdapter(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.checklist),
|
||||
title: Text('notifyAllRead'.tr),
|
||||
onTap: _isBusy ? null : () => _markAllRead(),
|
||||
if (nty.notifications
|
||||
.where((x) => x.readAt == null)
|
||||
.isEmpty)
|
||||
SliverToBoxAdapter(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.surfaceContainerHigh,
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.check),
|
||||
title: Text('notifyEmpty'.tr),
|
||||
subtitle: Text('notifyEmptyCaption'.tr),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (nty.notifications
|
||||
.where((x) => x.readAt == null)
|
||||
.isNotEmpty)
|
||||
SliverToBoxAdapter(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
color:
|
||||
Theme.of(context).colorScheme.secondaryContainer,
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.checklist),
|
||||
title: Text('notifyAllRead'.tr),
|
||||
onTap: nty.isBusy.value
|
||||
? null
|
||||
: () => nty.markAllRead(),
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverList.separated(
|
||||
itemCount: nty.notifications.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
var element = nty.notifications[index];
|
||||
return ClipRect(
|
||||
child: Dismissible(
|
||||
direction: element.readAt == null
|
||||
? DismissDirection.horizontal
|
||||
: DismissDirection.none,
|
||||
key: Key(const Uuid().v4()),
|
||||
background: Container(
|
||||
color: Colors.lightBlue,
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 20),
|
||||
alignment: Alignment.centerLeft,
|
||||
child:
|
||||
const Icon(Icons.check, color: Colors.white),
|
||||
),
|
||||
secondaryBackground: Container(
|
||||
color: Colors.lightBlue,
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 20),
|
||||
alignment: Alignment.centerRight,
|
||||
child:
|
||||
const Icon(Icons.check, color: Colors.white),
|
||||
),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 28,
|
||||
vertical: 16,
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(NotificationTopicIcons[element.topic]),
|
||||
const Gap(16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (element.readAt == null)
|
||||
Badge(
|
||||
label: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.new_releases_outlined,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface,
|
||||
size: 12,
|
||||
),
|
||||
const Gap(4),
|
||||
Text('unread'.tr),
|
||||
],
|
||||
),
|
||||
).paddingOnly(bottom: 4),
|
||||
Text(
|
||||
element.title,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium,
|
||||
),
|
||||
if (element.subtitle != null)
|
||||
Text(
|
||||
element.subtitle!,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleSmall,
|
||||
),
|
||||
if (element.subtitle != null)
|
||||
const Gap(4),
|
||||
MarkdownTextContent(
|
||||
content: element.body,
|
||||
isAutoWarp: true,
|
||||
isSelectable: true,
|
||||
parentId:
|
||||
'notification-${element.id}',
|
||||
),
|
||||
if ([
|
||||
'interactive.feedback',
|
||||
'interactive.subscription'
|
||||
].contains(element.topic) &&
|
||||
element.metadata?['related_post'] !=
|
||||
null)
|
||||
_PostRelatedNotificationWidget(
|
||||
metadata: element.metadata!,
|
||||
),
|
||||
const Gap(8),
|
||||
Opacity(
|
||||
opacity: 0.75,
|
||||
child: Row(
|
||||
children: [
|
||||
RelativeDate(
|
||||
element.createdAt,
|
||||
style: TextStyle(fontSize: 12),
|
||||
),
|
||||
const Gap(4),
|
||||
Text(
|
||||
'·',
|
||||
style: TextStyle(fontSize: 12),
|
||||
),
|
||||
const Gap(4),
|
||||
RelativeDate(
|
||||
element.createdAt,
|
||||
style: TextStyle(fontSize: 12),
|
||||
isFull: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
onDismissed: (_) => nty.markOneRead(element, index),
|
||||
),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (_, __) =>
|
||||
const Divider(thickness: 0.3, height: 0.3),
|
||||
),
|
||||
SliverList.separated(
|
||||
itemCount: ws.notifications.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
var element = ws.notifications[index];
|
||||
return Dismissible(
|
||||
key: Key(const Uuid().v4()),
|
||||
background: Container(
|
||||
color: Colors.lightBlue,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
alignment: Alignment.centerLeft,
|
||||
child: const Icon(Icons.check, color: Colors.white),
|
||||
),
|
||||
child: ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
vertical: 8,
|
||||
),
|
||||
title: Text(element.title),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (element.subtitle != null)
|
||||
Text(element.subtitle!),
|
||||
Text(element.body),
|
||||
],
|
||||
),
|
||||
),
|
||||
onDismissed: (_) => _markOneRead(element, index),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (_, __) =>
|
||||
const Divider(thickness: 0.3, height: 0.3),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
@ -156,7 +216,7 @@ class NotificationButton extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final WebSocketProvider provider = Get.find();
|
||||
final NotificationProvider nty = Get.find();
|
||||
|
||||
final button = IconButton(
|
||||
icon: const Icon(Icons.notifications),
|
||||
@ -166,16 +226,16 @@ class NotificationButton extends StatelessWidget {
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (context) => const NotificationScreen(),
|
||||
).then((_) => provider.notificationUnread.value = 0);
|
||||
).then((_) => nty.notificationUnread.value = 0);
|
||||
},
|
||||
);
|
||||
|
||||
return Obx(() {
|
||||
if (provider.notificationUnread.value > 0) {
|
||||
if (nty.notificationUnread.value > 0) {
|
||||
return Badge(
|
||||
isLabelVisible: true,
|
||||
offset: const Offset(-8, 2),
|
||||
label: Text(provider.notificationUnread.value.toString()),
|
||||
label: Text(nty.notificationUnread.value.toString()),
|
||||
child: button,
|
||||
);
|
||||
} else {
|
||||
@ -184,3 +244,31 @@ class NotificationButton extends StatelessWidget {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _PostRelatedNotificationWidget extends StatelessWidget {
|
||||
final Map<String, dynamic> metadata;
|
||||
|
||||
const _PostRelatedNotificationWidget({super.key, required this.metadata});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
child: Card(
|
||||
margin: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: PostItem(
|
||||
item: Post.fromJson(metadata['related_post']),
|
||||
isCompact: true,
|
||||
).paddingAll(8),
|
||||
),
|
||||
onTap: () {
|
||||
final data = Post.fromJson(metadata['related_post']);
|
||||
Navigator.pop(context);
|
||||
AppRouter.instance.pushNamed(
|
||||
'postDetail',
|
||||
pathParameters: {'id': data.id.toString()},
|
||||
extra: data,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,363 +0,0 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:image_cropper/image_cropper.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:solian/exts.dart';
|
||||
import 'package:solian/models/attachment.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/providers/content/attachment.dart';
|
||||
import 'package:solian/services.dart';
|
||||
import 'package:solian/widgets/account/account_avatar.dart';
|
||||
|
||||
class PersonalizeScreen extends StatefulWidget {
|
||||
const PersonalizeScreen({super.key});
|
||||
|
||||
@override
|
||||
State<PersonalizeScreen> createState() => _PersonalizeScreenState();
|
||||
}
|
||||
|
||||
class _PersonalizeScreenState extends State<PersonalizeScreen> {
|
||||
final _imagePicker = ImagePicker();
|
||||
|
||||
final _usernameController = TextEditingController();
|
||||
final _nicknameController = TextEditingController();
|
||||
final _firstNameController = TextEditingController();
|
||||
final _lastNameController = TextEditingController();
|
||||
final _descriptionController = TextEditingController();
|
||||
final _birthdayController = TextEditingController();
|
||||
|
||||
String? _avatar;
|
||||
String? _banner;
|
||||
DateTime? _birthday;
|
||||
|
||||
bool _isBusy = false;
|
||||
|
||||
void _selectBirthday() async {
|
||||
final DateTime? picked = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: _birthday?.toLocal(),
|
||||
firstDate: DateTime(DateTime.now().year - 200),
|
||||
lastDate: DateTime(DateTime.now().year),
|
||||
);
|
||||
if (picked != null && picked != _birthday) {
|
||||
setState(() {
|
||||
_birthday = picked;
|
||||
_birthdayController.text = DateFormat('y/M/d').format(_birthday!);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _syncWidget() async {
|
||||
_isBusy = true;
|
||||
|
||||
final AuthProvider auth = Get.find();
|
||||
final prof = auth.userProfile.value!;
|
||||
_usernameController.text = prof['name'];
|
||||
_nicknameController.text = prof['nick'];
|
||||
_descriptionController.text = prof['description'];
|
||||
_firstNameController.text = prof['profile']['first_name'];
|
||||
_lastNameController.text = prof['profile']['last_name'];
|
||||
_avatar = prof['avatar'];
|
||||
_banner = prof['banner'];
|
||||
if (prof['profile']['birthday'] != null) {
|
||||
_birthday = DateTime.parse(prof['profile']['birthday']);
|
||||
_birthdayController.text =
|
||||
DateFormat('yyyy-MM-dd').format(_birthday!.toLocal());
|
||||
}
|
||||
|
||||
_isBusy = false;
|
||||
}
|
||||
|
||||
Future<void> _editImage(String position) async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) return;
|
||||
|
||||
final image = await _imagePicker.pickImage(source: ImageSource.gallery);
|
||||
if (image == null) return;
|
||||
|
||||
CroppedFile? croppedFile = await ImageCropper().cropImage(
|
||||
sourcePath: image.path,
|
||||
uiSettings: [
|
||||
AndroidUiSettings(
|
||||
toolbarTitle: 'cropImage'.tr,
|
||||
toolbarColor: Theme.of(context).colorScheme.primary,
|
||||
toolbarWidgetColor: Theme.of(context).colorScheme.onPrimary,
|
||||
aspectRatioPresets: [
|
||||
if (position == 'avatar') CropAspectRatioPreset.square,
|
||||
if (position == 'banner') _BannerCropAspectRatioPreset(),
|
||||
],
|
||||
),
|
||||
IOSUiSettings(
|
||||
title: 'cropImage'.tr,
|
||||
aspectRatioPresets: [
|
||||
if (position == 'avatar') CropAspectRatioPreset.square,
|
||||
if (position == 'banner') _BannerCropAspectRatioPreset(),
|
||||
],
|
||||
),
|
||||
WebUiSettings(
|
||||
context: context,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
if (croppedFile == null) return;
|
||||
final file = File(croppedFile.path);
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
final AttachmentProvider attach = Get.find();
|
||||
|
||||
Attachment? attachResult;
|
||||
try {
|
||||
attachResult = await attach.createAttachmentDirectly(
|
||||
await file.readAsBytes(),
|
||||
file.path,
|
||||
'avatar',
|
||||
null,
|
||||
);
|
||||
} catch (e) {
|
||||
setState(() => _isBusy = false);
|
||||
context.showErrorDialog(e);
|
||||
return;
|
||||
}
|
||||
|
||||
final client = await auth.configureClient('auth');
|
||||
|
||||
final resp = await client.put(
|
||||
'/users/me/$position',
|
||||
{'attachment': attachResult.rid},
|
||||
);
|
||||
if (resp.statusCode == 200) {
|
||||
_syncWidget();
|
||||
context.showSnackbar('accountProfileApplied'.tr);
|
||||
} else {
|
||||
context.showErrorDialog(resp.bodyString);
|
||||
}
|
||||
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
|
||||
void _editUserInfo() async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) return;
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
final client = await auth.configureClient('auth');
|
||||
|
||||
_birthday?.toIso8601String();
|
||||
final resp = await client.put(
|
||||
'/users/me',
|
||||
{
|
||||
'nick': _nicknameController.value.text,
|
||||
'description': _descriptionController.value.text,
|
||||
'first_name': _firstNameController.value.text,
|
||||
'last_name': _lastNameController.value.text,
|
||||
'birthday': _birthday?.toUtc().toIso8601String(),
|
||||
},
|
||||
);
|
||||
if (resp.statusCode == 200) {
|
||||
_syncWidget();
|
||||
context.showSnackbar('accountProfileApplied'.tr);
|
||||
} else {
|
||||
context.showErrorDialog(resp.bodyString);
|
||||
}
|
||||
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_syncWidget();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const double padding = 32;
|
||||
|
||||
return Material(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: ListView(
|
||||
children: [
|
||||
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
|
||||
const Gap(24),
|
||||
Stack(
|
||||
children: [
|
||||
AccountAvatar(content: _avatar, radius: 40),
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
left: 40,
|
||||
child: FloatingActionButton.small(
|
||||
heroTag: const Key('avatar-editor'),
|
||||
onPressed: () => _editImage('avatar'),
|
||||
child: const Icon(
|
||||
Icons.camera,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
).paddingSymmetric(horizontal: padding),
|
||||
const Gap(16),
|
||||
Stack(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 16 / 9,
|
||||
child: Container(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
child: _banner != null
|
||||
? Image.network(
|
||||
ServiceFinder.buildUrl(
|
||||
'files', '/attachments/$_banner'),
|
||||
fit: BoxFit.cover,
|
||||
loadingBuilder: (BuildContext context, Widget child,
|
||||
ImageChunkEvent? loadingProgress) {
|
||||
if (loadingProgress == null) return child;
|
||||
return Center(
|
||||
child: CircularProgressIndicator(
|
||||
value: loadingProgress.expectedTotalBytes !=
|
||||
null
|
||||
? loadingProgress.cumulativeBytesLoaded /
|
||||
loadingProgress.expectedTotalBytes!
|
||||
: null,
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
: Container(),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 16,
|
||||
right: 16,
|
||||
child: FloatingActionButton(
|
||||
heroTag: const Key('banner-editor'),
|
||||
onPressed: () => _editImage('banner'),
|
||||
child: const Icon(
|
||||
Icons.camera_alt,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
).paddingSymmetric(horizontal: padding),
|
||||
const Gap(24),
|
||||
Row(
|
||||
children: [
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: TextField(
|
||||
readOnly: true,
|
||||
controller: _usernameController,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: 'username'.tr,
|
||||
prefixText: '@',
|
||||
),
|
||||
),
|
||||
),
|
||||
const Gap(16),
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: TextField(
|
||||
controller: _nicknameController,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: 'nickname'.tr,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
).paddingSymmetric(horizontal: padding),
|
||||
const Gap(16),
|
||||
Row(
|
||||
children: [
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: TextField(
|
||||
controller: _firstNameController,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: 'firstName'.tr,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Gap(16),
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: TextField(
|
||||
controller: _lastNameController,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: 'lastName'.tr,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
).paddingSymmetric(horizontal: padding),
|
||||
const Gap(16),
|
||||
TextField(
|
||||
controller: _descriptionController,
|
||||
keyboardType: TextInputType.multiline,
|
||||
maxLines: null,
|
||||
minLines: 3,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: 'description'.tr,
|
||||
),
|
||||
).paddingSymmetric(horizontal: padding),
|
||||
const Gap(16),
|
||||
TextField(
|
||||
controller: _birthdayController,
|
||||
readOnly: true,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: 'birthday'.tr,
|
||||
),
|
||||
onTap: () => _selectBirthday(),
|
||||
).paddingSymmetric(horizontal: padding),
|
||||
const Gap(16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: _isBusy ? null : () => _syncWidget(),
|
||||
child: Text('reset'.tr),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: _isBusy ? null : () => _editUserInfo(),
|
||||
child: Text('apply'.tr),
|
||||
),
|
||||
],
|
||||
).paddingSymmetric(horizontal: padding),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_usernameController.dispose();
|
||||
_nicknameController.dispose();
|
||||
_firstNameController.dispose();
|
||||
_lastNameController.dispose();
|
||||
_descriptionController.dispose();
|
||||
_birthdayController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class _BannerCropAspectRatioPreset extends CropAspectRatioPresetData {
|
||||
@override
|
||||
(int, int)? get data => (16, 7);
|
||||
|
||||
@override
|
||||
String get name => '16x7';
|
||||
}
|
115
lib/screens/account/preferences/notifications.dart
Normal file
115
lib/screens/account/preferences/notifications.dart
Normal file
@ -0,0 +1,115 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:get/get_connect/http/src/exceptions/exceptions.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:solian/exceptions/request.dart';
|
||||
import 'package:solian/exts.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/widgets/loading_indicator.dart';
|
||||
|
||||
class NotificationPreferencesScreen extends StatefulWidget {
|
||||
const NotificationPreferencesScreen({super.key});
|
||||
|
||||
@override
|
||||
State<NotificationPreferencesScreen> createState() =>
|
||||
_NotificationPreferencesScreenState();
|
||||
}
|
||||
|
||||
class _NotificationPreferencesScreenState
|
||||
extends State<NotificationPreferencesScreen> {
|
||||
bool _isBusy = true;
|
||||
|
||||
Map<String, bool> _config = {};
|
||||
|
||||
final Map<String, String> _topicMap = {
|
||||
'interactive.feedback': 'notificationTopicPostFeedback'.tr,
|
||||
'interactive.subscription': 'notificationTopicPostSubscription'.tr,
|
||||
};
|
||||
|
||||
Future<void> _getPreferences() async {
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
final auth = Get.find<AuthProvider>();
|
||||
if (!auth.isAuthorized.value) throw UnauthorizedException();
|
||||
|
||||
final client = await auth.configureClient('id');
|
||||
final resp = await client.get('/preferences/notifications');
|
||||
if (resp.statusCode != 200 && resp.statusCode != 404) {
|
||||
context.showErrorDialog(RequestException(resp));
|
||||
}
|
||||
|
||||
if (resp.statusCode == 200) {
|
||||
_config = resp.body['config']
|
||||
.map((k, v) => MapEntry(k, v as bool))
|
||||
.cast<String, bool>();
|
||||
}
|
||||
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
|
||||
Future<void> _savePreferences() async {
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
final auth = Get.find<AuthProvider>();
|
||||
if (!auth.isAuthorized.value) throw UnauthorizedException();
|
||||
|
||||
final client = await auth.configureClient('id');
|
||||
final resp = await client.put('/preferences/notifications', {
|
||||
'config': _config,
|
||||
});
|
||||
if (resp.statusCode != 200) {
|
||||
context.showErrorDialog(RequestException(resp));
|
||||
} else {
|
||||
context.showSnackbar('preferencesApplied'.tr);
|
||||
}
|
||||
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_getPreferences();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
ListTile(
|
||||
tileColor: Theme.of(context).colorScheme.surfaceContainer,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Icons.save),
|
||||
title: Text('save'.tr),
|
||||
enabled: !_isBusy,
|
||||
onTap: () {
|
||||
_savePreferences();
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: _topicMap.length,
|
||||
itemBuilder: (context, index) {
|
||||
final element = _topicMap.entries.elementAt(index);
|
||||
return CheckboxListTile(
|
||||
title: Text(element.value),
|
||||
subtitle: Text(
|
||||
element.key,
|
||||
style: GoogleFonts.robotoMono(fontSize: 12),
|
||||
),
|
||||
value: _config[element.key] ?? true,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_config[element.key] = value ?? false;
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
118
lib/screens/account/preferences/security.dart
Normal file
118
lib/screens/account/preferences/security.dart
Normal file
@ -0,0 +1,118 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:get/get_connect/http/src/exceptions/exceptions.dart';
|
||||
import 'package:solian/exceptions/request.dart';
|
||||
import 'package:solian/exts.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/widgets/loading_indicator.dart';
|
||||
|
||||
class AuthPreferencesScreen extends StatefulWidget {
|
||||
const AuthPreferencesScreen({super.key});
|
||||
|
||||
@override
|
||||
State<AuthPreferencesScreen> createState() => _AuthPreferencesScreenState();
|
||||
}
|
||||
|
||||
class _AuthPreferencesScreenState extends State<AuthPreferencesScreen> {
|
||||
bool _isBusy = true;
|
||||
|
||||
Map<String, dynamic> _config = {
|
||||
'maximum_auth_steps': 2,
|
||||
};
|
||||
|
||||
Future<void> _getPreferences() async {
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
final auth = Get.find<AuthProvider>();
|
||||
if (!auth.isAuthorized.value) throw UnauthorizedException();
|
||||
|
||||
final client = await auth.configureClient('id');
|
||||
final resp = await client.get('/preferences/auth');
|
||||
if (resp.statusCode != 200 && resp.statusCode != 404) {
|
||||
context.showErrorDialog(RequestException(resp));
|
||||
}
|
||||
|
||||
if (resp.statusCode == 200) {
|
||||
_config = resp.body;
|
||||
}
|
||||
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
|
||||
Future<void> _savePreferences() async {
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
final auth = Get.find<AuthProvider>();
|
||||
if (!auth.isAuthorized.value) throw UnauthorizedException();
|
||||
|
||||
final client = await auth.configureClient('id');
|
||||
final resp = await client.put('/preferences/auth', _config);
|
||||
if (resp.statusCode != 200) {
|
||||
context.showErrorDialog(RequestException(resp));
|
||||
} else {
|
||||
context.showSnackbar('preferencesApplied'.tr);
|
||||
}
|
||||
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_getPreferences();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
ListTile(
|
||||
tileColor: Theme.of(context).colorScheme.surfaceContainer,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Icons.save),
|
||||
title: Text('save'.tr),
|
||||
enabled: !_isBusy,
|
||||
onTap: () {
|
||||
_savePreferences();
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: ListView(
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text('authMaximumAuthSteps'.tr),
|
||||
subtitle: Text('authMaximumAuthStepsDesc'.tr),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
trailing: SizedBox(
|
||||
width: 60,
|
||||
child: _isBusy
|
||||
? null
|
||||
: TextFormField(
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
isDense: true,
|
||||
),
|
||||
initialValue:
|
||||
_config['maximum_auth_steps']?.toString() ?? '2',
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly
|
||||
],
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onChanged: (value) {
|
||||
_config['maximum_auth_steps'] =
|
||||
int.tryParse(value) ?? 2;
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
365
lib/screens/account/profile_edit.dart
Normal file
365
lib/screens/account/profile_edit.dart
Normal file
@ -0,0 +1,365 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:image_cropper/image_cropper.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:solian/exts.dart';
|
||||
import 'package:solian/models/attachment.dart';
|
||||
import 'package:solian/platform.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/providers/content/attachment.dart';
|
||||
import 'package:solian/services.dart';
|
||||
import 'package:solian/widgets/account/account_avatar.dart';
|
||||
import 'package:solian/widgets/loading_indicator.dart';
|
||||
|
||||
class PersonalizeScreen extends StatefulWidget {
|
||||
const PersonalizeScreen({super.key});
|
||||
|
||||
@override
|
||||
State<PersonalizeScreen> createState() => _PersonalizeScreenState();
|
||||
}
|
||||
|
||||
class _PersonalizeScreenState extends State<PersonalizeScreen> {
|
||||
final _imagePicker = ImagePicker();
|
||||
|
||||
final _usernameController = TextEditingController();
|
||||
final _nicknameController = TextEditingController();
|
||||
final _firstNameController = TextEditingController();
|
||||
final _lastNameController = TextEditingController();
|
||||
final _descriptionController = TextEditingController();
|
||||
final _birthdayController = TextEditingController();
|
||||
|
||||
String? _avatar;
|
||||
String? _banner;
|
||||
DateTime? _birthday;
|
||||
|
||||
bool _isBusy = false;
|
||||
|
||||
void _selectBirthday() async {
|
||||
final DateTime? picked = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: _birthday?.toLocal(),
|
||||
firstDate: DateTime(DateTime.now().year - 200),
|
||||
lastDate: DateTime(DateTime.now().year),
|
||||
);
|
||||
if (picked != null && picked != _birthday) {
|
||||
setState(() {
|
||||
_birthday = picked;
|
||||
_birthdayController.text = DateFormat('y/M/d').format(_birthday!);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _syncWidget() async {
|
||||
_isBusy = true;
|
||||
|
||||
final AuthProvider auth = Get.find();
|
||||
final prof = auth.userProfile.value!;
|
||||
_usernameController.text = prof['name'];
|
||||
_nicknameController.text = prof['nick'];
|
||||
_descriptionController.text = prof['description'];
|
||||
_firstNameController.text = prof['profile']['first_name'];
|
||||
_lastNameController.text = prof['profile']['last_name'];
|
||||
_avatar = prof['avatar'];
|
||||
_banner = prof['banner'];
|
||||
if (prof['profile']['birthday'] != null) {
|
||||
_birthday = DateTime.parse(prof['profile']['birthday']);
|
||||
_birthdayController.text =
|
||||
DateFormat('yyyy-MM-dd').format(_birthday!.toLocal());
|
||||
}
|
||||
|
||||
_isBusy = false;
|
||||
}
|
||||
|
||||
Future<void> _editImage(String position) async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) return;
|
||||
|
||||
XFile file;
|
||||
|
||||
final image = await _imagePicker.pickImage(source: ImageSource.gallery);
|
||||
if (image == null) return;
|
||||
|
||||
if (PlatformInfo.canCropImage) {
|
||||
CroppedFile? croppedFile = await ImageCropper().cropImage(
|
||||
sourcePath: image.path,
|
||||
uiSettings: [
|
||||
AndroidUiSettings(
|
||||
toolbarTitle: 'cropImage'.tr,
|
||||
toolbarColor: Theme.of(context).colorScheme.primary,
|
||||
toolbarWidgetColor: Theme.of(context).colorScheme.onPrimary,
|
||||
aspectRatioPresets: [
|
||||
if (position == 'avatar') CropAspectRatioPreset.square,
|
||||
if (position == 'banner') _BannerCropAspectRatioPreset(),
|
||||
],
|
||||
),
|
||||
IOSUiSettings(
|
||||
title: 'cropImage'.tr,
|
||||
aspectRatioPresets: [
|
||||
if (position == 'avatar') CropAspectRatioPreset.square,
|
||||
if (position == 'banner') _BannerCropAspectRatioPreset(),
|
||||
],
|
||||
),
|
||||
WebUiSettings(
|
||||
context: context,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
if (croppedFile == null) return;
|
||||
file = XFile(croppedFile.path);
|
||||
} else {
|
||||
file = XFile(image.path);
|
||||
}
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
final AttachmentProvider attach = Get.find();
|
||||
|
||||
Attachment? attachResult;
|
||||
try {
|
||||
attachResult = await attach.createAttachmentDirectly(
|
||||
await file.readAsBytes(),
|
||||
file.path,
|
||||
'avatar',
|
||||
null,
|
||||
);
|
||||
} catch (e) {
|
||||
setState(() => _isBusy = false);
|
||||
context.showErrorDialog(e);
|
||||
return;
|
||||
}
|
||||
|
||||
final client = await auth.configureClient('auth');
|
||||
|
||||
final resp = await client.put(
|
||||
'/users/me/$position',
|
||||
{'attachment': attachResult.rid},
|
||||
);
|
||||
if (resp.statusCode == 200) {
|
||||
_syncWidget();
|
||||
context.showSnackbar('accountProfileApplied'.tr);
|
||||
} else {
|
||||
context.showErrorDialog(resp.bodyString);
|
||||
}
|
||||
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
|
||||
void _editUserInfo() async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) return;
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
final client = await auth.configureClient('auth');
|
||||
|
||||
_birthday?.toIso8601String();
|
||||
final resp = await client.put(
|
||||
'/users/me',
|
||||
{
|
||||
'nick': _nicknameController.value.text,
|
||||
'description': _descriptionController.value.text,
|
||||
'first_name': _firstNameController.value.text,
|
||||
'last_name': _lastNameController.value.text,
|
||||
'birthday': _birthday?.toUtc().toIso8601String(),
|
||||
},
|
||||
);
|
||||
if (resp.statusCode == 200) {
|
||||
_syncWidget();
|
||||
context.showSnackbar('accountProfileApplied'.tr);
|
||||
} else {
|
||||
context.showErrorDialog(resp.bodyString);
|
||||
}
|
||||
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_syncWidget();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const double padding = 32;
|
||||
|
||||
return ListView(
|
||||
children: [
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
const Gap(24),
|
||||
Stack(
|
||||
children: [
|
||||
AttachedCircleAvatar(content: _avatar, radius: 40),
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
left: 40,
|
||||
child: FloatingActionButton.small(
|
||||
heroTag: const Key('avatar-editor'),
|
||||
onPressed: () => _editImage('avatar'),
|
||||
child: const Icon(
|
||||
Icons.camera,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
).paddingSymmetric(horizontal: padding),
|
||||
const Gap(16),
|
||||
Stack(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 16 / 9,
|
||||
child: Container(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
child: _banner != null
|
||||
? Image.network(
|
||||
ServiceFinder.buildUrl(
|
||||
'files', '/attachments/$_banner'),
|
||||
fit: BoxFit.cover,
|
||||
loadingBuilder: (BuildContext context, Widget child,
|
||||
ImageChunkEvent? loadingProgress) {
|
||||
if (loadingProgress == null) return child;
|
||||
return Center(
|
||||
child: CircularProgressIndicator(
|
||||
value: loadingProgress.expectedTotalBytes !=
|
||||
null
|
||||
? loadingProgress.cumulativeBytesLoaded /
|
||||
loadingProgress.expectedTotalBytes!
|
||||
: null,
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
: Container(),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 16,
|
||||
right: 16,
|
||||
child: FloatingActionButton(
|
||||
heroTag: const Key('banner-editor'),
|
||||
onPressed: () => _editImage('banner'),
|
||||
child: const Icon(
|
||||
Icons.camera_alt,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
).paddingSymmetric(horizontal: padding),
|
||||
const Gap(24),
|
||||
Row(
|
||||
children: [
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: TextField(
|
||||
readOnly: true,
|
||||
controller: _usernameController,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: 'username'.tr,
|
||||
prefixText: '@',
|
||||
),
|
||||
),
|
||||
),
|
||||
const Gap(16),
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: TextField(
|
||||
controller: _nicknameController,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: 'nickname'.tr,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
).paddingSymmetric(horizontal: padding),
|
||||
const Gap(16),
|
||||
Row(
|
||||
children: [
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: TextField(
|
||||
controller: _firstNameController,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: 'firstName'.tr,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Gap(16),
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: TextField(
|
||||
controller: _lastNameController,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: 'lastName'.tr,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
).paddingSymmetric(horizontal: padding),
|
||||
const Gap(16),
|
||||
TextField(
|
||||
controller: _descriptionController,
|
||||
keyboardType: TextInputType.multiline,
|
||||
maxLines: null,
|
||||
minLines: 3,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: 'description'.tr,
|
||||
),
|
||||
).paddingSymmetric(horizontal: padding),
|
||||
const Gap(16),
|
||||
TextField(
|
||||
controller: _birthdayController,
|
||||
readOnly: true,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: 'birthday'.tr,
|
||||
),
|
||||
onTap: () => _selectBirthday(),
|
||||
).paddingSymmetric(horizontal: padding),
|
||||
const Gap(16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: _isBusy ? null : () => _syncWidget(),
|
||||
child: Text('reset'.tr),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: _isBusy ? null : () => _editUserInfo(),
|
||||
child: Text('apply'.tr),
|
||||
),
|
||||
],
|
||||
).paddingSymmetric(horizontal: padding),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_usernameController.dispose();
|
||||
_nicknameController.dispose();
|
||||
_firstNameController.dispose();
|
||||
_lastNameController.dispose();
|
||||
_descriptionController.dispose();
|
||||
_birthdayController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class _BannerCropAspectRatioPreset extends CropAspectRatioPresetData {
|
||||
@override
|
||||
(int, int)? get data => (16, 7);
|
||||
|
||||
@override
|
||||
String get name => '16x7';
|
||||
}
|
@ -1,23 +1,33 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:solian/controllers/post_list_controller.dart';
|
||||
import 'package:solian/exts.dart';
|
||||
import 'package:solian/models/account.dart';
|
||||
import 'package:solian/models/attachment.dart';
|
||||
import 'package:solian/models/daily_sign.dart';
|
||||
import 'package:solian/models/pagination.dart';
|
||||
import 'package:solian/models/post.dart';
|
||||
import 'package:solian/models/relations.dart';
|
||||
import 'package:solian/models/subscription.dart';
|
||||
import 'package:solian/providers/account_status.dart';
|
||||
import 'package:solian/providers/relation.dart';
|
||||
import 'package:solian/providers/subscription.dart';
|
||||
import 'package:solian/services.dart';
|
||||
import 'package:solian/theme.dart';
|
||||
import 'package:solian/widgets/account/account_avatar.dart';
|
||||
import 'package:solian/widgets/account/account_heading.dart';
|
||||
import 'package:solian/widgets/app_bar_leading.dart';
|
||||
import 'package:solian/widgets/attachments/attachment_list.dart';
|
||||
import 'package:solian/widgets/daily_sign/history_chart.dart';
|
||||
import 'package:solian/widgets/posts/post_list.dart';
|
||||
import 'package:solian/widgets/posts/post_warped_list.dart';
|
||||
import 'package:solian/widgets/reports/abuse_report.dart';
|
||||
import 'package:solian/widgets/root_container.dart';
|
||||
import 'package:solian/widgets/sized_container.dart';
|
||||
|
||||
class AccountProfilePage extends StatefulWidget {
|
||||
@ -37,16 +47,36 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
|
||||
|
||||
bool _isBusy = true;
|
||||
bool _isMakingFriend = false;
|
||||
bool _isSubscribing = false;
|
||||
bool _showMature = false;
|
||||
|
||||
Account? _userinfo;
|
||||
Subscription? _subscription;
|
||||
Relationship? _relationship;
|
||||
List<Post> _pinnedPosts = List.empty();
|
||||
List<DailySignRecord> _dailySignRecords = List.empty();
|
||||
int _totalUpvote = 0, _totalDownvote = 0;
|
||||
|
||||
Future<void> _getSubscription() async {
|
||||
setState(() => _isSubscribing = true);
|
||||
_subscription = await Get.find<SubscriptionProvider>()
|
||||
.getSubscriptionOnUser(_userinfo!.id);
|
||||
setState(() => _isSubscribing = false);
|
||||
}
|
||||
|
||||
Future<void> _getRelationship() async {
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
final relations = Get.find<RelationshipProvider>();
|
||||
_relationship = await relations.getRelationship(_userinfo!.id);
|
||||
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
|
||||
Future<void> _getUserinfo() async {
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
var client = await ServiceFinder.configureClient('auth');
|
||||
var client = await ServiceFinder.configureClient('id');
|
||||
var resp = await client.get('/users/${widget.name}');
|
||||
if (resp.statusCode != 200) {
|
||||
context.showErrorDialog(resp.bodyString).then((_) {
|
||||
@ -56,7 +86,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
|
||||
_userinfo = Account.fromJson(resp.body);
|
||||
}
|
||||
|
||||
client = await ServiceFinder.configureClient('interactive');
|
||||
client = await ServiceFinder.configureClient('co');
|
||||
resp = await client.get('/users/${widget.name}');
|
||||
if (resp.statusCode != 200) {
|
||||
context.showErrorDialog(resp.bodyString).then((_) {
|
||||
@ -70,8 +100,8 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
|
||||
Future<void> getPinnedPosts() async {
|
||||
final client = await ServiceFinder.configureClient('interactive');
|
||||
Future<void> _getPinnedPosts() async {
|
||||
final client = await ServiceFinder.configureClient('co');
|
||||
final resp = await client.get('/users/${widget.name}/pin');
|
||||
if (resp.statusCode != 200) {
|
||||
context.showErrorDialog(resp.bodyString).then((_) {
|
||||
@ -85,6 +115,80 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _getDailySignRecords() async {
|
||||
final client = await ServiceFinder.configureClient('id');
|
||||
final resp = await client.get('/users/${widget.name}/daily?take=14');
|
||||
if (resp.statusCode != 200) {
|
||||
context.showErrorDialog(resp.bodyString).then((_) {
|
||||
Navigator.pop(context);
|
||||
});
|
||||
} else {
|
||||
final result = PaginationResult.fromJson(resp.body);
|
||||
setState(() {
|
||||
_dailySignRecords = List.from(
|
||||
result.data?.map((x) => DailySignRecord.fromJson(x)) ?? [],
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _subscribeToUser() async {
|
||||
setState(() => _isSubscribing = true);
|
||||
_subscription =
|
||||
await Get.find<SubscriptionProvider>().subscribeToUser(_userinfo!.id);
|
||||
setState(() => _isSubscribing = false);
|
||||
}
|
||||
|
||||
Future<void> _unsubscribeFromUser() async {
|
||||
setState(() => _isSubscribing = true);
|
||||
await Get.find<SubscriptionProvider>().unsubscribeFromUser(_userinfo!.id);
|
||||
_subscription = null;
|
||||
setState(() => _isSubscribing = false);
|
||||
}
|
||||
|
||||
Future<void> _makeFriend() async {
|
||||
setState(() => _isMakingFriend = true);
|
||||
try {
|
||||
_relationship = await _relationshipProvider.makeFriend(widget.name);
|
||||
context.showSnackbar(
|
||||
'accountFriendRequestSent'.tr,
|
||||
);
|
||||
} catch (e) {
|
||||
context.showErrorDialog(e);
|
||||
} finally {
|
||||
setState(() => _isMakingFriend = false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _blockUser() async {
|
||||
setState(() => _isMakingFriend = true);
|
||||
try {
|
||||
_relationship = await _relationshipProvider.blockUser(widget.name);
|
||||
context.showSnackbar(
|
||||
'accountBlocked'.tr,
|
||||
);
|
||||
} catch (e) {
|
||||
context.showErrorDialog(e);
|
||||
} finally {
|
||||
setState(() => _isMakingFriend = false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _unblockUser() async {
|
||||
setState(() => _isMakingFriend = true);
|
||||
try {
|
||||
_relationship =
|
||||
await _relationshipProvider.editRelation(_userinfo!.id, 1);
|
||||
context.showSnackbar(
|
||||
'accountUnblocked'.tr,
|
||||
);
|
||||
} catch (e) {
|
||||
context.showErrorDialog(e);
|
||||
} finally {
|
||||
setState(() => _isMakingFriend = false);
|
||||
}
|
||||
}
|
||||
|
||||
int get _userSocialCreditPoints {
|
||||
return _totalUpvote * 2 - _totalDownvote + _postController.postTotal.value;
|
||||
}
|
||||
@ -115,35 +219,25 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
|
||||
}
|
||||
});
|
||||
|
||||
_getUserinfo();
|
||||
getPinnedPosts();
|
||||
}
|
||||
|
||||
Widget _buildStatisticsEntry(String label, String content) {
|
||||
return Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
Text(
|
||||
content,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
_getUserinfo().then((_) {
|
||||
_getRelationship();
|
||||
_getSubscription();
|
||||
_getPinnedPosts();
|
||||
_getDailySignRecords();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_isBusy || _userinfo == null) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
return RootContainer(
|
||||
child: const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Material(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
return RootContainer(
|
||||
child: DefaultTabController(
|
||||
length: 3,
|
||||
child: NestedScrollView(
|
||||
@ -155,62 +249,76 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
|
||||
toolbarHeight: AppTheme.toolbarHeight(context),
|
||||
leadingWidth: 24,
|
||||
automaticallyImplyLeading: false,
|
||||
flexibleSpace: Row(
|
||||
children: [
|
||||
AppBarLeadingButton.adaptive(context) ?? const Gap(8),
|
||||
const Gap(8),
|
||||
if (_userinfo != null)
|
||||
AccountAvatar(content: _userinfo!.avatar, radius: 16),
|
||||
const Gap(12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (_userinfo != null)
|
||||
Text(
|
||||
_userinfo!.nick,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
if (_userinfo != null)
|
||||
Text(
|
||||
'@${_userinfo!.name}',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
flexibleSpace: SizedBox(
|
||||
height: 56,
|
||||
child: Row(
|
||||
children: [
|
||||
AppBarLeadingButton.adaptive(
|
||||
context,
|
||||
forceBack: true,
|
||||
) ??
|
||||
const Gap(8),
|
||||
const Gap(8),
|
||||
if (_userinfo != null)
|
||||
AttachedCircleAvatar(
|
||||
content: _userinfo!.avatar, radius: 16),
|
||||
const Gap(12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (_userinfo != null)
|
||||
Text(
|
||||
_userinfo!.nick,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
if (_userinfo != null)
|
||||
Text(
|
||||
'@${_userinfo!.name}',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_userinfo != null &&
|
||||
!_relationshipProvider.hasFriend(_userinfo!))
|
||||
IconButton(
|
||||
icon: const Icon(Icons.person_add),
|
||||
onPressed: _isMakingFriend
|
||||
? null
|
||||
: () async {
|
||||
setState(() => _isMakingFriend = true);
|
||||
try {
|
||||
await _relationshipProvider
|
||||
.makeFriend(widget.name);
|
||||
context.showSnackbar(
|
||||
'accountFriendRequestSent'.tr,
|
||||
);
|
||||
} catch (e) {
|
||||
context.showErrorDialog(e);
|
||||
} finally {
|
||||
setState(() => _isMakingFriend = false);
|
||||
}
|
||||
},
|
||||
)
|
||||
else
|
||||
const IconButton(
|
||||
icon: Icon(Icons.handshake),
|
||||
onPressed: null,
|
||||
if (_userinfo != null && _subscription == null)
|
||||
IconButton(
|
||||
style: const ButtonStyle(
|
||||
visualDensity:
|
||||
VisualDensity(horizontal: -4, vertical: -2),
|
||||
),
|
||||
onPressed: _isSubscribing ? null : _subscribeToUser,
|
||||
icon: const Icon(Icons.add_circle_outline),
|
||||
tooltip: 'subscribe'.tr,
|
||||
)
|
||||
else if (_userinfo != null)
|
||||
IconButton(
|
||||
style: const ButtonStyle(
|
||||
visualDensity:
|
||||
VisualDensity(horizontal: -4, vertical: -2),
|
||||
),
|
||||
onPressed:
|
||||
_isSubscribing ? null : _unsubscribeFromUser,
|
||||
icon: const Icon(Icons.remove_circle_outline),
|
||||
tooltip: 'unsubscribe'.tr,
|
||||
),
|
||||
if (_userinfo != null && _relationship == null)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.person_add),
|
||||
onPressed: _isMakingFriend ? null : _makeFriend,
|
||||
tooltip: 'friendAdd'.tr,
|
||||
)
|
||||
else
|
||||
const IconButton(
|
||||
icon: Icon(Icons.handshake),
|
||||
onPressed: null,
|
||||
),
|
||||
SizedBox(
|
||||
width: AppTheme.isLargeScreen(context) ? 8 : 16,
|
||||
),
|
||||
SizedBox(
|
||||
width: AppTheme.isLargeScreen(context) ? 8 : 16,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
).paddingOnly(top: MediaQuery.of(context).padding.top),
|
||||
bottom: TabBar(
|
||||
tabs: [
|
||||
Tab(text: 'profilePage'.tr),
|
||||
@ -224,28 +332,212 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
|
||||
body: TabBarView(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
children: [
|
||||
Column(
|
||||
ListView(
|
||||
padding: const EdgeInsets.only(top: 16, bottom: 16),
|
||||
children: [
|
||||
const Gap(16),
|
||||
AccountHeadingWidget(
|
||||
name: _userinfo!.name,
|
||||
nick: _userinfo!.nick,
|
||||
desc: _userinfo!.description,
|
||||
badges: _userinfo!.badges,
|
||||
banner: _userinfo!.banner,
|
||||
avatar: _userinfo!.avatar,
|
||||
status: Get.find<StatusProvider>()
|
||||
.getSomeoneStatus(_userinfo!.name),
|
||||
detail: _userinfo,
|
||||
profile: _userinfo!.profile,
|
||||
extraWidgets: const [],
|
||||
CenteredContainer(
|
||||
child: AccountHeadingWidget(
|
||||
name: _userinfo!.name,
|
||||
nick: _userinfo!.nick,
|
||||
desc: _userinfo!.description,
|
||||
badges: _userinfo!.badges,
|
||||
banner: _userinfo!.banner,
|
||||
avatar: _userinfo!.avatar,
|
||||
status: Get.find<StatusProvider>()
|
||||
.getSomeoneStatus(_userinfo!.name),
|
||||
detail: _userinfo,
|
||||
profile: _userinfo!.profile,
|
||||
extraWidgets: [
|
||||
if (_dailySignRecords.length > 1)
|
||||
Card(
|
||||
child: SizedBox(
|
||||
height: 180,
|
||||
width:
|
||||
max(640, MediaQuery.of(context).size.width),
|
||||
child: LineChart(
|
||||
LineChartData(
|
||||
lineBarsData: [
|
||||
LineChartBarData(
|
||||
isCurved: true,
|
||||
isStrokeCapRound: true,
|
||||
isStrokeJoinRound: true,
|
||||
color:
|
||||
Theme.of(context).colorScheme.primary,
|
||||
belowBarData: BarAreaData(
|
||||
show: true,
|
||||
gradient: LinearGradient(
|
||||
colors: List.filled(
|
||||
_dailySignRecords.length,
|
||||
Theme.of(context)
|
||||
.colorScheme
|
||||
.primary
|
||||
.withOpacity(0.3),
|
||||
).toList(),
|
||||
),
|
||||
),
|
||||
spots: _dailySignRecords
|
||||
.map(
|
||||
(x) => FlSpot(
|
||||
x.createdAt
|
||||
.copyWith(
|
||||
hour: 0,
|
||||
minute: 0,
|
||||
second: 0,
|
||||
millisecond: 0,
|
||||
microsecond: 0,
|
||||
)
|
||||
.millisecondsSinceEpoch
|
||||
.toDouble(),
|
||||
x.resultTier.toDouble(),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
)
|
||||
],
|
||||
lineTouchData: LineTouchData(
|
||||
touchTooltipData: LineTouchTooltipData(
|
||||
getTooltipItems: (spots) => spots
|
||||
.map((spot) => LineTooltipItem(
|
||||
'${DailySignHistoryChartDialog.signSymbols[spot.y.toInt()]}\n${DateFormat('MM/dd').format(DateTime.fromMillisecondsSinceEpoch(spot.x.toInt()))}',
|
||||
TextStyle(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface,
|
||||
),
|
||||
))
|
||||
.toList(),
|
||||
getTooltipColor: (_) => Theme.of(context)
|
||||
.colorScheme
|
||||
.surfaceContainerHigh,
|
||||
),
|
||||
),
|
||||
titlesData: FlTitlesData(
|
||||
topTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
rightTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
leftTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
reservedSize: 40,
|
||||
interval: 1,
|
||||
getTitlesWidget: (value, _) => Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Text(
|
||||
DailySignHistoryChartDialog
|
||||
.signSymbols[value.toInt()],
|
||||
textAlign: TextAlign.right,
|
||||
).paddingOnly(right: 8),
|
||||
),
|
||||
),
|
||||
),
|
||||
bottomTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
reservedSize: 28,
|
||||
interval: 86400000,
|
||||
getTitlesWidget: (value, _) => Text(
|
||||
DateFormat('dd').format(
|
||||
DateTime.fromMillisecondsSinceEpoch(
|
||||
value.toInt(),
|
||||
),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
).paddingOnly(top: 8),
|
||||
),
|
||||
),
|
||||
),
|
||||
gridData: const FlGridData(show: false),
|
||||
borderData: FlBorderData(show: false),
|
||||
),
|
||||
),
|
||||
).marginOnly(
|
||||
right: 24,
|
||||
left: 12,
|
||||
bottom: 8,
|
||||
top: 24,
|
||||
),
|
||||
)
|
||||
],
|
||||
appendWidgets: [
|
||||
Card(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 4,
|
||||
horizontal: 8,
|
||||
),
|
||||
width: double.maxFinite,
|
||||
child: Wrap(
|
||||
alignment: WrapAlignment.spaceAround,
|
||||
children: [
|
||||
TextButton.icon(
|
||||
style: const ButtonStyle(
|
||||
visualDensity: VisualDensity(
|
||||
horizontal: -4,
|
||||
vertical: -2,
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AbuseReportDialog(
|
||||
resourceId: 'user:${_userinfo!.id}',
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.flag,
|
||||
size: 16,
|
||||
),
|
||||
label: Text('reportAbuse'.tr),
|
||||
),
|
||||
if (_relationship?.status != 2)
|
||||
TextButton.icon(
|
||||
style: const ButtonStyle(
|
||||
visualDensity: VisualDensity(
|
||||
horizontal: -4,
|
||||
vertical: -2,
|
||||
),
|
||||
),
|
||||
onPressed:
|
||||
_isMakingFriend ? null : _blockUser,
|
||||
icon: const Icon(
|
||||
Icons.block,
|
||||
size: 16,
|
||||
),
|
||||
label: Text('blockUser'.tr),
|
||||
)
|
||||
else
|
||||
TextButton.icon(
|
||||
style: const ButtonStyle(
|
||||
visualDensity: VisualDensity(
|
||||
horizontal: -4,
|
||||
vertical: -2,
|
||||
),
|
||||
),
|
||||
onPressed:
|
||||
_isMakingFriend ? null : _unblockUser,
|
||||
icon: const Icon(
|
||||
Icons.add_circle_outline,
|
||||
size: 16,
|
||||
),
|
||||
label: Text('unblockUser'.tr),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
RefreshIndicator(
|
||||
onRefresh: () => Future.wait([
|
||||
_postController.reloadAllOver(),
|
||||
getPinnedPosts(),
|
||||
_getPinnedPosts(),
|
||||
]),
|
||||
child: CustomScrollView(slivers: [
|
||||
SliverToBoxAdapter(
|
||||
@ -254,7 +546,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
_buildStatisticsEntry(
|
||||
_StatsWidget(
|
||||
'totalSocialCreditPoints'.tr,
|
||||
_userinfo != null
|
||||
? _userSocialCreditPoints.toString()
|
||||
@ -267,16 +559,16 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
Obx(
|
||||
() => _buildStatisticsEntry(
|
||||
() => _StatsWidget(
|
||||
'totalPostCount'.tr,
|
||||
_postController.postTotal.value.toString(),
|
||||
),
|
||||
),
|
||||
_buildStatisticsEntry(
|
||||
_StatsWidget(
|
||||
'totalUpvote'.tr,
|
||||
_totalUpvote.toString(),
|
||||
),
|
||||
_buildStatisticsEntry(
|
||||
_StatsWidget(
|
||||
'totalDownvote'.tr,
|
||||
_totalDownvote.toString(),
|
||||
),
|
||||
@ -296,12 +588,11 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
|
||||
color:
|
||||
Theme.of(context).colorScheme.surfaceContainerLow,
|
||||
child: PostListEntryWidget(
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.surfaceContainerLow,
|
||||
item: element,
|
||||
isClickable: true,
|
||||
isNestedClickable: true,
|
||||
isShowEmbed: true,
|
||||
showFeaturedReply: true,
|
||||
onUpdate: () {
|
||||
_postController.reloadAllOver();
|
||||
},
|
||||
@ -316,7 +607,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
if (_userinfo != null)
|
||||
PostWarpedListWidget(
|
||||
ControlledPostListWidget(
|
||||
isPinned: false,
|
||||
controller: _postController.pagingController,
|
||||
onUpdate: () => _postController.reloadAllOver(),
|
||||
@ -325,8 +616,9 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
|
||||
),
|
||||
CenteredContainer(
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () =>
|
||||
Future.sync(() => _albumPagingController.refresh()),
|
||||
onRefresh: () => Future.sync(
|
||||
() => _albumPagingController.refresh(),
|
||||
),
|
||||
child: PagedGridView<int, Attachment>(
|
||||
padding: EdgeInsets.zero,
|
||||
pagingController: _albumPagingController,
|
||||
@ -352,7 +644,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
|
||||
child: AttachmentListEntry(
|
||||
item: item,
|
||||
isDense: true,
|
||||
parentId: 'album',
|
||||
parentId: 'album-$index',
|
||||
showMature: _showMature,
|
||||
onReveal: (value) {
|
||||
setState(() => _showMature = value);
|
||||
@ -372,3 +664,28 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _StatsWidget extends StatelessWidget {
|
||||
final String label;
|
||||
final String content;
|
||||
|
||||
const _StatsWidget(this.label, this.content);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
Text(
|
||||
content,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,186 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
import 'package:solian/models/pagination.dart';
|
||||
import 'package:solian/models/stickers.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/providers/stickers.dart';
|
||||
import 'package:solian/services.dart';
|
||||
import 'package:solian/widgets/auto_cache_image.dart';
|
||||
import 'package:solian/widgets/stickers/sticker_uploader.dart';
|
||||
|
||||
class StickerScreen extends StatefulWidget {
|
||||
const StickerScreen({super.key});
|
||||
|
||||
@override
|
||||
State<StickerScreen> createState() => _StickerScreenState();
|
||||
}
|
||||
|
||||
class _StickerScreenState extends State<StickerScreen> {
|
||||
final PagingController<int, StickerPack> _pagingController =
|
||||
PagingController(firstPageKey: 0);
|
||||
|
||||
Future<bool> _promptDelete(Sticker item, String prefix) async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) return false;
|
||||
|
||||
final confirm = await showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text('stickerDeletionConfirm'.tr),
|
||||
content: Text(
|
||||
'stickerDeletionConfirmCaption'.trParams({
|
||||
'name': ':${'$prefix${item.alias}'.camelCase}:',
|
||||
}),
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text('cancel'.tr),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
child: Text('confirm'.tr),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (confirm != true) return false;
|
||||
|
||||
final client = await auth.configureClient('files');
|
||||
final resp = await client.delete('/stickers/${item.id}');
|
||||
|
||||
return resp.statusCode == 200;
|
||||
}
|
||||
|
||||
Future<bool?> _promptUploadSticker({Sticker? edit}) {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => StickerUploadDialog(
|
||||
edit: edit,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmoteEntry(Sticker item, String prefix) {
|
||||
final imageUrl = ServiceFinder.buildUrl(
|
||||
'files',
|
||||
'/attachments/${item.attachment.rid}',
|
||||
);
|
||||
return ListTile(
|
||||
title: Text(item.name),
|
||||
subtitle: Text(item.textWarpedPlaceholder),
|
||||
contentPadding: const EdgeInsets.only(left: 16, right: 14),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit_square),
|
||||
onPressed: () {
|
||||
_promptUploadSticker(edit: item).then((value) {
|
||||
if (value == true) _pagingController.refresh();
|
||||
});
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete),
|
||||
onPressed: () {
|
||||
_promptDelete(item, prefix).then((value) {
|
||||
if (value == true) _pagingController.refresh();
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
leading: AutoCacheImage(
|
||||
imageUrl,
|
||||
width: 28,
|
||||
height: 28,
|
||||
noErrorWidget: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
final AuthProvider auth = Get.find();
|
||||
final name = auth.userProfile.value!['name'];
|
||||
_pagingController.addPageRequestListener((pageKey) async {
|
||||
final client = await ServiceFinder.configureClient('files');
|
||||
final resp = await client.get(
|
||||
'/stickers/manifest?take=10&offset=$pageKey&author=$name',
|
||||
);
|
||||
if (resp.statusCode == 200) {
|
||||
final result = PaginationResult.fromJson(resp.body);
|
||||
final out = result.data?.map((e) => StickerPack.fromJson(e)).toList();
|
||||
if (out != null && result.data!.length >= 10) {
|
||||
_pagingController.appendPage(out, pageKey + out.length);
|
||||
} else if (out != null) {
|
||||
_pagingController.appendLastPage(out);
|
||||
}
|
||||
} else {
|
||||
_pagingController.error = resp.bodyString;
|
||||
}
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
final StickerProvider sticker = Get.find();
|
||||
sticker.refreshAvailableStickers();
|
||||
_pagingController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
floatingActionButton: FloatingActionButton(
|
||||
child: const Icon(Icons.add),
|
||||
onPressed: () {
|
||||
_promptUploadSticker().then((value) {
|
||||
if (value == true) _pagingController.refresh();
|
||||
});
|
||||
},
|
||||
),
|
||||
body: RefreshIndicator(
|
||||
onRefresh: () => Future.sync(() => _pagingController.refresh()),
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
PagedSliverList<int, StickerPack>(
|
||||
pagingController: _pagingController,
|
||||
builderDelegate: PagedChildBuilderDelegate(
|
||||
itemBuilder: (BuildContext context, item, int index) {
|
||||
return ExpansionTile(
|
||||
title: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(item.name),
|
||||
const Gap(6),
|
||||
Badge(
|
||||
label: Text('#${item.id}'),
|
||||
)
|
||||
],
|
||||
),
|
||||
subtitle: Text(
|
||||
item.description,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
children: item.stickers?.map((x) {
|
||||
x.pack = item;
|
||||
return _buildEmoteEntry(x, item.prefix);
|
||||
}).toList() ??
|
||||
List.empty(),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -7,9 +7,12 @@ import 'package:solian/exceptions/request.dart';
|
||||
import 'package:solian/exts.dart';
|
||||
import 'package:solian/models/auth.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/providers/websocket.dart';
|
||||
import 'package:solian/providers/content/realm.dart';
|
||||
import 'package:solian/providers/notifications.dart';
|
||||
import 'package:solian/providers/relation.dart';
|
||||
import 'package:solian/services.dart';
|
||||
import 'package:solian/widgets/sized_container.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
class SignInScreen extends StatefulWidget {
|
||||
const SignInScreen({super.key});
|
||||
@ -153,7 +156,7 @@ class _SignInScreenState extends State<SignInScreen> {
|
||||
|
||||
try {
|
||||
// Check ticket
|
||||
final resp = await client.patch('/auth', {
|
||||
final resp = await client.request('/auth', 'PATCH', body: {
|
||||
'ticket_id': _currentTicket!.id,
|
||||
'factor_id': _factorPicked!,
|
||||
'code': password,
|
||||
@ -173,16 +176,20 @@ class _SignInScreenState extends State<SignInScreen> {
|
||||
await auth.refreshAuthorizeStatus();
|
||||
await auth.refreshUserProfile();
|
||||
|
||||
Get.find<WebSocketProvider>().registerPushNotifications();
|
||||
Get.find<RealmProvider>().refreshAvailableRealms();
|
||||
Get.find<RelationshipProvider>().refreshRelativeList();
|
||||
Get.find<NotificationProvider>().registerPushNotifications();
|
||||
autoConfigureBackgroundNotificationService();
|
||||
autoStartBackgroundNotificationService();
|
||||
|
||||
Navigator.pop(context, true);
|
||||
_passwordController.clear();
|
||||
});
|
||||
} else {
|
||||
// Skip the first step
|
||||
_factorPicked = null;
|
||||
_factorPickedType = null;
|
||||
_passwordController.clear();
|
||||
setState(() => _period += 2);
|
||||
}
|
||||
} catch (e) {
|
||||
@ -203,17 +210,16 @@ class _SignInScreenState extends State<SignInScreen> {
|
||||
case 2:
|
||||
_passwordController.clear();
|
||||
_factorPickedType = null;
|
||||
default:
|
||||
setState(() => _period--);
|
||||
}
|
||||
setState(() => _period--);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: CenteredContainer(
|
||||
maxWidth: 360,
|
||||
return CenteredContainer(
|
||||
maxWidth: 360,
|
||||
child: Theme(
|
||||
data: Theme.of(context).copyWith(canvasColor: Colors.transparent),
|
||||
child: PageTransitionSwitcher(
|
||||
transitionBuilder: (
|
||||
Widget child,
|
||||
@ -228,16 +234,18 @@ class _SignInScreenState extends State<SignInScreen> {
|
||||
);
|
||||
},
|
||||
child: switch (_period % 3) {
|
||||
1 => Column(
|
||||
1 => ListView(
|
||||
shrinkWrap: true,
|
||||
key: const ValueKey<int>(1),
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child:
|
||||
Image.asset('assets/logo.png', width: 64, height: 64),
|
||||
).paddingOnly(bottom: 8, left: 4),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child:
|
||||
Image.asset('assets/logo.png', width: 64, height: 64),
|
||||
).paddingOnly(bottom: 8, left: 4),
|
||||
),
|
||||
Text(
|
||||
'signinPickFactor'.tr,
|
||||
style: const TextStyle(
|
||||
@ -316,16 +324,18 @@ class _SignInScreenState extends State<SignInScreen> {
|
||||
),
|
||||
],
|
||||
),
|
||||
2 => Column(
|
||||
2 => ListView(
|
||||
key: const ValueKey<int>(2),
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
shrinkWrap: true,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child:
|
||||
Image.asset('assets/logo.png', width: 64, height: 64),
|
||||
).paddingOnly(bottom: 8, left: 4),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child:
|
||||
Image.asset('assets/logo.png', width: 64, height: 64),
|
||||
).paddingOnly(bottom: 8, left: 4),
|
||||
),
|
||||
Text(
|
||||
'signinEnterPassword'.tr,
|
||||
style: const TextStyle(
|
||||
@ -389,16 +399,18 @@ class _SignInScreenState extends State<SignInScreen> {
|
||||
),
|
||||
],
|
||||
),
|
||||
_ => Column(
|
||||
_ => ListView(
|
||||
key: const ValueKey<int>(0),
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
shrinkWrap: true,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child:
|
||||
Image.asset('assets/logo.png', width: 64, height: 64),
|
||||
).paddingOnly(bottom: 8, left: 4),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child:
|
||||
Image.asset('assets/logo.png', width: 64, height: 64),
|
||||
).paddingOnly(bottom: 8, left: 4),
|
||||
),
|
||||
Text(
|
||||
'signinGreeting'.tr,
|
||||
style: const TextStyle(
|
||||
@ -444,11 +456,50 @@ class _SignInScreenState extends State<SignInScreen> {
|
||||
),
|
||||
],
|
||||
),
|
||||
const Gap(12),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 290),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
'termAcceptNextWithAgree'.tr,
|
||||
textAlign: TextAlign.end,
|
||||
style:
|
||||
Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface
|
||||
.withOpacity(0.75),
|
||||
),
|
||||
),
|
||||
Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('termAcceptLink'.tr),
|
||||
const Gap(4),
|
||||
const Icon(Icons.launch, size: 14),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
launchUrlString('https://solsynth.dev/terms');
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
).paddingSymmetric(horizontal: 16),
|
||||
),
|
||||
],
|
||||
),
|
||||
},
|
||||
),
|
||||
),
|
||||
).paddingAll(24),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import 'package:get/get.dart';
|
||||
import 'package:solian/exts.dart';
|
||||
import 'package:solian/services.dart';
|
||||
import 'package:solian/widgets/sized_container.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
class SignUpScreen extends StatefulWidget {
|
||||
const SignUpScreen({super.key});
|
||||
@ -18,7 +19,7 @@ class _SignUpScreenState extends State<SignUpScreen> {
|
||||
final _nicknameController = TextEditingController();
|
||||
final _passwordController = TextEditingController();
|
||||
|
||||
void performAction(BuildContext context) async {
|
||||
void _performAction(BuildContext context) async {
|
||||
final email = _emailController.value.text;
|
||||
final username = _usernameController.value.text;
|
||||
final nickname = _nicknameController.value.text;
|
||||
@ -60,101 +61,146 @@ class _SignUpScreenState extends State<SignUpScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
bool _isTermAccepted = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: CenteredContainer(
|
||||
maxWidth: 360,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ClipRRect(
|
||||
return CenteredContainer(
|
||||
maxWidth: 360,
|
||||
child: ListView(
|
||||
shrinkWrap: true,
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: Image.asset('assets/logo.png', width: 64, height: 64),
|
||||
).paddingOnly(bottom: 8, left: 4),
|
||||
Text(
|
||||
'signupGreeting'.tr,
|
||||
style: const TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
).paddingOnly(left: 4, bottom: 16),
|
||||
TextField(
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
controller: _usernameController,
|
||||
autofillHints: const [AutofillHints.username],
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: 'username'.tr,
|
||||
),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
Text(
|
||||
'signupGreeting'.tr,
|
||||
style: const TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
const Gap(12),
|
||||
TextField(
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
controller: _nicknameController,
|
||||
autofillHints: const [AutofillHints.nickname],
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: 'nickname'.tr,
|
||||
),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
).paddingOnly(left: 4, bottom: 16),
|
||||
TextField(
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
controller: _usernameController,
|
||||
autofillHints: const [AutofillHints.username],
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: 'username'.tr,
|
||||
),
|
||||
const Gap(12),
|
||||
TextField(
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
controller: _emailController,
|
||||
autofillHints: const [AutofillHints.email],
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: 'email'.tr,
|
||||
),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const Gap(12),
|
||||
TextField(
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
controller: _nicknameController,
|
||||
autofillHints: const [AutofillHints.nickname],
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: 'nickname'.tr,
|
||||
),
|
||||
const Gap(12),
|
||||
TextField(
|
||||
obscureText: true,
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
autofillHints: const [AutofillHints.password],
|
||||
controller: _passwordController,
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: 'password'.tr,
|
||||
),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onSubmitted: (_) => performAction(context),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const Gap(12),
|
||||
TextField(
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
controller: _emailController,
|
||||
autofillHints: const [AutofillHints.email],
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: 'email'.tr,
|
||||
),
|
||||
const Gap(16),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: TextButton(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('next'.tr),
|
||||
const Icon(Icons.chevron_right),
|
||||
],
|
||||
),
|
||||
onPressed: () => performAction(context),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const Gap(12),
|
||||
TextField(
|
||||
obscureText: true,
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
autofillHints: const [AutofillHints.password],
|
||||
controller: _passwordController,
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: 'password'.tr,
|
||||
),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onSubmitted: (_) => _performAction(context),
|
||||
),
|
||||
const Gap(8),
|
||||
CheckboxListTile(
|
||||
value: _isTermAccepted,
|
||||
title: Text(
|
||||
'termAccept'.tr,
|
||||
style: const TextStyle(height: 1.2),
|
||||
).paddingOnly(bottom: 4),
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(8),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
subtitle: RichText(
|
||||
text: TextSpan(
|
||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface
|
||||
.withOpacity(0.75),
|
||||
),
|
||||
children: [
|
||||
TextSpan(text: 'termAcceptDesc'.tr),
|
||||
WidgetSpan(
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('termAcceptLink'.tr),
|
||||
const Gap(4),
|
||||
const Icon(Icons.launch, size: 14),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
launchUrlString('https://solsynth.dev/terms');
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
onChanged: (value) {
|
||||
setState(() => _isTermAccepted = value ?? false);
|
||||
},
|
||||
),
|
||||
const Gap(16),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: TextButton(
|
||||
onPressed:
|
||||
!_isTermAccepted ? null : () => _performAction(context),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('next'.tr),
|
||||
const Icon(Icons.chevron_right),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
).paddingAll(24),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import 'package:solian/widgets/app_bar_leading.dart';
|
||||
import 'package:solian/widgets/chat/call/call_controls.dart';
|
||||
import 'package:solian/widgets/chat/call/call_participant.dart';
|
||||
import 'package:livekit_client/livekit_client.dart' as livekit;
|
||||
import 'package:solian/widgets/root_container.dart';
|
||||
|
||||
class CallScreen extends StatefulWidget {
|
||||
final bool hideAppBar;
|
||||
@ -197,8 +198,7 @@ class _CallScreenState extends State<CallScreen> with TickerProviderStateMixin {
|
||||
Widget build(BuildContext context) {
|
||||
final ChatCallProvider ctrl = Get.find();
|
||||
|
||||
return Material(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
return ResponsiveRootContainer(
|
||||
child: Scaffold(
|
||||
appBar: widget.hideAppBar
|
||||
? null
|
||||
|
@ -3,6 +3,7 @@ import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:solian/controllers/chat_events_controller.dart';
|
||||
import 'package:solian/exts.dart';
|
||||
import 'package:solian/models/call.dart';
|
||||
@ -25,6 +26,7 @@ import 'package:solian/widgets/chat/chat_event_list.dart';
|
||||
import 'package:solian/widgets/chat/chat_message_input.dart';
|
||||
import 'package:solian/widgets/chat/chat_typing_indicator.dart';
|
||||
import 'package:solian/widgets/current_state_action.dart';
|
||||
import 'package:solian/widgets/root_container.dart';
|
||||
|
||||
class ChannelChatScreen extends StatefulWidget {
|
||||
final String alias;
|
||||
@ -179,6 +181,8 @@ class _ChannelChatScreenState extends State<ChannelChatScreen>
|
||||
}
|
||||
}
|
||||
|
||||
late SharedPreferences _prefs;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@ -189,10 +193,13 @@ class _ChannelChatScreenState extends State<ChannelChatScreen>
|
||||
_chatController = ChatEventController();
|
||||
_chatController.initialize();
|
||||
|
||||
_getOngoingCall();
|
||||
_getChannel().then((_) {
|
||||
_chatController.getInitialEvents(_channel!, widget.realm);
|
||||
_listenMessages();
|
||||
SharedPreferences.getInstance().then((inst) {
|
||||
_prefs = inst;
|
||||
_getOngoingCall();
|
||||
_getChannel().then((_) {
|
||||
_chatController.getInitialEvents(_channel!, widget.realm);
|
||||
_listenMessages();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -201,151 +208,159 @@ class _ChannelChatScreenState extends State<ChannelChatScreen>
|
||||
String title = _channel?.name ?? 'loading'.tr;
|
||||
String? placeholder;
|
||||
|
||||
if (_channel?.type == 1) {
|
||||
final otherside =
|
||||
_channel!.members!.where((e) => e.account.id != _accountId).first;
|
||||
final otherside =
|
||||
_channel?.members!.where((e) => e.account.id != _accountId).firstOrNull;
|
||||
|
||||
if (_channel?.type == 1 && otherside != null) {
|
||||
title = otherside.account.nick;
|
||||
placeholder = 'messageInputPlaceholder'.trParams(
|
||||
{'channel': '@${otherside.account.name}'},
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: AppBarLeadingButton.adaptive(context),
|
||||
title: AppBarTitle(title),
|
||||
centerTitle: false,
|
||||
titleSpacing: AppTheme.titleSpacing(context),
|
||||
toolbarHeight: AppTheme.toolbarHeight(context),
|
||||
actions: [
|
||||
const BackgroundStateWidget(),
|
||||
Builder(builder: (context) {
|
||||
if (_isBusy || _channel == null) return const SizedBox.shrink();
|
||||
return ResponsiveRootContainer(
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: AppBarLeadingButton.adaptive(context),
|
||||
title: AppBarTitle(title),
|
||||
centerTitle: false,
|
||||
titleSpacing: AppTheme.titleSpacing(context),
|
||||
toolbarHeight: AppTheme.toolbarHeight(context),
|
||||
actions: [
|
||||
const BackgroundStateWidget(),
|
||||
Builder(builder: (context) {
|
||||
if (_isBusy || _channel == null) return const SizedBox.shrink();
|
||||
|
||||
return ChatCallButton(
|
||||
realm: _channel!.realm,
|
||||
channel: _channel!,
|
||||
ongoingCall: _ongoingCall,
|
||||
);
|
||||
}),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.more_vert),
|
||||
onPressed: () {
|
||||
if (_channel == null) return;
|
||||
return ChatCallButton(
|
||||
realm: _channel!.realm,
|
||||
channel: _channel!,
|
||||
ongoingCall: _ongoingCall,
|
||||
);
|
||||
}),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.more_vert),
|
||||
onPressed: () {
|
||||
if (_channel == null) return;
|
||||
|
||||
AppRouter.instance
|
||||
.pushNamed(
|
||||
'channelDetail',
|
||||
pathParameters: {'alias': widget.alias},
|
||||
queryParameters: {'realm': widget.realm},
|
||||
extra: ChannelDetailArguments(
|
||||
profile: _channelProfile!,
|
||||
channel: _channel!,
|
||||
),
|
||||
)
|
||||
.then((value) {
|
||||
if (value == false) AppRouter.instance.pop();
|
||||
if (value != null) {
|
||||
final resp = Channel.fromJson(value as Map<String, dynamic>);
|
||||
_getChannel(alias: resp.alias);
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
SizedBox(
|
||||
width: AppTheme.isLargeScreen(context) ? 8 : 16,
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Builder(builder: (context) {
|
||||
if (_isBusy || _channel == null) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
if (_ongoingCall != null)
|
||||
ChannelCallIndicator(
|
||||
channel: _channel!,
|
||||
ongoingCall: _ongoingCall!,
|
||||
onJoin: () {
|
||||
if (!AppTheme.isLargeScreen(context)) {
|
||||
final ChatCallProvider call = Get.find();
|
||||
call.gotoScreen(context);
|
||||
}
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: ChatEventList(
|
||||
scope: widget.realm,
|
||||
channel: _channel!,
|
||||
chatController: _chatController,
|
||||
onEdit: (item) {
|
||||
setState(() => _messageToEditing = item);
|
||||
},
|
||||
onReply: (item) {
|
||||
setState(() => _messageToReplying = item);
|
||||
},
|
||||
),
|
||||
AppRouter.instance
|
||||
.pushNamed(
|
||||
'channelDetail',
|
||||
pathParameters: {'alias': widget.alias},
|
||||
queryParameters: {'realm': widget.realm},
|
||||
extra: ChannelDetailArguments(
|
||||
profile: _channelProfile!,
|
||||
channel: _channel!,
|
||||
),
|
||||
ClipRect(
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 50, sigmaY: 50),
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
ChatTypingIndicator(users: _typingUsers),
|
||||
ChatMessageInput(
|
||||
edit: _messageToEditing,
|
||||
reply: _messageToReplying,
|
||||
realm: widget.realm,
|
||||
placeholder: placeholder,
|
||||
channel: _channel!,
|
||||
onSent: (Event item) {
|
||||
setState(() {
|
||||
_chatController.addPendingEvent(item);
|
||||
});
|
||||
},
|
||||
onReset: () {
|
||||
setState(() {
|
||||
_messageToReplying = null;
|
||||
_messageToEditing = null;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
.then((value) {
|
||||
if (value == false) AppRouter.instance.pop();
|
||||
if (value != null) {
|
||||
final resp =
|
||||
Channel.fromJson(value as Map<String, dynamic>);
|
||||
_getChannel(alias: resp.alias);
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
SizedBox(
|
||||
width: AppTheme.isLargeScreen(context) ? 8 : 16,
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Builder(builder: (context) {
|
||||
if (_isBusy || _channel == null) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
if (_ongoingCall != null)
|
||||
ChannelCallIndicator(
|
||||
channel: _channel!,
|
||||
ongoingCall: _ongoingCall!,
|
||||
onJoin: () {
|
||||
if (!AppTheme.isUltraLargeScreen(context)) {
|
||||
final ChatCallProvider call = Get.find();
|
||||
call.gotoScreen(context);
|
||||
}
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: ChatEventList(
|
||||
noAnimated:
|
||||
_prefs.getBool('non_animated_message_list') ??
|
||||
false,
|
||||
scope: widget.realm,
|
||||
channel: _channel!,
|
||||
chatController: _chatController,
|
||||
onEdit: (item) {
|
||||
setState(() => _messageToEditing = item);
|
||||
},
|
||||
onReply: (item) {
|
||||
setState(() => _messageToReplying = item);
|
||||
},
|
||||
),
|
||||
),
|
||||
ClipRect(
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 50, sigmaY: 50),
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
ChatTypingIndicator(users: _typingUsers),
|
||||
ChatMessageInput(
|
||||
edit: _messageToEditing,
|
||||
reply: _messageToReplying,
|
||||
realm: widget.realm,
|
||||
placeholder: placeholder,
|
||||
channel: _channel!,
|
||||
onSent: (Event item) {
|
||||
setState(() {
|
||||
_chatController.addPendingEvent(item);
|
||||
});
|
||||
},
|
||||
onReset: () {
|
||||
setState(() {
|
||||
_messageToReplying = null;
|
||||
_messageToEditing = null;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Obx(() {
|
||||
final ChatCallProvider call = Get.find();
|
||||
if (call.isMounted.value && AppTheme.isLargeScreen(context)) {
|
||||
return const Expanded(
|
||||
child: Row(children: [
|
||||
VerticalDivider(width: 0.3, thickness: 0.3),
|
||||
Expanded(
|
||||
child: CallScreen(
|
||||
hideAppBar: true,
|
||||
isExpandable: true,
|
||||
Obx(() {
|
||||
final ChatCallProvider call = Get.find();
|
||||
if (call.isMounted.value &&
|
||||
AppTheme.isUltraLargeScreen(context)) {
|
||||
return const Expanded(
|
||||
child: Row(children: [
|
||||
VerticalDivider(width: 0.3, thickness: 0.3),
|
||||
Expanded(
|
||||
child: CallScreen(
|
||||
hideAppBar: true,
|
||||
isExpandable: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
]),
|
||||
);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
}),
|
||||
],
|
||||
);
|
||||
}),
|
||||
]),
|
||||
);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
}),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -114,7 +114,8 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
|
||||
ListTile(
|
||||
leading: const Icon(Icons.settings),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
title: Text('channelSettings'.tr.capitalize!),
|
||||
title: Text('channelSettings'.tr),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
onTap: () async {
|
||||
AppRouter.instance
|
||||
.pushNamed(
|
||||
@ -173,7 +174,8 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.notifications_active),
|
||||
title: Text('channelNotifyLevel'.tr.capitalize!),
|
||||
title: Text('channelNotifyLevel'.tr),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
trailing: DropdownButtonHideUnderline(
|
||||
child: DropdownButton2<int>(
|
||||
isExpanded: true,
|
||||
@ -206,14 +208,16 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Icons.supervisor_account),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
title: Text('channelMembers'.tr.capitalize!),
|
||||
title: Text('channelMembers'.tr),
|
||||
onTap: () => showMemberList(),
|
||||
),
|
||||
...(_isOwned ? ownerActions : List.empty()),
|
||||
const Divider(thickness: 0.3),
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: _isOwned
|
||||
? const Icon(Icons.delete)
|
||||
: const Icon(Icons.exit_to_app),
|
||||
|
@ -1,5 +1,4 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/exts.dart';
|
||||
import 'package:solian/models/channel.dart';
|
||||
@ -9,6 +8,8 @@ import 'package:solian/providers/content/channel.dart';
|
||||
import 'package:solian/router.dart';
|
||||
import 'package:solian/theme.dart';
|
||||
import 'package:solian/widgets/app_bar_title.dart';
|
||||
import 'package:solian/widgets/loading_indicator.dart';
|
||||
import 'package:solian/widgets/root_container.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class ChannelOrganizeArguments {
|
||||
@ -35,13 +36,14 @@ class _ChannelOrganizeScreenState extends State<ChannelOrganizeScreen> {
|
||||
final _nameController = TextEditingController();
|
||||
final _descriptionController = TextEditingController();
|
||||
|
||||
bool _isEncrypted = false;
|
||||
bool _isPublic = false;
|
||||
bool _isCommunity = false;
|
||||
|
||||
void applyChannel() async {
|
||||
void _applyChannel() async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) return;
|
||||
|
||||
if (_aliasController.value.text.isEmpty) randomizeAlias();
|
||||
if (_aliasController.value.text.isEmpty) _randomizeAlias();
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
@ -52,7 +54,7 @@ class _ChannelOrganizeScreenState extends State<ChannelOrganizeScreen> {
|
||||
'alias': _aliasController.value.text.toLowerCase(),
|
||||
'name': _nameController.value.text,
|
||||
'description': _descriptionController.value.text,
|
||||
'is_encrypted': _isEncrypted,
|
||||
'is_encrypted': _isPublic,
|
||||
};
|
||||
|
||||
Response? resp;
|
||||
@ -71,41 +73,49 @@ class _ChannelOrganizeScreenState extends State<ChannelOrganizeScreen> {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
|
||||
void randomizeAlias() {
|
||||
void _randomizeAlias() {
|
||||
_aliasController.text =
|
||||
const Uuid().v4().replaceAll('-', '').substring(0, 12);
|
||||
}
|
||||
|
||||
void syncWidget() {
|
||||
void _syncWidget() {
|
||||
if (widget.edit != null) {
|
||||
_aliasController.text = widget.edit!.alias;
|
||||
_nameController.text = widget.edit!.name;
|
||||
_descriptionController.text = widget.edit!.description;
|
||||
_isEncrypted = widget.edit!.isEncrypted;
|
||||
_isPublic = widget.edit!.isPublic;
|
||||
_isCommunity = widget.edit!.isCommunity;
|
||||
}
|
||||
}
|
||||
|
||||
void cancelAction() {
|
||||
void _cancelAction() {
|
||||
AppRouter.instance.pop();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
syncWidget();
|
||||
_syncWidget();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_aliasController.dispose();
|
||||
_nameController.dispose();
|
||||
_descriptionController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final notifyBannerActions = [
|
||||
TextButton(
|
||||
onPressed: cancelAction,
|
||||
onPressed: _cancelAction,
|
||||
child: Text('cancel'.tr),
|
||||
),
|
||||
];
|
||||
|
||||
return Material(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
return ResponsiveRootContainer(
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: AppBarTitle('channelOrganizing'.tr),
|
||||
@ -113,7 +123,7 @@ class _ChannelOrganizeScreenState extends State<ChannelOrganizeScreen> {
|
||||
toolbarHeight: AppTheme.toolbarHeight(context),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: _isBusy ? null : () => applyChannel(),
|
||||
onPressed: _isBusy ? null : () => _applyChannel(),
|
||||
child: Text('apply'.tr.toUpperCase()),
|
||||
)
|
||||
],
|
||||
@ -122,7 +132,7 @@ class _ChannelOrganizeScreenState extends State<ChannelOrganizeScreen> {
|
||||
top: false,
|
||||
child: Column(
|
||||
children: [
|
||||
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
if (widget.edit != null)
|
||||
MaterialBanner(
|
||||
leading: const Icon(Icons.edit),
|
||||
@ -164,7 +174,7 @@ class _ChannelOrganizeScreenState extends State<ChannelOrganizeScreen> {
|
||||
visualDensity:
|
||||
const VisualDensity(horizontal: -2, vertical: -2),
|
||||
),
|
||||
onPressed: () => randomizeAlias(),
|
||||
onPressed: () => _randomizeAlias(),
|
||||
child: const Icon(Icons.refresh),
|
||||
)
|
||||
],
|
||||
@ -196,12 +206,17 @@ class _ChannelOrganizeScreenState extends State<ChannelOrganizeScreen> {
|
||||
),
|
||||
const Divider(thickness: 0.3),
|
||||
CheckboxListTile(
|
||||
title: Text('channelEncrypted'.tr),
|
||||
value: _isEncrypted,
|
||||
onChanged: (widget.edit?.isEncrypted ?? false)
|
||||
? null
|
||||
: (newValue) =>
|
||||
setState(() => _isEncrypted = newValue ?? false),
|
||||
title: Text('channelPublic'.tr),
|
||||
value: _isPublic,
|
||||
onChanged: (value) =>
|
||||
setState(() => _isPublic = value ?? false),
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
),
|
||||
CheckboxListTile(
|
||||
title: Text('channelCommunity'.tr),
|
||||
value: _isCommunity,
|
||||
onChanged: (value) =>
|
||||
setState(() => _isCommunity = value ?? false),
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
),
|
||||
],
|
||||
|
@ -1,141 +1,338 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_resizable_container/flutter_resizable_container.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/controllers/chat_events_controller.dart';
|
||||
import 'package:solian/exts.dart';
|
||||
import 'package:solian/models/channel.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/providers/content/channel.dart';
|
||||
import 'package:solian/providers/content/realm.dart';
|
||||
import 'package:solian/providers/database/database.dart';
|
||||
import 'package:solian/router.dart';
|
||||
import 'package:solian/screens/account/notification.dart';
|
||||
import 'package:solian/theme.dart';
|
||||
import 'package:solian/widgets/account/account_avatar.dart';
|
||||
import 'package:solian/widgets/account/signin_required_overlay.dart';
|
||||
import 'package:solian/widgets/app_bar_leading.dart';
|
||||
import 'package:solian/widgets/app_bar_title.dart';
|
||||
import 'package:solian/widgets/channel/channel_list.dart';
|
||||
import 'package:solian/widgets/chat/call/chat_call_indicator.dart';
|
||||
import 'package:solian/widgets/current_state_action.dart';
|
||||
import 'package:solian/widgets/sized_container.dart';
|
||||
import 'package:solian/widgets/loading_indicator.dart';
|
||||
import 'package:solian/widgets/root_container.dart';
|
||||
import 'package:solian/widgets/sidebar/empty_placeholder.dart';
|
||||
|
||||
class ChatScreen extends StatefulWidget {
|
||||
class ChatScreen extends StatelessWidget {
|
||||
const ChatScreen({super.key});
|
||||
|
||||
@override
|
||||
State<ChatScreen> createState() => _ChatScreenState();
|
||||
Widget build(BuildContext context) {
|
||||
return const ResponsiveRootContainer(
|
||||
child: ChatList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ChatScreenState extends State<ChatScreen> {
|
||||
late final ChannelProvider _channels;
|
||||
class ChatListShell extends StatelessWidget {
|
||||
final Widget? child;
|
||||
|
||||
const ChatListShell({super.key, required this.child});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RootContainer(
|
||||
child: ResizableContainer(
|
||||
direction: Axis.horizontal,
|
||||
divider: ResizableDivider(
|
||||
thickness: 0.3,
|
||||
color: Theme.of(context).dividerColor.withOpacity(0.3),
|
||||
),
|
||||
children: [
|
||||
const ResizableChild(
|
||||
minSize: 280,
|
||||
maxSize: 520,
|
||||
size: ResizableSize.pixels(360),
|
||||
child: ChatList(),
|
||||
),
|
||||
ResizableChild(
|
||||
minSize: 280,
|
||||
child: child ?? const EmptyPagePlaceholder(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ChatList extends StatefulWidget {
|
||||
const ChatList({super.key});
|
||||
|
||||
@override
|
||||
State<ChatList> createState() => _ChatListState();
|
||||
}
|
||||
|
||||
class _ChatListState extends State<ChatList> {
|
||||
List<Channel> _normalChannels = List.empty();
|
||||
List<Channel> _directChannels = List.empty();
|
||||
final Map<String, List<Channel>> _realmChannels = {};
|
||||
|
||||
late final ChannelProvider _channels = Get.find();
|
||||
|
||||
bool _isBusy = true;
|
||||
|
||||
List<Channel> _sortChannels(List<Channel> channels) {
|
||||
channels.sort(
|
||||
(a, b) =>
|
||||
_lastMessages?[b.id]?.createdAt.compareTo(
|
||||
_lastMessages?[a.id]?.createdAt ??
|
||||
DateTime.fromMillisecondsSinceEpoch(0),
|
||||
) ??
|
||||
0,
|
||||
);
|
||||
return channels;
|
||||
}
|
||||
|
||||
Future<void> _loadNormalChannels() async {
|
||||
final resp = await _channels.listAvailableChannel(isDirect: false);
|
||||
setState(() {
|
||||
_normalChannels = _sortChannels(resp);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _loadDirectChannels() async {
|
||||
final resp = await _channels.listAvailableChannel(isDirect: true);
|
||||
setState(() {
|
||||
_directChannels = _sortChannels(resp);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _loadRealmChannels(String realm) async {
|
||||
final resp = await _channels.listAvailableChannel(scope: realm);
|
||||
setState(() {
|
||||
_realmChannels[realm] = _sortChannels(List.from(resp));
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _loadAllChannels() async {
|
||||
final RealmProvider realms = Get.find();
|
||||
Future.wait([
|
||||
_loadNormalChannels(),
|
||||
_loadDirectChannels(),
|
||||
...realms.availableRealms.map((x) => _loadRealmChannels(x.alias)),
|
||||
]);
|
||||
}
|
||||
|
||||
Map<int, LocalMessageEventTableData>? _lastMessages;
|
||||
|
||||
Future<void> _loadLastMessages() async {
|
||||
final ctrl = ChatEventController();
|
||||
await ctrl.initialize();
|
||||
final messages = await ctrl.src.getLastInAllChannels();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_lastMessages = messages
|
||||
.map((k, v) => MapEntry(k, v.firstOrNull))
|
||||
.cast<int, LocalMessageEventTableData>();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
try {
|
||||
_channels = Get.find();
|
||||
_channels.refreshAvailableChannel();
|
||||
} catch (e) {
|
||||
context.showErrorDialog(e);
|
||||
}
|
||||
_loadLastMessages().then((_) {
|
||||
if (!mounted) return;
|
||||
_loadAllChannels().then((_) {
|
||||
if (mounted) {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final AuthProvider auth = Get.find();
|
||||
final RealmProvider realms = Get.find();
|
||||
|
||||
return Material(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: AppBarLeadingButton.adaptive(context),
|
||||
title: AppBarTitle('chat'.tr),
|
||||
centerTitle: true,
|
||||
toolbarHeight: AppTheme.toolbarHeight(context),
|
||||
actions: [
|
||||
const BackgroundStateWidget(),
|
||||
const NotificationButton(),
|
||||
PopupMenuButton(
|
||||
icon: const Icon(Icons.add_circle),
|
||||
itemBuilder: (BuildContext context) => [
|
||||
PopupMenuItem(
|
||||
child: ListTile(
|
||||
title: Text('channelOrganizeCommon'.tr),
|
||||
leading: const Icon(Icons.tag),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
),
|
||||
onTap: () {
|
||||
AppRouter.instance.pushNamed('channelOrganizing').then(
|
||||
(value) {
|
||||
if (value != null) {
|
||||
_channels.refreshAvailableChannel();
|
||||
}
|
||||
return Obx(
|
||||
() => DefaultTabController(
|
||||
length: 2 + realms.availableRealms.length,
|
||||
child: ResponsiveRootContainer(
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: AppBarLeadingButton.adaptive(context),
|
||||
title: AppBarTitle('chat'.tr),
|
||||
centerTitle: true,
|
||||
toolbarHeight: AppTheme.toolbarHeight(context),
|
||||
actions: [
|
||||
const BackgroundStateWidget(),
|
||||
const NotificationButton(),
|
||||
PopupMenuButton(
|
||||
icon: const Icon(Icons.add_circle),
|
||||
itemBuilder: (BuildContext context) => [
|
||||
PopupMenuItem(
|
||||
child: ListTile(
|
||||
title: Text('channelOrganizeCommon'.tr),
|
||||
leading: const Icon(Icons.tag),
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 8),
|
||||
),
|
||||
onTap: () {
|
||||
AppRouter.instance.pushNamed('channelOrganizing').then(
|
||||
(value) {
|
||||
if (value != null) {
|
||||
_loadAllChannels();
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: ListTile(
|
||||
title: Text('channelOrganizeDirect'.tr),
|
||||
leading: const FaIcon(
|
||||
FontAwesomeIcons.userGroup,
|
||||
size: 16,
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
),
|
||||
onTap: () {
|
||||
final ChannelProvider channels = Get.find();
|
||||
channels
|
||||
.createDirectChannel(context, 'global')
|
||||
.then((resp) {
|
||||
if (resp != null) {
|
||||
_channels.refreshAvailableChannel();
|
||||
}
|
||||
}).catchError((e) {
|
||||
context.showErrorDialog(e);
|
||||
});
|
||||
},
|
||||
PopupMenuItem(
|
||||
child: ListTile(
|
||||
title: Text('channelOrganizeDirect'.tr),
|
||||
leading: const FaIcon(
|
||||
FontAwesomeIcons.userGroup,
|
||||
size: 16,
|
||||
),
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 8),
|
||||
),
|
||||
onTap: () {
|
||||
final ChannelProvider channels = Get.find();
|
||||
channels
|
||||
.createDirectChannel(context, 'global')
|
||||
.then((resp) {
|
||||
if (resp != null) {
|
||||
_loadAllChannels();
|
||||
}
|
||||
}).catchError((e) {
|
||||
context.showErrorDialog(e);
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
width: AppTheme.isLargeScreen(context) ? 8 : 16,
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
width: AppTheme.isLargeScreen(context) ? 8 : 16,
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Obx(() {
|
||||
if (auth.isAuthorized.isFalse) {
|
||||
return SigninRequiredOverlay(
|
||||
onDone: () => _channels.refreshAvailableChannel(),
|
||||
);
|
||||
}
|
||||
|
||||
final selfId = auth.userProfile.value!['id'];
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Obx(() {
|
||||
if (_channels.isLoading.isFalse) {
|
||||
return const SizedBox.shrink();
|
||||
} else {
|
||||
return const LinearProgressIndicator();
|
||||
}
|
||||
}),
|
||||
const ChatCallCurrentIndicator(),
|
||||
Expanded(
|
||||
child: CenteredContainer(
|
||||
child: RefreshIndicator(
|
||||
onRefresh: _channels.refreshAvailableChannel,
|
||||
child: Obx(
|
||||
() => ChannelListWidget(
|
||||
noCategory: true,
|
||||
channels: _channels.directChannels,
|
||||
selfId: selfId,
|
||||
useReplace: true,
|
||||
),
|
||||
bottom: TabBar(
|
||||
isScrollable: true,
|
||||
dividerHeight: 0.3,
|
||||
tabAlignment: TabAlignment.startOffset,
|
||||
tabs: [
|
||||
Tab(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 14,
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.primary,
|
||||
child: const Icon(
|
||||
Icons.forum,
|
||||
size: 16,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
Text('all'.tr),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Tab(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const CircleAvatar(
|
||||
radius: 14,
|
||||
child: Icon(
|
||||
Icons.chat_bubble,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
Text('channelTypeDirect'.tr),
|
||||
],
|
||||
),
|
||||
),
|
||||
...realms.availableRealms.map((x) => Tab(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
AttachedCircleAvatar(
|
||||
content: x.avatar,
|
||||
radius: 14,
|
||||
fallbackWidget: const Icon(
|
||||
Icons.workspaces,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
Text(x.name),
|
||||
],
|
||||
),
|
||||
)),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
body: Obx(() {
|
||||
if (auth.isAuthorized.isFalse) {
|
||||
return SigninRequiredOverlay(
|
||||
onDone: () => _loadAllChannels(),
|
||||
);
|
||||
}
|
||||
|
||||
final selfId = auth.userProfile.value!['id'];
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
const ChatCallCurrentIndicator(),
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
children: [
|
||||
RefreshIndicator(
|
||||
onRefresh: _loadNormalChannels,
|
||||
child: ChannelListWidget(
|
||||
channels: _sortChannels([
|
||||
..._normalChannels,
|
||||
..._directChannels,
|
||||
..._realmChannels.values.expand((x) => x),
|
||||
]),
|
||||
selfId: selfId,
|
||||
useReplace: AppTheme.isLargeScreen(context),
|
||||
),
|
||||
),
|
||||
RefreshIndicator(
|
||||
onRefresh: _loadDirectChannels,
|
||||
child: ChannelListWidget(
|
||||
channels: _directChannels,
|
||||
selfId: selfId,
|
||||
useReplace: AppTheme.isLargeScreen(context),
|
||||
),
|
||||
),
|
||||
...realms.availableRealms.map(
|
||||
(x) => RefreshIndicator(
|
||||
onRefresh: () => _loadRealmChannels(x.alias),
|
||||
child: ChannelListWidget(
|
||||
channels: _realmChannels[x.alias] ?? [],
|
||||
selfId: selfId,
|
||||
useReplace: AppTheme.isLargeScreen(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ import 'package:solian/providers/content/posts.dart';
|
||||
import 'package:solian/providers/daily_sign.dart';
|
||||
import 'package:solian/providers/database/services/messages.dart';
|
||||
import 'package:solian/providers/last_read.dart';
|
||||
import 'package:solian/providers/websocket.dart';
|
||||
import 'package:solian/providers/notifications.dart';
|
||||
import 'package:solian/router.dart';
|
||||
import 'package:solian/screens/account/notification.dart';
|
||||
import 'package:solian/theme.dart';
|
||||
@ -38,7 +38,7 @@ class DashboardScreen extends StatefulWidget {
|
||||
class _DashboardScreenState extends State<DashboardScreen> {
|
||||
late final AuthProvider _auth = Get.find();
|
||||
late final LastReadProvider _lastRead = Get.find();
|
||||
late final WebSocketProvider _ws = Get.find();
|
||||
late final NotificationProvider _nty = Get.find();
|
||||
late final PostProvider _posts = Get.find();
|
||||
late final DailySignProvider _dailySign = Get.find();
|
||||
|
||||
@ -46,7 +46,7 @@ class _DashboardScreenState extends State<DashboardScreen> {
|
||||
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
|
||||
|
||||
List<Notification> get _pendingNotifications =>
|
||||
List<Notification>.from(_ws.notifications)
|
||||
List<Notification>.from(_nty.notifications.where((x) => x.readAt == null))
|
||||
..sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
||||
|
||||
List<Post>? _currentPosts;
|
||||
@ -75,10 +75,12 @@ class _DashboardScreenState extends State<DashboardScreen> {
|
||||
final src = Get.find<MessagesFetchingProvider>();
|
||||
final out = await src.getWhatsNewEvents(_lastRead.messagesLastReadAt!);
|
||||
if (out == null) return;
|
||||
setState(() {
|
||||
_currentMessages = out.$1;
|
||||
_currentMessagesCount = out.$2;
|
||||
});
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_currentMessages = out.$1;
|
||||
_currentMessagesCount = out.$2;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
bool _signingDaily = true;
|
||||
@ -88,14 +90,18 @@ class _DashboardScreenState extends State<DashboardScreen> {
|
||||
Future<void> _pullDaily() async {
|
||||
try {
|
||||
_signRecord = await _dailySign.getToday();
|
||||
_dailySign.listLastRecord(30).then((value) {
|
||||
setState(() => _signRecordHistory = value);
|
||||
_dailySign.listLastRecord(14).then((value) {
|
||||
if (mounted) {
|
||||
setState(() => _signRecordHistory = value);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
context.showErrorDialog(e);
|
||||
}
|
||||
|
||||
setState(() => _signingDaily = false);
|
||||
if (mounted) {
|
||||
setState(() => _signingDaily = false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _signDaily() async {
|
||||
@ -103,7 +109,7 @@ class _DashboardScreenState extends State<DashboardScreen> {
|
||||
|
||||
try {
|
||||
_signRecord = await _dailySign.signToday();
|
||||
_dailySign.listLastRecord(30).then((value) {
|
||||
_dailySign.listLastRecord(14).then((value) {
|
||||
setState(() => _signRecordHistory = value);
|
||||
});
|
||||
} catch (e) {
|
||||
@ -147,7 +153,7 @@ class _DashboardScreenState extends State<DashboardScreen> {
|
||||
),
|
||||
Text(DateFormat('yyyy/MM/dd').format(DateTime.now().toUtc())),
|
||||
],
|
||||
).paddingOnly(top: 8, left: 18, right: 18, bottom: 12),
|
||||
).paddingOnly(top: 16, left: 18, right: 18, bottom: 12),
|
||||
Card(
|
||||
child: Column(
|
||||
children: [
|
||||
@ -248,7 +254,7 @@ class _DashboardScreenState extends State<DashboardScreen> {
|
||||
),
|
||||
Text(
|
||||
'notificationUnreadCount'.trParams({
|
||||
'count': _ws.notifications.length.toString(),
|
||||
'count': _pendingNotifications.length.toString(),
|
||||
}),
|
||||
),
|
||||
],
|
||||
@ -261,12 +267,12 @@ class _DashboardScreenState extends State<DashboardScreen> {
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (context) => const NotificationScreen(),
|
||||
).then((_) => _ws.notificationUnread.value = 0);
|
||||
).then((_) => _nty.notificationUnread.value = 0);
|
||||
},
|
||||
),
|
||||
],
|
||||
).paddingOnly(left: 18, right: 18, bottom: 8),
|
||||
if (_ws.notifications.isNotEmpty)
|
||||
if (_pendingNotifications.isNotEmpty)
|
||||
SizedBox(
|
||||
height: 76,
|
||||
child: ListView.separated(
|
||||
@ -354,7 +360,7 @@ class _DashboardScreenState extends State<DashboardScreen> {
|
||||
IconButton(
|
||||
icon: const Icon(Icons.arrow_forward),
|
||||
onPressed: () {
|
||||
AppRouter.instance.goNamed('feed');
|
||||
AppRouter.instance.goNamed('explore');
|
||||
},
|
||||
),
|
||||
],
|
||||
@ -379,12 +385,14 @@ class _DashboardScreenState extends State<DashboardScreen> {
|
||||
isClickable: true,
|
||||
isShowEmbed: true,
|
||||
isNestedClickable: true,
|
||||
showFeaturedReply: true,
|
||||
onUpdate: (_) {
|
||||
_pullPosts();
|
||||
},
|
||||
backgroundColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.surfaceContainerLow,
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: 8,
|
||||
horizontal: 4,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -518,7 +526,7 @@ class _DashboardScreenState extends State<DashboardScreen> {
|
||||
style: TextStyle(color: _unFocusColor, fontSize: 12),
|
||||
)
|
||||
],
|
||||
).paddingAll(8),
|
||||
).paddingOnly(left: 8, right: 8, top: 8, bottom: 50),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
251
lib/screens/explore.dart
Normal file
251
lib/screens/explore.dart
Normal file
@ -0,0 +1,251 @@
|
||||
import 'dart:async';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/controllers/post_list_controller.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/providers/navigation.dart';
|
||||
import 'package:solian/router.dart';
|
||||
import 'package:solian/screens/account/notification.dart';
|
||||
import 'package:solian/theme.dart';
|
||||
import 'package:solian/widgets/account/signin_required_overlay.dart';
|
||||
import 'package:solian/widgets/current_state_action.dart';
|
||||
import 'package:solian/widgets/app_bar_leading.dart';
|
||||
import 'package:solian/widgets/navigation/realm_switcher.dart';
|
||||
import 'package:solian/widgets/posts/post_creation.dart';
|
||||
import 'package:solian/widgets/posts/post_list.dart';
|
||||
import 'package:solian/widgets/posts/post_shuffle_swiper.dart';
|
||||
import 'package:solian/widgets/root_container.dart';
|
||||
|
||||
class ExploreScreen extends StatefulWidget {
|
||||
const ExploreScreen({super.key});
|
||||
|
||||
@override
|
||||
State<ExploreScreen> createState() => _ExploreScreenState();
|
||||
}
|
||||
|
||||
class _ExploreScreenState extends State<ExploreScreen>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final PostListController _postController;
|
||||
late final TabController _tabController;
|
||||
|
||||
List<StreamSubscription>? _subscriptions;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final navState = Get.find<NavigationStateProvider>();
|
||||
_postController = PostListController();
|
||||
_postController.realm = navState.focusedRealm.value?.alias;
|
||||
_tabController = TabController(length: 3, vsync: this);
|
||||
_tabController.addListener(() {
|
||||
if (_postController.mode.value == _tabController.index) return;
|
||||
_postController.mode.value = _tabController.index;
|
||||
_postController.reloadAllOver();
|
||||
});
|
||||
_subscriptions = [
|
||||
Get.find<NavigationStateProvider>().focusedRealm.listen((value) {
|
||||
if (value?.alias != _postController.realm) {
|
||||
_postController.realm = value?.alias;
|
||||
_postController.reloadAllOver();
|
||||
}
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final AuthProvider auth = Get.find();
|
||||
|
||||
return RootContainer(
|
||||
child: Scaffold(
|
||||
floatingActionButton: FloatingActionButton(
|
||||
child: const Icon(Icons.add),
|
||||
onPressed: () async {
|
||||
final value = await showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (context) => const PostCreatePopup(),
|
||||
);
|
||||
if (value is Future) {
|
||||
value.then((_) {
|
||||
_postController.reloadAllOver();
|
||||
});
|
||||
} else if (value != null) {
|
||||
_postController.reloadAllOver();
|
||||
}
|
||||
},
|
||||
),
|
||||
body: NestedScrollView(
|
||||
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
|
||||
return [
|
||||
SliverLayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final scrollOffset = constraints.scrollOffset;
|
||||
final colorChangeOffset = 120;
|
||||
|
||||
final scrollProgress =
|
||||
(scrollOffset / colorChangeOffset).clamp(0.0, 1.0);
|
||||
final blurSigma = lerpDouble(0, 10, scrollProgress) ?? 0;
|
||||
|
||||
return SliverAppBar(
|
||||
flexibleSpace: ClipRRect(
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(
|
||||
sigmaX: blurSigma,
|
||||
sigmaY: blurSigma,
|
||||
),
|
||||
child: ListView(
|
||||
padding: EdgeInsets.zero,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 48,
|
||||
child: const Row(
|
||||
children: [
|
||||
RealmSwitcher(),
|
||||
],
|
||||
).paddingSymmetric(horizontal: 8),
|
||||
).paddingSymmetric(vertical: 4),
|
||||
TabBar(
|
||||
controller: _tabController,
|
||||
dividerHeight: scrollProgress > 0 ? 0 : 0.3,
|
||||
tabAlignment: TabAlignment.fill,
|
||||
tabs: [
|
||||
Tab(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.feed, size: 20),
|
||||
const Gap(8),
|
||||
Text('postListNews'.tr),
|
||||
],
|
||||
),
|
||||
),
|
||||
Tab(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.people, size: 20),
|
||||
const Gap(8),
|
||||
Text('postListFriends'.tr),
|
||||
],
|
||||
),
|
||||
),
|
||||
Tab(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.shuffle_on_outlined,
|
||||
size: 20,
|
||||
),
|
||||
const Gap(8),
|
||||
Text('postListShuffle'.tr),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
).paddingOnly(top: MediaQuery.of(context).padding.top),
|
||||
),
|
||||
),
|
||||
expandedHeight: 104,
|
||||
snap: true,
|
||||
floating: true,
|
||||
toolbarHeight: AppTheme.toolbarHeight(context),
|
||||
leading: AppBarLeadingButton.adaptive(context),
|
||||
actions: [
|
||||
const BackgroundStateWidget(),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.search),
|
||||
onPressed: () {
|
||||
AppRouter.instance.pushNamed('postSearch');
|
||||
},
|
||||
),
|
||||
const NotificationButton(),
|
||||
SizedBox(
|
||||
width: AppTheme.isLargeScreen(context) ? 8 : 16,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
)
|
||||
];
|
||||
},
|
||||
body: Obx(() {
|
||||
if (_postController.isPreparing.isTrue) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
controller: _tabController,
|
||||
children: [
|
||||
RefreshIndicator(
|
||||
onRefresh: () => _postController.reloadAllOver(),
|
||||
child: CustomScrollView(slivers: [
|
||||
ControlledPostListWidget(
|
||||
padding: AppTheme.isLargeScreen(context)
|
||||
? EdgeInsets.symmetric(
|
||||
horizontal: 4,
|
||||
vertical: 8,
|
||||
)
|
||||
: EdgeInsets.zero,
|
||||
controller: _postController.pagingController,
|
||||
onUpdate: () => _postController.reloadAllOver(),
|
||||
),
|
||||
]),
|
||||
),
|
||||
Obx(() {
|
||||
if (auth.isAuthorized.value) {
|
||||
return RefreshIndicator(
|
||||
onRefresh: () => _postController.reloadAllOver(),
|
||||
child: CustomScrollView(slivers: [
|
||||
ControlledPostListWidget(
|
||||
padding: AppTheme.isLargeScreen(context)
|
||||
? EdgeInsets.symmetric(horizontal: 16)
|
||||
: EdgeInsets.zero,
|
||||
controller: _postController.pagingController,
|
||||
onUpdate: () => _postController.reloadAllOver(),
|
||||
),
|
||||
]),
|
||||
);
|
||||
} else {
|
||||
return SigninRequiredOverlay(
|
||||
onDone: () => _postController.reloadAllOver(),
|
||||
);
|
||||
}
|
||||
}),
|
||||
PostShuffleSwiper(controller: _postController),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_postController.dispose();
|
||||
if (_subscriptions != null) {
|
||||
for (final subscription in _subscriptions!) {
|
||||
subscription.cancel();
|
||||
}
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
}
|
@ -1,283 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/controllers/post_list_controller.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/providers/navigation.dart';
|
||||
import 'package:solian/router.dart';
|
||||
import 'package:solian/screens/account/notification.dart';
|
||||
import 'package:solian/theme.dart';
|
||||
import 'package:solian/widgets/account/signin_required_overlay.dart';
|
||||
import 'package:solian/widgets/app_bar_title.dart';
|
||||
import 'package:solian/widgets/current_state_action.dart';
|
||||
import 'package:solian/widgets/app_bar_leading.dart';
|
||||
import 'package:solian/widgets/posts/post_shuffle_swiper.dart';
|
||||
import 'package:solian/widgets/posts/post_warped_list.dart';
|
||||
|
||||
class FeedScreen extends StatefulWidget {
|
||||
const FeedScreen({super.key});
|
||||
|
||||
@override
|
||||
State<FeedScreen> createState() => _FeedScreenState();
|
||||
}
|
||||
|
||||
class _FeedScreenState extends State<FeedScreen>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final PostListController _postController;
|
||||
late final TabController _tabController;
|
||||
|
||||
List<StreamSubscription>? _subscriptions;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final navState = Get.find<NavigationStateProvider>();
|
||||
_postController = PostListController();
|
||||
_postController.realm = navState.focusedRealm.value?.alias;
|
||||
_tabController = TabController(length: 3, vsync: this);
|
||||
_tabController.addListener(() {
|
||||
if (_postController.mode.value == _tabController.index) return;
|
||||
_postController.mode.value = _tabController.index;
|
||||
_postController.reloadAllOver();
|
||||
});
|
||||
_subscriptions = [
|
||||
Get.find<NavigationStateProvider>().focusedRealm.listen((value) {
|
||||
if (value?.alias != _postController.realm) {
|
||||
_postController.realm = value?.alias;
|
||||
_postController.reloadAllOver();
|
||||
}
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final AuthProvider auth = Get.find();
|
||||
final NavigationStateProvider navState = Get.find();
|
||||
|
||||
return Material(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: Scaffold(
|
||||
floatingActionButton: FloatingActionButton(
|
||||
child: const Icon(Icons.add),
|
||||
onPressed: () async {
|
||||
final value = await showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (context) => const PostCreatePopup(),
|
||||
);
|
||||
if (value is Future) {
|
||||
value.then((_) {
|
||||
_postController.reloadAllOver();
|
||||
});
|
||||
} else if (value != null) {
|
||||
_postController.reloadAllOver();
|
||||
}
|
||||
},
|
||||
),
|
||||
body: NestedScrollView(
|
||||
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
|
||||
return [
|
||||
SliverAppBar(
|
||||
title: AppBarTitle('feed'.tr),
|
||||
centerTitle: false,
|
||||
floating: true,
|
||||
toolbarHeight: AppTheme.toolbarHeight(context),
|
||||
leading: AppBarLeadingButton.adaptive(context),
|
||||
actions: [
|
||||
const BackgroundStateWidget(),
|
||||
const NotificationButton(),
|
||||
SizedBox(
|
||||
width: AppTheme.isLargeScreen(context) ? 8 : 16,
|
||||
),
|
||||
],
|
||||
bottom: TabBar(
|
||||
controller: _tabController,
|
||||
tabs: [
|
||||
Tab(text: 'postListNews'.tr),
|
||||
Tab(text: 'postListFriends'.tr),
|
||||
Tab(text: 'postListShuffle'.tr),
|
||||
],
|
||||
),
|
||||
)
|
||||
];
|
||||
},
|
||||
body: Obx(() {
|
||||
if (_postController.isPreparing.isTrue) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
if (navState.focusedRealm.value != null)
|
||||
MaterialBanner(
|
||||
leading: const Icon(Icons.layers),
|
||||
content: Text(
|
||||
'postBrowsingIn'.trParams({
|
||||
'region': '#${navState.focusedRealm.value!.alias}',
|
||||
}),
|
||||
),
|
||||
actions: const [SizedBox.shrink()],
|
||||
),
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
controller: _tabController,
|
||||
children: [
|
||||
RefreshIndicator(
|
||||
onRefresh: () => _postController.reloadAllOver(),
|
||||
child: CustomScrollView(slivers: [
|
||||
PostWarpedListWidget(
|
||||
controller: _postController.pagingController,
|
||||
onUpdate: () => _postController.reloadAllOver(),
|
||||
),
|
||||
]),
|
||||
),
|
||||
Obx(() {
|
||||
if (auth.isAuthorized.value) {
|
||||
return RefreshIndicator(
|
||||
onRefresh: () => _postController.reloadAllOver(),
|
||||
child: CustomScrollView(slivers: [
|
||||
PostWarpedListWidget(
|
||||
controller: _postController.pagingController,
|
||||
onUpdate: () => _postController.reloadAllOver(),
|
||||
),
|
||||
]),
|
||||
);
|
||||
} else {
|
||||
return SigninRequiredOverlay(
|
||||
onDone: () => _postController.reloadAllOver(),
|
||||
);
|
||||
}
|
||||
}),
|
||||
PostShuffleSwiper(controller: _postController),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_postController.dispose();
|
||||
if (_subscriptions != null) {
|
||||
for (final subscription in _subscriptions!) {
|
||||
subscription.cancel();
|
||||
}
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class PostCreatePopup extends StatelessWidget {
|
||||
final bool hideDraftBox;
|
||||
|
||||
const PostCreatePopup({
|
||||
super.key,
|
||||
this.hideDraftBox = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final AuthProvider auth = Get.find();
|
||||
|
||||
if (auth.isAuthorized.isFalse) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final List<dynamic> actionList = [
|
||||
(
|
||||
icon: const Icon(Icons.post_add),
|
||||
label: 'postEditorModeStory'.tr,
|
||||
onTap: () {
|
||||
Navigator.pop(
|
||||
context,
|
||||
AppRouter.instance.pushNamed(
|
||||
'postEditor',
|
||||
queryParameters: {
|
||||
'mode': 0.toString(),
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
(
|
||||
icon: const Icon(Icons.description),
|
||||
label: 'postEditorModeArticle'.tr,
|
||||
onTap: () {
|
||||
Navigator.pop(
|
||||
context,
|
||||
AppRouter.instance.pushNamed(
|
||||
'postEditor',
|
||||
queryParameters: {
|
||||
'mode': 1.toString(),
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
(
|
||||
icon: const Icon(Icons.drafts),
|
||||
label: 'draftBoxOpen'.tr,
|
||||
onTap: () {
|
||||
Navigator.pop(
|
||||
context,
|
||||
AppRouter.instance.pushNamed('draftBox'),
|
||||
);
|
||||
},
|
||||
),
|
||||
];
|
||||
|
||||
return SizedBox(
|
||||
height: MediaQuery.of(context).size.height * 0.38,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'postNew'.tr,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
).paddingOnly(left: 24, right: 24, top: 32, bottom: 16),
|
||||
Expanded(
|
||||
child: GridView.count(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
crossAxisCount: 3,
|
||||
children: actionList
|
||||
.map((x) => Card(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
child: InkWell(
|
||||
borderRadius:
|
||||
const BorderRadius.all(Radius.circular(8)),
|
||||
onTap: x.onTap,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
x.icon,
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
x.label,
|
||||
overflow: TextOverflow.fade,
|
||||
),
|
||||
),
|
||||
],
|
||||
).paddingAll(18),
|
||||
),
|
||||
))
|
||||
.toList(),
|
||||
).paddingSymmetric(horizontal: 20),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,114 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
import 'package:solian/models/pagination.dart';
|
||||
import 'package:solian/models/post.dart';
|
||||
import 'package:solian/providers/content/posts.dart';
|
||||
import 'package:solian/theme.dart';
|
||||
import 'package:solian/widgets/app_bar_leading.dart';
|
||||
import 'package:solian/widgets/app_bar_title.dart';
|
||||
import 'package:solian/widgets/posts/post_action.dart';
|
||||
import 'package:solian/widgets/posts/post_owned_list.dart';
|
||||
|
||||
class DraftBoxScreen extends StatefulWidget {
|
||||
const DraftBoxScreen({super.key});
|
||||
|
||||
@override
|
||||
State<DraftBoxScreen> createState() => _DraftBoxScreenState();
|
||||
}
|
||||
|
||||
class _DraftBoxScreenState extends State<DraftBoxScreen> {
|
||||
final PagingController<int, Post> _pagingController =
|
||||
PagingController(firstPageKey: 0);
|
||||
|
||||
_getPosts(int pageKey) async {
|
||||
final PostProvider provider = Get.find();
|
||||
|
||||
Response resp;
|
||||
try {
|
||||
resp = await provider.listDraft(pageKey);
|
||||
} catch (e) {
|
||||
_pagingController.error = e;
|
||||
return;
|
||||
}
|
||||
|
||||
final PaginationResult result = PaginationResult.fromJson(resp.body);
|
||||
if (result.count == 0) {
|
||||
_pagingController.appendLastPage([]);
|
||||
return;
|
||||
}
|
||||
|
||||
final parsed = result.data?.map((e) => Post.fromJson(e)).toList();
|
||||
if (parsed != null && parsed.length >= 10) {
|
||||
_pagingController.appendPage(parsed, pageKey + parsed.length);
|
||||
} else if (parsed != null) {
|
||||
_pagingController.appendLastPage(parsed);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_pagingController.addPageRequestListener(_getPosts);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: AppBarLeadingButton.adaptive(context),
|
||||
title: AppBarTitle('draftBox'.tr),
|
||||
centerTitle: false,
|
||||
toolbarHeight: AppTheme.toolbarHeight(context),
|
||||
actions: [
|
||||
SizedBox(
|
||||
width: AppTheme.isLargeScreen(context) ? 8 : 16,
|
||||
),
|
||||
],
|
||||
),
|
||||
body: RefreshIndicator(
|
||||
onRefresh: () => Future.sync(() => _pagingController.refresh()),
|
||||
child: PagedListView<int, Post>(
|
||||
pagingController: _pagingController,
|
||||
builderDelegate: PagedChildBuilderDelegate(
|
||||
itemBuilder: (context, item, index) {
|
||||
return PostOwnedListEntry(
|
||||
item: item,
|
||||
isFullContent: true,
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.surfaceContainerLow,
|
||||
onTap: () async {
|
||||
showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
context: context,
|
||||
builder: (context) => PostAction(
|
||||
item: item,
|
||||
noReact: true,
|
||||
),
|
||||
).then((value) {
|
||||
if (value is Future) {
|
||||
value.then((_) {
|
||||
_pagingController.refresh();
|
||||
});
|
||||
} else if (value != null) {
|
||||
_pagingController.refresh();
|
||||
}
|
||||
});
|
||||
},
|
||||
).paddingOnly(left: 12, right: 12, bottom: 4);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pagingController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
@ -1,99 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
import 'package:solian/models/pagination.dart';
|
||||
import 'package:solian/providers/content/posts.dart';
|
||||
import 'package:solian/widgets/posts/post_warped_list.dart';
|
||||
|
||||
import '../../models/post.dart';
|
||||
|
||||
class FeedSearchScreen extends StatefulWidget {
|
||||
final String? tag;
|
||||
final String? category;
|
||||
|
||||
const FeedSearchScreen({super.key, this.tag, this.category});
|
||||
|
||||
@override
|
||||
State<FeedSearchScreen> createState() => _FeedSearchScreenState();
|
||||
}
|
||||
|
||||
class _FeedSearchScreenState extends State<FeedSearchScreen> {
|
||||
final PagingController<int, Post> _pagingController =
|
||||
PagingController(firstPageKey: 0);
|
||||
|
||||
getPosts(int pageKey) async {
|
||||
final PostProvider provider = Get.find();
|
||||
|
||||
Response resp;
|
||||
try {
|
||||
resp = await provider.listPost(
|
||||
pageKey,
|
||||
tag: widget.tag,
|
||||
category: widget.category,
|
||||
);
|
||||
} catch (e) {
|
||||
_pagingController.error = e;
|
||||
return;
|
||||
}
|
||||
|
||||
final PaginationResult result = PaginationResult.fromJson(resp.body);
|
||||
final parsed = result.data?.map((e) => Post.fromJson(e)).toList();
|
||||
if (parsed != null && parsed.length >= 10) {
|
||||
_pagingController.appendPage(parsed, pageKey + parsed.length);
|
||||
} else if (parsed != null) {
|
||||
_pagingController.appendLastPage(parsed);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_pagingController.addPageRequestListener(getPosts);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Material(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: Column(
|
||||
children: [
|
||||
if (widget.tag != null)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.label),
|
||||
tileColor: Theme.of(context).colorScheme.surfaceContainer,
|
||||
title: Text('feedSearchWithTag'.trParams({'key': widget.tag!})),
|
||||
),
|
||||
if (widget.category != null)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.category),
|
||||
tileColor: Theme.of(context).colorScheme.surfaceContainer,
|
||||
title: Text('feedSearchWithCategory'
|
||||
.trParams({'key': widget.category!})),
|
||||
),
|
||||
Expanded(
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () => Future.sync(() => _pagingController.refresh()),
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
PostWarpedListWidget(
|
||||
controller: _pagingController,
|
||||
onUpdate: () => _pagingController.refresh(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pagingController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
128
lib/screens/posts/draft_box.dart
Normal file
128
lib/screens/posts/draft_box.dart
Normal file
@ -0,0 +1,128 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/models/pagination.dart';
|
||||
import 'package:solian/models/post.dart';
|
||||
import 'package:solian/providers/content/posts.dart';
|
||||
import 'package:solian/theme.dart';
|
||||
import 'package:solian/widgets/app_bar_leading.dart';
|
||||
import 'package:solian/widgets/app_bar_title.dart';
|
||||
import 'package:solian/widgets/loading_indicator.dart';
|
||||
import 'package:solian/widgets/posts/post_action.dart';
|
||||
import 'package:solian/widgets/posts/post_item.dart';
|
||||
import 'package:solian/widgets/root_container.dart';
|
||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||
|
||||
class DraftBoxScreen extends StatefulWidget {
|
||||
const DraftBoxScreen({super.key});
|
||||
|
||||
@override
|
||||
State<DraftBoxScreen> createState() => _DraftBoxScreenState();
|
||||
}
|
||||
|
||||
class _DraftBoxScreenState extends State<DraftBoxScreen> {
|
||||
bool _isBusy = true;
|
||||
int? _totalPosts;
|
||||
final List<Post> _posts = List.empty(growable: true);
|
||||
|
||||
_getPosts() async {
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
final PostProvider posts = Get.find();
|
||||
final resp = await posts.listDraft(_posts.length);
|
||||
|
||||
final PaginationResult result = PaginationResult.fromJson(resp.body);
|
||||
|
||||
final parsed = result.data?.map((e) => Post.fromJson(e)).toList();
|
||||
_totalPosts = result.count;
|
||||
_posts.addAll(parsed ?? List.empty());
|
||||
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
|
||||
Future<void> _openActions(Post item) async {
|
||||
showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
context: context,
|
||||
builder: (context) => PostAction(
|
||||
item: item,
|
||||
noReact: true,
|
||||
),
|
||||
).then((value) {
|
||||
if (value is Future) {
|
||||
value.then((_) {
|
||||
_posts.clear();
|
||||
_getPosts();
|
||||
});
|
||||
} else if (value != null) {
|
||||
_posts.clear();
|
||||
_getPosts();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_getPosts();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RootContainer(
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: AppBarLeadingButton.adaptive(context),
|
||||
title: AppBarTitle('draftBox'.tr),
|
||||
centerTitle: false,
|
||||
toolbarHeight: AppTheme.toolbarHeight(context),
|
||||
actions: [
|
||||
SizedBox(
|
||||
width: AppTheme.isLargeScreen(context) ? 8 : 16,
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
Expanded(
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () {
|
||||
_posts.clear();
|
||||
return _getPosts();
|
||||
},
|
||||
child: InfiniteList(
|
||||
itemCount: _posts.length,
|
||||
hasReachedMax: _totalPosts == _posts.length,
|
||||
isLoading: _isBusy,
|
||||
onFetchData: () => _getPosts(),
|
||||
itemBuilder: (context, index) {
|
||||
final item = _posts[index];
|
||||
return Card(
|
||||
child: GestureDetector(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
PostItem(
|
||||
key: Key('p${item.id}'),
|
||||
item: item,
|
||||
isShowEmbed: false,
|
||||
isClickable: false,
|
||||
isShowReply: false,
|
||||
isReactable: false,
|
||||
onTapMore: () => _openActions(item),
|
||||
).paddingSymmetric(vertical: 8),
|
||||
],
|
||||
),
|
||||
onTap: () => _openActions(item),
|
||||
),
|
||||
).paddingOnly(left: 12, right: 12, bottom: 4);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -3,6 +3,10 @@ import 'package:get/get.dart';
|
||||
import 'package:solian/exts.dart';
|
||||
import 'package:solian/models/post.dart';
|
||||
import 'package:solian/providers/content/posts.dart';
|
||||
import 'package:solian/providers/last_read.dart';
|
||||
import 'package:solian/theme.dart';
|
||||
import 'package:solian/widgets/loading_indicator.dart';
|
||||
import 'package:solian/widgets/posts/post_action.dart';
|
||||
import 'package:solian/widgets/posts/post_item.dart';
|
||||
import 'package:solian/widgets/posts/post_replies.dart';
|
||||
|
||||
@ -21,73 +25,109 @@ class PostDetailScreen extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _PostDetailScreenState extends State<PostDetailScreen> {
|
||||
Post? item;
|
||||
bool _isBusy = true;
|
||||
|
||||
Future<Post?> getDetail() async {
|
||||
if (widget.post != null) {
|
||||
item = widget.post;
|
||||
return widget.post;
|
||||
}
|
||||
Post? _item;
|
||||
|
||||
final PostProvider provider = Get.find();
|
||||
Future<void> _getDetail() async {
|
||||
final PostProvider posts = Get.find();
|
||||
|
||||
try {
|
||||
final resp = await provider.getPost(widget.id);
|
||||
item = Post.fromJson(resp.body);
|
||||
final resp = await posts.getPost(widget.id);
|
||||
_item = Post.fromJson(resp.body);
|
||||
} catch (e) {
|
||||
context.showErrorDialog(e).then((_) => Navigator.pop(context));
|
||||
}
|
||||
|
||||
return item;
|
||||
Get.find<LastReadProvider>().feedLastReadAt = _item?.id;
|
||||
|
||||
if (mounted) setState(() => _isBusy = false);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.post != null) {
|
||||
_item = widget.post;
|
||||
}
|
||||
_getDetail();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: FutureBuilder(
|
||||
future: getDetail(),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData || snapshot.data == null) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
if (_isBusy && _item == null) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: PostItem(
|
||||
item: item!,
|
||||
isClickable: false,
|
||||
isOverrideEmbedClickable: true,
|
||||
isFullDate: true,
|
||||
isFullContent: true,
|
||||
isShowReply: false,
|
||||
isContentSelectable: true,
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: LoadingIndicator(isActive: _isBusy),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: PostItem(
|
||||
key: ValueKey(_item),
|
||||
item: _item!,
|
||||
isClickable: false,
|
||||
isOverrideEmbedClickable: true,
|
||||
isFullDate: true,
|
||||
isShowReply: false,
|
||||
isContentSelectable: true,
|
||||
padding: AppTheme.isLargeScreen(context)
|
||||
? EdgeInsets.symmetric(
|
||||
horizontal: 4,
|
||||
vertical: 8,
|
||||
)
|
||||
: EdgeInsets.zero,
|
||||
onTapMore: () {
|
||||
showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
context: context,
|
||||
builder: (context) => PostAction(
|
||||
item: _item!,
|
||||
noReact: true,
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: const Divider(thickness: 0.3, height: 1)
|
||||
.paddingOnly(top: 4),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
'postReplies'.tr,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
).paddingOnly(left: 24, right: 24, top: 16),
|
||||
),
|
||||
),
|
||||
PostReplyList(item: item!),
|
||||
SliverToBoxAdapter(
|
||||
child: SizedBox(height: MediaQuery.of(context).padding.bottom),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
).then((value) {
|
||||
if (value is Future) {
|
||||
value.then((_) {
|
||||
_getDetail();
|
||||
});
|
||||
} else if (value != null) {
|
||||
_getDetail();
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: const Divider(thickness: 0.3, height: 1).paddingOnly(
|
||||
top: 8,
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
'postReplies'.tr,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
).paddingOnly(left: 24, right: 24, top: 16),
|
||||
),
|
||||
),
|
||||
PostReplyList(
|
||||
item: _item!,
|
||||
padding: AppTheme.isLargeScreen(context)
|
||||
? EdgeInsets.symmetric(
|
||||
horizontal: 4,
|
||||
vertical: 8,
|
||||
)
|
||||
: EdgeInsets.zero,
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: SizedBox(height: MediaQuery.of(context).padding.bottom),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -16,9 +16,11 @@ import 'package:solian/router.dart';
|
||||
import 'package:solian/theme.dart';
|
||||
import 'package:solian/widgets/app_bar_leading.dart';
|
||||
import 'package:solian/widgets/app_bar_title.dart';
|
||||
import 'package:solian/widgets/loading_indicator.dart';
|
||||
import 'package:solian/widgets/markdown_text_content.dart';
|
||||
import 'package:solian/widgets/posts/post_item.dart';
|
||||
import 'package:badges/badges.dart' as badges;
|
||||
import 'package:solian/widgets/root_container.dart';
|
||||
|
||||
class PostPublishArguments {
|
||||
final Post? edit;
|
||||
@ -151,8 +153,7 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
|
||||
)
|
||||
];
|
||||
|
||||
return Material(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
return RootContainer(
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: AppBarLeadingButton.adaptive(context),
|
||||
@ -182,19 +183,22 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ListTile(
|
||||
tileColor: Theme.of(context).colorScheme.surfaceContainerLow,
|
||||
title: Row(
|
||||
tileColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.surfaceContainerLow
|
||||
.withOpacity(0.5),
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
_editorController.title ?? 'title'.tr,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const Gap(6),
|
||||
if (_editorController.aliasController.text.isNotEmpty)
|
||||
Badge(
|
||||
label: Text('#${_editorController.aliasController.text}'),
|
||||
),
|
||||
).paddingOnly(bottom: 2),
|
||||
],
|
||||
),
|
||||
subtitle: Text(
|
||||
@ -271,117 +275,71 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
|
||||
),
|
||||
],
|
||||
),
|
||||
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
Expanded(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ListView(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
child: TextField(
|
||||
maxLines: null,
|
||||
autofocus: true,
|
||||
autocorrect: true,
|
||||
keyboardType: TextInputType.multiline,
|
||||
controller:
|
||||
_editorController.contentController,
|
||||
focusNode: _contentFocusNode,
|
||||
decoration: InputDecoration.collapsed(
|
||||
hintText: 'postContentPlaceholder'.tr,
|
||||
),
|
||||
onTapOutside: (_) => FocusManager
|
||||
.instance.primaryFocus
|
||||
?.unfocus(),
|
||||
),
|
||||
),
|
||||
const Gap(120)
|
||||
child: DefaultTabController(
|
||||
length: 2,
|
||||
child: AppTheme.isLargeScreen(context)
|
||||
? Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: _PostEditorTextField(
|
||||
focusNode: _contentFocusNode,
|
||||
controller: _editorController,
|
||||
onUpdate: () => setState(() {}),
|
||||
),
|
||||
),
|
||||
const VerticalDivider(width: 0.3, thickness: 0.3)
|
||||
.paddingSymmetric(horizontal: 16),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding:
|
||||
const EdgeInsets.only(top: 12, bottom: 64),
|
||||
child: MarkdownTextContent(
|
||||
isAutoWarp: _editorController.mode.value == 0,
|
||||
content:
|
||||
_editorController.contentController.text,
|
||||
parentId: 'post-editor-preview',
|
||||
).paddingOnly(right: 16),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Column(
|
||||
children: [
|
||||
TabBar(
|
||||
tabs: [
|
||||
const Tab(icon: Icon(Icons.edit)),
|
||||
const Tab(icon: Icon(Icons.preview)),
|
||||
],
|
||||
),
|
||||
),
|
||||
Obx(() {
|
||||
final textStyle = TextStyle(
|
||||
fontSize: 12,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface
|
||||
.withOpacity(0.75),
|
||||
);
|
||||
final showFactors = [
|
||||
_editorController.isRestoreFromLocal.value,
|
||||
_editorController.lastSaveTime.value != null,
|
||||
];
|
||||
final doShow = showFactors.any((x) => x);
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 4,
|
||||
horizontal: 16,
|
||||
),
|
||||
child: Row(
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
children: [
|
||||
if (showFactors[0])
|
||||
Text('postRestoreFromLocal'.tr,
|
||||
style: textStyle)
|
||||
.paddingOnly(right: 4),
|
||||
if (showFactors[0])
|
||||
InkWell(
|
||||
child: Text('clear'.tr, style: textStyle),
|
||||
onTap: () {
|
||||
_editorController.localClear();
|
||||
_editorController.currentClear();
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
if (showFactors.where((x) => x).length > 1)
|
||||
Text(
|
||||
'·',
|
||||
style: textStyle,
|
||||
).paddingSymmetric(horizontal: 8),
|
||||
if (showFactors[1])
|
||||
Text(
|
||||
'postAutoSaveAt'.trParams({
|
||||
'date': DateFormat('HH:mm:ss').format(
|
||||
_editorController.lastSaveTime.value ??
|
||||
DateTime.now(),
|
||||
)
|
||||
}),
|
||||
style: textStyle,
|
||||
_PostEditorTextField(
|
||||
focusNode: _contentFocusNode,
|
||||
controller: _editorController,
|
||||
onUpdate: () => setState(() {}),
|
||||
),
|
||||
SingleChildScrollView(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 12,
|
||||
bottom: 64,
|
||||
),
|
||||
child: MarkdownTextContent(
|
||||
isAutoWarp:
|
||||
_editorController.mode.value == 0,
|
||||
content: _editorController
|
||||
.contentController.text,
|
||||
parentId: 'post-editor-preview',
|
||||
).paddingOnly(left: 16, right: 16),
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
.animate(
|
||||
key: const Key('post-editor-hint-animation'),
|
||||
target: doShow ? 1 : 0,
|
||||
)
|
||||
.fade(curve: Curves.easeInOut, duration: 300.ms);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (AppTheme.isLargeScreen(context))
|
||||
const VerticalDivider(width: 0.3, thickness: 0.3)
|
||||
.paddingSymmetric(
|
||||
horizontal: 16,
|
||||
),
|
||||
if (AppTheme.isLargeScreen(context))
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: MarkdownTextContent(
|
||||
content: _editorController.contentController.text,
|
||||
parentId: 'post-editor-preview',
|
||||
).paddingOnly(top: 12, right: 16),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Material(
|
||||
@ -390,6 +348,26 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Divider(thickness: 0.3, height: 0.3),
|
||||
SizedBox(
|
||||
height: 40,
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: MarkdownToolbar(
|
||||
width: 38,
|
||||
height: 38,
|
||||
iconSize: 20,
|
||||
spacing: 8,
|
||||
hideImage: true,
|
||||
useIncludedTextField: false,
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
iconColor: Theme.of(context).colorScheme.onSurface,
|
||||
controller: _editorController.contentController,
|
||||
focusNode: _contentFocusNode,
|
||||
borderRadius:
|
||||
const BorderRadius.all(Radius.circular(20)),
|
||||
).paddingSymmetric(horizontal: 12),
|
||||
),
|
||||
).paddingOnly(top: 12),
|
||||
SizedBox(
|
||||
height: 56,
|
||||
child: ListView(
|
||||
@ -519,7 +497,7 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
|
||||
top: -4,
|
||||
end: -6,
|
||||
),
|
||||
child: const Icon(Icons.preview),
|
||||
child: const Icon(Icons.wallpaper),
|
||||
);
|
||||
}),
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
@ -546,18 +524,6 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
|
||||
_editorController.editPublishDate(context);
|
||||
},
|
||||
),
|
||||
MarkdownToolbar(
|
||||
hideImage: true,
|
||||
useIncludedTextField: false,
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.surface,
|
||||
iconColor: Theme.of(context).colorScheme.onSurface,
|
||||
controller: _editorController.contentController,
|
||||
focusNode: _contentFocusNode,
|
||||
borderRadius:
|
||||
const BorderRadius.all(Radius.circular(20)),
|
||||
width: 40,
|
||||
).paddingSymmetric(horizontal: 2),
|
||||
],
|
||||
).paddingSymmetric(horizontal: 6, vertical: 8),
|
||||
),
|
||||
@ -577,3 +543,101 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class _PostEditorTextField extends StatelessWidget {
|
||||
final FocusNode focusNode;
|
||||
final PostEditorController controller;
|
||||
final Function onUpdate;
|
||||
|
||||
const _PostEditorTextField({
|
||||
required this.focusNode,
|
||||
required this.controller,
|
||||
required this.onUpdate,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ListView(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
child: TextField(
|
||||
maxLines: null,
|
||||
autofocus: true,
|
||||
autocorrect: true,
|
||||
keyboardType: TextInputType.multiline,
|
||||
controller: controller.contentController,
|
||||
focusNode: focusNode,
|
||||
decoration: InputDecoration.collapsed(
|
||||
hintText: 'postContentPlaceholder'.tr,
|
||||
),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
),
|
||||
const Gap(120)
|
||||
],
|
||||
),
|
||||
),
|
||||
Obx(() {
|
||||
final textStyle = TextStyle(
|
||||
fontSize: 12,
|
||||
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.75),
|
||||
);
|
||||
final showFactors = [
|
||||
controller.isRestoreFromLocal.value,
|
||||
controller.lastSaveTime.value != null,
|
||||
];
|
||||
final doShow = showFactors.any((x) => x);
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 4,
|
||||
horizontal: 16,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
if (showFactors[0])
|
||||
Text('postRestoreFromLocal'.tr, style: textStyle)
|
||||
.paddingOnly(right: 4),
|
||||
if (showFactors[0])
|
||||
InkWell(
|
||||
child: Text('clear'.tr, style: textStyle),
|
||||
onTap: () {
|
||||
controller.localClear();
|
||||
controller.currentClear();
|
||||
onUpdate();
|
||||
},
|
||||
),
|
||||
if (showFactors.where((x) => x).length > 1)
|
||||
Text(
|
||||
'·',
|
||||
style: textStyle,
|
||||
).paddingSymmetric(horizontal: 8),
|
||||
if (showFactors[1])
|
||||
Text(
|
||||
'postAutoSaveAt'.trParams({
|
||||
'date': DateFormat('HH:mm:ss').format(
|
||||
controller.lastSaveTime.value ?? DateTime.now(),
|
||||
)
|
||||
}),
|
||||
style: textStyle,
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
.animate(
|
||||
key: const Key('post-editor-hint-animation'),
|
||||
target: doShow ? 1 : 0,
|
||||
)
|
||||
.fade(curve: Curves.easeInOut, duration: 300.ms);
|
||||
}),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
206
lib/screens/posts/post_search.dart
Normal file
206
lib/screens/posts/post_search.dart
Normal file
@ -0,0 +1,206 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
import 'package:solian/models/pagination.dart';
|
||||
import 'package:solian/providers/content/posts.dart';
|
||||
import 'package:solian/widgets/loading_indicator.dart';
|
||||
import 'package:solian/widgets/posts/post_list.dart';
|
||||
|
||||
import '../../models/post.dart';
|
||||
|
||||
class PostSearchScreen extends StatefulWidget {
|
||||
final String? tag;
|
||||
final String? category;
|
||||
|
||||
const PostSearchScreen({super.key, this.tag, this.category});
|
||||
|
||||
@override
|
||||
State<PostSearchScreen> createState() => _PostSearchScreenState();
|
||||
}
|
||||
|
||||
class _PostSearchScreenState extends State<PostSearchScreen> {
|
||||
int? _totalCount;
|
||||
Duration? _lastTook;
|
||||
|
||||
final TextEditingController _probeController = TextEditingController();
|
||||
final PagingController<int, Post> _pagingController =
|
||||
PagingController(firstPageKey: 0);
|
||||
|
||||
late bool _isBusy = widget.tag != null || widget.category != null;
|
||||
|
||||
_searchPosts(int pageKey) async {
|
||||
if (widget.tag == null &&
|
||||
widget.category == null &&
|
||||
_probeController.text.isEmpty) {
|
||||
_pagingController.appendLastPage([]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_isBusy) {
|
||||
setState(() => _isBusy = true);
|
||||
}
|
||||
|
||||
if (pageKey == 0) {
|
||||
_pagingController.itemList?.clear();
|
||||
_pagingController.nextPageKey = 0;
|
||||
}
|
||||
|
||||
final PostProvider posts = Get.find();
|
||||
|
||||
Stopwatch stopwatch = new Stopwatch()..start();
|
||||
|
||||
Response resp;
|
||||
try {
|
||||
if (_probeController.text.isEmpty) {
|
||||
resp = await posts.listPost(
|
||||
pageKey,
|
||||
tag: widget.tag,
|
||||
category: widget.category,
|
||||
);
|
||||
} else {
|
||||
resp = await posts.searchPost(
|
||||
_probeController.text,
|
||||
pageKey,
|
||||
tag: widget.tag,
|
||||
category: widget.category,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
_pagingController.error = e;
|
||||
return;
|
||||
}
|
||||
|
||||
final PaginationResult result = PaginationResult.fromJson(resp.body);
|
||||
final parsed = result.data?.map((e) => Post.fromJson(e)).toList();
|
||||
if (parsed != null && parsed.length >= 10) {
|
||||
_pagingController.appendPage(parsed, pageKey + parsed.length);
|
||||
} else if (parsed != null) {
|
||||
_pagingController.appendLastPage(parsed);
|
||||
}
|
||||
|
||||
stopwatch.stop();
|
||||
|
||||
_totalCount = result.count;
|
||||
_lastTook = stopwatch.elapsed;
|
||||
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_pagingController.addPageRequestListener(_searchPosts);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_probeController.dispose();
|
||||
_pagingController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Color get _unFocusColor =>
|
||||
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Column(
|
||||
children: [
|
||||
if (widget.tag != null)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.label),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
tileColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.surfaceContainer
|
||||
.withOpacity(0.5),
|
||||
title: Text('postSearchWithTag'.trParams({'key': widget.tag!})),
|
||||
),
|
||||
if (widget.category != null)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.category),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
tileColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.surfaceContainer
|
||||
.withOpacity(0.5),
|
||||
title: Text('postSearchWithCategory'.trParams({
|
||||
'key': widget.category!,
|
||||
})),
|
||||
),
|
||||
Container(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.secondaryContainer
|
||||
.withOpacity(0.5),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||
child: TextField(
|
||||
controller: _probeController,
|
||||
decoration: InputDecoration(
|
||||
isCollapsed: true,
|
||||
border: InputBorder.none,
|
||||
hintText: 'search'.tr,
|
||||
),
|
||||
onSubmitted: (_) {
|
||||
_searchPosts(0);
|
||||
},
|
||||
),
|
||||
),
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
if (_totalCount != null || _lastTook != null)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.summarize_outlined,
|
||||
size: 16,
|
||||
color: _unFocusColor,
|
||||
),
|
||||
const Gap(4),
|
||||
if (_totalCount != null)
|
||||
Text(
|
||||
'searchResult'.trParams({
|
||||
'count': _totalCount!.toString(),
|
||||
}),
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: _unFocusColor,
|
||||
),
|
||||
),
|
||||
const Gap(4),
|
||||
if (_lastTook != null)
|
||||
Text(
|
||||
'searchTook'.trParams({
|
||||
'time':
|
||||
'${(_lastTook!.inMilliseconds / 1000).toStringAsFixed(3)}s',
|
||||
}),
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: _unFocusColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () => Future.sync(() => _pagingController.refresh()),
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
ControlledPostListWidget(
|
||||
controller: _pagingController,
|
||||
onUpdate: () => _pagingController.refresh(),
|
||||
),
|
||||
SliverGap(MediaQuery.of(context).padding.bottom),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/models/realm.dart';
|
||||
@ -7,11 +6,16 @@ import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/providers/content/realm.dart';
|
||||
import 'package:solian/router.dart';
|
||||
import 'package:solian/screens/account/notification.dart';
|
||||
import 'package:solian/services.dart';
|
||||
import 'package:solian/theme.dart';
|
||||
import 'package:solian/widgets/account/account_avatar.dart';
|
||||
import 'package:solian/widgets/account/signin_required_overlay.dart';
|
||||
import 'package:solian/widgets/app_bar_leading.dart';
|
||||
import 'package:solian/widgets/app_bar_title.dart';
|
||||
import 'package:solian/widgets/auto_cache_image.dart';
|
||||
import 'package:solian/widgets/current_state_action.dart';
|
||||
import 'package:solian/widgets/loading_indicator.dart';
|
||||
import 'package:solian/widgets/root_container.dart';
|
||||
import 'package:solian/widgets/sized_container.dart';
|
||||
|
||||
class RealmListScreen extends StatefulWidget {
|
||||
@ -55,8 +59,7 @@ class _RealmListScreenState extends State<RealmListScreen> {
|
||||
Widget build(BuildContext context) {
|
||||
final AuthProvider auth = Get.find();
|
||||
|
||||
return Material(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
return RootContainer(
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: AppBarLeadingButton.adaptive(context),
|
||||
@ -90,12 +93,13 @@ class _RealmListScreenState extends State<RealmListScreen> {
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
Expanded(
|
||||
child: CenteredContainer(
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () => _getRealms(),
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
itemCount: _realms.length,
|
||||
itemBuilder: (context, index) {
|
||||
final element = _realms[index];
|
||||
@ -128,19 +132,34 @@ class _RealmListScreenState extends State<RealmListScreen> {
|
||||
children: [
|
||||
Container(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
child: (element.banner?.isEmpty ?? true)
|
||||
? const SizedBox.shrink()
|
||||
: AutoCacheImage(
|
||||
ServiceFinder.buildUrl(
|
||||
'uc',
|
||||
'/attachments/${element.banner}',
|
||||
),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
const Positioned(
|
||||
Positioned(
|
||||
bottom: -30,
|
||||
left: 18,
|
||||
child: CircleAvatar(
|
||||
radius: 24,
|
||||
backgroundColor: Colors.indigo,
|
||||
child: FaIcon(
|
||||
FontAwesomeIcons.globe,
|
||||
color: Colors.white,
|
||||
size: 18,
|
||||
),
|
||||
),
|
||||
child: (element.avatar?.isEmpty ?? true)
|
||||
? CircleAvatar(
|
||||
radius: 24,
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.primary,
|
||||
child: const FaIcon(
|
||||
FontAwesomeIcons.globe,
|
||||
color: Colors.white,
|
||||
size: 18,
|
||||
),
|
||||
)
|
||||
: AttachedCircleAvatar(
|
||||
content: element.avatar!,
|
||||
bgColor: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -7,6 +7,7 @@ import 'package:solian/router.dart';
|
||||
import 'package:solian/screens/realms/realm_organize.dart';
|
||||
import 'package:solian/widgets/realms/realm_deletion.dart';
|
||||
import 'package:solian/widgets/realms/realm_member.dart';
|
||||
import 'package:solian/widgets/root_container.dart';
|
||||
|
||||
class RealmDetailScreen extends StatefulWidget {
|
||||
final String alias;
|
||||
@ -69,7 +70,8 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> {
|
||||
ListTile(
|
||||
leading: const Icon(Icons.settings),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
title: Text('realmSettings'.tr.capitalize!),
|
||||
title: Text('realmSettings'.tr),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
onTap: () async {
|
||||
AppRouter.instance
|
||||
.pushNamed(
|
||||
@ -85,59 +87,63 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> {
|
||||
),
|
||||
];
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
const CircleAvatar(
|
||||
radius: 28,
|
||||
backgroundColor: Colors.teal,
|
||||
child: Icon(Icons.group, color: Colors.white),
|
||||
),
|
||||
const Gap(16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(widget.realm.name,
|
||||
style: Theme.of(context).textTheme.bodyLarge),
|
||||
Text(widget.realm.description,
|
||||
style: Theme.of(context).textTheme.bodySmall),
|
||||
Text(
|
||||
'#${widget.realm.id.toString().padLeft(8, '0')} · ${widget.realm.alias}',
|
||||
style: const TextStyle(fontSize: 11),
|
||||
),
|
||||
],
|
||||
return RootContainer(
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
const CircleAvatar(
|
||||
radius: 28,
|
||||
backgroundColor: Colors.teal,
|
||||
child: Icon(Icons.group, color: Colors.white),
|
||||
),
|
||||
)
|
||||
],
|
||||
const Gap(16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(widget.realm.name,
|
||||
style: Theme.of(context).textTheme.bodyLarge),
|
||||
Text(widget.realm.description,
|
||||
style: Theme.of(context).textTheme.bodySmall),
|
||||
Text(
|
||||
'#${widget.realm.id.toString().padLeft(8, '0')} · ${widget.realm.alias}',
|
||||
style: const TextStyle(fontSize: 11),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const Divider(thickness: 0.3),
|
||||
Expanded(
|
||||
child: ListView(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.supervisor_account),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
title: Text('realmMembers'.tr.capitalize!),
|
||||
onTap: () => showMemberList(),
|
||||
),
|
||||
...(_isOwned ? ownerActions : List.empty()),
|
||||
const Divider(thickness: 0.3),
|
||||
ListTile(
|
||||
leading: _isOwned
|
||||
? const Icon(Icons.delete)
|
||||
: const Icon(Icons.exit_to_app),
|
||||
title: Text(_isOwned ? 'delete'.tr : 'leave'.tr),
|
||||
onTap: () => promptLeaveChannel(),
|
||||
),
|
||||
],
|
||||
const Divider(thickness: 0.3),
|
||||
Expanded(
|
||||
child: ListView(
|
||||
children: [
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Icons.supervisor_account),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
title: Text('realmMembers'.tr),
|
||||
onTap: () => showMemberList(),
|
||||
),
|
||||
...(_isOwned ? ownerActions : List.empty()),
|
||||
const Divider(thickness: 0.3),
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: _isOwned
|
||||
? const Icon(Icons.delete)
|
||||
: const Icon(Icons.exit_to_app),
|
||||
title: Text(_isOwned ? 'delete'.tr : 'leave'.tr),
|
||||
onTap: () => promptLeaveChannel(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +1,19 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:image_cropper/image_cropper.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:solian/exts.dart';
|
||||
import 'package:solian/models/attachment.dart';
|
||||
import 'package:solian/models/realm.dart';
|
||||
import 'package:solian/platform.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/providers/content/attachment.dart';
|
||||
import 'package:solian/router.dart';
|
||||
import 'package:solian/theme.dart';
|
||||
import 'package:solian/widgets/app_bar_leading.dart';
|
||||
import 'package:solian/widgets/app_bar_title.dart';
|
||||
import 'package:solian/widgets/loading_indicator.dart';
|
||||
import 'package:solian/widgets/root_container.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class RealmOrganizeArguments {
|
||||
@ -29,17 +35,19 @@ class _RealmOrganizeScreenState extends State<RealmOrganizeScreen> {
|
||||
bool _isBusy = false;
|
||||
|
||||
final _aliasController = TextEditingController();
|
||||
final _avatarController = TextEditingController();
|
||||
final _bannerController = TextEditingController();
|
||||
final _nameController = TextEditingController();
|
||||
final _descriptionController = TextEditingController();
|
||||
|
||||
bool _isCommunity = false;
|
||||
bool _isPublic = false;
|
||||
|
||||
void applyRealm() async {
|
||||
void _applyRealm() async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) return;
|
||||
|
||||
if (_aliasController.value.text.isEmpty) randomizeAlias();
|
||||
if (_aliasController.value.text.isEmpty) _randomizeAlias();
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
@ -49,6 +57,8 @@ class _RealmOrganizeScreenState extends State<RealmOrganizeScreen> {
|
||||
'alias': _aliasController.value.text.toLowerCase(),
|
||||
'name': _nameController.value.text,
|
||||
'description': _descriptionController.value.text,
|
||||
'avatar': _avatarController.value.text,
|
||||
'banner': _bannerController.value.text,
|
||||
'is_public': _isPublic,
|
||||
'is_community': _isCommunity,
|
||||
};
|
||||
@ -68,35 +78,119 @@ class _RealmOrganizeScreenState extends State<RealmOrganizeScreen> {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
|
||||
void randomizeAlias() {
|
||||
final _imagePicker = ImagePicker();
|
||||
|
||||
Future<void> _editImage(String position) async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) return;
|
||||
|
||||
XFile file;
|
||||
|
||||
final image = await _imagePicker.pickImage(source: ImageSource.gallery);
|
||||
if (image == null) return;
|
||||
|
||||
if (PlatformInfo.canCropImage) {
|
||||
CroppedFile? croppedFile = await ImageCropper().cropImage(
|
||||
sourcePath: image.path,
|
||||
uiSettings: [
|
||||
AndroidUiSettings(
|
||||
toolbarTitle: 'cropImage'.tr,
|
||||
toolbarColor: Theme.of(context).colorScheme.primary,
|
||||
toolbarWidgetColor: Theme.of(context).colorScheme.onPrimary,
|
||||
aspectRatioPresets: [
|
||||
if (position == 'avatar') CropAspectRatioPreset.square,
|
||||
if (position == 'banner') _BannerCropAspectRatioPreset(),
|
||||
],
|
||||
),
|
||||
IOSUiSettings(
|
||||
title: 'cropImage'.tr,
|
||||
aspectRatioPresets: [
|
||||
if (position == 'avatar') CropAspectRatioPreset.square,
|
||||
if (position == 'banner') _BannerCropAspectRatioPreset(),
|
||||
],
|
||||
),
|
||||
WebUiSettings(
|
||||
context: context,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
if (croppedFile == null) return;
|
||||
file = XFile(croppedFile.path);
|
||||
} else {
|
||||
file = XFile(image.path);
|
||||
}
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
final AttachmentProvider attach = Get.find();
|
||||
|
||||
Attachment? attachResult;
|
||||
try {
|
||||
attachResult = await attach.createAttachmentDirectly(
|
||||
await file.readAsBytes(),
|
||||
file.path,
|
||||
'avatar',
|
||||
null,
|
||||
);
|
||||
} catch (e) {
|
||||
setState(() => _isBusy = false);
|
||||
context.showErrorDialog(e);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (position) {
|
||||
case 'avatar':
|
||||
_avatarController.text = attachResult.rid;
|
||||
break;
|
||||
case 'banner':
|
||||
_bannerController.text = attachResult.rid;
|
||||
break;
|
||||
}
|
||||
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
|
||||
void _randomizeAlias() {
|
||||
_aliasController.text =
|
||||
const Uuid().v4().replaceAll('-', '').substring(0, 12);
|
||||
}
|
||||
|
||||
void syncWidget() {
|
||||
void _syncWidget() {
|
||||
if (widget.edit != null) {
|
||||
_aliasController.text = widget.edit!.alias;
|
||||
_nameController.text = widget.edit!.name;
|
||||
_descriptionController.text = widget.edit!.description;
|
||||
_avatarController.text = widget.edit!.avatar ?? '';
|
||||
_bannerController.text = widget.edit!.banner ?? '';
|
||||
_isPublic = widget.edit!.isPublic;
|
||||
_isCommunity = widget.edit!.isCommunity;
|
||||
}
|
||||
}
|
||||
|
||||
void cancelAction() {
|
||||
void _cancelAction() {
|
||||
AppRouter.instance.pop();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
syncWidget();
|
||||
_syncWidget();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_aliasController.dispose();
|
||||
_avatarController.dispose();
|
||||
_bannerController.dispose();
|
||||
_nameController.dispose();
|
||||
_descriptionController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
return RootContainer(
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: AppBarLeadingButton.adaptive(context),
|
||||
@ -105,7 +199,7 @@ class _RealmOrganizeScreenState extends State<RealmOrganizeScreen> {
|
||||
toolbarHeight: AppTheme.toolbarHeight(context),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: _isBusy ? null : () => applyRealm(),
|
||||
onPressed: _isBusy ? null : () => _applyRealm(),
|
||||
child: Text('apply'.tr.toUpperCase()),
|
||||
)
|
||||
],
|
||||
@ -114,7 +208,7 @@ class _RealmOrganizeScreenState extends State<RealmOrganizeScreen> {
|
||||
top: false,
|
||||
child: Column(
|
||||
children: [
|
||||
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
if (widget.edit != null)
|
||||
MaterialBanner(
|
||||
leading: const Icon(Icons.edit),
|
||||
@ -126,7 +220,7 @@ class _RealmOrganizeScreenState extends State<RealmOrganizeScreen> {
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: cancelAction,
|
||||
onPressed: _cancelAction,
|
||||
child: Text('cancel'.tr),
|
||||
),
|
||||
],
|
||||
@ -150,7 +244,7 @@ class _RealmOrganizeScreenState extends State<RealmOrganizeScreen> {
|
||||
visualDensity:
|
||||
const VisualDensity(horizontal: -2, vertical: -2),
|
||||
),
|
||||
onPressed: () => randomizeAlias(),
|
||||
onPressed: () => _randomizeAlias(),
|
||||
child: const Icon(Icons.refresh),
|
||||
)
|
||||
],
|
||||
@ -166,6 +260,55 @@ class _RealmOrganizeScreenState extends State<RealmOrganizeScreen> {
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
).paddingSymmetric(horizontal: 16, vertical: 8),
|
||||
const Divider(thickness: 0.3),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
autofocus: true,
|
||||
controller: _avatarController,
|
||||
decoration: InputDecoration.collapsed(
|
||||
hintText: 'realmAvatar'.tr,
|
||||
),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
shape: const CircleBorder(),
|
||||
visualDensity:
|
||||
const VisualDensity(horizontal: -2, vertical: -2),
|
||||
),
|
||||
onPressed: _isBusy ? null : () => _editImage('avatar'),
|
||||
child: const Icon(Icons.upload),
|
||||
)
|
||||
],
|
||||
).paddingSymmetric(horizontal: 16, vertical: 2),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
autofocus: true,
|
||||
controller: _bannerController,
|
||||
decoration: InputDecoration.collapsed(
|
||||
hintText: 'realmBanner'.tr,
|
||||
),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
shape: const CircleBorder(),
|
||||
visualDensity:
|
||||
const VisualDensity(horizontal: -2, vertical: -2),
|
||||
),
|
||||
onPressed: _isBusy ? null : () => _editImage('banner'),
|
||||
child: const Icon(Icons.upload),
|
||||
)
|
||||
],
|
||||
).paddingSymmetric(horizontal: 16, vertical: 2),
|
||||
const Divider(thickness: 0.3),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
minLines: 5,
|
||||
@ -202,3 +345,11 @@ class _RealmOrganizeScreenState extends State<RealmOrganizeScreen> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _BannerCropAspectRatioPreset extends CropAspectRatioPresetData {
|
||||
@override
|
||||
(int, int)? get data => (16, 7);
|
||||
|
||||
@override
|
||||
String get name => '16x7';
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ import 'package:solian/theme.dart';
|
||||
import 'package:solian/widgets/app_bar_leading.dart';
|
||||
import 'package:solian/widgets/channel/channel_list.dart';
|
||||
import 'package:solian/widgets/posts/post_list.dart';
|
||||
import 'package:solian/widgets/root_container.dart';
|
||||
|
||||
class RealmViewScreen extends StatefulWidget {
|
||||
final String alias;
|
||||
@ -34,7 +35,7 @@ class _RealmViewScreenState extends State<RealmViewScreen> {
|
||||
final List<Channel> _channels = List.empty(growable: true);
|
||||
|
||||
getRealm({String? overrideAlias}) async {
|
||||
final RealmProvider provider = Get.find();
|
||||
final RealmProvider realm = Get.find();
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
@ -43,7 +44,7 @@ class _RealmViewScreenState extends State<RealmViewScreen> {
|
||||
}
|
||||
|
||||
try {
|
||||
final resp = await provider.getRealm(_overrideAlias ?? widget.alias);
|
||||
final resp = await realm.getRealm(_overrideAlias ?? widget.alias);
|
||||
setState(() => _realm = Realm.fromJson(resp.body));
|
||||
} catch (e) {
|
||||
context.showErrorDialog(e);
|
||||
@ -55,14 +56,21 @@ class _RealmViewScreenState extends State<RealmViewScreen> {
|
||||
getChannels() async {
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
final ChannelProvider provider = Get.find();
|
||||
final resp = await provider.listChannel(scope: _realm!.alias);
|
||||
final ChannelProvider channel = Get.find();
|
||||
final resp = await channel.listChannel(scope: _realm!.alias);
|
||||
final availableResp = await channel.listAvailableChannel(
|
||||
scope: _realm!.alias,
|
||||
);
|
||||
|
||||
final Set<int> channelIdx = {};
|
||||
|
||||
setState(() {
|
||||
_channels.clear();
|
||||
_channels.addAll(
|
||||
resp.body.map((e) => Channel.fromJson(e)).toList().cast<Channel>(),
|
||||
);
|
||||
_channels.addAll(availableResp);
|
||||
_channels.retainWhere((x) => channelIdx.add(x.id));
|
||||
});
|
||||
|
||||
setState(() => _isBusy = false);
|
||||
@ -79,8 +87,7 @@ class _RealmViewScreenState extends State<RealmViewScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
return RootContainer(
|
||||
child: DefaultTabController(
|
||||
length: 2,
|
||||
child: NestedScrollView(
|
||||
@ -248,7 +255,6 @@ class RealmChannelListWidget extends StatelessWidget {
|
||||
child: ChannelListWidget(
|
||||
channels: channels,
|
||||
selfId: auth.userProfile.value!['id'],
|
||||
noCategory: true,
|
||||
),
|
||||
)
|
||||
],
|
||||
|
@ -1,13 +1,24 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:in_app_review/in_app_review.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:solian/exceptions/request.dart';
|
||||
import 'package:solian/exts.dart';
|
||||
import 'package:solian/models/theme.dart';
|
||||
import 'package:solian/platform.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/providers/database/database.dart';
|
||||
import 'package:solian/providers/theme_switcher.dart';
|
||||
import 'package:solian/router.dart';
|
||||
import 'package:solian/theme.dart';
|
||||
import 'package:solian/widgets/reports/abuse_report.dart';
|
||||
|
||||
class SettingScreen extends StatefulWidget {
|
||||
const SettingScreen({super.key});
|
||||
@ -18,6 +29,7 @@ class SettingScreen extends StatefulWidget {
|
||||
|
||||
class _SettingScreenState extends State<SettingScreen> {
|
||||
SharedPreferences? _prefs;
|
||||
String _docBasepath = '/';
|
||||
|
||||
Widget _buildCaptionHeader(String title) {
|
||||
return Container(
|
||||
@ -28,39 +40,38 @@ class _SettingScreenState extends State<SettingScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildThemeColorButton(String label, Color color) {
|
||||
return IconButton(
|
||||
icon: Icon(Icons.circle, color: color),
|
||||
tooltip: label,
|
||||
onPressed: () {
|
||||
context.read<ThemeSwitcher>().setTheme(
|
||||
AppTheme.build(
|
||||
Brightness.light,
|
||||
seedColor: color,
|
||||
),
|
||||
AppTheme.build(
|
||||
Brightness.dark,
|
||||
seedColor: color,
|
||||
),
|
||||
);
|
||||
_prefs?.setInt('global_theme_color', color.value);
|
||||
context.clearSnackbar();
|
||||
context.showSnackbar('themeColorApplied'.tr);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
static final List<(String, Color)> _presentTheme = [
|
||||
('themeColorRed', const Color.fromRGBO(154, 98, 91, 1)),
|
||||
('themeColorBlue', const Color.fromRGBO(103, 96, 193, 1)),
|
||||
('themeColorMiku', const Color.fromRGBO(56, 120, 126, 1)),
|
||||
('themeColorKagamine', const Color.fromRGBO(244, 183, 63, 1)),
|
||||
('themeColorLuka', const Color.fromRGBO(243, 174, 218, 1)),
|
||||
static final List<SolianThemeData> _presentTheme = [
|
||||
SolianThemeData(
|
||||
id: 'themeColorRed',
|
||||
seedColor: const Color.fromRGBO(154, 98, 91, 1),
|
||||
),
|
||||
SolianThemeData(
|
||||
id: 'themeColorBlue',
|
||||
seedColor: const Color.fromRGBO(103, 96, 193, 1),
|
||||
),
|
||||
SolianThemeData(
|
||||
id: 'themeColorMiku',
|
||||
seedColor: const Color.fromRGBO(56, 120, 126, 1),
|
||||
),
|
||||
SolianThemeData(
|
||||
id: 'themeColorKagamine',
|
||||
seedColor: const Color.fromRGBO(244, 183, 63, 1),
|
||||
),
|
||||
SolianThemeData(
|
||||
id: 'themeColorLuka',
|
||||
seedColor: const Color.fromRGBO(243, 174, 218, 1),
|
||||
),
|
||||
];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
getApplicationDocumentsDirectory().then((dir) {
|
||||
_docBasepath = dir.path;
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
SharedPreferences.getInstance().then((inst) {
|
||||
_prefs = inst;
|
||||
if (mounted) {
|
||||
@ -71,85 +82,258 @@ class _SettingScreenState extends State<SettingScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: ListView(
|
||||
children: [
|
||||
_buildCaptionHeader('themeColor'.tr),
|
||||
SizedBox(
|
||||
height: 56,
|
||||
child: ListView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
children: _presentTheme
|
||||
.map((x) => _buildThemeColorButton(x.$1, x.$2))
|
||||
.toList(),
|
||||
).paddingSymmetric(horizontal: 12, vertical: 8),
|
||||
),
|
||||
_buildCaptionHeader('notification'.tr),
|
||||
Tooltip(
|
||||
message: 'settingsNotificationBgServiceDesc'.tr,
|
||||
child: CheckboxListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
|
||||
secondary: const Icon(Icons.system_security_update_warning),
|
||||
enabled: PlatformInfo.isAndroid,
|
||||
title: Text('settingsNotificationBgService'.tr),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('holdToSeeDetail'.tr),
|
||||
Text(
|
||||
'needRestartToApply'.tr,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
)
|
||||
],
|
||||
return ListView(
|
||||
children: [
|
||||
_buildCaptionHeader('theme'.tr),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.palette),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
|
||||
title: Text('globalTheme'.tr),
|
||||
trailing: DropdownButtonHideUnderline(
|
||||
child: DropdownButton2<SolianThemeData>(
|
||||
isExpanded: true,
|
||||
hint: Text(
|
||||
'theme'.tr,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
),
|
||||
value:
|
||||
_prefs?.getBool('service_background_notification') ?? false,
|
||||
onChanged: (value) {
|
||||
_prefs
|
||||
?.setBool('service_background_notification', value ?? false)
|
||||
.then((_) {
|
||||
setState(() {});
|
||||
});
|
||||
items: _presentTheme
|
||||
.map((SolianThemeData item) =>
|
||||
DropdownMenuItem<SolianThemeData>(
|
||||
value: item,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.circle, color: item.seedColor),
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
item.id.tr,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
))
|
||||
.toList(),
|
||||
value: (_prefs?.containsKey('global_theme') ?? false)
|
||||
? SolianThemeData.fromJson(
|
||||
jsonDecode(_prefs!.getString('global_theme')!),
|
||||
)
|
||||
: null,
|
||||
onChanged: (SolianThemeData? value) {
|
||||
context.read<ThemeSwitcher>().setThemeData(value);
|
||||
setState(() {});
|
||||
},
|
||||
buttonStyleData: const ButtonStyleData(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8),
|
||||
height: 40,
|
||||
width: 140,
|
||||
),
|
||||
menuItemStyleData: const MenuItemStyleData(
|
||||
height: 40,
|
||||
),
|
||||
),
|
||||
),
|
||||
_buildCaptionHeader('more'.tr),
|
||||
),
|
||||
CheckboxListTile(
|
||||
secondary: const Icon(Icons.military_tech),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
|
||||
title: Text('agedTheme'.tr),
|
||||
subtitle: Text('agedThemeDesc'.tr),
|
||||
value: _prefs?.getBool('aged_theme') ?? false,
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
context.read<ThemeSwitcher>().setAgedTheme(value);
|
||||
}
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
if (!PlatformInfo.isWeb)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.delete_sweep),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
subtitle: FutureBuilder(
|
||||
future: AppDatabase.getDatabaseSize(),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return Text('localDatabaseSize'.trParams(
|
||||
{'size': 'unknown'.tr},
|
||||
));
|
||||
}
|
||||
return Text('localDatabaseSize'.trParams(
|
||||
{'size': snapshot.data!.formatBytes()},
|
||||
));
|
||||
},
|
||||
),
|
||||
leading: const Icon(Icons.wallpaper),
|
||||
contentPadding: const EdgeInsets.only(left: 22, right: 31),
|
||||
title: Text('appBackgroundImage'.tr),
|
||||
subtitle: Text('appBackgroundImageDesc'.tr),
|
||||
trailing: File('$_docBasepath/app_background_image').existsSync()
|
||||
? const Icon(Icons.check_box)
|
||||
: const Icon(Icons.check_box_outline_blank),
|
||||
onTap: () async {
|
||||
if (File('$_docBasepath/app_background_image').existsSync()) {
|
||||
File('$_docBasepath/app_background_image').deleteSync();
|
||||
} else {
|
||||
final image = await ImagePicker().pickImage(
|
||||
source: ImageSource.gallery,
|
||||
);
|
||||
if (image == null) return;
|
||||
|
||||
await File(image.path)
|
||||
.copy('$_docBasepath/app_background_image');
|
||||
}
|
||||
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
_buildCaptionHeader('notification'.tr),
|
||||
Tooltip(
|
||||
message: 'settingsNotificationBgServiceDesc'.tr,
|
||||
child: CheckboxListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
|
||||
title: Text('localDatabaseWipe'.tr),
|
||||
onTap: () {
|
||||
AppDatabase.removeDatabase().then((_) {
|
||||
secondary: const Icon(Icons.system_security_update_warning),
|
||||
enabled: PlatformInfo.isAndroid,
|
||||
title: Text('settingsNotificationBgService'.tr),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('holdToSeeDetail'.tr),
|
||||
Text(
|
||||
'needRestartToApply'.tr,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
)
|
||||
],
|
||||
),
|
||||
value: _prefs?.getBool('service_background_notification') ?? false,
|
||||
onChanged: (value) {
|
||||
_prefs
|
||||
?.setBool('service_background_notification', value ?? false)
|
||||
.then((_) {
|
||||
setState(() {});
|
||||
});
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.info_outline),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
|
||||
title: Text('about'.tr),
|
||||
onTap: () {
|
||||
AppRouter.instance.pushNamed('about');
|
||||
),
|
||||
_buildCaptionHeader('update'.tr),
|
||||
CheckboxListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
|
||||
secondary: const Icon(Icons.sync_alt),
|
||||
title: Text('updateCheckStrictly'.tr),
|
||||
subtitle: Text('updateCheckStrictlyDesc'.tr),
|
||||
value: _prefs?.getBool('check_update_strictly') ?? false,
|
||||
onChanged: (value) {
|
||||
_prefs?.setBool('check_update_strictly', value ?? false).then((_) {
|
||||
setState(() {});
|
||||
});
|
||||
},
|
||||
),
|
||||
Obx(() {
|
||||
final AuthProvider auth = Get.find<AuthProvider>();
|
||||
if (!auth.isAuthorized.value) return const SizedBox.shrink();
|
||||
return Column(
|
||||
children: [
|
||||
_buildCaptionHeader('account'.tr),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.flag),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
|
||||
title: Text('reportAbuse'.tr),
|
||||
subtitle: Text('reportAbuseDesc'.tr),
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => const AbuseReportDialog(),
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.person_remove),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
|
||||
title: Text('accountDeletion'.tr),
|
||||
subtitle: Text('accountDeletionDesc'.tr),
|
||||
onTap: () {
|
||||
context
|
||||
.showSlideToConfirmDialog(
|
||||
'accountDeletionConfirm'.tr,
|
||||
'accountDeletionConfirmDesc'.trParams({
|
||||
'account': '@${auth.userProfile.value!['name']}',
|
||||
}),
|
||||
)
|
||||
.then((value) async {
|
||||
if (value != true) return;
|
||||
final client = await auth.configureClient('id');
|
||||
final resp = await client.post('/users/me/deletion', {});
|
||||
if (resp.statusCode != 200) {
|
||||
context.showErrorDialog(RequestException(resp));
|
||||
} else {
|
||||
context.showSnackbar('accountDeletionRequested'.tr);
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
_buildCaptionHeader('performance'.tr),
|
||||
CheckboxListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
|
||||
secondary: const Icon(Icons.message),
|
||||
title: Text('animatedMessageList'.tr),
|
||||
subtitle: Text('animatedMessageListDesc'.tr),
|
||||
value: _prefs?.getBool('non_animated_message_list') ?? false,
|
||||
onChanged: (value) {
|
||||
_prefs
|
||||
?.setBool('non_animated_message_list', value ?? false)
|
||||
.then((_) {
|
||||
setState(() {});
|
||||
});
|
||||
},
|
||||
),
|
||||
_buildCaptionHeader('more'.tr),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.delete_sweep),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
subtitle: FutureBuilder(
|
||||
future: AppDatabase.getDatabaseSize(),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return Text('localDatabaseSize'.trParams(
|
||||
{'size': 'unknown'.tr},
|
||||
));
|
||||
}
|
||||
return Text('localDatabaseSize'.trParams(
|
||||
{'size': snapshot.data!.formatBytes()},
|
||||
));
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
|
||||
title: Text('localDatabaseWipe'.tr),
|
||||
onTap: () {
|
||||
AppDatabase.removeDatabase().then((_) {
|
||||
setState(() {});
|
||||
});
|
||||
},
|
||||
),
|
||||
if (PlatformInfo.canRateTheApp)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.star),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
|
||||
title: Text('rateTheApp'.tr),
|
||||
subtitle: Text('rateTheAppDesc'.tr),
|
||||
onTap: () {
|
||||
final inAppReview = InAppReview.instance;
|
||||
|
||||
inAppReview.openStoreListing(
|
||||
appStoreId: '6499032345',
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.info_outline),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
|
||||
title: Text('about'.tr),
|
||||
onTap: () {
|
||||
AppRouter.instance.pushNamed('about');
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,9 @@ import 'package:firebase_analytics/firebase_analytics.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:solian/theme.dart';
|
||||
import 'package:solian/widgets/navigation/app_navigation_drawer.dart';
|
||||
import 'package:solian/widgets/navigation/app_navigation.dart';
|
||||
import 'package:solian/widgets/navigation/app_navigation_bottom.dart';
|
||||
import 'package:solian/widgets/navigation/app_navigation_rail.dart';
|
||||
|
||||
final GlobalKey<ScaffoldState> rootScaffoldKey = GlobalKey<ScaffoldState>();
|
||||
|
||||
@ -39,17 +41,41 @@ class RootShell extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
final showRailNavigation = AppTheme.isLargeScreen(context);
|
||||
|
||||
final destNames = [
|
||||
'postDetail',
|
||||
...AppNavigation.destinations.map((x) => x.page),
|
||||
];
|
||||
final showBottomNavigation =
|
||||
destNames.contains(routeName) && !showRailNavigation;
|
||||
|
||||
return Scaffold(
|
||||
key: rootScaffoldKey,
|
||||
drawer: AppTheme.isLargeScreen(context)
|
||||
? null
|
||||
: AppNavigationDrawer(routeName: routeName),
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
bottomNavigationBar: showBottomNavigation
|
||||
? AppNavigationBottom(
|
||||
initialIndex: AppNavigation.destinations
|
||||
.map((x) => x.page)
|
||||
.toList()
|
||||
.indexOf(routeName ?? 'page'),
|
||||
)
|
||||
: null,
|
||||
body: AppTheme.isLargeScreen(context)
|
||||
? Row(
|
||||
children: [
|
||||
if (showNavigation) AppNavigationDrawer(routeName: routeName),
|
||||
if (showNavigation)
|
||||
const VerticalDivider(thickness: 0.3, width: 1),
|
||||
if (showRailNavigation)
|
||||
AppNavigationRail(
|
||||
initialIndex: AppNavigation.destinations
|
||||
.map((x) => x.page)
|
||||
.toList()
|
||||
.indexOf(routeName ?? 'page'),
|
||||
),
|
||||
if (showRailNavigation)
|
||||
const VerticalDivider(
|
||||
width: 0.3,
|
||||
thickness: 0.3,
|
||||
),
|
||||
Expanded(child: child),
|
||||
],
|
||||
)
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user