Compare commits
53 Commits
Author | SHA1 | Date | |
---|---|---|---|
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 |
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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -4,3 +4,4 @@ android.enableJetifier=true
|
|||||||
android.defaults.buildfeatures.buildconfig=true
|
android.defaults.buildfeatures.buildconfig=true
|
||||||
android.nonTransitiveRClass=false
|
android.nonTransitiveRClass=false
|
||||||
android.nonFinalResIds=false
|
android.nonFinalResIds=false
|
||||||
|
kotlin.jvm.target.validation.mode = IGNORE
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -463,5 +463,26 @@
|
|||||||
"friendAdd": "Add as friend",
|
"friendAdd": "Add as friend",
|
||||||
"blockUser": "Block user",
|
"blockUser": "Block user",
|
||||||
"unblockUser": "Unblock user",
|
"unblockUser": "Unblock user",
|
||||||
"learnMoreAboutPerson": "Learn more about that person"
|
"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": "See more interesting posts on Solar Network"
|
||||||
}
|
}
|
||||||
|
@ -266,7 +266,7 @@
|
|||||||
"channelMembersAddHint": "到 @channel",
|
"channelMembersAddHint": "到 @channel",
|
||||||
"channelType": "频道类型",
|
"channelType": "频道类型",
|
||||||
"channelTypeCommon": "普通频道",
|
"channelTypeCommon": "普通频道",
|
||||||
"channelTypeDirect": "私信聊天",
|
"channelTypeDirect": "私信",
|
||||||
"channelAdjust": "调整频道",
|
"channelAdjust": "调整频道",
|
||||||
"channelDetail": "频道详情",
|
"channelDetail": "频道详情",
|
||||||
"channelSettings": "频道设置",
|
"channelSettings": "频道设置",
|
||||||
@ -459,5 +459,26 @@
|
|||||||
"friendAdd": "添加好友",
|
"friendAdd": "添加好友",
|
||||||
"blockUser": "屏蔽用户",
|
"blockUser": "屏蔽用户",
|
||||||
"unblockUser": "解除屏蔽用户",
|
"unblockUser": "解除屏蔽用户",
|
||||||
"learnMoreAboutPerson": "了解关于 TA 的更多"
|
"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 看更多有趣帖子"
|
||||||
}
|
}
|
||||||
|
144
ios/Podfile.lock
144
ios/Podfile.lock
@ -38,45 +38,45 @@ PODS:
|
|||||||
- file_picker (0.0.1):
|
- file_picker (0.0.1):
|
||||||
- DKImagePickerController/PhotoGallery
|
- DKImagePickerController/PhotoGallery
|
||||||
- Flutter
|
- Flutter
|
||||||
- Firebase/Analytics (11.0.0):
|
- Firebase/Analytics (11.2.0):
|
||||||
- Firebase/Core
|
- Firebase/Core
|
||||||
- Firebase/Core (11.0.0):
|
- Firebase/Core (11.2.0):
|
||||||
- Firebase/CoreOnly
|
- Firebase/CoreOnly
|
||||||
- FirebaseAnalytics (~> 11.0.0)
|
- FirebaseAnalytics (~> 11.2.0)
|
||||||
- Firebase/CoreOnly (11.0.0):
|
- Firebase/CoreOnly (11.2.0):
|
||||||
- FirebaseCore (= 11.0.0)
|
- FirebaseCore (= 11.2.0)
|
||||||
- Firebase/Crashlytics (11.0.0):
|
- Firebase/Crashlytics (11.2.0):
|
||||||
- Firebase/CoreOnly
|
- Firebase/CoreOnly
|
||||||
- FirebaseCrashlytics (~> 11.0.0)
|
- FirebaseCrashlytics (~> 11.2.0)
|
||||||
- Firebase/Messaging (11.0.0):
|
- Firebase/Messaging (11.2.0):
|
||||||
- Firebase/CoreOnly
|
- Firebase/CoreOnly
|
||||||
- FirebaseMessaging (~> 11.0.0)
|
- FirebaseMessaging (~> 11.2.0)
|
||||||
- Firebase/Performance (11.0.0):
|
- Firebase/Performance (11.2.0):
|
||||||
- Firebase/CoreOnly
|
- Firebase/CoreOnly
|
||||||
- FirebasePerformance (~> 11.0.0)
|
- FirebasePerformance (~> 11.2.0)
|
||||||
- firebase_analytics (11.3.2):
|
- firebase_analytics (11.3.3):
|
||||||
- Firebase/Analytics (= 11.0.0)
|
- Firebase/Analytics (= 11.2.0)
|
||||||
- firebase_core
|
- firebase_core
|
||||||
- Flutter
|
- Flutter
|
||||||
- firebase_core (3.5.0):
|
- firebase_core (3.6.0):
|
||||||
- Firebase/CoreOnly (= 11.0.0)
|
- Firebase/CoreOnly (= 11.2.0)
|
||||||
- Flutter
|
- Flutter
|
||||||
- firebase_crashlytics (4.1.2):
|
- firebase_crashlytics (4.1.3):
|
||||||
- Firebase/Crashlytics (= 11.0.0)
|
- Firebase/Crashlytics (= 11.2.0)
|
||||||
- firebase_core
|
- firebase_core
|
||||||
- Flutter
|
- Flutter
|
||||||
- firebase_messaging (15.1.2):
|
- firebase_messaging (15.1.3):
|
||||||
- Firebase/Messaging (= 11.0.0)
|
- Firebase/Messaging (= 11.2.0)
|
||||||
- firebase_core
|
- firebase_core
|
||||||
- Flutter
|
- Flutter
|
||||||
- firebase_performance (0.10.0-7):
|
- firebase_performance (0.10.0-8):
|
||||||
- Firebase/Performance (= 11.0.0)
|
- Firebase/Performance (= 11.2.0)
|
||||||
- firebase_core
|
- firebase_core
|
||||||
- Flutter
|
- Flutter
|
||||||
- FirebaseABTesting (11.2.0):
|
- FirebaseABTesting (11.3.0):
|
||||||
- FirebaseCore (~> 11.0)
|
- FirebaseCore (~> 11.0)
|
||||||
- FirebaseAnalytics (11.0.0):
|
- FirebaseAnalytics (11.2.0):
|
||||||
- FirebaseAnalytics/AdIdSupport (= 11.0.0)
|
- FirebaseAnalytics/AdIdSupport (= 11.2.0)
|
||||||
- FirebaseCore (~> 11.0)
|
- FirebaseCore (~> 11.0)
|
||||||
- FirebaseInstallations (~> 11.0)
|
- FirebaseInstallations (~> 11.0)
|
||||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||||
@ -84,24 +84,24 @@ PODS:
|
|||||||
- GoogleUtilities/Network (~> 8.0)
|
- GoogleUtilities/Network (~> 8.0)
|
||||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||||
- nanopb (~> 3.30910.0)
|
- nanopb (~> 3.30910.0)
|
||||||
- FirebaseAnalytics/AdIdSupport (11.0.0):
|
- FirebaseAnalytics/AdIdSupport (11.2.0):
|
||||||
- FirebaseCore (~> 11.0)
|
- FirebaseCore (~> 11.0)
|
||||||
- FirebaseInstallations (~> 11.0)
|
- FirebaseInstallations (~> 11.0)
|
||||||
- GoogleAppMeasurement (= 11.0.0)
|
- GoogleAppMeasurement (= 11.2.0)
|
||||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||||
- GoogleUtilities/Network (~> 8.0)
|
- GoogleUtilities/Network (~> 8.0)
|
||||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||||
- nanopb (~> 3.30910.0)
|
- nanopb (~> 3.30910.0)
|
||||||
- FirebaseCore (11.0.0):
|
- FirebaseCore (11.2.0):
|
||||||
- FirebaseCoreInternal (~> 11.0)
|
- FirebaseCoreInternal (~> 11.0)
|
||||||
- GoogleUtilities/Environment (~> 8.0)
|
- GoogleUtilities/Environment (~> 8.0)
|
||||||
- GoogleUtilities/Logger (~> 8.0)
|
- GoogleUtilities/Logger (~> 8.0)
|
||||||
- FirebaseCoreExtension (11.2.0):
|
- FirebaseCoreExtension (11.3.0):
|
||||||
- FirebaseCore (~> 11.0)
|
- FirebaseCore (~> 11.0)
|
||||||
- FirebaseCoreInternal (11.2.0):
|
- FirebaseCoreInternal (11.3.0):
|
||||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||||
- FirebaseCrashlytics (11.0.0):
|
- FirebaseCrashlytics (11.2.0):
|
||||||
- FirebaseCore (~> 11.0)
|
- FirebaseCore (~> 11.0)
|
||||||
- FirebaseInstallations (~> 11.0)
|
- FirebaseInstallations (~> 11.0)
|
||||||
- FirebaseRemoteConfigInterop (~> 11.0)
|
- FirebaseRemoteConfigInterop (~> 11.0)
|
||||||
@ -110,12 +110,12 @@ PODS:
|
|||||||
- GoogleUtilities/Environment (~> 8.0)
|
- GoogleUtilities/Environment (~> 8.0)
|
||||||
- nanopb (~> 3.30910.0)
|
- nanopb (~> 3.30910.0)
|
||||||
- PromisesObjC (~> 2.4)
|
- PromisesObjC (~> 2.4)
|
||||||
- FirebaseInstallations (11.2.0):
|
- FirebaseInstallations (11.3.0):
|
||||||
- FirebaseCore (~> 11.0)
|
- FirebaseCore (~> 11.0)
|
||||||
- GoogleUtilities/Environment (~> 8.0)
|
- GoogleUtilities/Environment (~> 8.0)
|
||||||
- GoogleUtilities/UserDefaults (~> 8.0)
|
- GoogleUtilities/UserDefaults (~> 8.0)
|
||||||
- PromisesObjC (~> 2.4)
|
- PromisesObjC (~> 2.4)
|
||||||
- FirebaseMessaging (11.0.0):
|
- FirebaseMessaging (11.2.0):
|
||||||
- FirebaseCore (~> 11.0)
|
- FirebaseCore (~> 11.0)
|
||||||
- FirebaseInstallations (~> 11.0)
|
- FirebaseInstallations (~> 11.0)
|
||||||
- GoogleDataTransport (~> 10.0)
|
- GoogleDataTransport (~> 10.0)
|
||||||
@ -124,7 +124,7 @@ PODS:
|
|||||||
- GoogleUtilities/Reachability (~> 8.0)
|
- GoogleUtilities/Reachability (~> 8.0)
|
||||||
- GoogleUtilities/UserDefaults (~> 8.0)
|
- GoogleUtilities/UserDefaults (~> 8.0)
|
||||||
- nanopb (~> 3.30910.0)
|
- nanopb (~> 3.30910.0)
|
||||||
- FirebasePerformance (11.0.0):
|
- FirebasePerformance (11.2.0):
|
||||||
- FirebaseCore (~> 11.0)
|
- FirebaseCore (~> 11.0)
|
||||||
- FirebaseInstallations (~> 11.0)
|
- FirebaseInstallations (~> 11.0)
|
||||||
- FirebaseRemoteConfig (~> 11.0)
|
- FirebaseRemoteConfig (~> 11.0)
|
||||||
@ -134,7 +134,7 @@ PODS:
|
|||||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||||
- GoogleUtilities/UserDefaults (~> 8.0)
|
- GoogleUtilities/UserDefaults (~> 8.0)
|
||||||
- nanopb (~> 3.30910.0)
|
- nanopb (~> 3.30910.0)
|
||||||
- FirebaseRemoteConfig (11.2.0):
|
- FirebaseRemoteConfig (11.3.0):
|
||||||
- FirebaseABTesting (~> 11.0)
|
- FirebaseABTesting (~> 11.0)
|
||||||
- FirebaseCore (~> 11.0)
|
- FirebaseCore (~> 11.0)
|
||||||
- FirebaseInstallations (~> 11.0)
|
- FirebaseInstallations (~> 11.0)
|
||||||
@ -142,8 +142,8 @@ PODS:
|
|||||||
- FirebaseSharedSwift (~> 11.0)
|
- FirebaseSharedSwift (~> 11.0)
|
||||||
- GoogleUtilities/Environment (~> 8.0)
|
- GoogleUtilities/Environment (~> 8.0)
|
||||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||||
- FirebaseRemoteConfigInterop (11.2.0)
|
- FirebaseRemoteConfigInterop (11.3.0)
|
||||||
- FirebaseSessions (11.2.0):
|
- FirebaseSessions (11.3.0):
|
||||||
- FirebaseCore (~> 11.0)
|
- FirebaseCore (~> 11.0)
|
||||||
- FirebaseCoreExtension (~> 11.0)
|
- FirebaseCoreExtension (~> 11.0)
|
||||||
- FirebaseInstallations (~> 11.0)
|
- FirebaseInstallations (~> 11.0)
|
||||||
@ -152,7 +152,7 @@ PODS:
|
|||||||
- GoogleUtilities/UserDefaults (~> 8.0)
|
- GoogleUtilities/UserDefaults (~> 8.0)
|
||||||
- nanopb (~> 3.30910.0)
|
- nanopb (~> 3.30910.0)
|
||||||
- PromisesSwift (~> 2.1)
|
- PromisesSwift (~> 2.1)
|
||||||
- FirebaseSharedSwift (11.2.0)
|
- FirebaseSharedSwift (11.3.0)
|
||||||
- Flutter (1.0.0)
|
- Flutter (1.0.0)
|
||||||
- flutter_app_update (0.0.1):
|
- flutter_app_update (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
@ -166,27 +166,30 @@ PODS:
|
|||||||
- Flutter
|
- Flutter
|
||||||
- flutter_secure_storage (6.0.0):
|
- flutter_secure_storage (6.0.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
- flutter_udid (0.0.1):
|
||||||
|
- Flutter
|
||||||
|
- SAMKeychain
|
||||||
- flutter_webrtc (0.11.3):
|
- flutter_webrtc (0.11.3):
|
||||||
- Flutter
|
- Flutter
|
||||||
- WebRTC-SDK (= 125.6422.04)
|
- WebRTC-SDK (= 125.6422.04)
|
||||||
- gal (1.0.0):
|
- gal (1.0.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- GoogleAppMeasurement (11.0.0):
|
- GoogleAppMeasurement (11.2.0):
|
||||||
- GoogleAppMeasurement/AdIdSupport (= 11.0.0)
|
- GoogleAppMeasurement/AdIdSupport (= 11.2.0)
|
||||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||||
- GoogleUtilities/Network (~> 8.0)
|
- GoogleUtilities/Network (~> 8.0)
|
||||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||||
- nanopb (~> 3.30910.0)
|
- nanopb (~> 3.30910.0)
|
||||||
- GoogleAppMeasurement/AdIdSupport (11.0.0):
|
- GoogleAppMeasurement/AdIdSupport (11.2.0):
|
||||||
- GoogleAppMeasurement/WithoutAdIdSupport (= 11.0.0)
|
- GoogleAppMeasurement/WithoutAdIdSupport (= 11.2.0)
|
||||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||||
- GoogleUtilities/Network (~> 8.0)
|
- GoogleUtilities/Network (~> 8.0)
|
||||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||||
- nanopb (~> 3.30910.0)
|
- nanopb (~> 3.30910.0)
|
||||||
- GoogleAppMeasurement/WithoutAdIdSupport (11.0.0):
|
- GoogleAppMeasurement/WithoutAdIdSupport (11.2.0):
|
||||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||||
- GoogleUtilities/Network (~> 8.0)
|
- GoogleUtilities/Network (~> 8.0)
|
||||||
@ -259,6 +262,7 @@ PODS:
|
|||||||
- PromisesObjC (= 2.4.0)
|
- PromisesObjC (= 2.4.0)
|
||||||
- protocol_handler_ios (0.0.1):
|
- protocol_handler_ios (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
- SAMKeychain (1.5.3)
|
||||||
- screen_brightness_ios (0.1.0):
|
- screen_brightness_ios (0.1.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- SDWebImage (5.19.7):
|
- SDWebImage (5.19.7):
|
||||||
@ -269,7 +273,7 @@ PODS:
|
|||||||
- shared_preferences_foundation (0.0.1):
|
- shared_preferences_foundation (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- sqflite (0.0.3):
|
- sqflite_darwin (0.0.4):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- "sqlite3 (3.46.1+1)":
|
- "sqlite3 (3.46.1+1)":
|
||||||
@ -316,6 +320,7 @@ DEPENDENCIES:
|
|||||||
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
|
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
|
||||||
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
||||||
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/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`)
|
- flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`)
|
||||||
- gal (from `.symlinks/plugins/gal/darwin`)
|
- gal (from `.symlinks/plugins/gal/darwin`)
|
||||||
- image_cropper (from `.symlinks/plugins/image_cropper/ios`)
|
- image_cropper (from `.symlinks/plugins/image_cropper/ios`)
|
||||||
@ -334,7 +339,7 @@ DEPENDENCIES:
|
|||||||
- screen_brightness_ios (from `.symlinks/plugins/screen_brightness_ios/ios`)
|
- screen_brightness_ios (from `.symlinks/plugins/screen_brightness_ios/ios`)
|
||||||
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
||||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
- 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`)
|
- sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/ios`)
|
||||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||||
- volume_controller (from `.symlinks/plugins/volume_controller/ios`)
|
- volume_controller (from `.symlinks/plugins/volume_controller/ios`)
|
||||||
@ -364,6 +369,7 @@ SPEC REPOS:
|
|||||||
- nanopb
|
- nanopb
|
||||||
- PromisesObjC
|
- PromisesObjC
|
||||||
- PromisesSwift
|
- PromisesSwift
|
||||||
|
- SAMKeychain
|
||||||
- SDWebImage
|
- SDWebImage
|
||||||
- sqlite3
|
- sqlite3
|
||||||
- SwiftyGif
|
- SwiftyGif
|
||||||
@ -401,6 +407,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
||||||
flutter_secure_storage:
|
flutter_secure_storage:
|
||||||
:path: ".symlinks/plugins/flutter_secure_storage/ios"
|
:path: ".symlinks/plugins/flutter_secure_storage/ios"
|
||||||
|
flutter_udid:
|
||||||
|
:path: ".symlinks/plugins/flutter_udid/ios"
|
||||||
flutter_webrtc:
|
flutter_webrtc:
|
||||||
:path: ".symlinks/plugins/flutter_webrtc/ios"
|
:path: ".symlinks/plugins/flutter_webrtc/ios"
|
||||||
gal:
|
gal:
|
||||||
@ -437,8 +445,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/share_plus/ios"
|
:path: ".symlinks/plugins/share_plus/ios"
|
||||||
shared_preferences_foundation:
|
shared_preferences_foundation:
|
||||||
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
||||||
sqflite:
|
sqflite_darwin:
|
||||||
:path: ".symlinks/plugins/sqflite/darwin"
|
:path: ".symlinks/plugins/sqflite_darwin/darwin"
|
||||||
sqlite3_flutter_libs:
|
sqlite3_flutter_libs:
|
||||||
:path: ".symlinks/plugins/sqlite3_flutter_libs/ios"
|
:path: ".symlinks/plugins/sqlite3_flutter_libs/ios"
|
||||||
url_launcher_ios:
|
url_launcher_ios:
|
||||||
@ -454,25 +462,25 @@ SPEC CHECKSUMS:
|
|||||||
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
|
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
|
||||||
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
||||||
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
|
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
|
||||||
Firebase: 9f574c08c2396885b5e7e100ed4293d956218af9
|
Firebase: 98e6bf5278170668a7983e12971a66b2cd57fc8c
|
||||||
firebase_analytics: 4fd10182fd08bb8358f26ac8aca8dad7b6d0f592
|
firebase_analytics: fbc57838bdb94eef1e0ff504f127d974ff2981ad
|
||||||
firebase_core: 2ec6b789859c7c24766344ec71fdf78639402d56
|
firebase_core: 2bedc3136ec7c7b8561c6123ed0239387b53f2af
|
||||||
firebase_crashlytics: 60630a0f91ee432275fa1660fd8593079761448a
|
firebase_crashlytics: 37d104d457b51760b48504a93a12b3bf70995d77
|
||||||
firebase_messaging: a18e1e02b2e8e69097c8173e0c851be223b21c50
|
firebase_messaging: 15d114e1a41fc31e4fbabcd48d765a19eec94a38
|
||||||
firebase_performance: 12d45fdf120992fa879d990929bf73d4a5ced053
|
firebase_performance: 26ad47755d3e8d7b04b9bb36bdfbf1cec8d8dfcc
|
||||||
FirebaseABTesting: 2104d957ce33888a3d6f3bde298cdee376dde8f1
|
FirebaseABTesting: c4559fcd2eba9f6bdaf0599e2c37ded01c343e4c
|
||||||
FirebaseAnalytics: 27eb78b97880ea4a004839b9bac0b58880f5a92a
|
FirebaseAnalytics: c36efd5710c60c17558650fa58c2066eca7e9265
|
||||||
FirebaseCore: 3cf438f431f18c12cdf2aaf64434648b63f7e383
|
FirebaseCore: a282032ae9295c795714ded2ec9c522fc237f8da
|
||||||
FirebaseCoreExtension: cda74ddfb001224bd8fd1d6e74698b4ed07803de
|
FirebaseCoreExtension: 30bb063476ef66cd46925243d64ad8b2c8ac3264
|
||||||
FirebaseCoreInternal: 0c569513412da9f3b31bd0b340013bbee8f295c5
|
FirebaseCoreInternal: ac26d09a70c730e497936430af4e60fb0c68ec4e
|
||||||
FirebaseCrashlytics: 745d8f0221fe49c62865391d1bf56f5a12eeec0b
|
FirebaseCrashlytics: cfc69af5b53565dc6a5e563788809b5778ac4eac
|
||||||
FirebaseInstallations: 771177d89d6c451dc6e50085ec82e2fc77ed0a4a
|
FirebaseInstallations: 58cf94dabf1e2bb2fa87725a9be5c2249171cda0
|
||||||
FirebaseMessaging: d2d1d9c62c46dd2db49a952f7deb5b16ad2c9742
|
FirebaseMessaging: c9ec7b90c399c7a6100297e9d16f8a27fc7f7152
|
||||||
FirebasePerformance: efdc02bacb1b4710588c9f867011605c081cdf79
|
FirebasePerformance: c39138c0700b8ef6040f0b80b5707320808e2862
|
||||||
FirebaseRemoteConfig: fca0b2d017fc1de52b28a4e5bcf2007c1a840457
|
FirebaseRemoteConfig: 5be2ca4f9870d475b39214210955fdaeecf7e5ca
|
||||||
FirebaseRemoteConfigInterop: 477b26fdeb8fb5fbaf22fa9db5343b42289dc7db
|
FirebaseRemoteConfigInterop: c3a5c31b3c22079f41ba1dc645df889d9ce38cb9
|
||||||
FirebaseSessions: adcec8b72d0066a385e3affcd1bcb1ebb3908ce6
|
FirebaseSessions: 655ff17f3cc1a635cbdc2d69b953878001f9e25b
|
||||||
FirebaseSharedSwift: 7a0d78d155ede78407f0fdc89fbc914014c7c540
|
FirebaseSharedSwift: d39c2ad64a11a8d936ce25a42b00df47078bb59c
|
||||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||||
flutter_app_update: 65f61da626cb111d1b24674abc4b01728d7723bc
|
flutter_app_update: 65f61da626cb111d1b24674abc4b01728d7723bc
|
||||||
flutter_background_service_ios: e30e0d3ee69e4cee66272d0c78eacd48c2e94aac
|
flutter_background_service_ios: e30e0d3ee69e4cee66272d0c78eacd48c2e94aac
|
||||||
@ -480,9 +488,10 @@ SPEC CHECKSUMS:
|
|||||||
flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086
|
flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086
|
||||||
flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778
|
flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778
|
||||||
flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
|
flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
|
||||||
|
flutter_udid: a2482c67a61b9c806ef59dd82ed8d007f1b7ac04
|
||||||
flutter_webrtc: 75b868e4f9e817c7a9a42ca4b6169063de4eec9f
|
flutter_webrtc: 75b868e4f9e817c7a9a42ca4b6169063de4eec9f
|
||||||
gal: 61e868295d28fe67ffa297fae6dacebf56fd53e1
|
gal: 61e868295d28fe67ffa297fae6dacebf56fd53e1
|
||||||
GoogleAppMeasurement: 6e49ffac7d3f2c3ded9cc663f912a13b67bbd0de
|
GoogleAppMeasurement: 76d4f8b36b03bd8381fa9a7fe2cc7f99c0a2e93a
|
||||||
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||||
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
|
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
|
||||||
image_cropper: 37d40f62177c101ff4c164906d259ea2c3aa70cf
|
image_cropper: 37d40f62177c101ff4c164906d259ea2c3aa70cf
|
||||||
@ -501,11 +510,12 @@ SPEC CHECKSUMS:
|
|||||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||||
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
|
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
|
||||||
protocol_handler_ios: a5db8abc38526ee326988b808be621e5fd568990
|
protocol_handler_ios: a5db8abc38526ee326988b808be621e5fd568990
|
||||||
|
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
||||||
screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625
|
screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625
|
||||||
SDWebImage: 8a6b7b160b4d710e2a22b6900e25301075c34cb3
|
SDWebImage: 8a6b7b160b4d710e2a22b6900e25301075c34cb3
|
||||||
share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad
|
share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad
|
||||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||||
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
|
sqflite_darwin: a553b1fd6fe66f53bbb0fe5b4f5bab93f08d7a13
|
||||||
sqlite3: 0bb0e6389d824e40296f531b858a2a0b71c0d2fb
|
sqlite3: 0bb0e6389d824e40296f531b858a2a0b71c0d2fb
|
||||||
sqlite3_flutter_libs: c00457ebd31e59fa6bb830380ddba24d44fbcd3b
|
sqlite3_flutter_libs: c00457ebd31e59fa6bb830380ddba24d44fbcd3b
|
||||||
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
||||||
|
@ -59,6 +59,7 @@
|
|||||||
ignoresPersistentStateOnLaunch = "NO"
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
debugDocumentVersioning = "YES"
|
debugDocumentVersioning = "YES"
|
||||||
debugServiceExtension = "internal"
|
debugServiceExtension = "internal"
|
||||||
|
showGraphicsOverview = "Yes"
|
||||||
allowLocationSimulation = "YES">
|
allowLocationSimulation = "YES">
|
||||||
<BuildableProductRunnable
|
<BuildableProductRunnable
|
||||||
runnableDebuggingMode = "0">
|
runnableDebuggingMode = "0">
|
||||||
|
@ -83,7 +83,6 @@
|
|||||||
</array>
|
</array>
|
||||||
<key>CFBundleLocalizations</key>
|
<key>CFBundleLocalizations</key>
|
||||||
<array>
|
<array>
|
||||||
<string>zh_CN</string>
|
|
||||||
<string>en</string>
|
<string>en</string>
|
||||||
</array>
|
</array>
|
||||||
<key>UIStatusBarHidden</key>
|
<key>UIStatusBarHidden</key>
|
||||||
|
@ -12,12 +12,12 @@ import 'package:solian/exceptions/request.dart';
|
|||||||
import 'package:solian/exts.dart';
|
import 'package:solian/exts.dart';
|
||||||
import 'package:solian/platform.dart';
|
import 'package:solian/platform.dart';
|
||||||
import 'package:solian/providers/auth.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/content/realm.dart';
|
||||||
import 'package:solian/providers/relation.dart';
|
import 'package:solian/providers/relation.dart';
|
||||||
import 'package:solian/providers/theme_switcher.dart';
|
import 'package:solian/providers/theme_switcher.dart';
|
||||||
import 'package:solian/providers/websocket.dart';
|
import 'package:solian/providers/websocket.dart';
|
||||||
import 'package:solian/services.dart';
|
import 'package:solian/services.dart';
|
||||||
|
import 'package:solian/widgets/root_container.dart';
|
||||||
import 'package:solian/widgets/sized_container.dart';
|
import 'package:solian/widgets/sized_container.dart';
|
||||||
import 'package:flutter_app_update/flutter_app_update.dart';
|
import 'package:flutter_app_update/flutter_app_update.dart';
|
||||||
import 'package:version/version.dart';
|
import 'package:version/version.dart';
|
||||||
@ -198,8 +198,6 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
|
|||||||
final AuthProvider auth = Get.find();
|
final AuthProvider auth = Get.find();
|
||||||
try {
|
try {
|
||||||
await Future.wait([
|
await Future.wait([
|
||||||
if (auth.isAuthorized.isTrue)
|
|
||||||
Get.find<ChannelProvider>().refreshAvailableChannel(),
|
|
||||||
if (auth.isAuthorized.isTrue)
|
if (auth.isAuthorized.isTrue)
|
||||||
Get.find<RelationshipProvider>().refreshRelativeList(),
|
Get.find<RelationshipProvider>().refreshRelativeList(),
|
||||||
if (auth.isAuthorized.isTrue)
|
if (auth.isAuthorized.isTrue)
|
||||||
@ -258,8 +256,7 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (_isBusy || _isErrored) {
|
if (_isBusy || _isErrored) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
child: Material(
|
child: RootContainer(
|
||||||
color: Theme.of(context).colorScheme.surface,
|
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.max,
|
mainAxisSize: MainAxisSize.max,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.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/pagination.dart';
|
||||||
import 'package:solian/models/post.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/content/posts.dart';
|
||||||
import 'package:solian/providers/last_read.dart';
|
import 'package:solian/providers/last_read.dart';
|
||||||
|
|
||||||
@ -31,9 +34,18 @@ class PostListController extends GetxController {
|
|||||||
pagingController.addPageRequestListener(_onPagingControllerRequest);
|
pagingController.addPageRequestListener(_onPagingControllerRequest);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Completer<void>? _pagingLoadCompleter;
|
||||||
|
|
||||||
Future<void> _onPagingControllerRequest(int pageKey) async {
|
Future<void> _onPagingControllerRequest(int pageKey) async {
|
||||||
try {
|
try {
|
||||||
|
if (_pagingLoadCompleter != null) {
|
||||||
|
await _pagingLoadCompleter!.future;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_pagingLoadCompleter = Completer();
|
||||||
final result = await loadMore();
|
final result = await loadMore();
|
||||||
|
_pagingLoadCompleter!.complete();
|
||||||
|
_pagingLoadCompleter = null;
|
||||||
|
|
||||||
if (result != null && hasMore.value) {
|
if (result != null && hasMore.value) {
|
||||||
pagingController.appendPage(result, nextPageKey.value);
|
pagingController.appendPage(result, nextPageKey.value);
|
||||||
@ -97,9 +109,6 @@ class PostListController extends GetxController {
|
|||||||
hasMore.value = false;
|
hasMore.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
final idx = <dynamic>{};
|
|
||||||
postList.retainWhere((x) => idx.add(x.id));
|
|
||||||
|
|
||||||
if (postList.isNotEmpty) {
|
if (postList.isNotEmpty) {
|
||||||
var lastId = postList.map((x) => x.id).reduce(max);
|
var lastId = postList.map((x) => x.id).reduce(max);
|
||||||
Get.find<LastReadProvider>().feedLastReadAt = lastId;
|
Get.find<LastReadProvider>().feedLastReadAt = lastId;
|
||||||
@ -111,35 +120,39 @@ class PostListController extends GetxController {
|
|||||||
Future<List<Post>?> _loadPosts(int pageKey) async {
|
Future<List<Post>?> _loadPosts(int pageKey) async {
|
||||||
isBusy.value = true;
|
isBusy.value = true;
|
||||||
|
|
||||||
final PostProvider provider = Get.find();
|
final PostProvider posts = Get.find();
|
||||||
|
|
||||||
Response resp;
|
Response resp;
|
||||||
try {
|
try {
|
||||||
if (author != null) {
|
if (author != null) {
|
||||||
resp = await provider.listPost(
|
resp = await posts.listPost(
|
||||||
pageKey,
|
pageKey,
|
||||||
author: author,
|
author: author,
|
||||||
|
take: 10,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
switch (mode.value) {
|
switch (mode.value) {
|
||||||
case 2:
|
case 2:
|
||||||
resp = await provider.listRecommendations(
|
resp = await posts.listRecommendations(
|
||||||
pageKey,
|
pageKey,
|
||||||
channel: 'shuffle',
|
channel: 'shuffle',
|
||||||
realm: realm,
|
realm: realm,
|
||||||
|
take: 10,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case 1:
|
case 1:
|
||||||
resp = await provider.listRecommendations(
|
resp = await posts.listRecommendations(
|
||||||
pageKey,
|
pageKey,
|
||||||
channel: 'friends',
|
channel: 'friends',
|
||||||
realm: realm,
|
realm: realm,
|
||||||
|
take: 10,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
resp = await provider.listRecommendations(
|
resp = await posts.listRecommendations(
|
||||||
pageKey,
|
pageKey,
|
||||||
realm: realm,
|
realm: realm,
|
||||||
|
take: 10,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -153,6 +166,27 @@ class PostListController extends GetxController {
|
|||||||
final result = PaginationResult.fromJson(resp.body);
|
final result = PaginationResult.fromJson(resp.body);
|
||||||
final out = result.data?.map((e) => Post.fromJson(e)).toList();
|
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;
|
postTotal.value = result.count;
|
||||||
|
|
||||||
return out;
|
return out;
|
||||||
|
@ -57,6 +57,8 @@ void main() async {
|
|||||||
|
|
||||||
Future<void> _initializeFirebase() async {
|
Future<void> _initializeFirebase() async {
|
||||||
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
|
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
|
||||||
|
if (PlatformInfo.isIOS || PlatformInfo.isAndroid || PlatformInfo.isMacOS) {
|
||||||
|
// Initialize firebase crashlytics for the platform that supported
|
||||||
FlutterError.onError = (errorDetails) {
|
FlutterError.onError = (errorDetails) {
|
||||||
FirebaseCrashlytics.instance.recordFlutterFatalError(errorDetails);
|
FirebaseCrashlytics.instance.recordFlutterFatalError(errorDetails);
|
||||||
};
|
};
|
||||||
@ -64,6 +66,7 @@ Future<void> _initializeFirebase() async {
|
|||||||
FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
|
FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _initializeBackgroundNotificationService() async {
|
Future<void> _initializeBackgroundNotificationService() async {
|
||||||
|
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,10 +1,19 @@
|
|||||||
import 'package:json_annotation/json_annotation.dart';
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
import 'package:solian/models/account.dart';
|
import 'package:solian/models/account.dart';
|
||||||
|
import 'package:solian/models/attachment.dart';
|
||||||
import 'package:solian/models/post_categories.dart';
|
import 'package:solian/models/post_categories.dart';
|
||||||
import 'package:solian/models/realm.dart';
|
import 'package:solian/models/realm.dart';
|
||||||
|
|
||||||
part 'post.g.dart';
|
part 'post.g.dart';
|
||||||
|
|
||||||
|
class PostPreload {
|
||||||
|
List<Attachment> attachments;
|
||||||
|
|
||||||
|
PostPreload({
|
||||||
|
required this.attachments,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class Post {
|
class Post {
|
||||||
int id;
|
int id;
|
||||||
@ -15,6 +24,7 @@ class Post {
|
|||||||
String? alias;
|
String? alias;
|
||||||
String? areaAlias;
|
String? areaAlias;
|
||||||
dynamic body;
|
dynamic body;
|
||||||
|
int visibility;
|
||||||
List<Tag>? tags;
|
List<Tag>? tags;
|
||||||
List<Category>? categories;
|
List<Category>? categories;
|
||||||
List<Post>? replies;
|
List<Post>? replies;
|
||||||
@ -33,6 +43,9 @@ class Post {
|
|||||||
Account author;
|
Account author;
|
||||||
PostMetric? metric;
|
PostMetric? metric;
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
PostPreload? preload;
|
||||||
|
|
||||||
Post({
|
Post({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.createdAt,
|
required this.createdAt,
|
||||||
@ -43,6 +56,7 @@ class Post {
|
|||||||
required this.areaAlias,
|
required this.areaAlias,
|
||||||
required this.type,
|
required this.type,
|
||||||
required this.body,
|
required this.body,
|
||||||
|
required this.visibility,
|
||||||
required this.tags,
|
required this.tags,
|
||||||
required this.categories,
|
required this.categories,
|
||||||
required this.replies,
|
required this.replies,
|
||||||
|
@ -20,6 +20,7 @@ Post _$PostFromJson(Map<String, dynamic> json) => Post(
|
|||||||
areaAlias: json['area_alias'] as String?,
|
areaAlias: json['area_alias'] as String?,
|
||||||
type: json['type'] as String,
|
type: json['type'] as String,
|
||||||
body: json['body'],
|
body: json['body'],
|
||||||
|
visibility: (json['visibility'] as num).toInt(),
|
||||||
tags: (json['tags'] as List<dynamic>?)
|
tags: (json['tags'] as List<dynamic>?)
|
||||||
?.map((e) => Tag.fromJson(e as Map<String, dynamic>))
|
?.map((e) => Tag.fromJson(e as Map<String, dynamic>))
|
||||||
.toList(),
|
.toList(),
|
||||||
@ -67,6 +68,7 @@ Map<String, dynamic> _$PostToJson(Post instance) => <String, dynamic>{
|
|||||||
'alias': instance.alias,
|
'alias': instance.alias,
|
||||||
'area_alias': instance.areaAlias,
|
'area_alias': instance.areaAlias,
|
||||||
'body': instance.body,
|
'body': instance.body,
|
||||||
|
'visibility': instance.visibility,
|
||||||
'tags': instance.tags?.map((e) => e.toJson()).toList(),
|
'tags': instance.tags?.map((e) => e.toJson()).toList(),
|
||||||
'categories': instance.categories?.map((e) => e.toJson()).toList(),
|
'categories': instance.categories?.map((e) => e.toJson()).toList(),
|
||||||
'replies': instance.replies?.map((e) => e.toJson()).toList(),
|
'replies': instance.replies?.map((e) => e.toJson()).toList(),
|
||||||
|
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,
|
||||||
|
};
|
@ -29,6 +29,8 @@ abstract class PlatformInfo {
|
|||||||
|
|
||||||
static bool get canRateTheApp => isIOS || isMacOS;
|
static bool get canRateTheApp => isIOS || isMacOS;
|
||||||
|
|
||||||
|
static bool get canCropImage => isIOS || isAndroid || isWeb;
|
||||||
|
|
||||||
static bool get canRecord => (isMobile || isMacOS);
|
static bool get canRecord => (isMobile || isMacOS);
|
||||||
|
|
||||||
static bool get canPushNotification => isAndroid || isIOS || isMacOS;
|
static bool get canPushNotification => isAndroid || isIOS || isMacOS;
|
||||||
|
@ -125,7 +125,7 @@ class AuthProvider extends GetConnect {
|
|||||||
userAgent: await ServiceFinder.getUserAgent(),
|
userAgent: await ServiceFinder.getUserAgent(),
|
||||||
sendUserAgent: true,
|
sendUserAgent: true,
|
||||||
);
|
);
|
||||||
client.httpClient.addAuthenticator(requestAuthenticator);
|
client.httpClient.addRequestModifier(requestAuthenticator);
|
||||||
client.httpClient.baseUrl = ServiceFinder.buildUrl(service, null);
|
client.httpClient.baseUrl = ServiceFinder.buildUrl(service, null);
|
||||||
|
|
||||||
return client;
|
return client;
|
||||||
|
@ -392,7 +392,7 @@ class ChatCallProvider extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future gotoScreen(BuildContext context) {
|
Future gotoScreen(BuildContext context) {
|
||||||
return Navigator.of(context, rootNavigator: true).push(
|
return Navigator.of(context).push(
|
||||||
MaterialPageRoute(builder: (context) => const CallScreen()),
|
MaterialPageRoute(builder: (context) => const CallScreen()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,21 @@ class AttachmentProvider extends GetConnect {
|
|||||||
|
|
||||||
final Map<String, Attachment> _cachedResponses = {};
|
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(
|
Future<List<Attachment?>> listMetadata(
|
||||||
List<String> rid, {
|
List<String> rid, {
|
||||||
noCache = false,
|
noCache = false,
|
||||||
@ -41,6 +56,7 @@ class AttachmentProvider extends GetConnect {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (pendingQuery.isNotEmpty) {
|
||||||
final resp = await get(
|
final resp = await get(
|
||||||
'/attachments?take=${pendingQuery.length}&id=${pendingQuery.join(',')}',
|
'/attachments?take=${pendingQuery.length}&id=${pendingQuery.join(',')}',
|
||||||
);
|
);
|
||||||
@ -63,6 +79,7 @@ class AttachmentProvider extends GetConnect {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
@ -9,25 +9,6 @@ import 'package:uuid/uuid.dart';
|
|||||||
|
|
||||||
class ChannelProvider extends GetxController {
|
class ChannelProvider extends GetxController {
|
||||||
RxBool isLoading = false.obs;
|
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 {
|
Future<Response> getChannel(String alias, {String realm = 'global'}) async {
|
||||||
final AuthProvider auth = Get.find();
|
final AuthProvider auth = Get.find();
|
||||||
@ -89,18 +70,22 @@ class ChannelProvider extends GetxController {
|
|||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Response> listAvailableChannel({String scope = 'global'}) async {
|
Future<List<Channel>> listAvailableChannel({
|
||||||
|
String scope = 'global',
|
||||||
|
bool isDirect = false,
|
||||||
|
}) async {
|
||||||
final AuthProvider auth = Get.find();
|
final AuthProvider auth = Get.find();
|
||||||
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
|
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
|
||||||
|
|
||||||
final client = await auth.configureClient('messaging');
|
final client = await auth.configureClient('messaging');
|
||||||
|
|
||||||
final resp = await client.get('/channels/$scope/me/available');
|
final resp =
|
||||||
|
await client.get('/channels/$scope/me/available?direct=$isDirect');
|
||||||
if (resp.statusCode != 200) {
|
if (resp.statusCode != 200) {
|
||||||
throw RequestException(resp);
|
throw RequestException(resp);
|
||||||
}
|
}
|
||||||
|
|
||||||
return resp;
|
return List.from(resp.body.map((x) => Channel.fromJson(x)));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Response> createChannel(String scope, dynamic payload) async {
|
Future<Response> createChannel(String scope, dynamic payload) async {
|
||||||
|
@ -3,22 +3,11 @@ import 'package:solian/exceptions/request.dart';
|
|||||||
import 'package:solian/exceptions/unauthorized.dart';
|
import 'package:solian/exceptions/unauthorized.dart';
|
||||||
import 'package:solian/models/post.dart';
|
import 'package:solian/models/post.dart';
|
||||||
import 'package:solian/providers/auth.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 {
|
Future<Response> seeWhatsNew(int pivot) async {
|
||||||
GetConnect client;
|
|
||||||
final AuthProvider auth = Get.find();
|
final AuthProvider auth = Get.find();
|
||||||
if (auth.isAuthorized.value) {
|
final client = await auth.configureClient('co');
|
||||||
client = await auth.configureClient('co');
|
|
||||||
} else {
|
|
||||||
client = await ServiceFinder.configureClient('co');
|
|
||||||
}
|
|
||||||
final resp = await client.get('/whats-new?pivot=$pivot');
|
final resp = await client.get('/whats-new?pivot=$pivot');
|
||||||
if (resp.statusCode != 200) {
|
if (resp.statusCode != 200) {
|
||||||
throw RequestException(resp);
|
throw RequestException(resp);
|
||||||
@ -28,19 +17,14 @@ class PostProvider extends GetConnect {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<Response> listRecommendations(int page,
|
Future<Response> listRecommendations(int page,
|
||||||
{String? realm, String? channel}) async {
|
{String? realm, String? channel, int take = 10}) async {
|
||||||
GetConnect client;
|
|
||||||
final AuthProvider auth = Get.find();
|
|
||||||
final queries = [
|
final queries = [
|
||||||
'take=${10}',
|
'take=$take',
|
||||||
'offset=$page',
|
'offset=$page',
|
||||||
if (realm != null) 'realm=$realm',
|
if (realm != null) 'realm=$realm',
|
||||||
];
|
];
|
||||||
if (auth.isAuthorized.value) {
|
final AuthProvider auth = Get.find();
|
||||||
client = await auth.configureClient('co');
|
final client = await auth.configureClient('interactive');
|
||||||
} else {
|
|
||||||
client = await ServiceFinder.configureClient('co');
|
|
||||||
}
|
|
||||||
final resp = await client.get(
|
final resp = await client.get(
|
||||||
channel == null
|
channel == null
|
||||||
? '/recommendations?${queries.join('&')}'
|
? '/recommendations?${queries.join('&')}'
|
||||||
@ -70,17 +54,40 @@ class PostProvider extends GetConnect {
|
|||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Response> listPost(int page,
|
Future<Response> searchPost(String probe, int page,
|
||||||
{String? realm, String? author, tag, category}) async {
|
{String? realm, String? author, tag, category, int take = 10}) async {
|
||||||
final queries = [
|
final queries = [
|
||||||
'take=${10}',
|
'probe=$probe',
|
||||||
|
'take=$take',
|
||||||
'offset=$page',
|
'offset=$page',
|
||||||
if (tag != null) 'tag=$tag',
|
if (tag != null) 'tag=$tag',
|
||||||
if (category != null) 'category=$category',
|
if (category != null) 'category=$category',
|
||||||
if (author != null) 'author=$author',
|
if (author != null) 'author=$author',
|
||||||
if (realm != null) 'realm=$realm',
|
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/search?${queries.join('&')}');
|
||||||
|
if (resp.statusCode != 200) {
|
||||||
|
throw RequestException(resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Response> listPost(int page,
|
||||||
|
{String? realm, String? author, tag, category, int take = 10}) async {
|
||||||
|
final queries = [
|
||||||
|
'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?${queries.join('&')}');
|
||||||
if (resp.statusCode != 200) {
|
if (resp.statusCode != 200) {
|
||||||
throw RequestException(resp);
|
throw RequestException(resp);
|
||||||
}
|
}
|
||||||
@ -89,7 +96,10 @@ class PostProvider extends GetConnect {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<Response> listPostReplies(String alias, int page) async {
|
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) {
|
if (resp.statusCode != 200) {
|
||||||
throw RequestException(resp);
|
throw RequestException(resp);
|
||||||
}
|
}
|
||||||
@ -98,7 +108,9 @@ class PostProvider extends GetConnect {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<List<Post>> listPostFeaturedReply(String alias, {int take = 1}) async {
|
Future<List<Post>> listPostFeaturedReply(String alias, {int take = 1}) async {
|
||||||
final resp = await get('/posts/$alias/replies/featured?take=$take');
|
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) {
|
if (resp.statusCode != 200) {
|
||||||
throw RequestException(resp);
|
throw RequestException(resp);
|
||||||
}
|
}
|
||||||
@ -107,16 +119,9 @@ class PostProvider extends GetConnect {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<Response> getPost(String alias) async {
|
Future<Response> getPost(String alias) async {
|
||||||
final resp = await get('/posts/$alias');
|
final AuthProvider auth = Get.find();
|
||||||
if (resp.statusCode != 200) {
|
final client = await auth.configureClient('co');
|
||||||
throw RequestException(resp);
|
final resp = await client.get('/posts/$alias');
|
||||||
}
|
|
||||||
|
|
||||||
return resp;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Response> getArticle(String alias) async {
|
|
||||||
final resp = await get('/articles/$alias');
|
|
||||||
if (resp.statusCode != 200) {
|
if (resp.statusCode != 200) {
|
||||||
throw RequestException(resp);
|
throw RequestException(resp);
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:get/get.dart' hide Value;
|
import 'package:get/get.dart' hide Value;
|
||||||
import 'package:solian/exceptions/request.dart';
|
import 'package:solian/exceptions/request.dart';
|
||||||
@ -182,4 +185,26 @@ class MessagesFetchingProvider extends GetxController {
|
|||||||
..orderBy([(t) => OrderingTerm.desc(t.id)]))
|
..orderBy([(t) => OrderingTerm.desc(t.id)]))
|
||||||
.getSingleOrNull();
|
.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 {
|
class ExperienceProvider extends GetxController {
|
||||||
static List<int> experienceToLevelRequirements = [
|
static List<int> experienceToLevelRequirements = [
|
||||||
0, // Level 0
|
0, // Level 0
|
||||||
100, // Level 1
|
1000, // Level 1
|
||||||
400, // Level 2
|
4000, // Level 2
|
||||||
900, // Level 3
|
9000, // Level 3
|
||||||
1600, // Level 4
|
16000, // Level 4
|
||||||
2500, // Level 5
|
25000, // Level 5
|
||||||
3600, // Level 6
|
36000, // Level 6
|
||||||
4900, // Level 7
|
49000, // Level 7
|
||||||
6400, // Level 8
|
64000, // Level 8
|
||||||
8100, // Level 9
|
81000, // Level 9
|
||||||
10000, // Level 10
|
100000, // Level 10
|
||||||
12100, // Level 11
|
121000, // Level 11
|
||||||
14400, // Level 12
|
144000, // Level 12
|
||||||
36800 // Level 13
|
368000 // Level 13
|
||||||
];
|
];
|
||||||
|
|
||||||
static List<String> levelLabelMapping =
|
static List<String> levelLabelMapping =
|
||||||
@ -35,7 +35,7 @@ class ExperienceProvider extends GetxController {
|
|||||||
final idx = experienceToLevelRequirements.indexOf(exp);
|
final idx = experienceToLevelRequirements.indexOf(exp);
|
||||||
if (idx + 1 >= experienceToLevelRequirements.length) return 1;
|
if (idx + 1 >= experienceToLevelRequirements.length) return 1;
|
||||||
final nextExp = experienceToLevelRequirements[idx + 1];
|
final nextExp = experienceToLevelRequirements[idx + 1];
|
||||||
return exp / nextExp;
|
return (experience - exp).abs() / (exp - nextExp).abs();
|
||||||
}
|
}
|
||||||
|
|
||||||
static String calcLevelUpProgressLevel(int experience) {
|
static String calcLevelUpProgressLevel(int experience) {
|
||||||
@ -43,9 +43,9 @@ class ExperienceProvider extends GetxController {
|
|||||||
.firstWhere((x) => x <= experience);
|
.firstWhere((x) => x <= experience);
|
||||||
final idx = experienceToLevelRequirements.indexOf(exp);
|
final idx = experienceToLevelRequirements.indexOf(exp);
|
||||||
if (idx + 1 >= experienceToLevelRequirements.length) return 'Infinity';
|
if (idx + 1 >= experienceToLevelRequirements.length) return 'Infinity';
|
||||||
final nextExp = experienceToLevelRequirements[idx + 1];
|
final nextExp = exp - experienceToLevelRequirements[idx + 1];
|
||||||
final formatter =
|
final formatter =
|
||||||
NumberFormat.compactCurrency(symbol: '', decimalDigits: 1);
|
NumberFormat.compactCurrency(symbol: '', decimalDigits: 1);
|
||||||
return '${formatter.format(exp)}/${formatter.format(nextExp)}';
|
return '${formatter.format((exp - experience).abs())}/${formatter.format(nextExp.abs())}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'package:solian/models/theme.dart';
|
||||||
import 'package:solian/theme.dart';
|
import 'package:solian/theme.dart';
|
||||||
|
|
||||||
class ThemeSwitcher extends ChangeNotifier {
|
class ThemeSwitcher extends ChangeNotifier {
|
||||||
@ -13,11 +16,21 @@ class ThemeSwitcher extends ChangeNotifier {
|
|||||||
|
|
||||||
Future<void> restoreTheme() async {
|
Future<void> restoreTheme() async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
if (prefs.containsKey('global_theme_color')) {
|
if (prefs.containsKey('global_theme')) {
|
||||||
final value = prefs.getInt('global_theme_color')!;
|
final value = SolianThemeData.fromJson(
|
||||||
final color = Color(value);
|
jsonDecode(prefs.getString('global_theme')!),
|
||||||
lightThemeData = AppTheme.build(Brightness.light, seedColor: color);
|
);
|
||||||
darkThemeData = AppTheme.build(Brightness.dark, seedColor: color);
|
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();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -27,4 +40,25 @@ class ThemeSwitcher extends ChangeNotifier {
|
|||||||
darkThemeData = dark;
|
darkThemeData = dark;
|
||||||
notifyListeners();
|
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,9 +3,9 @@ import 'dart:convert';
|
|||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:device_info_plus/device_info_plus.dart';
|
|
||||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||||
|
import 'package:flutter_udid/flutter_udid.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:solian/exceptions/request.dart';
|
import 'package:solian/exceptions/request.dart';
|
||||||
@ -165,10 +165,11 @@ class WebSocketProvider extends GetxController {
|
|||||||
|
|
||||||
late final String? token;
|
late final String? token;
|
||||||
late final String provider;
|
late final String provider;
|
||||||
final deviceUuid = await _getDeviceUuid();
|
var deviceUuid = await _getDeviceUuid();
|
||||||
|
|
||||||
if (deviceUuid == null || deviceUuid.isEmpty) {
|
if (deviceUuid == null || deviceUuid.isEmpty) {
|
||||||
log("Unable to active push notifications, couldn't get device uuid");
|
log("Unable to active push notifications, couldn't get device uuid");
|
||||||
|
return;
|
||||||
} else {
|
} else {
|
||||||
log('Device UUID is $deviceUuid');
|
log('Device UUID is $deviceUuid');
|
||||||
}
|
}
|
||||||
@ -195,33 +196,7 @@ class WebSocketProvider extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<String?> _getDeviceUuid() async {
|
Future<String?> _getDeviceUuid() async {
|
||||||
DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
|
if (PlatformInfo.isWeb) return null;
|
||||||
if (PlatformInfo.isWeb) {
|
return await FlutterUdid.consistentUdid;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,11 +2,14 @@ import 'package:animations/animations.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:solian/bootstrapper.dart';
|
import 'package:solian/bootstrapper.dart';
|
||||||
|
import 'package:solian/models/post.dart';
|
||||||
import 'package:solian/models/realm.dart';
|
import 'package:solian/models/realm.dart';
|
||||||
import 'package:solian/screens/about.dart';
|
import 'package:solian/screens/about.dart';
|
||||||
import 'package:solian/screens/account.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/friend.dart';
|
||||||
import 'package:solian/screens/account/preferences/notifications.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_edit.dart';
|
||||||
import 'package:solian/screens/account/profile_page.dart';
|
import 'package:solian/screens/account/profile_page.dart';
|
||||||
import 'package:solian/screens/auth/signin.dart';
|
import 'package:solian/screens/auth/signin.dart';
|
||||||
@ -16,9 +19,9 @@ import 'package:solian/screens/channel/channel_detail.dart';
|
|||||||
import 'package:solian/screens/channel/channel_organize.dart';
|
import 'package:solian/screens/channel/channel_organize.dart';
|
||||||
import 'package:solian/screens/chat.dart';
|
import 'package:solian/screens/chat.dart';
|
||||||
import 'package:solian/screens/dashboard.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/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.dart';
|
||||||
import 'package:solian/screens/realms/realm_detail.dart';
|
import 'package:solian/screens/realms/realm_detail.dart';
|
||||||
import 'package:solian/screens/realms/realm_organize.dart';
|
import 'package:solian/screens/realms/realm_organize.dart';
|
||||||
@ -28,6 +31,8 @@ import 'package:solian/screens/posts/post_editor.dart';
|
|||||||
import 'package:solian/screens/settings.dart';
|
import 'package:solian/screens/settings.dart';
|
||||||
import 'package:solian/shells/root_shell.dart';
|
import 'package:solian/shells/root_shell.dart';
|
||||||
import 'package:solian/shells/title_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 {
|
abstract class AppRouter {
|
||||||
static GoRouter instance = GoRouter(
|
static GoRouter instance = GoRouter(
|
||||||
@ -92,7 +97,7 @@ abstract class AppRouter {
|
|||||||
name: 'postSearch',
|
name: 'postSearch',
|
||||||
builder: (context, state) => TitleShell(
|
builder: (context, state) => TitleShell(
|
||||||
state: state,
|
state: state,
|
||||||
child: FeedSearchScreen(
|
child: PostSearchScreen(
|
||||||
tag: state.uri.queryParameters['tag'],
|
tag: state.uri.queryParameters['tag'],
|
||||||
category: state.uri.queryParameters['category'],
|
category: state.uri.queryParameters['category'],
|
||||||
),
|
),
|
||||||
@ -105,6 +110,7 @@ abstract class AppRouter {
|
|||||||
state: state,
|
state: state,
|
||||||
child: PostDetailScreen(
|
child: PostDetailScreen(
|
||||||
id: state.pathParameters['id']!,
|
id: state.pathParameters['id']!,
|
||||||
|
post: state.extra as Post?,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -137,12 +143,15 @@ abstract class AppRouter {
|
|||||||
);
|
);
|
||||||
|
|
||||||
static final ShellRoute _chatRoute = ShellRoute(
|
static final ShellRoute _chatRoute = ShellRoute(
|
||||||
builder: (context, state, child) => child,
|
builder: (context, state, child) =>
|
||||||
|
AppTheme.isLargeScreen(context) ? ChatListShell(child: child) : child,
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/chat',
|
path: '/chat',
|
||||||
name: 'chat',
|
name: 'chat',
|
||||||
builder: (context, state) => const ChatScreen(),
|
builder: (context, state) => AppTheme.isLargeScreen(context)
|
||||||
|
? const EmptyPagePlaceholder()
|
||||||
|
: const ChatScreen(),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/chat/organize',
|
path: '/chat/organize',
|
||||||
@ -173,6 +182,7 @@ abstract class AppRouter {
|
|||||||
final arguments = state.extra as ChannelDetailArguments;
|
final arguments = state.extra as ChannelDetailArguments;
|
||||||
return TitleShell(
|
return TitleShell(
|
||||||
state: state,
|
state: state,
|
||||||
|
isResponsive: true,
|
||||||
child: ChannelDetailScreen(
|
child: ChannelDetailScreen(
|
||||||
channel: arguments.channel,
|
channel: arguments.channel,
|
||||||
profile: arguments.profile,
|
profile: arguments.profile,
|
||||||
@ -258,6 +268,22 @@ abstract class AppRouter {
|
|||||||
child: const NotificationPreferencesScreen(),
|
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(
|
GoRoute(
|
||||||
path: '/account/view/:name',
|
path: '/account/view/:name',
|
||||||
name: 'accountProfilePage',
|
name: 'accountProfilePage',
|
||||||
|
@ -15,9 +15,7 @@ class AboutScreen extends StatelessWidget {
|
|||||||
const denseButtonStyle =
|
const denseButtonStyle =
|
||||||
ButtonStyle(visualDensity: VisualDensity(vertical: -4));
|
ButtonStyle(visualDensity: VisualDensity(vertical: -4));
|
||||||
|
|
||||||
return Material(
|
return SizedBox(
|
||||||
color: Theme.of(context).colorScheme.surface,
|
|
||||||
child: SizedBox(
|
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
@ -142,7 +140,6 @@ class AboutScreen extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -49,9 +49,7 @@ class _AccountScreenState extends State<AccountScreen> {
|
|||||||
|
|
||||||
final AuthProvider auth = Get.find();
|
final AuthProvider auth = Get.find();
|
||||||
|
|
||||||
return Material(
|
return SafeArea(
|
||||||
color: Theme.of(context).colorScheme.surface,
|
|
||||||
child: SafeArea(
|
|
||||||
child: Obx(() {
|
child: Obx(() {
|
||||||
if (auth.isAuthorized.isFalse) {
|
if (auth.isAuthorized.isFalse) {
|
||||||
return Center(
|
return Center(
|
||||||
@ -108,7 +106,7 @@ class _AccountScreenState extends State<AccountScreen> {
|
|||||||
child: ListView(
|
child: ListView(
|
||||||
children: [
|
children: [
|
||||||
if (auth.userProfile.value != null)
|
if (auth.userProfile.value != null)
|
||||||
const AccountHeading().paddingOnly(bottom: 8, top: 8),
|
const AccountHeading().paddingOnly(bottom: 8, top: 16),
|
||||||
...(actionItems.map(
|
...(actionItems.map(
|
||||||
(x) => ListTile(
|
(x) => ListTile(
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 34),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 34),
|
||||||
@ -131,6 +129,24 @@ class _AccountScreenState extends State<AccountScreen> {
|
|||||||
AppRouter.instance.pushNamed('settings');
|
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)
|
if (auth.isAuthorized.value)
|
||||||
ListTile(
|
ListTile(
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 34),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 34),
|
||||||
@ -155,7 +171,6 @@ class _AccountScreenState extends State<AccountScreen> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
94
lib/screens/account/audit_log.dart
Normal file
94
lib/screens/account/audit_log.dart
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.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=10&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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_getEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return InfiniteList(
|
||||||
|
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,
|
||||||
|
endChild: Container(
|
||||||
|
child: Card(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
element.type,
|
||||||
|
style: GoogleFonts.robotoMono(fontSize: 15),
|
||||||
|
),
|
||||||
|
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/providers/relation.dart';
|
||||||
import 'package:solian/theme.dart';
|
import 'package:solian/theme.dart';
|
||||||
import 'package:solian/widgets/account/relative_list.dart';
|
import 'package:solian/widgets/account/relative_list.dart';
|
||||||
|
import 'package:solian/widgets/root_container.dart';
|
||||||
|
|
||||||
class FriendScreen extends StatefulWidget {
|
class FriendScreen extends StatefulWidget {
|
||||||
const FriendScreen({super.key});
|
const FriendScreen({super.key});
|
||||||
@ -117,8 +118,7 @@ class _FriendScreenState extends State<FriendScreen>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Material(
|
return RootContainer(
|
||||||
color: Theme.of(context).colorScheme.surface,
|
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
|
@ -59,9 +59,9 @@ class _NotificationPreferencesScreenState
|
|||||||
});
|
});
|
||||||
if (resp.statusCode != 200) {
|
if (resp.statusCode != 200) {
|
||||||
context.showErrorDialog(RequestException(resp));
|
context.showErrorDialog(RequestException(resp));
|
||||||
}
|
} else {
|
||||||
|
|
||||||
context.showSnackbar('preferencesApplied'.tr);
|
context.showSnackbar('preferencesApplied'.tr);
|
||||||
|
}
|
||||||
|
|
||||||
setState(() => _isBusy = false);
|
setState(() => _isBusy = false);
|
||||||
}
|
}
|
||||||
@ -74,9 +74,7 @@ class _NotificationPreferencesScreenState
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Material(
|
return Column(
|
||||||
color: Theme.of(context).colorScheme.surface,
|
|
||||||
child: Column(
|
|
||||||
children: [
|
children: [
|
||||||
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
|
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
|
||||||
ListTile(
|
ListTile(
|
||||||
@ -112,7 +110,6 @@ class _NotificationPreferencesScreenState
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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:flutter_animate/flutter_animate.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';
|
||||||
|
|
||||||
|
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: [
|
||||||
|
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
|
||||||
|
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;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,3 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_animate/flutter_animate.dart';
|
import 'package:flutter_animate/flutter_animate.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
@ -9,6 +7,7 @@ import 'package:image_picker/image_picker.dart';
|
|||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:solian/exts.dart';
|
import 'package:solian/exts.dart';
|
||||||
import 'package:solian/models/attachment.dart';
|
import 'package:solian/models/attachment.dart';
|
||||||
|
import 'package:solian/platform.dart';
|
||||||
import 'package:solian/providers/auth.dart';
|
import 'package:solian/providers/auth.dart';
|
||||||
import 'package:solian/providers/content/attachment.dart';
|
import 'package:solian/providers/content/attachment.dart';
|
||||||
import 'package:solian/services.dart';
|
import 'package:solian/services.dart';
|
||||||
@ -77,9 +76,12 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
|
|||||||
final AuthProvider auth = Get.find();
|
final AuthProvider auth = Get.find();
|
||||||
if (auth.isAuthorized.isFalse) return;
|
if (auth.isAuthorized.isFalse) return;
|
||||||
|
|
||||||
|
XFile file;
|
||||||
|
|
||||||
final image = await _imagePicker.pickImage(source: ImageSource.gallery);
|
final image = await _imagePicker.pickImage(source: ImageSource.gallery);
|
||||||
if (image == null) return;
|
if (image == null) return;
|
||||||
|
|
||||||
|
if (PlatformInfo.canCropImage) {
|
||||||
CroppedFile? croppedFile = await ImageCropper().cropImage(
|
CroppedFile? croppedFile = await ImageCropper().cropImage(
|
||||||
sourcePath: image.path,
|
sourcePath: image.path,
|
||||||
uiSettings: [
|
uiSettings: [
|
||||||
@ -106,7 +108,10 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (croppedFile == null) return;
|
if (croppedFile == null) return;
|
||||||
final file = File(croppedFile.path);
|
file = XFile(croppedFile.path);
|
||||||
|
} else {
|
||||||
|
file = XFile(image.path);
|
||||||
|
}
|
||||||
|
|
||||||
setState(() => _isBusy = true);
|
setState(() => _isBusy = true);
|
||||||
|
|
||||||
@ -181,15 +186,13 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
const double padding = 32;
|
const double padding = 32;
|
||||||
|
|
||||||
return Material(
|
return ListView(
|
||||||
color: Theme.of(context).colorScheme.surface,
|
|
||||||
child: ListView(
|
|
||||||
children: [
|
children: [
|
||||||
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
|
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
|
||||||
const Gap(24),
|
const Gap(24),
|
||||||
Stack(
|
Stack(
|
||||||
children: [
|
children: [
|
||||||
AccountAvatar(content: _avatar, radius: 40),
|
AttachedCircleAvatar(content: _avatar, radius: 40),
|
||||||
Positioned(
|
Positioned(
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
left: 40,
|
left: 40,
|
||||||
@ -338,7 +341,6 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
|
|||||||
],
|
],
|
||||||
).paddingSymmetric(horizontal: padding),
|
).paddingSymmetric(horizontal: padding),
|
||||||
],
|
],
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,8 +26,8 @@ import 'package:solian/widgets/app_bar_leading.dart';
|
|||||||
import 'package:solian/widgets/attachments/attachment_list.dart';
|
import 'package:solian/widgets/attachments/attachment_list.dart';
|
||||||
import 'package:solian/widgets/daily_sign/history_chart.dart';
|
import 'package:solian/widgets/daily_sign/history_chart.dart';
|
||||||
import 'package:solian/widgets/posts/post_list.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/reports/abuse_report.dart';
|
||||||
|
import 'package:solian/widgets/root_container.dart';
|
||||||
import 'package:solian/widgets/sized_container.dart';
|
import 'package:solian/widgets/sized_container.dart';
|
||||||
|
|
||||||
class AccountProfilePage extends StatefulWidget {
|
class AccountProfilePage extends StatefulWidget {
|
||||||
@ -230,11 +230,14 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (_isBusy || _userinfo == null) {
|
if (_isBusy || _userinfo == null) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return RootContainer(
|
||||||
|
child: const Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Material(
|
return RootContainer(
|
||||||
color: Theme.of(context).colorScheme.surface,
|
|
||||||
child: DefaultTabController(
|
child: DefaultTabController(
|
||||||
length: 3,
|
length: 3,
|
||||||
child: NestedScrollView(
|
child: NestedScrollView(
|
||||||
@ -250,10 +253,15 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
|
|||||||
height: 56,
|
height: 56,
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
AppBarLeadingButton.adaptive(context) ?? const Gap(8),
|
AppBarLeadingButton.adaptive(
|
||||||
|
context,
|
||||||
|
forceBack: true,
|
||||||
|
) ??
|
||||||
|
const Gap(8),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
if (_userinfo != null)
|
if (_userinfo != null)
|
||||||
AccountAvatar(content: _userinfo!.avatar, radius: 16),
|
AttachedCircleAvatar(
|
||||||
|
content: _userinfo!.avatar, radius: 16),
|
||||||
const Gap(12),
|
const Gap(12),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
@ -580,8 +588,6 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
|
|||||||
color:
|
color:
|
||||||
Theme.of(context).colorScheme.surfaceContainerLow,
|
Theme.of(context).colorScheme.surfaceContainerLow,
|
||||||
child: PostListEntryWidget(
|
child: PostListEntryWidget(
|
||||||
backgroundColor:
|
|
||||||
Theme.of(context).colorScheme.surfaceContainerLow,
|
|
||||||
item: element,
|
item: element,
|
||||||
isClickable: true,
|
isClickable: true,
|
||||||
isNestedClickable: true,
|
isNestedClickable: true,
|
||||||
@ -601,7 +607,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
|
|||||||
child: Center(child: CircularProgressIndicator()),
|
child: Center(child: CircularProgressIndicator()),
|
||||||
),
|
),
|
||||||
if (_userinfo != null)
|
if (_userinfo != null)
|
||||||
PostWarpedListWidget(
|
ControlledPostListWidget(
|
||||||
isPinned: false,
|
isPinned: false,
|
||||||
controller: _postController.pagingController,
|
controller: _postController.pagingController,
|
||||||
onUpdate: () => _postController.reloadAllOver(),
|
onUpdate: () => _postController.reloadAllOver(),
|
||||||
|
@ -7,7 +7,6 @@ import 'package:solian/exceptions/request.dart';
|
|||||||
import 'package:solian/exts.dart';
|
import 'package:solian/exts.dart';
|
||||||
import 'package:solian/models/auth.dart';
|
import 'package:solian/models/auth.dart';
|
||||||
import 'package:solian/providers/auth.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/content/realm.dart';
|
||||||
import 'package:solian/providers/relation.dart';
|
import 'package:solian/providers/relation.dart';
|
||||||
import 'package:solian/providers/websocket.dart';
|
import 'package:solian/providers/websocket.dart';
|
||||||
@ -177,7 +176,6 @@ class _SignInScreenState extends State<SignInScreen> {
|
|||||||
await auth.refreshAuthorizeStatus();
|
await auth.refreshAuthorizeStatus();
|
||||||
await auth.refreshUserProfile();
|
await auth.refreshUserProfile();
|
||||||
|
|
||||||
Get.find<ChannelProvider>().refreshAvailableChannel();
|
|
||||||
Get.find<RealmProvider>().refreshAvailableRealms();
|
Get.find<RealmProvider>().refreshAvailableRealms();
|
||||||
Get.find<RelationshipProvider>().refreshRelativeList();
|
Get.find<RelationshipProvider>().refreshRelativeList();
|
||||||
Get.find<WebSocketProvider>().registerPushNotifications();
|
Get.find<WebSocketProvider>().registerPushNotifications();
|
||||||
@ -218,10 +216,10 @@ class _SignInScreenState extends State<SignInScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Material(
|
return CenteredContainer(
|
||||||
color: Theme.of(context).colorScheme.surface,
|
|
||||||
child: CenteredContainer(
|
|
||||||
maxWidth: 360,
|
maxWidth: 360,
|
||||||
|
child: Theme(
|
||||||
|
data: Theme.of(context).copyWith(canvasColor: Colors.transparent),
|
||||||
child: PageTransitionSwitcher(
|
child: PageTransitionSwitcher(
|
||||||
transitionBuilder: (
|
transitionBuilder: (
|
||||||
Widget child,
|
Widget child,
|
||||||
|
@ -65,9 +65,7 @@ class _SignUpScreenState extends State<SignUpScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Material(
|
return CenteredContainer(
|
||||||
color: Theme.of(context).colorScheme.surface,
|
|
||||||
child: CenteredContainer(
|
|
||||||
maxWidth: 360,
|
maxWidth: 360,
|
||||||
child: ListView(
|
child: ListView(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
@ -96,8 +94,7 @@ class _SignUpScreenState extends State<SignUpScreen> {
|
|||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
labelText: 'username'.tr,
|
labelText: 'username'.tr,
|
||||||
),
|
),
|
||||||
onTapOutside: (_) =>
|
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
FocusManager.instance.primaryFocus?.unfocus(),
|
|
||||||
),
|
),
|
||||||
const Gap(12),
|
const Gap(12),
|
||||||
TextField(
|
TextField(
|
||||||
@ -110,8 +107,7 @@ class _SignUpScreenState extends State<SignUpScreen> {
|
|||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
labelText: 'nickname'.tr,
|
labelText: 'nickname'.tr,
|
||||||
),
|
),
|
||||||
onTapOutside: (_) =>
|
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
FocusManager.instance.primaryFocus?.unfocus(),
|
|
||||||
),
|
),
|
||||||
const Gap(12),
|
const Gap(12),
|
||||||
TextField(
|
TextField(
|
||||||
@ -124,8 +120,7 @@ class _SignUpScreenState extends State<SignUpScreen> {
|
|||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
labelText: 'email'.tr,
|
labelText: 'email'.tr,
|
||||||
),
|
),
|
||||||
onTapOutside: (_) =>
|
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
FocusManager.instance.primaryFocus?.unfocus(),
|
|
||||||
),
|
),
|
||||||
const Gap(12),
|
const Gap(12),
|
||||||
TextField(
|
TextField(
|
||||||
@ -139,8 +134,7 @@ class _SignUpScreenState extends State<SignUpScreen> {
|
|||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
labelText: 'password'.tr,
|
labelText: 'password'.tr,
|
||||||
),
|
),
|
||||||
onTapOutside: (_) =>
|
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
FocusManager.instance.primaryFocus?.unfocus(),
|
|
||||||
onSubmitted: (_) => _performAction(context),
|
onSubmitted: (_) => _performAction(context),
|
||||||
),
|
),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
@ -206,7 +200,6 @@ class _SignUpScreenState extends State<SignUpScreen> {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
|
||||||
).paddingAll(24),
|
).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_controls.dart';
|
||||||
import 'package:solian/widgets/chat/call/call_participant.dart';
|
import 'package:solian/widgets/chat/call/call_participant.dart';
|
||||||
import 'package:livekit_client/livekit_client.dart' as livekit;
|
import 'package:livekit_client/livekit_client.dart' as livekit;
|
||||||
|
import 'package:solian/widgets/root_container.dart';
|
||||||
|
|
||||||
class CallScreen extends StatefulWidget {
|
class CallScreen extends StatefulWidget {
|
||||||
final bool hideAppBar;
|
final bool hideAppBar;
|
||||||
@ -197,8 +198,7 @@ class _CallScreenState extends State<CallScreen> with TickerProviderStateMixin {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final ChatCallProvider ctrl = Get.find();
|
final ChatCallProvider ctrl = Get.find();
|
||||||
|
|
||||||
return Material(
|
return RootContainer(
|
||||||
color: Theme.of(context).colorScheme.surface,
|
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
appBar: widget.hideAppBar
|
appBar: widget.hideAppBar
|
||||||
? null
|
? null
|
||||||
|
@ -3,6 +3,7 @@ import 'dart:ui';
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:solian/controllers/chat_events_controller.dart';
|
import 'package:solian/controllers/chat_events_controller.dart';
|
||||||
import 'package:solian/exts.dart';
|
import 'package:solian/exts.dart';
|
||||||
import 'package:solian/models/call.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_message_input.dart';
|
||||||
import 'package:solian/widgets/chat/chat_typing_indicator.dart';
|
import 'package:solian/widgets/chat/chat_typing_indicator.dart';
|
||||||
import 'package:solian/widgets/current_state_action.dart';
|
import 'package:solian/widgets/current_state_action.dart';
|
||||||
|
import 'package:solian/widgets/root_container.dart';
|
||||||
|
|
||||||
class ChannelChatScreen extends StatefulWidget {
|
class ChannelChatScreen extends StatefulWidget {
|
||||||
final String alias;
|
final String alias;
|
||||||
@ -179,6 +181,8 @@ class _ChannelChatScreenState extends State<ChannelChatScreen>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
late SharedPreferences _prefs;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@ -189,11 +193,14 @@ class _ChannelChatScreenState extends State<ChannelChatScreen>
|
|||||||
_chatController = ChatEventController();
|
_chatController = ChatEventController();
|
||||||
_chatController.initialize();
|
_chatController.initialize();
|
||||||
|
|
||||||
|
SharedPreferences.getInstance().then((inst) {
|
||||||
|
_prefs = inst;
|
||||||
_getOngoingCall();
|
_getOngoingCall();
|
||||||
_getChannel().then((_) {
|
_getChannel().then((_) {
|
||||||
_chatController.getInitialEvents(_channel!, widget.realm);
|
_chatController.getInitialEvents(_channel!, widget.realm);
|
||||||
_listenMessages();
|
_listenMessages();
|
||||||
});
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -201,16 +208,18 @@ class _ChannelChatScreenState extends State<ChannelChatScreen>
|
|||||||
String title = _channel?.name ?? 'loading'.tr;
|
String title = _channel?.name ?? 'loading'.tr;
|
||||||
String? placeholder;
|
String? placeholder;
|
||||||
|
|
||||||
if (_channel?.type == 1) {
|
|
||||||
final otherside =
|
final otherside =
|
||||||
_channel!.members!.where((e) => e.account.id != _accountId).first;
|
_channel?.members!.where((e) => e.account.id != _accountId).firstOrNull;
|
||||||
|
|
||||||
|
if (_channel?.type == 1 && otherside != null) {
|
||||||
title = otherside.account.nick;
|
title = otherside.account.nick;
|
||||||
placeholder = 'messageInputPlaceholder'.trParams(
|
placeholder = 'messageInputPlaceholder'.trParams(
|
||||||
{'channel': '@${otherside.account.name}'},
|
{'channel': '@${otherside.account.name}'},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return ResponsiveRootContainer(
|
||||||
|
child: Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: AppBarLeadingButton.adaptive(context),
|
leading: AppBarLeadingButton.adaptive(context),
|
||||||
title: AppBarTitle(title),
|
title: AppBarTitle(title),
|
||||||
@ -246,7 +255,8 @@ class _ChannelChatScreenState extends State<ChannelChatScreen>
|
|||||||
.then((value) {
|
.then((value) {
|
||||||
if (value == false) AppRouter.instance.pop();
|
if (value == false) AppRouter.instance.pop();
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
final resp = Channel.fromJson(value as Map<String, dynamic>);
|
final resp =
|
||||||
|
Channel.fromJson(value as Map<String, dynamic>);
|
||||||
_getChannel(alias: resp.alias);
|
_getChannel(alias: resp.alias);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -274,7 +284,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen>
|
|||||||
channel: _channel!,
|
channel: _channel!,
|
||||||
ongoingCall: _ongoingCall!,
|
ongoingCall: _ongoingCall!,
|
||||||
onJoin: () {
|
onJoin: () {
|
||||||
if (!AppTheme.isLargeScreen(context)) {
|
if (!AppTheme.isUltraLargeScreen(context)) {
|
||||||
final ChatCallProvider call = Get.find();
|
final ChatCallProvider call = Get.find();
|
||||||
call.gotoScreen(context);
|
call.gotoScreen(context);
|
||||||
}
|
}
|
||||||
@ -282,6 +292,9 @@ class _ChannelChatScreenState extends State<ChannelChatScreen>
|
|||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ChatEventList(
|
child: ChatEventList(
|
||||||
|
noAnimated:
|
||||||
|
_prefs.getBool('non_animated_message_list') ??
|
||||||
|
false,
|
||||||
scope: widget.realm,
|
scope: widget.realm,
|
||||||
channel: _channel!,
|
channel: _channel!,
|
||||||
chatController: _chatController,
|
chatController: _chatController,
|
||||||
@ -328,7 +341,8 @@ class _ChannelChatScreenState extends State<ChannelChatScreen>
|
|||||||
),
|
),
|
||||||
Obx(() {
|
Obx(() {
|
||||||
final ChatCallProvider call = Get.find();
|
final ChatCallProvider call = Get.find();
|
||||||
if (call.isMounted.value && AppTheme.isLargeScreen(context)) {
|
if (call.isMounted.value &&
|
||||||
|
AppTheme.isUltraLargeScreen(context)) {
|
||||||
return const Expanded(
|
return const Expanded(
|
||||||
child: Row(children: [
|
child: Row(children: [
|
||||||
VerticalDivider(width: 0.3, thickness: 0.3),
|
VerticalDivider(width: 0.3, thickness: 0.3),
|
||||||
@ -346,6 +360,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen>
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ import 'package:solian/providers/content/channel.dart';
|
|||||||
import 'package:solian/router.dart';
|
import 'package:solian/router.dart';
|
||||||
import 'package:solian/theme.dart';
|
import 'package:solian/theme.dart';
|
||||||
import 'package:solian/widgets/app_bar_title.dart';
|
import 'package:solian/widgets/app_bar_title.dart';
|
||||||
|
import 'package:solian/widgets/root_container.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
class ChannelOrganizeArguments {
|
class ChannelOrganizeArguments {
|
||||||
@ -114,8 +115,7 @@ class _ChannelOrganizeScreenState extends State<ChannelOrganizeScreen> {
|
|||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
return Material(
|
return ResponsiveRootContainer(
|
||||||
color: Theme.of(context).colorScheme.surface,
|
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: AppBarTitle('channelOrganizing'.tr),
|
title: AppBarTitle('channelOrganizing'.tr),
|
||||||
|
@ -1,47 +1,156 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:solian/controllers/chat_events_controller.dart';
|
||||||
import 'package:solian/exts.dart';
|
import 'package:solian/exts.dart';
|
||||||
|
import 'package:solian/models/channel.dart';
|
||||||
import 'package:solian/providers/auth.dart';
|
import 'package:solian/providers/auth.dart';
|
||||||
import 'package:solian/providers/content/channel.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/router.dart';
|
||||||
import 'package:solian/screens/account/notification.dart';
|
import 'package:solian/screens/account/notification.dart';
|
||||||
import 'package:solian/theme.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/account/signin_required_overlay.dart';
|
||||||
import 'package:solian/widgets/app_bar_leading.dart';
|
import 'package:solian/widgets/app_bar_leading.dart';
|
||||||
import 'package:solian/widgets/app_bar_title.dart';
|
import 'package:solian/widgets/app_bar_title.dart';
|
||||||
import 'package:solian/widgets/channel/channel_list.dart';
|
import 'package:solian/widgets/channel/channel_list.dart';
|
||||||
import 'package:solian/widgets/chat/call/chat_call_indicator.dart';
|
import 'package:solian/widgets/chat/call/chat_call_indicator.dart';
|
||||||
import 'package:solian/widgets/current_state_action.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});
|
const ChatScreen({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ChatScreen> createState() => _ChatScreenState();
|
Widget build(BuildContext context) {
|
||||||
|
return const ResponsiveRootContainer(
|
||||||
|
child: ChatList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ChatScreenState extends State<ChatScreen> {
|
class ChatListShell extends StatelessWidget {
|
||||||
late final ChannelProvider _channels;
|
final Widget? child;
|
||||||
|
|
||||||
|
const ChatListShell({super.key, required this.child});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return RootContainer(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const SizedBox(
|
||||||
|
width: 360,
|
||||||
|
child: ChatList(),
|
||||||
|
),
|
||||||
|
const VerticalDivider(thickness: 0.3, width: 0.3),
|
||||||
|
Expanded(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
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
try {
|
_loadLastMessages().then((_) {
|
||||||
_channels = Get.find();
|
if (!mounted) return;
|
||||||
_channels.refreshAvailableChannel();
|
_loadAllChannels().then((_) {
|
||||||
} catch (e) {
|
if (mounted) {
|
||||||
context.showErrorDialog(e);
|
setState(() => _isBusy = false);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final AuthProvider auth = Get.find();
|
final AuthProvider auth = Get.find();
|
||||||
|
final RealmProvider realms = Get.find();
|
||||||
|
|
||||||
return Material(
|
return Obx(
|
||||||
color: Theme.of(context).colorScheme.surface,
|
() => DefaultTabController(
|
||||||
|
length: 2 + realms.availableRealms.length,
|
||||||
|
child: ResponsiveRootContainer(
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: AppBarLeadingButton.adaptive(context),
|
leading: AppBarLeadingButton.adaptive(context),
|
||||||
@ -58,13 +167,14 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
child: ListTile(
|
child: ListTile(
|
||||||
title: Text('channelOrganizeCommon'.tr),
|
title: Text('channelOrganizeCommon'.tr),
|
||||||
leading: const Icon(Icons.tag),
|
leading: const Icon(Icons.tag),
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
|
contentPadding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 8),
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
AppRouter.instance.pushNamed('channelOrganizing').then(
|
AppRouter.instance.pushNamed('channelOrganizing').then(
|
||||||
(value) {
|
(value) {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
_channels.refreshAvailableChannel();
|
_loadAllChannels();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -77,7 +187,8 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
FontAwesomeIcons.userGroup,
|
FontAwesomeIcons.userGroup,
|
||||||
size: 16,
|
size: 16,
|
||||||
),
|
),
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
|
contentPadding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 8),
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
final ChannelProvider channels = Get.find();
|
final ChannelProvider channels = Get.find();
|
||||||
@ -85,7 +196,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
.createDirectChannel(context, 'global')
|
.createDirectChannel(context, 'global')
|
||||||
.then((resp) {
|
.then((resp) {
|
||||||
if (resp != null) {
|
if (resp != null) {
|
||||||
_channels.refreshAvailableChannel();
|
_loadAllChannels();
|
||||||
}
|
}
|
||||||
}).catchError((e) {
|
}).catchError((e) {
|
||||||
context.showErrorDialog(e);
|
context.showErrorDialog(e);
|
||||||
@ -98,11 +209,70 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
width: AppTheme.isLargeScreen(context) ? 8 : 16,
|
width: AppTheme.isLargeScreen(context) ? 8 : 16,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
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(() {
|
body: Obx(() {
|
||||||
if (auth.isAuthorized.isFalse) {
|
if (auth.isAuthorized.isFalse) {
|
||||||
return SigninRequiredOverlay(
|
return SigninRequiredOverlay(
|
||||||
onDone: () => _channels.refreshAvailableChannel(),
|
onDone: () => _loadAllChannels(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,37 +280,50 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
Obx(() {
|
|
||||||
if (_channels.isLoading.isFalse) {
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
} else {
|
|
||||||
return const LinearProgressIndicator();
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
const ChatCallCurrentIndicator(),
|
const ChatCallCurrentIndicator(),
|
||||||
|
if (_isBusy) const LoadingIndicator(),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: CenteredContainer(
|
child: TabBarView(
|
||||||
child: RefreshIndicator(
|
children: [
|
||||||
onRefresh: _channels.refreshAvailableChannel,
|
RefreshIndicator(
|
||||||
child: Obx(
|
onRefresh: _loadNormalChannels,
|
||||||
() => ChannelListWidget(
|
child: ChannelListWidget(
|
||||||
noCategory: true,
|
channels: _sortChannels([
|
||||||
channels: List.from([
|
..._normalChannels,
|
||||||
..._channels.groupChannels
|
..._directChannels,
|
||||||
.where((x) => x.realmId == null),
|
..._realmChannels.values.expand((x) => x),
|
||||||
..._channels.directChannels
|
|
||||||
]),
|
]),
|
||||||
selfId: selfId,
|
selfId: selfId,
|
||||||
useReplace: true,
|
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),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -75,11 +75,13 @@ class _DashboardScreenState extends State<DashboardScreen> {
|
|||||||
final src = Get.find<MessagesFetchingProvider>();
|
final src = Get.find<MessagesFetchingProvider>();
|
||||||
final out = await src.getWhatsNewEvents(_lastRead.messagesLastReadAt!);
|
final out = await src.getWhatsNewEvents(_lastRead.messagesLastReadAt!);
|
||||||
if (out == null) return;
|
if (out == null) return;
|
||||||
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_currentMessages = out.$1;
|
_currentMessages = out.$1;
|
||||||
_currentMessagesCount = out.$2;
|
_currentMessagesCount = out.$2;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool _signingDaily = true;
|
bool _signingDaily = true;
|
||||||
DailySignRecord? _signRecord;
|
DailySignRecord? _signRecord;
|
||||||
@ -89,14 +91,18 @@ class _DashboardScreenState extends State<DashboardScreen> {
|
|||||||
try {
|
try {
|
||||||
_signRecord = await _dailySign.getToday();
|
_signRecord = await _dailySign.getToday();
|
||||||
_dailySign.listLastRecord(14).then((value) {
|
_dailySign.listLastRecord(14).then((value) {
|
||||||
|
if (mounted) {
|
||||||
setState(() => _signRecordHistory = value);
|
setState(() => _signRecordHistory = value);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
context.showErrorDialog(e);
|
context.showErrorDialog(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
setState(() => _signingDaily = false);
|
setState(() => _signingDaily = false);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _signDaily() async {
|
Future<void> _signDaily() async {
|
||||||
setState(() => _signingDaily = true);
|
setState(() => _signingDaily = true);
|
||||||
@ -147,7 +153,7 @@ class _DashboardScreenState extends State<DashboardScreen> {
|
|||||||
),
|
),
|
||||||
Text(DateFormat('yyyy/MM/dd').format(DateTime.now().toUtc())),
|
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(
|
Card(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
@ -354,7 +360,7 @@ class _DashboardScreenState extends State<DashboardScreen> {
|
|||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.arrow_forward),
|
icon: const Icon(Icons.arrow_forward),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
AppRouter.instance.goNamed('feed');
|
AppRouter.instance.goNamed('explore');
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -383,9 +389,10 @@ class _DashboardScreenState extends State<DashboardScreen> {
|
|||||||
onUpdate: (_) {
|
onUpdate: (_) {
|
||||||
_pullPosts();
|
_pullPosts();
|
||||||
},
|
},
|
||||||
backgroundColor: Theme.of(context)
|
padding: EdgeInsets.symmetric(
|
||||||
.colorScheme
|
vertical: 8,
|
||||||
.surfaceContainerLow,
|
horizontal: 4,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -519,7 +526,7 @@ class _DashboardScreenState extends State<DashboardScreen> {
|
|||||||
style: TextStyle(color: _unFocusColor, fontSize: 12),
|
style: TextStyle(color: _unFocusColor, fontSize: 12),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
).paddingAll(8),
|
).paddingOnly(left: 8, right: 8, top: 8, bottom: 50),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
@ -10,11 +11,13 @@ import 'package:solian/router.dart';
|
|||||||
import 'package:solian/screens/account/notification.dart';
|
import 'package:solian/screens/account/notification.dart';
|
||||||
import 'package:solian/theme.dart';
|
import 'package:solian/theme.dart';
|
||||||
import 'package:solian/widgets/account/signin_required_overlay.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/current_state_action.dart';
|
||||||
import 'package:solian/widgets/app_bar_leading.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/posts/post_shuffle_swiper.dart';
|
||||||
import 'package:solian/widgets/posts/post_warped_list.dart';
|
import 'package:solian/widgets/root_container.dart';
|
||||||
|
|
||||||
class ExploreScreen extends StatefulWidget {
|
class ExploreScreen extends StatefulWidget {
|
||||||
const ExploreScreen({super.key});
|
const ExploreScreen({super.key});
|
||||||
@ -55,10 +58,8 @@ class _ExploreScreenState extends State<ExploreScreen>
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final AuthProvider auth = Get.find();
|
final AuthProvider auth = Get.find();
|
||||||
final NavigationStateProvider navState = Get.find();
|
|
||||||
|
|
||||||
return Material(
|
return RootContainer(
|
||||||
color: Theme.of(context).colorScheme.surface,
|
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
floatingActionButton: FloatingActionButton(
|
floatingActionButton: FloatingActionButton(
|
||||||
child: const Icon(Icons.add),
|
child: const Icon(Icons.add),
|
||||||
@ -81,27 +82,98 @@ class _ExploreScreenState extends State<ExploreScreen>
|
|||||||
body: NestedScrollView(
|
body: NestedScrollView(
|
||||||
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
|
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
|
||||||
return [
|
return [
|
||||||
SliverAppBar(
|
SliverLayoutBuilder(
|
||||||
title: AppBarTitle('explore'.tr),
|
builder: (context, constraints) {
|
||||||
centerTitle: false,
|
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,
|
floating: true,
|
||||||
toolbarHeight: AppTheme.toolbarHeight(context),
|
toolbarHeight: AppTheme.toolbarHeight(context),
|
||||||
leading: AppBarLeadingButton.adaptive(context),
|
leading: AppBarLeadingButton.adaptive(context),
|
||||||
actions: [
|
actions: [
|
||||||
const BackgroundStateWidget(),
|
const BackgroundStateWidget(),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.search),
|
||||||
|
onPressed: () {
|
||||||
|
AppRouter.instance.pushNamed('postSearch');
|
||||||
|
},
|
||||||
|
),
|
||||||
const NotificationButton(),
|
const NotificationButton(),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: AppTheme.isLargeScreen(context) ? 8 : 16,
|
width: AppTheme.isLargeScreen(context) ? 8 : 16,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
bottom: TabBar(
|
);
|
||||||
controller: _tabController,
|
},
|
||||||
tabs: [
|
|
||||||
Tab(text: 'postListNews'.tr),
|
|
||||||
Tab(text: 'postListFriends'.tr),
|
|
||||||
Tab(text: 'postListShuffle'.tr),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
@ -114,16 +186,6 @@ class _ExploreScreenState extends State<ExploreScreen>
|
|||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
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(
|
Expanded(
|
||||||
child: TabBarView(
|
child: TabBarView(
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
@ -132,7 +194,13 @@ class _ExploreScreenState extends State<ExploreScreen>
|
|||||||
RefreshIndicator(
|
RefreshIndicator(
|
||||||
onRefresh: () => _postController.reloadAllOver(),
|
onRefresh: () => _postController.reloadAllOver(),
|
||||||
child: CustomScrollView(slivers: [
|
child: CustomScrollView(slivers: [
|
||||||
PostWarpedListWidget(
|
ControlledPostListWidget(
|
||||||
|
padding: AppTheme.isLargeScreen(context)
|
||||||
|
? EdgeInsets.symmetric(
|
||||||
|
horizontal: 4,
|
||||||
|
vertical: 8,
|
||||||
|
)
|
||||||
|
: EdgeInsets.zero,
|
||||||
controller: _postController.pagingController,
|
controller: _postController.pagingController,
|
||||||
onUpdate: () => _postController.reloadAllOver(),
|
onUpdate: () => _postController.reloadAllOver(),
|
||||||
),
|
),
|
||||||
@ -143,7 +211,10 @@ class _ExploreScreenState extends State<ExploreScreen>
|
|||||||
return RefreshIndicator(
|
return RefreshIndicator(
|
||||||
onRefresh: () => _postController.reloadAllOver(),
|
onRefresh: () => _postController.reloadAllOver(),
|
||||||
child: CustomScrollView(slivers: [
|
child: CustomScrollView(slivers: [
|
||||||
PostWarpedListWidget(
|
ControlledPostListWidget(
|
||||||
|
padding: AppTheme.isLargeScreen(context)
|
||||||
|
? EdgeInsets.symmetric(horizontal: 16)
|
||||||
|
: EdgeInsets.zero,
|
||||||
controller: _postController.pagingController,
|
controller: _postController.pagingController,
|
||||||
onUpdate: () => _postController.reloadAllOver(),
|
onUpdate: () => _postController.reloadAllOver(),
|
||||||
),
|
),
|
||||||
@ -178,106 +249,3 @@ class _ExploreScreenState extends State<ExploreScreen>
|
|||||||
super.dispose();
|
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,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('postSearchWithTag'.trParams({'key': widget.tag!})),
|
|
||||||
),
|
|
||||||
if (widget.category != null)
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(Icons.category),
|
|
||||||
tileColor: Theme.of(context).colorScheme.surfaceContainer,
|
|
||||||
title: Text('postSearchWithCategory'
|
|
||||||
.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();
|
|
||||||
}
|
|
||||||
}
|
|
@ -9,6 +9,7 @@ import 'package:solian/widgets/app_bar_leading.dart';
|
|||||||
import 'package:solian/widgets/app_bar_title.dart';
|
import 'package:solian/widgets/app_bar_title.dart';
|
||||||
import 'package:solian/widgets/posts/post_action.dart';
|
import 'package:solian/widgets/posts/post_action.dart';
|
||||||
import 'package:solian/widgets/posts/post_owned_list.dart';
|
import 'package:solian/widgets/posts/post_owned_list.dart';
|
||||||
|
import 'package:solian/widgets/root_container.dart';
|
||||||
|
|
||||||
class DraftBoxScreen extends StatefulWidget {
|
class DraftBoxScreen extends StatefulWidget {
|
||||||
const DraftBoxScreen({super.key});
|
const DraftBoxScreen({super.key});
|
||||||
@ -54,8 +55,7 @@ class _DraftBoxScreenState extends State<DraftBoxScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Material(
|
return RootContainer(
|
||||||
color: Theme.of(context).colorScheme.surface,
|
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: AppBarLeadingButton.adaptive(context),
|
leading: AppBarLeadingButton.adaptive(context),
|
@ -4,6 +4,8 @@ import 'package:solian/exts.dart';
|
|||||||
import 'package:solian/models/post.dart';
|
import 'package:solian/models/post.dart';
|
||||||
import 'package:solian/providers/content/posts.dart';
|
import 'package:solian/providers/content/posts.dart';
|
||||||
import 'package:solian/providers/last_read.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_item.dart';
|
import 'package:solian/widgets/posts/post_item.dart';
|
||||||
import 'package:solian/widgets/posts/post_replies.dart';
|
import 'package:solian/widgets/posts/post_replies.dart';
|
||||||
|
|
||||||
@ -22,37 +24,37 @@ class PostDetailScreen extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _PostDetailScreenState extends State<PostDetailScreen> {
|
class _PostDetailScreenState extends State<PostDetailScreen> {
|
||||||
Post? item;
|
bool _isBusy = true;
|
||||||
|
|
||||||
Future<Post?> getDetail() async {
|
Post? _item;
|
||||||
if (widget.post != null) {
|
|
||||||
item = widget.post;
|
|
||||||
Get.find<LastReadProvider>().feedLastReadAt = item?.id;
|
|
||||||
return widget.post;
|
|
||||||
}
|
|
||||||
|
|
||||||
final PostProvider provider = Get.find();
|
Future<void> _getDetail() async {
|
||||||
|
final PostProvider posts = Get.find();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final resp = await provider.getPost(widget.id);
|
final resp = await posts.getPost(widget.id);
|
||||||
item = Post.fromJson(resp.body);
|
_item = Post.fromJson(resp.body);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
context.showErrorDialog(e).then((_) => Navigator.pop(context));
|
context.showErrorDialog(e).then((_) => Navigator.pop(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
Get.find<LastReadProvider>().feedLastReadAt = item?.id;
|
Get.find<LastReadProvider>().feedLastReadAt = _item?.id;
|
||||||
|
|
||||||
return item;
|
setState(() => _isBusy = false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
if (widget.post != null) {
|
||||||
|
_item = widget.post;
|
||||||
|
}
|
||||||
|
_getDetail();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Material(
|
if (_isBusy && _item == null) {
|
||||||
color: Theme.of(context).colorScheme.surface,
|
|
||||||
child: FutureBuilder(
|
|
||||||
future: getDetail(),
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
if (!snapshot.hasData || snapshot.data == null) {
|
|
||||||
return const Center(
|
return const Center(
|
||||||
child: CircularProgressIndicator(),
|
child: CircularProgressIndicator(),
|
||||||
);
|
);
|
||||||
@ -60,20 +62,30 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
|
|||||||
|
|
||||||
return CustomScrollView(
|
return CustomScrollView(
|
||||||
slivers: [
|
slivers: [
|
||||||
|
if (_isBusy)
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: LoadingIndicator(),
|
||||||
|
),
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: PostItem(
|
child: PostItem(
|
||||||
item: item!,
|
item: _item!,
|
||||||
isClickable: false,
|
isClickable: false,
|
||||||
isOverrideEmbedClickable: true,
|
isOverrideEmbedClickable: true,
|
||||||
isFullDate: true,
|
isFullDate: true,
|
||||||
isFullContent: true,
|
|
||||||
isShowReply: false,
|
isShowReply: false,
|
||||||
isContentSelectable: true,
|
isContentSelectable: true,
|
||||||
|
padding: AppTheme.isLargeScreen(context)
|
||||||
|
? EdgeInsets.symmetric(
|
||||||
|
horizontal: 4,
|
||||||
|
vertical: 8,
|
||||||
|
)
|
||||||
|
: EdgeInsets.zero,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: const Divider(thickness: 0.3, height: 1)
|
child: const Divider(thickness: 0.3, height: 1).paddingOnly(
|
||||||
.paddingOnly(top: 4),
|
top: 8,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: Align(
|
child: Align(
|
||||||
@ -84,14 +96,19 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
|
|||||||
).paddingOnly(left: 24, right: 24, top: 16),
|
).paddingOnly(left: 24, right: 24, top: 16),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
PostReplyList(item: item!),
|
PostReplyList(
|
||||||
|
item: _item!,
|
||||||
|
padding: AppTheme.isLargeScreen(context)
|
||||||
|
? EdgeInsets.symmetric(
|
||||||
|
horizontal: 4,
|
||||||
|
vertical: 8,
|
||||||
|
)
|
||||||
|
: EdgeInsets.zero,
|
||||||
|
),
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: SizedBox(height: MediaQuery.of(context).padding.bottom),
|
child: SizedBox(height: MediaQuery.of(context).padding.bottom),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,7 @@ import 'package:solian/widgets/app_bar_title.dart';
|
|||||||
import 'package:solian/widgets/markdown_text_content.dart';
|
import 'package:solian/widgets/markdown_text_content.dart';
|
||||||
import 'package:solian/widgets/posts/post_item.dart';
|
import 'package:solian/widgets/posts/post_item.dart';
|
||||||
import 'package:badges/badges.dart' as badges;
|
import 'package:badges/badges.dart' as badges;
|
||||||
|
import 'package:solian/widgets/root_container.dart';
|
||||||
|
|
||||||
class PostPublishArguments {
|
class PostPublishArguments {
|
||||||
final Post? edit;
|
final Post? edit;
|
||||||
@ -151,8 +152,7 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
|
|||||||
)
|
)
|
||||||
];
|
];
|
||||||
|
|
||||||
return Material(
|
return RootContainer(
|
||||||
color: Theme.of(context).colorScheme.surface,
|
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: AppBarLeadingButton.adaptive(context),
|
leading: AppBarLeadingButton.adaptive(context),
|
||||||
@ -182,7 +182,10 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
ListTile(
|
ListTile(
|
||||||
tileColor: Theme.of(context).colorScheme.surfaceContainerLow,
|
tileColor: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.surfaceContainerLow
|
||||||
|
.withOpacity(0.5),
|
||||||
title: Column(
|
title: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@ -273,115 +276,69 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
|
|||||||
),
|
),
|
||||||
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
|
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Row(
|
child: DefaultTabController(
|
||||||
|
length: 2,
|
||||||
|
child: AppTheme.isLargeScreen(context)
|
||||||
|
? Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: _PostEditorTextField(
|
||||||
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,
|
focusNode: _contentFocusNode,
|
||||||
decoration: InputDecoration.collapsed(
|
controller: _editorController,
|
||||||
hintText: 'postContentPlaceholder'.tr,
|
onUpdate: () => setState(() {}),
|
||||||
),
|
|
||||||
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 = [
|
|
||||||
_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(
|
|
||||||
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,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.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)
|
const VerticalDivider(width: 0.3, thickness: 0.3)
|
||||||
.paddingSymmetric(
|
.paddingSymmetric(horizontal: 16),
|
||||||
horizontal: 16,
|
|
||||||
),
|
|
||||||
if (AppTheme.isLargeScreen(context))
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.only(top: 12, bottom: 64),
|
||||||
child: MarkdownTextContent(
|
child: MarkdownTextContent(
|
||||||
content: _editorController.contentController.text,
|
isAutoWarp: _editorController.mode.value == 0,
|
||||||
|
content:
|
||||||
|
_editorController.contentController.text,
|
||||||
parentId: 'post-editor-preview',
|
parentId: 'post-editor-preview',
|
||||||
).paddingOnly(top: 12, right: 16),
|
).paddingOnly(right: 16),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
)
|
||||||
|
: Column(
|
||||||
|
children: [
|
||||||
|
TabBar(
|
||||||
|
tabs: [
|
||||||
|
const Tab(icon: Icon(Icons.edit)),
|
||||||
|
const Tab(icon: Icon(Icons.preview)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: TabBarView(
|
||||||
|
children: [
|
||||||
|
_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),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Material(
|
Material(
|
||||||
@ -390,6 +347,26 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const Divider(thickness: 0.3, height: 0.3),
|
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(
|
SizedBox(
|
||||||
height: 56,
|
height: 56,
|
||||||
child: ListView(
|
child: ListView(
|
||||||
@ -519,7 +496,7 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
|
|||||||
top: -4,
|
top: -4,
|
||||||
end: -6,
|
end: -6,
|
||||||
),
|
),
|
||||||
child: const Icon(Icons.preview),
|
child: const Icon(Icons.wallpaper),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
color: Theme.of(context).colorScheme.primary,
|
color: Theme.of(context).colorScheme.primary,
|
||||||
@ -546,18 +523,6 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
|
|||||||
_editorController.editPublishDate(context);
|
_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),
|
).paddingSymmetric(horizontal: 6, vertical: 8),
|
||||||
),
|
),
|
||||||
@ -577,3 +542,101 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
|
|||||||
super.dispose();
|
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);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
157
lib/screens/posts/post_search.dart
Normal file
157
lib/screens/posts/post_search.dart
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
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> {
|
||||||
|
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 provider = Get.find();
|
||||||
|
|
||||||
|
Response resp;
|
||||||
|
try {
|
||||||
|
if (_probeController.text.isEmpty) {
|
||||||
|
resp = await provider.listPost(
|
||||||
|
pageKey,
|
||||||
|
tag: widget.tag,
|
||||||
|
category: widget.category,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
resp = await provider.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() => _isBusy = false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_pagingController.addPageRequestListener(_searchPosts);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_probeController.dispose();
|
||||||
|
_pagingController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@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);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (_isBusy) const LoadingIndicator(),
|
||||||
|
Expanded(
|
||||||
|
child: RefreshIndicator(
|
||||||
|
onRefresh: () => Future.sync(() => _pagingController.refresh()),
|
||||||
|
child: CustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
ControlledPostListWidget(
|
||||||
|
controller: _pagingController,
|
||||||
|
onUpdate: () => _pagingController.refresh(),
|
||||||
|
),
|
||||||
|
SliverGap(MediaQuery.of(context).padding.bottom),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -15,6 +15,7 @@ import 'package:solian/widgets/app_bar_leading.dart';
|
|||||||
import 'package:solian/widgets/app_bar_title.dart';
|
import 'package:solian/widgets/app_bar_title.dart';
|
||||||
import 'package:solian/widgets/auto_cache_image.dart';
|
import 'package:solian/widgets/auto_cache_image.dart';
|
||||||
import 'package:solian/widgets/current_state_action.dart';
|
import 'package:solian/widgets/current_state_action.dart';
|
||||||
|
import 'package:solian/widgets/root_container.dart';
|
||||||
import 'package:solian/widgets/sized_container.dart';
|
import 'package:solian/widgets/sized_container.dart';
|
||||||
|
|
||||||
class RealmListScreen extends StatefulWidget {
|
class RealmListScreen extends StatefulWidget {
|
||||||
@ -58,8 +59,7 @@ class _RealmListScreenState extends State<RealmListScreen> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final AuthProvider auth = Get.find();
|
final AuthProvider auth = Get.find();
|
||||||
|
|
||||||
return Material(
|
return RootContainer(
|
||||||
color: Theme.of(context).colorScheme.surface,
|
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: AppBarLeadingButton.adaptive(context),
|
leading: AppBarLeadingButton.adaptive(context),
|
||||||
@ -99,6 +99,7 @@ class _RealmListScreenState extends State<RealmListScreen> {
|
|||||||
child: RefreshIndicator(
|
child: RefreshIndicator(
|
||||||
onRefresh: () => _getRealms(),
|
onRefresh: () => _getRealms(),
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
itemCount: _realms.length,
|
itemCount: _realms.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final element = _realms[index];
|
final element = _realms[index];
|
||||||
@ -155,7 +156,7 @@ class _RealmListScreenState extends State<RealmListScreen> {
|
|||||||
size: 18,
|
size: 18,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: AccountAvatar(
|
: AttachedCircleAvatar(
|
||||||
content: element.avatar!,
|
content: element.avatar!,
|
||||||
bgColor: Theme.of(context).colorScheme.primary,
|
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/screens/realms/realm_organize.dart';
|
||||||
import 'package:solian/widgets/realms/realm_deletion.dart';
|
import 'package:solian/widgets/realms/realm_deletion.dart';
|
||||||
import 'package:solian/widgets/realms/realm_member.dart';
|
import 'package:solian/widgets/realms/realm_member.dart';
|
||||||
|
import 'package:solian/widgets/root_container.dart';
|
||||||
|
|
||||||
class RealmDetailScreen extends StatefulWidget {
|
class RealmDetailScreen extends StatefulWidget {
|
||||||
final String alias;
|
final String alias;
|
||||||
@ -86,7 +87,8 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> {
|
|||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
return Column(
|
return RootContainer(
|
||||||
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
@ -141,6 +143,7 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_animate/flutter_animate.dart';
|
import 'package:flutter_animate/flutter_animate.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
@ -8,12 +6,14 @@ import 'package:image_picker/image_picker.dart';
|
|||||||
import 'package:solian/exts.dart';
|
import 'package:solian/exts.dart';
|
||||||
import 'package:solian/models/attachment.dart';
|
import 'package:solian/models/attachment.dart';
|
||||||
import 'package:solian/models/realm.dart';
|
import 'package:solian/models/realm.dart';
|
||||||
|
import 'package:solian/platform.dart';
|
||||||
import 'package:solian/providers/auth.dart';
|
import 'package:solian/providers/auth.dart';
|
||||||
import 'package:solian/providers/content/attachment.dart';
|
import 'package:solian/providers/content/attachment.dart';
|
||||||
import 'package:solian/router.dart';
|
import 'package:solian/router.dart';
|
||||||
import 'package:solian/theme.dart';
|
import 'package:solian/theme.dart';
|
||||||
import 'package:solian/widgets/app_bar_leading.dart';
|
import 'package:solian/widgets/app_bar_leading.dart';
|
||||||
import 'package:solian/widgets/app_bar_title.dart';
|
import 'package:solian/widgets/app_bar_title.dart';
|
||||||
|
import 'package:solian/widgets/root_container.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
class RealmOrganizeArguments {
|
class RealmOrganizeArguments {
|
||||||
@ -84,9 +84,12 @@ class _RealmOrganizeScreenState extends State<RealmOrganizeScreen> {
|
|||||||
final AuthProvider auth = Get.find();
|
final AuthProvider auth = Get.find();
|
||||||
if (auth.isAuthorized.isFalse) return;
|
if (auth.isAuthorized.isFalse) return;
|
||||||
|
|
||||||
|
XFile file;
|
||||||
|
|
||||||
final image = await _imagePicker.pickImage(source: ImageSource.gallery);
|
final image = await _imagePicker.pickImage(source: ImageSource.gallery);
|
||||||
if (image == null) return;
|
if (image == null) return;
|
||||||
|
|
||||||
|
if (PlatformInfo.canCropImage) {
|
||||||
CroppedFile? croppedFile = await ImageCropper().cropImage(
|
CroppedFile? croppedFile = await ImageCropper().cropImage(
|
||||||
sourcePath: image.path,
|
sourcePath: image.path,
|
||||||
uiSettings: [
|
uiSettings: [
|
||||||
@ -113,7 +116,10 @@ class _RealmOrganizeScreenState extends State<RealmOrganizeScreen> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (croppedFile == null) return;
|
if (croppedFile == null) return;
|
||||||
final file = File(croppedFile.path);
|
file = XFile(croppedFile.path);
|
||||||
|
} else {
|
||||||
|
file = XFile(image.path);
|
||||||
|
}
|
||||||
|
|
||||||
setState(() => _isBusy = true);
|
setState(() => _isBusy = true);
|
||||||
|
|
||||||
@ -184,8 +190,7 @@ class _RealmOrganizeScreenState extends State<RealmOrganizeScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Material(
|
return RootContainer(
|
||||||
color: Theme.of(context).colorScheme.surface,
|
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: AppBarLeadingButton.adaptive(context),
|
leading: AppBarLeadingButton.adaptive(context),
|
||||||
|
@ -16,6 +16,7 @@ import 'package:solian/theme.dart';
|
|||||||
import 'package:solian/widgets/app_bar_leading.dart';
|
import 'package:solian/widgets/app_bar_leading.dart';
|
||||||
import 'package:solian/widgets/channel/channel_list.dart';
|
import 'package:solian/widgets/channel/channel_list.dart';
|
||||||
import 'package:solian/widgets/posts/post_list.dart';
|
import 'package:solian/widgets/posts/post_list.dart';
|
||||||
|
import 'package:solian/widgets/root_container.dart';
|
||||||
|
|
||||||
class RealmViewScreen extends StatefulWidget {
|
class RealmViewScreen extends StatefulWidget {
|
||||||
final String alias;
|
final String alias;
|
||||||
@ -68,12 +69,7 @@ class _RealmViewScreenState extends State<RealmViewScreen> {
|
|||||||
_channels.addAll(
|
_channels.addAll(
|
||||||
resp.body.map((e) => Channel.fromJson(e)).toList().cast<Channel>(),
|
resp.body.map((e) => Channel.fromJson(e)).toList().cast<Channel>(),
|
||||||
);
|
);
|
||||||
_channels.addAll(
|
_channels.addAll(availableResp);
|
||||||
availableResp.body
|
|
||||||
.map((e) => Channel.fromJson(e))
|
|
||||||
.toList()
|
|
||||||
.cast<Channel>(),
|
|
||||||
);
|
|
||||||
_channels.retainWhere((x) => channelIdx.add(x.id));
|
_channels.retainWhere((x) => channelIdx.add(x.id));
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -91,8 +87,7 @@ class _RealmViewScreenState extends State<RealmViewScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Material(
|
return RootContainer(
|
||||||
color: Theme.of(context).colorScheme.surface,
|
|
||||||
child: DefaultTabController(
|
child: DefaultTabController(
|
||||||
length: 2,
|
length: 2,
|
||||||
child: NestedScrollView(
|
child: NestedScrollView(
|
||||||
@ -260,7 +255,6 @@ class RealmChannelListWidget extends StatelessWidget {
|
|||||||
child: ChannelListWidget(
|
child: ChannelListWidget(
|
||||||
channels: channels,
|
channels: channels,
|
||||||
selfId: auth.userProfile.value!['id'],
|
selfId: auth.userProfile.value!['id'],
|
||||||
noCategory: true,
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
@ -1,16 +1,23 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:in_app_review/in_app_review.dart';
|
import 'package:in_app_review/in_app_review.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:solian/exceptions/request.dart';
|
import 'package:solian/exceptions/request.dart';
|
||||||
import 'package:solian/exts.dart';
|
import 'package:solian/exts.dart';
|
||||||
|
import 'package:solian/models/theme.dart';
|
||||||
import 'package:solian/platform.dart';
|
import 'package:solian/platform.dart';
|
||||||
import 'package:solian/providers/auth.dart';
|
import 'package:solian/providers/auth.dart';
|
||||||
import 'package:solian/providers/database/database.dart';
|
import 'package:solian/providers/database/database.dart';
|
||||||
import 'package:solian/providers/theme_switcher.dart';
|
import 'package:solian/providers/theme_switcher.dart';
|
||||||
import 'package:solian/router.dart';
|
import 'package:solian/router.dart';
|
||||||
import 'package:solian/theme.dart';
|
|
||||||
import 'package:solian/widgets/reports/abuse_report.dart';
|
import 'package:solian/widgets/reports/abuse_report.dart';
|
||||||
|
|
||||||
class SettingScreen extends StatefulWidget {
|
class SettingScreen extends StatefulWidget {
|
||||||
@ -22,6 +29,7 @@ class SettingScreen extends StatefulWidget {
|
|||||||
|
|
||||||
class _SettingScreenState extends State<SettingScreen> {
|
class _SettingScreenState extends State<SettingScreen> {
|
||||||
SharedPreferences? _prefs;
|
SharedPreferences? _prefs;
|
||||||
|
String _docBasepath = '/';
|
||||||
|
|
||||||
Widget _buildCaptionHeader(String title) {
|
Widget _buildCaptionHeader(String title) {
|
||||||
return Container(
|
return Container(
|
||||||
@ -32,39 +40,38 @@ class _SettingScreenState extends State<SettingScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildThemeColorButton(String label, Color color) {
|
static final List<SolianThemeData> _presentTheme = [
|
||||||
return IconButton(
|
SolianThemeData(
|
||||||
icon: Icon(Icons.circle, color: color),
|
id: 'themeColorRed',
|
||||||
tooltip: label,
|
seedColor: const Color.fromRGBO(154, 98, 91, 1),
|
||||||
onPressed: () {
|
|
||||||
context.read<ThemeSwitcher>().setTheme(
|
|
||||||
AppTheme.build(
|
|
||||||
Brightness.light,
|
|
||||||
seedColor: color,
|
|
||||||
),
|
),
|
||||||
AppTheme.build(
|
SolianThemeData(
|
||||||
Brightness.dark,
|
id: 'themeColorBlue',
|
||||||
seedColor: color,
|
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),
|
||||||
),
|
),
|
||||||
);
|
|
||||||
_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)),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
getApplicationDocumentsDirectory().then((dir) {
|
||||||
|
_docBasepath = dir.path;
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
});
|
||||||
SharedPreferences.getInstance().then((inst) {
|
SharedPreferences.getInstance().then((inst) {
|
||||||
_prefs = inst;
|
_prefs = inst;
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
@ -75,19 +82,103 @@ class _SettingScreenState extends State<SettingScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Material(
|
return ListView(
|
||||||
color: Theme.of(context).colorScheme.surface,
|
|
||||||
child: ListView(
|
|
||||||
children: [
|
children: [
|
||||||
_buildCaptionHeader('themeColor'.tr),
|
_buildCaptionHeader('theme'.tr),
|
||||||
SizedBox(
|
ListTile(
|
||||||
height: 56,
|
leading: const Icon(Icons.palette),
|
||||||
child: ListView(
|
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
|
||||||
scrollDirection: Axis.horizontal,
|
title: Text('globalTheme'.tr),
|
||||||
children: _presentTheme
|
trailing: DropdownButtonHideUnderline(
|
||||||
.map((x) => _buildThemeColorButton(x.$1, x.$2))
|
child: DropdownButton2<SolianThemeData>(
|
||||||
|
isExpanded: true,
|
||||||
|
hint: Text(
|
||||||
|
'theme'.tr,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Theme.of(context).hintColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
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(),
|
.toList(),
|
||||||
).paddingSymmetric(horizontal: 12, vertical: 8),
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
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.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),
|
_buildCaptionHeader('notification'.tr),
|
||||||
Tooltip(
|
Tooltip(
|
||||||
@ -107,8 +198,7 @@ class _SettingScreenState extends State<SettingScreen> {
|
|||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
value:
|
value: _prefs?.getBool('service_background_notification') ?? false,
|
||||||
_prefs?.getBool('service_background_notification') ?? false,
|
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
_prefs
|
_prefs
|
||||||
?.setBool('service_background_notification', value ?? false)
|
?.setBool('service_background_notification', value ?? false)
|
||||||
@ -126,9 +216,7 @@ class _SettingScreenState extends State<SettingScreen> {
|
|||||||
subtitle: Text('updateCheckStrictlyDesc'.tr),
|
subtitle: Text('updateCheckStrictlyDesc'.tr),
|
||||||
value: _prefs?.getBool('check_update_strictly') ?? false,
|
value: _prefs?.getBool('check_update_strictly') ?? false,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
_prefs
|
_prefs?.setBool('check_update_strictly', value ?? false).then((_) {
|
||||||
?.setBool('check_update_strictly', value ?? false)
|
|
||||||
.then((_) {
|
|
||||||
setState(() {});
|
setState(() {});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -181,6 +269,21 @@ class _SettingScreenState extends State<SettingScreen> {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
_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),
|
_buildCaptionHeader('more'.tr),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.delete_sweep),
|
leading: const Icon(Icons.delete_sweep),
|
||||||
@ -231,7 +334,6 @@ class _SettingScreenState extends State<SettingScreen> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,9 @@ import 'package:firebase_analytics/firebase_analytics.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:solian/theme.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>();
|
final GlobalKey<ScaffoldState> rootScaffoldKey = GlobalKey<ScaffoldState>();
|
||||||
|
|
||||||
@ -39,17 +41,29 @@ class RootShell extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final showRailNavigation = AppTheme.isLargeScreen(context);
|
||||||
|
|
||||||
|
final destNames = AppNavigation.destinations.map((x) => x.page).toList();
|
||||||
|
final showBottomNavigation =
|
||||||
|
destNames.contains(routeName) && !showRailNavigation;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
key: rootScaffoldKey,
|
key: rootScaffoldKey,
|
||||||
drawer: AppTheme.isLargeScreen(context)
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
? null
|
bottomNavigationBar: showBottomNavigation
|
||||||
: AppNavigationDrawer(routeName: routeName),
|
? AppNavigationBottom(
|
||||||
|
initialIndex: destNames.indexOf(routeName ?? 'page'),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
body: AppTheme.isLargeScreen(context)
|
body: AppTheme.isLargeScreen(context)
|
||||||
? Row(
|
? Row(
|
||||||
children: [
|
children: [
|
||||||
if (showNavigation) AppNavigationDrawer(routeName: routeName),
|
if (showRailNavigation) const AppNavigationRail(),
|
||||||
if (showNavigation)
|
if (showRailNavigation)
|
||||||
const VerticalDivider(thickness: 0.3, width: 1),
|
const VerticalDivider(
|
||||||
|
width: 0.3,
|
||||||
|
thickness: 0.3,
|
||||||
|
),
|
||||||
Expanded(child: child),
|
Expanded(child: child),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -1,62 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:go_router/go_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/sidebar/sidebar_placeholder.dart';
|
|
||||||
|
|
||||||
class SidebarShell extends StatelessWidget {
|
|
||||||
final bool showAppBar;
|
|
||||||
final GoRouterState state;
|
|
||||||
final Widget child;
|
|
||||||
|
|
||||||
final bool sidebarFirst;
|
|
||||||
final Widget? sidebar;
|
|
||||||
|
|
||||||
const SidebarShell({
|
|
||||||
super.key,
|
|
||||||
required this.child,
|
|
||||||
required this.state,
|
|
||||||
this.showAppBar = true,
|
|
||||||
this.sidebarFirst = false,
|
|
||||||
this.sidebar,
|
|
||||||
});
|
|
||||||
|
|
||||||
List<Widget> buildContent(BuildContext context) {
|
|
||||||
return [
|
|
||||||
Flexible(
|
|
||||||
flex: 2,
|
|
||||||
child: child,
|
|
||||||
),
|
|
||||||
if (AppTheme.isExtraLargeScreen(context))
|
|
||||||
const VerticalDivider(thickness: 0.3, width: 1),
|
|
||||||
if (AppTheme.isExtraLargeScreen(context))
|
|
||||||
Flexible(
|
|
||||||
flex: 1,
|
|
||||||
child: sidebar ?? const SidebarPlaceholder(),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
appBar: showAppBar
|
|
||||||
? AppBar(
|
|
||||||
leading: AppBarLeadingButton.adaptive(context),
|
|
||||||
title: AppBarTitle(state.topRoute?.name?.tr ?? 'page'.tr),
|
|
||||||
centerTitle: false,
|
|
||||||
toolbarHeight: AppTheme.toolbarHeight(context),
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
body: AppTheme.isLargeScreen(context)
|
|
||||||
? Row(
|
|
||||||
children: sidebarFirst
|
|
||||||
? buildContent(context).reversed.toList()
|
|
||||||
: buildContent(context),
|
|
||||||
)
|
|
||||||
: child,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -5,10 +5,12 @@ import 'package:solian/theme.dart';
|
|||||||
import 'package:solian/widgets/app_bar_title.dart';
|
import 'package:solian/widgets/app_bar_title.dart';
|
||||||
import 'package:solian/widgets/app_bar_leading.dart';
|
import 'package:solian/widgets/app_bar_leading.dart';
|
||||||
import 'package:solian/widgets/current_state_action.dart';
|
import 'package:solian/widgets/current_state_action.dart';
|
||||||
|
import 'package:solian/widgets/root_container.dart';
|
||||||
|
|
||||||
class TitleShell extends StatelessWidget {
|
class TitleShell extends StatelessWidget {
|
||||||
final bool showAppBar;
|
final bool showAppBar;
|
||||||
final bool isCenteredTitle;
|
final bool isCenteredTitle;
|
||||||
|
final bool isResponsive;
|
||||||
final String? title;
|
final String? title;
|
||||||
final GoRouterState? state;
|
final GoRouterState? state;
|
||||||
final Widget child;
|
final Widget child;
|
||||||
@ -20,13 +22,14 @@ class TitleShell extends StatelessWidget {
|
|||||||
this.state,
|
this.state,
|
||||||
this.showAppBar = true,
|
this.showAppBar = true,
|
||||||
this.isCenteredTitle = false,
|
this.isCenteredTitle = false,
|
||||||
|
this.isResponsive = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
assert(state != null || title != null);
|
assert(state != null || title != null);
|
||||||
|
|
||||||
return Scaffold(
|
final widget = Scaffold(
|
||||||
appBar: showAppBar
|
appBar: showAppBar
|
||||||
? AppBar(
|
? AppBar(
|
||||||
leading: AppBarLeadingButton.adaptive(context),
|
leading: AppBarLeadingButton.adaptive(context),
|
||||||
@ -45,5 +48,11 @@ class TitleShell extends StatelessWidget {
|
|||||||
: null,
|
: null,
|
||||||
body: child,
|
body: child,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (isResponsive) {
|
||||||
|
return ResponsiveRootContainer(child: widget);
|
||||||
|
} else {
|
||||||
|
return RootContainer(child: widget);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:solian/models/theme.dart';
|
||||||
import 'package:solian/platform.dart';
|
import 'package:solian/platform.dart';
|
||||||
|
|
||||||
abstract class AppTheme {
|
abstract class AppTheme {
|
||||||
@ -6,7 +7,10 @@ abstract class AppTheme {
|
|||||||
MediaQuery.of(context).size.width > 640;
|
MediaQuery.of(context).size.width > 640;
|
||||||
|
|
||||||
static bool isExtraLargeScreen(BuildContext context) =>
|
static bool isExtraLargeScreen(BuildContext context) =>
|
||||||
MediaQuery.of(context).size.width > 720;
|
MediaQuery.of(context).size.width > 920;
|
||||||
|
|
||||||
|
static bool isUltraLargeScreen(BuildContext context) =>
|
||||||
|
MediaQuery.of(context).size.width > 1200;
|
||||||
|
|
||||||
static bool isSpecializedMacOS(BuildContext context) =>
|
static bool isSpecializedMacOS(BuildContext context) =>
|
||||||
PlatformInfo.isMacOS && !AppTheme.isLargeScreen(context);
|
PlatformInfo.isMacOS && !AppTheme.isLargeScreen(context);
|
||||||
@ -38,6 +42,10 @@ abstract class AppTheme {
|
|||||||
snackBarTheme: const SnackBarThemeData(
|
snackBarTheme: const SnackBarThemeData(
|
||||||
behavior: SnackBarBehavior.floating,
|
behavior: SnackBarBehavior.floating,
|
||||||
),
|
),
|
||||||
|
scaffoldBackgroundColor: Colors.transparent,
|
||||||
|
appBarTheme: const AppBarTheme(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
),
|
||||||
fontFamily: 'Comfortaa',
|
fontFamily: 'Comfortaa',
|
||||||
fontFamilyFallback: [
|
fontFamilyFallback: [
|
||||||
'NotoSansSC',
|
'NotoSansSC',
|
||||||
@ -52,4 +60,37 @@ abstract class AppTheme {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static ThemeData buildFromData(
|
||||||
|
Brightness brightness,
|
||||||
|
SolianThemeData data, {
|
||||||
|
bool useMaterial3 = true,
|
||||||
|
}) {
|
||||||
|
return ThemeData(
|
||||||
|
brightness: brightness,
|
||||||
|
useMaterial3: useMaterial3,
|
||||||
|
colorScheme: ColorScheme.fromSeed(
|
||||||
|
brightness: brightness,
|
||||||
|
seedColor: data.seedColor,
|
||||||
|
),
|
||||||
|
snackBarTheme: const SnackBarThemeData(
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
),
|
||||||
|
scaffoldBackgroundColor: Colors.transparent,
|
||||||
|
appBarTheme: const AppBarTheme(backgroundColor: Colors.transparent),
|
||||||
|
fontFamily: data.fontFamily ?? 'Comfortaa',
|
||||||
|
fontFamilyFallback: data.fontFamilyFallback ??
|
||||||
|
[
|
||||||
|
'NotoSansSC',
|
||||||
|
'NotoSansHK',
|
||||||
|
'NotoSansJP',
|
||||||
|
if (PlatformInfo.isWeb) 'NotoSansEmoji',
|
||||||
|
],
|
||||||
|
typography: Typography.material2021(
|
||||||
|
colorScheme: brightness == Brightness.light
|
||||||
|
? const ColorScheme.light()
|
||||||
|
: const ColorScheme.dark(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,19 +1,22 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:solian/services.dart';
|
import 'package:solian/services.dart';
|
||||||
|
import 'package:solian/widgets/account/account_profile_popup.dart';
|
||||||
import 'package:solian/widgets/auto_cache_image.dart';
|
import 'package:solian/widgets/auto_cache_image.dart';
|
||||||
|
|
||||||
class AccountAvatar extends StatelessWidget {
|
class AttachedCircleAvatar extends StatelessWidget {
|
||||||
final dynamic content;
|
final dynamic content;
|
||||||
final Color? bgColor;
|
final Color? bgColor;
|
||||||
final Color? feColor;
|
final Color? feColor;
|
||||||
final double? radius;
|
final double? radius;
|
||||||
|
final Widget? fallbackWidget;
|
||||||
|
|
||||||
const AccountAvatar({
|
const AttachedCircleAvatar({
|
||||||
super.key,
|
super.key,
|
||||||
required this.content,
|
required this.content,
|
||||||
this.bgColor,
|
this.bgColor,
|
||||||
this.feColor,
|
this.feColor,
|
||||||
this.radius,
|
this.radius,
|
||||||
|
this.fallbackWidget,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -35,12 +38,61 @@ class AccountAvatar extends StatelessWidget {
|
|||||||
backgroundColor: bgColor,
|
backgroundColor: bgColor,
|
||||||
backgroundImage: !isEmpty ? AutoCacheImage.provider(url) : null,
|
backgroundImage: !isEmpty ? AutoCacheImage.provider(url) : null,
|
||||||
child: isEmpty
|
child: isEmpty
|
||||||
? Icon(
|
? (fallbackWidget ??
|
||||||
|
Icon(
|
||||||
|
Icons.image,
|
||||||
|
size: radius != null ? radius! * 1.2 : 24,
|
||||||
|
color: feColor,
|
||||||
|
))
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AccountAvatar extends StatelessWidget {
|
||||||
|
final dynamic content;
|
||||||
|
final String username;
|
||||||
|
final Color? bgColor;
|
||||||
|
final Color? feColor;
|
||||||
|
final double? radius;
|
||||||
|
final Widget? fallbackWidget;
|
||||||
|
|
||||||
|
const AccountAvatar({
|
||||||
|
super.key,
|
||||||
|
required this.content,
|
||||||
|
required this.username,
|
||||||
|
this.bgColor,
|
||||||
|
this.feColor,
|
||||||
|
this.radius,
|
||||||
|
this.fallbackWidget,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GestureDetector(
|
||||||
|
child: AttachedCircleAvatar(
|
||||||
|
content: content,
|
||||||
|
bgColor: bgColor,
|
||||||
|
feColor: feColor,
|
||||||
|
radius: radius,
|
||||||
|
fallbackWidget: (fallbackWidget ??
|
||||||
|
Icon(
|
||||||
Icons.account_circle,
|
Icons.account_circle,
|
||||||
size: radius != null ? radius! * 1.2 : 24,
|
size: radius != null ? radius! * 1.2 : 24,
|
||||||
color: feColor,
|
color: feColor,
|
||||||
)
|
)),
|
||||||
: null,
|
),
|
||||||
|
onTap: () {
|
||||||
|
showModalBottomSheet(
|
||||||
|
useRootNavigator: true,
|
||||||
|
isScrollControlled: true,
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AccountProfilePopup(
|
||||||
|
name: username,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -84,7 +84,7 @@ class AccountHeadingWidget extends StatelessWidget {
|
|||||||
Positioned(
|
Positioned(
|
||||||
bottom: -30,
|
bottom: -30,
|
||||||
left: 32,
|
left: 32,
|
||||||
child: AccountAvatar(content: avatar, radius: 40),
|
child: AttachedCircleAvatar(content: avatar, radius: 40),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -89,8 +89,7 @@ class _AccountProfilePopupState extends State<AccountProfilePopup> {
|
|||||||
|
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: MediaQuery.of(context).size.height * 0.75,
|
height: MediaQuery.of(context).size.height * 0.75,
|
||||||
child: Column(
|
child: ListView(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
children: [
|
||||||
AccountHeadingWidget(
|
AccountHeadingWidget(
|
||||||
avatar: _userinfo!.avatar,
|
avatar: _userinfo!.avatar,
|
||||||
@ -118,7 +117,7 @@ class _AccountProfilePopupState extends State<AccountProfilePopup> {
|
|||||||
const VisualDensity(horizontal: -4, vertical: -2),
|
const VisualDensity(horizontal: -4, vertical: -2),
|
||||||
trailing: const Icon(Icons.chevron_right),
|
trailing: const Icon(Icons.chevron_right),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
AppRouter.instance.goNamed(
|
AppRouter.instance.pushNamed(
|
||||||
'accountProfilePage',
|
'accountProfilePage',
|
||||||
pathParameters: {'name': _userinfo!.name},
|
pathParameters: {'name': _userinfo!.name},
|
||||||
);
|
);
|
||||||
|
@ -138,7 +138,7 @@ class _AccountSelectorState extends State<AccountSelector> {
|
|||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text(element.nick),
|
title: Text(element.nick),
|
||||||
subtitle: Text(element.name),
|
subtitle: Text(element.name),
|
||||||
leading: AccountAvatar(content: element.avatar),
|
leading: AttachedCircleAvatar(content: element.avatar),
|
||||||
trailing: widget.trailingBuilder != null
|
trailing: widget.trailingBuilder != null
|
||||||
? widget.trailingBuilder!(element)
|
? widget.trailingBuilder!(element)
|
||||||
: _checkSelected(element)
|
: _checkSelected(element)
|
||||||
|
@ -23,7 +23,7 @@ class SilverRelativeList extends StatelessWidget {
|
|||||||
title: Text(element.related.nick),
|
title: Text(element.related.nick),
|
||||||
subtitle: Text(element.related.name),
|
subtitle: Text(element.related.name),
|
||||||
leading: GestureDetector(
|
leading: GestureDetector(
|
||||||
child: AccountAvatar(content: element.related.avatar),
|
child: AttachedCircleAvatar(content: element.related.avatar),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
useRootNavigator: true,
|
useRootNavigator: true,
|
||||||
|
@ -56,7 +56,7 @@ class _RelativeSelectorState extends State<RelativeSelector> {
|
|||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text(element.nick),
|
title: Text(element.nick),
|
||||||
subtitle: Text(element.name),
|
subtitle: Text(element.name),
|
||||||
leading: AccountAvatar(content: element.avatar),
|
leading: AttachedCircleAvatar(content: element.avatar),
|
||||||
trailing: widget.trailingBuilder != null
|
trailing: widget.trailingBuilder != null
|
||||||
? widget.trailingBuilder!(element)
|
? widget.trailingBuilder!(element)
|
||||||
: null,
|
: null,
|
||||||
|
@ -1,28 +1,22 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:solian/shells/root_shell.dart';
|
|
||||||
|
|
||||||
class AppBarLeadingButton extends StatelessWidget {
|
class AppBarLeadingButton extends StatelessWidget {
|
||||||
const AppBarLeadingButton({super.key});
|
final bool forceBack;
|
||||||
|
|
||||||
static Widget? adaptive(BuildContext context) {
|
const AppBarLeadingButton({super.key, this.forceBack = false});
|
||||||
final hasContent =
|
|
||||||
Navigator.canPop(context) || rootScaffoldKey.currentState!.hasDrawer;
|
static Widget? adaptive(BuildContext context, {bool forceBack = false}) {
|
||||||
return hasContent ? const AppBarLeadingButton() : null;
|
final hasContent = Navigator.canPop(context) || forceBack;
|
||||||
|
return hasContent ? AppBarLeadingButton(forceBack: forceBack) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (Navigator.canPop(context)) {
|
if (Navigator.canPop(context) || forceBack) {
|
||||||
return BackButton(
|
return BackButton(
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (rootScaffoldKey.currentState!.hasDrawer) {
|
|
||||||
return DrawerButton(
|
|
||||||
onPressed: () => rootScaffoldKey.currentState!.openDrawer(),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -396,7 +396,8 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
|
|||||||
),
|
),
|
||||||
if (!element.isCompleted &&
|
if (!element.isCompleted &&
|
||||||
element.error == null &&
|
element.error == null &&
|
||||||
canBeCrop)
|
canBeCrop &&
|
||||||
|
PlatformInfo.canCropImage)
|
||||||
Obx(
|
Obx(
|
||||||
() => IconButton(
|
() => IconButton(
|
||||||
color: Colors.teal,
|
color: Colors.teal,
|
||||||
|
@ -175,7 +175,7 @@ class _AttachmentFullScreenState extends State<AttachmentFullScreen> {
|
|||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
IgnorePointer(
|
IgnorePointer(
|
||||||
child: AccountAvatar(
|
child: AttachedCircleAvatar(
|
||||||
content: widget.item.account!.avatar,
|
content: widget.item.account!.avatar,
|
||||||
radius: 19,
|
radius: 19,
|
||||||
),
|
),
|
||||||
|
@ -155,11 +155,18 @@ class _AttachmentItemImage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
if (showBadge && badge != null)
|
if (showBadge && badge != null)
|
||||||
Positioned(
|
Positioned(
|
||||||
right: 12,
|
right: 8,
|
||||||
bottom: 8,
|
bottom: 4,
|
||||||
child: Material(
|
child: Material(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
child: Chip(label: Text(badge!)),
|
child: Chip(
|
||||||
|
label: Text(badge!),
|
||||||
|
labelStyle: GoogleFonts.robotoMono(),
|
||||||
|
visualDensity: const VisualDensity(
|
||||||
|
horizontal: -4,
|
||||||
|
vertical: -2,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (showHideButton && item.isMature)
|
if (showHideButton && item.isMature)
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:carousel_slider/carousel_slider.dart';
|
|
||||||
import 'package:dismissible_page/dismissible_page.dart';
|
import 'package:dismissible_page/dismissible_page.dart';
|
||||||
import 'package:flutter/material.dart' hide CarouselController;
|
import 'package:flutter/material.dart' hide CarouselController;
|
||||||
import 'package:flutter_animate/flutter_animate.dart';
|
import 'package:flutter_animate/flutter_animate.dart';
|
||||||
@ -15,27 +14,29 @@ import 'package:solian/widgets/sized_container.dart';
|
|||||||
|
|
||||||
class AttachmentList extends StatefulWidget {
|
class AttachmentList extends StatefulWidget {
|
||||||
final String parentId;
|
final String parentId;
|
||||||
final List<String> attachmentsId;
|
final List<String>? attachmentIds;
|
||||||
|
final List<Attachment>? attachments;
|
||||||
final bool isGrid;
|
final bool isGrid;
|
||||||
final bool isColumn;
|
final bool isColumn;
|
||||||
final bool isForceGrid;
|
final bool isFullWidth;
|
||||||
final bool autoload;
|
final bool autoload;
|
||||||
final double flatMaxHeight;
|
|
||||||
final double columnMaxWidth;
|
final double columnMaxWidth;
|
||||||
|
|
||||||
|
final EdgeInsets? padding;
|
||||||
final double? width;
|
final double? width;
|
||||||
final double? viewport;
|
final double? viewport;
|
||||||
|
|
||||||
const AttachmentList({
|
const AttachmentList({
|
||||||
super.key,
|
super.key,
|
||||||
required this.parentId,
|
required this.parentId,
|
||||||
required this.attachmentsId,
|
this.attachmentIds,
|
||||||
|
this.attachments,
|
||||||
this.isGrid = false,
|
this.isGrid = false,
|
||||||
this.isColumn = false,
|
this.isColumn = false,
|
||||||
this.isForceGrid = false,
|
this.isFullWidth = false,
|
||||||
this.autoload = false,
|
this.autoload = false,
|
||||||
this.flatMaxHeight = 720,
|
|
||||||
this.columnMaxWidth = 480,
|
this.columnMaxWidth = 480,
|
||||||
|
this.padding,
|
||||||
this.width,
|
this.width,
|
||||||
this.viewport,
|
this.viewport,
|
||||||
});
|
});
|
||||||
@ -50,21 +51,21 @@ class _AttachmentListState extends State<AttachmentList> {
|
|||||||
|
|
||||||
double _aspectRatio = 1;
|
double _aspectRatio = 1;
|
||||||
|
|
||||||
List<Attachment?> _attachmentsMeta = List.empty();
|
List<Attachment?> _attachments = List.empty();
|
||||||
|
|
||||||
void _getMetadataList() {
|
void _getMetadataList() {
|
||||||
final AttachmentProvider attach = Get.find();
|
final AttachmentProvider attach = Get.find();
|
||||||
|
|
||||||
if (widget.attachmentsId.isEmpty) {
|
if (widget.attachmentIds?.isEmpty ?? false) {
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
_attachmentsMeta = List.filled(widget.attachmentsId.length, null);
|
_attachments = List.filled(widget.attachmentIds!.length, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
attach.listMetadata(widget.attachmentsId).then((result) {
|
attach.listMetadata(widget.attachmentIds!).then((result) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_attachmentsMeta = result;
|
_attachments = result;
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -76,7 +77,7 @@ class _AttachmentListState extends State<AttachmentList> {
|
|||||||
bool isConsistent = true;
|
bool isConsistent = true;
|
||||||
double? consistentValue;
|
double? consistentValue;
|
||||||
int portrait = 0, square = 0, landscape = 0;
|
int portrait = 0, square = 0, landscape = 0;
|
||||||
for (var entry in _attachmentsMeta) {
|
for (var entry in _attachments) {
|
||||||
if (entry == null) continue;
|
if (entry == null) continue;
|
||||||
if (entry.metadata?['ratio'] != null) {
|
if (entry.metadata?['ratio'] != null) {
|
||||||
if (entry.metadata?['ratio'] is int) {
|
if (entry.metadata?['ratio'] is int) {
|
||||||
@ -117,10 +118,9 @@ class _AttachmentListState extends State<AttachmentList> {
|
|||||||
item: element,
|
item: element,
|
||||||
parentId: widget.parentId,
|
parentId: widget.parentId,
|
||||||
width: width ?? widget.width,
|
width: width ?? widget.width,
|
||||||
badgeContent: '${idx + 1}/${_attachmentsMeta.length}',
|
badgeContent: '${idx + 1}/${_attachments.length}',
|
||||||
showBadge:
|
showBadge: _attachments.length > 1 && !widget.isGrid && !widget.isColumn,
|
||||||
_attachmentsMeta.length > 1 && !widget.isGrid && !widget.isColumn,
|
showBorder: _attachments.length > 1,
|
||||||
showBorder: widget.attachmentsId.length > 1,
|
|
||||||
showMature: _showMature,
|
showMature: _showMature,
|
||||||
autoload: widget.autoload,
|
autoload: widget.autoload,
|
||||||
onReveal: (value) {
|
onReveal: (value) {
|
||||||
@ -132,15 +132,34 @@ class _AttachmentListState extends State<AttachmentList> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
assert(widget.attachmentIds != null || widget.attachments != null);
|
||||||
|
if (widget.attachments == null) {
|
||||||
|
final AttachmentProvider attach = Get.find();
|
||||||
|
final cachedResult = attach.listMetadataFromCache(widget.attachmentIds!);
|
||||||
|
if (cachedResult.every((x) => x != null)) {
|
||||||
|
setState(() {
|
||||||
|
_attachments = cachedResult;
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
_calculateAspectRatio();
|
||||||
|
} else {
|
||||||
_getMetadataList();
|
_getMetadataList();
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
setState(() {
|
||||||
|
_attachments = widget.attachments!;
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
_calculateAspectRatio();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Color get _unFocusColor =>
|
Color get _unFocusColor =>
|
||||||
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
|
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (widget.attachmentsId.isEmpty) {
|
if (widget.attachmentIds?.isEmpty ?? widget.attachments!.isEmpty) {
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -153,9 +172,7 @@ class _AttachmentListState extends State<AttachmentList> {
|
|||||||
color: _unFocusColor,
|
color: _unFocusColor,
|
||||||
).paddingOnly(right: 5),
|
).paddingOnly(right: 5),
|
||||||
Text(
|
Text(
|
||||||
'attachmentHint'.trParams(
|
'attachmentHint'.trParams({'count': _attachments.toString()}),
|
||||||
{'count': widget.attachmentsId.length.toString()},
|
|
||||||
),
|
|
||||||
style: TextStyle(color: _unFocusColor, fontSize: 12),
|
style: TextStyle(color: _unFocusColor, fontSize: 12),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
@ -165,21 +182,90 @@ class _AttachmentListState extends State<AttachmentList> {
|
|||||||
.fadeIn(duration: 1250.ms);
|
.fadeIn(duration: 1250.ms);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const radius = BorderRadius.all(Radius.circular(8));
|
||||||
|
|
||||||
|
if (widget.isFullWidth && _attachments.length == 1) {
|
||||||
|
final element = _attachments.first;
|
||||||
|
final isImage = element!.mimetype.split('/').firstOrNull == 'image';
|
||||||
|
double ratio =
|
||||||
|
element.metadata?['ratio']?.toDouble() ?? (isImage ? 1 : 16 / 9);
|
||||||
|
return Container(
|
||||||
|
width: MediaQuery.of(context).size.width,
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
maxHeight: 640,
|
||||||
|
),
|
||||||
|
child: AspectRatio(
|
||||||
|
aspectRatio: ratio,
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.surfaceContainer
|
||||||
|
.withOpacity(0.5),
|
||||||
|
border: Border.symmetric(
|
||||||
|
horizontal: BorderSide(
|
||||||
|
color: Theme.of(context).dividerColor,
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: _buildEntry(element, 0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final isNotPureImage = _attachments.any(
|
||||||
|
(x) => x?.mimetype.split('/').firstOrNull != 'image',
|
||||||
|
);
|
||||||
|
if (widget.isGrid && !isNotPureImage) {
|
||||||
|
return GridView.builder(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
primary: false,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
shrinkWrap: true,
|
||||||
|
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
|
crossAxisCount: math.min(3, _attachments.length),
|
||||||
|
mainAxisSpacing: 8.0,
|
||||||
|
crossAxisSpacing: 8.0,
|
||||||
|
),
|
||||||
|
itemCount: _attachments.length,
|
||||||
|
itemBuilder: (context, idx) {
|
||||||
|
final element = _attachments[idx];
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.surfaceContainer
|
||||||
|
.withOpacity(0.5),
|
||||||
|
border: Border.all(
|
||||||
|
color: Theme.of(context).dividerColor,
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
borderRadius: radius,
|
||||||
|
),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: radius,
|
||||||
|
child: _buildEntry(element, idx),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (widget.isColumn) {
|
if (widget.isColumn) {
|
||||||
var idx = 0;
|
var idx = 0;
|
||||||
const radius = BorderRadius.all(Radius.circular(8));
|
|
||||||
return Wrap(
|
return Wrap(
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
runSpacing: 8,
|
runSpacing: 8,
|
||||||
children: widget.attachmentsId.map((x) {
|
children: _attachments.map((x) {
|
||||||
final element = _attachmentsMeta[idx];
|
final element = _attachments[idx];
|
||||||
idx++;
|
idx++;
|
||||||
if (element == null) return const SizedBox.shrink();
|
if (element == null) return const SizedBox.shrink();
|
||||||
double ratio = element.metadata?['ratio']?.toDouble() ?? 16 / 9;
|
final isImage = element.mimetype.split('/').firstOrNull == 'image';
|
||||||
|
double ratio =
|
||||||
|
element.metadata?['ratio']?.toDouble() ?? (isImage ? 1 : 16 / 9);
|
||||||
return Container(
|
return Container(
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
|
||||||
),
|
|
||||||
constraints: BoxConstraints(
|
constraints: BoxConstraints(
|
||||||
maxWidth: widget.columnMaxWidth,
|
maxWidth: widget.columnMaxWidth,
|
||||||
maxHeight: 640,
|
maxHeight: 640,
|
||||||
@ -188,6 +274,10 @@ class _AttachmentListState extends State<AttachmentList> {
|
|||||||
aspectRatio: ratio,
|
aspectRatio: ratio,
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.surfaceContainer
|
||||||
|
.withOpacity(0.5),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: Theme.of(context).dividerColor,
|
color: Theme.of(context).dividerColor,
|
||||||
width: 1,
|
width: 1,
|
||||||
@ -205,27 +295,37 @@ class _AttachmentListState extends State<AttachmentList> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final isNotPureImage = _attachmentsMeta.any(
|
|
||||||
(x) => x?.mimetype.split('/').firstOrNull != 'image',
|
|
||||||
);
|
|
||||||
if (widget.isGrid && (widget.isForceGrid || !isNotPureImage)) {
|
|
||||||
const radius = BorderRadius.all(Radius.circular(8));
|
|
||||||
return GridView.builder(
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
primary: false,
|
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
|
||||||
shrinkWrap: true,
|
|
||||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
|
||||||
crossAxisCount: math.min(3, widget.attachmentsId.length),
|
|
||||||
mainAxisSpacing: 8.0,
|
|
||||||
crossAxisSpacing: 8.0,
|
|
||||||
),
|
|
||||||
itemCount: widget.attachmentsId.length,
|
|
||||||
itemBuilder: (context, idx) {
|
|
||||||
final element = _attachmentsMeta[idx];
|
|
||||||
return Container(
|
return Container(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
maxHeight: 320,
|
||||||
|
),
|
||||||
|
child: ListView.separated(
|
||||||
|
padding: widget.padding,
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemCount: _attachments.length,
|
||||||
|
itemBuilder: (context, idx) {
|
||||||
|
final element = _attachments[idx];
|
||||||
|
if (element == null) const SizedBox.shrink();
|
||||||
|
final isImage = element!.mimetype.split('/').firstOrNull == 'image';
|
||||||
|
double ratio =
|
||||||
|
element.metadata?['ratio']?.toDouble() ?? (isImage ? 1 : 16 / 9);
|
||||||
|
return Container(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
maxWidth: math.min(
|
||||||
|
widget.columnMaxWidth,
|
||||||
|
MediaQuery.of(context).size.width -
|
||||||
|
(widget.padding?.horizontal ?? 0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: AspectRatio(
|
||||||
|
aspectRatio: ratio,
|
||||||
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.surfaceContainer
|
||||||
|
.withOpacity(0.5),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: Theme.of(context).dividerColor,
|
color: Theme.of(context).dividerColor,
|
||||||
width: 1,
|
width: 1,
|
||||||
@ -236,37 +336,11 @@ class _AttachmentListState extends State<AttachmentList> {
|
|||||||
borderRadius: radius,
|
borderRadius: radius,
|
||||||
child: _buildEntry(element, idx),
|
child: _buildEntry(element, idx),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
).paddingSymmetric(horizontal: 24);
|
separatorBuilder: (context, _) => const Gap(8),
|
||||||
}
|
|
||||||
|
|
||||||
return Container(
|
|
||||||
width: MediaQuery.of(context).size.width,
|
|
||||||
constraints: BoxConstraints(
|
|
||||||
maxHeight: widget.flatMaxHeight,
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
|
||||||
border: Border.symmetric(
|
|
||||||
horizontal: BorderSide(
|
|
||||||
width: 0.3,
|
|
||||||
color: Theme.of(context).dividerColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: CarouselSlider.builder(
|
|
||||||
options: CarouselOptions(
|
|
||||||
aspectRatio: _aspectRatio,
|
|
||||||
viewportFraction:
|
|
||||||
widget.viewport ?? (widget.attachmentsId.length > 1 ? 0.95 : 1),
|
|
||||||
enableInfiniteScroll: false,
|
|
||||||
),
|
|
||||||
itemCount: _attachmentsMeta.length,
|
|
||||||
itemBuilder: (context, idx, _) {
|
|
||||||
final element = _attachmentsMeta[idx];
|
|
||||||
return _buildEntry(element, idx);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -319,6 +393,7 @@ class AttachmentListEntry extends StatelessWidget {
|
|||||||
width: width ?? MediaQuery.of(context).size.width,
|
width: width ?? MediaQuery.of(context).size.width,
|
||||||
height: height,
|
height: height,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.transparent,
|
||||||
border: showBorder
|
border: showBorder
|
||||||
? Border.symmetric(
|
? Border.symmetric(
|
||||||
vertical: BorderSide(
|
vertical: BorderSide(
|
||||||
|
@ -98,12 +98,12 @@ class ChannelCallIndicator extends StatelessWidget {
|
|||||||
child: Text('callJoin'.tr),
|
child: Text('callJoin'.tr),
|
||||||
);
|
);
|
||||||
} else if (call.channel.value?.id == channel.id &&
|
} else if (call.channel.value?.id == channel.id &&
|
||||||
!AppTheme.isLargeScreen(context)) {
|
!AppTheme.isUltraLargeScreen(context)) {
|
||||||
return TextButton(
|
return TextButton(
|
||||||
onPressed: () => onJoin(),
|
onPressed: () => onJoin(),
|
||||||
child: Text('callResume'.tr),
|
child: Text('callResume'.tr),
|
||||||
);
|
);
|
||||||
} else if (!AppTheme.isLargeScreen(context)) {
|
} else if (!AppTheme.isUltraLargeScreen(context)) {
|
||||||
return TextButton(
|
return TextButton(
|
||||||
onPressed: null,
|
onPressed: null,
|
||||||
child: Text('callJoin'.tr),
|
child: Text('callJoin'.tr),
|
||||||
|
@ -4,18 +4,18 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
import 'package:solian/controllers/chat_events_controller.dart';
|
import 'package:solian/controllers/chat_events_controller.dart';
|
||||||
import 'package:solian/models/channel.dart';
|
import 'package:solian/models/channel.dart';
|
||||||
import 'package:solian/platform.dart';
|
import 'package:solian/platform.dart';
|
||||||
|
import 'package:solian/providers/database/database.dart';
|
||||||
import 'package:solian/router.dart';
|
import 'package:solian/router.dart';
|
||||||
import 'package:solian/widgets/account/account_avatar.dart';
|
import 'package:solian/widgets/account/account_avatar.dart';
|
||||||
|
import 'package:badges/badges.dart' as badges;
|
||||||
|
|
||||||
class ChannelListWidget extends StatefulWidget {
|
class ChannelListWidget extends StatefulWidget {
|
||||||
final List<Channel> channels;
|
final List<Channel> channels;
|
||||||
final int selfId;
|
final int selfId;
|
||||||
final bool isDense;
|
|
||||||
final bool isCollapsed;
|
|
||||||
final bool noCategory;
|
|
||||||
final bool useReplace;
|
final bool useReplace;
|
||||||
final Function(Channel)? onSelected;
|
final Function(Channel)? onSelected;
|
||||||
|
|
||||||
@ -23,9 +23,6 @@ class ChannelListWidget extends StatefulWidget {
|
|||||||
super.key,
|
super.key,
|
||||||
required this.channels,
|
required this.channels,
|
||||||
required this.selfId,
|
required this.selfId,
|
||||||
this.isDense = false,
|
|
||||||
this.isCollapsed = false,
|
|
||||||
this.noCategory = false,
|
|
||||||
this.useReplace = false,
|
this.useReplace = false,
|
||||||
this.onSelected,
|
this.onSelected,
|
||||||
});
|
});
|
||||||
@ -35,43 +32,25 @@ class ChannelListWidget extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _ChannelListWidgetState extends State<ChannelListWidget> {
|
class _ChannelListWidgetState extends State<ChannelListWidget> {
|
||||||
final List<Channel> _globalChannels = List.empty(growable: true);
|
Map<int, LocalMessageEventTableData>? _lastMessages;
|
||||||
final Map<String, List<Channel>> _inRealms = {};
|
|
||||||
|
|
||||||
final ChatEventController _eventController = ChatEventController();
|
final ChatEventController _eventController = ChatEventController();
|
||||||
|
|
||||||
void _mapChannels() {
|
Future<void> _loadLastMessages() async {
|
||||||
_inRealms.clear();
|
final messages = await _eventController.src.getLastInAllChannels();
|
||||||
_globalChannels.clear();
|
setState(() {
|
||||||
|
_lastMessages = messages
|
||||||
if (widget.noCategory) {
|
.map((k, v) => MapEntry(k, v.firstOrNull))
|
||||||
_globalChannels.addAll(widget.channels);
|
.cast<int, LocalMessageEventTableData>();
|
||||||
return;
|
});
|
||||||
}
|
|
||||||
|
|
||||||
for (final channel in widget.channels) {
|
|
||||||
if (channel.realmId != null) {
|
|
||||||
if (_inRealms[channel.realm!.alias] == null) {
|
|
||||||
_inRealms[channel.realm!.alias] = List.empty(growable: true);
|
|
||||||
}
|
|
||||||
_inRealms[channel.realm!.alias]!.add(channel);
|
|
||||||
} else {
|
|
||||||
_globalChannels.add(channel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didUpdateWidget(covariant ChannelListWidget oldWidget) {
|
|
||||||
super.didUpdateWidget(oldWidget);
|
|
||||||
setState(() => _mapChannels());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_mapChannels();
|
_eventController.initialize().then((_) {
|
||||||
_eventController.initialize();
|
_loadLastMessages();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _gotoChannel(Channel item) {
|
void _gotoChannel(Channel item) {
|
||||||
@ -98,107 +77,183 @@ class _ChannelListWidgetState extends State<ChannelListWidget> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildDirectMessageDescription(Channel item, ChannelMember otherside) {
|
Widget _buildTitle(Channel item, ChannelMember? otherside) {
|
||||||
if (PlatformInfo.isWeb) {
|
if (otherside != null) {
|
||||||
return Text('channelDirectDescription'.trParams(
|
return Row(
|
||||||
{'username': '@${otherside.account.name}'},
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
));
|
children: [
|
||||||
}
|
Expanded(child: Text(otherside.account.nick)),
|
||||||
|
if (_lastMessages != null && _lastMessages![item.id] != null)
|
||||||
return FutureBuilder(
|
Text(
|
||||||
future: Future.delayed(
|
DateFormat('MM/dd').format(
|
||||||
const Duration(milliseconds: 500),
|
_lastMessages![item.id]!.createdAt.toLocal(),
|
||||||
() => _eventController.src.getLastInChannel(item),
|
|
||||||
),
|
),
|
||||||
builder: (context, snapshot) {
|
style: TextStyle(
|
||||||
if (!snapshot.hasData && snapshot.data == null) {
|
fontSize: 12,
|
||||||
return Text('channelDirectDescription'.trParams(
|
color:
|
||||||
{'username': '@${otherside.account.name}'},
|
Theme.of(context).colorScheme.onSurface.withOpacity(0.75),
|
||||||
));
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Expanded(child: Text(item.name)),
|
||||||
|
if (_lastMessages != null && _lastMessages![item.id] != null)
|
||||||
|
Text(
|
||||||
|
DateFormat('MM/dd').format(
|
||||||
|
_lastMessages![item.id]!.createdAt.toLocal(),
|
||||||
|
),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.75),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final data = snapshot.data!.data!;
|
Widget _buildSubtitle(Channel item, ChannelMember? otherside) {
|
||||||
return Text(
|
if (PlatformInfo.isWeb) {
|
||||||
'${data.sender.account.nick}: ${data.body['text'] ?? 'Unsupported message to preview'}',
|
return otherside != null
|
||||||
|
? Text(
|
||||||
|
'channelDirectDescription'.trParams(
|
||||||
|
{'username': '@${otherside.account.name}'},
|
||||||
|
),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
|
)
|
||||||
|
: Text(
|
||||||
|
item.description,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return AnimatedSwitcher(
|
||||||
|
switchInCurve: Curves.easeIn,
|
||||||
|
switchOutCurve: Curves.easeOut,
|
||||||
|
transitionBuilder: (child, animation) {
|
||||||
|
return FadeTransition(opacity: animation, child: child);
|
||||||
|
},
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
child: (_lastMessages == null || _lastMessages![item.id] == null)
|
||||||
|
? Builder(
|
||||||
|
builder: (context) {
|
||||||
|
return otherside != null
|
||||||
|
? Text(
|
||||||
|
'channelDirectDescription'.trParams(
|
||||||
|
{'username': '@${otherside.account.name}'},
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
)
|
||||||
|
: Text(
|
||||||
|
item.description,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: Builder(
|
||||||
|
builder: (context) {
|
||||||
|
final data = _lastMessages![item.id]!.data!;
|
||||||
|
return Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
if (item.type == 0)
|
||||||
|
Badge(
|
||||||
|
label: Text(data.sender.account.nick),
|
||||||
|
backgroundColor:
|
||||||
|
Theme.of(context).colorScheme.secondaryContainer,
|
||||||
|
textColor:
|
||||||
|
Theme.of(context).colorScheme.onSecondaryContainer,
|
||||||
|
),
|
||||||
|
if (item.type == 0) const Gap(6),
|
||||||
|
if (data.body['text'] != null)
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
data.body['text'],
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Badge(label: Text('unablePreview'.tr)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
layoutBuilder: (currentChild, previousChildren) {
|
||||||
|
return Stack(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
children: <Widget>[
|
||||||
|
...previousChildren,
|
||||||
|
if (currentChild != null) currentChild,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildEntry(Channel item) {
|
Widget _buildEntry(Channel item) {
|
||||||
final padding = widget.isDense
|
const padding = EdgeInsets.symmetric(horizontal: 20);
|
||||||
? const EdgeInsets.symmetric(horizontal: 20)
|
|
||||||
: const EdgeInsets.symmetric(horizontal: 16);
|
|
||||||
|
|
||||||
if (item.type == 1) {
|
|
||||||
final otherside =
|
final otherside =
|
||||||
item.members!.where((e) => e.account.id != widget.selfId).first;
|
item.members!.where((e) => e.account.id != widget.selfId).firstOrNull;
|
||||||
|
|
||||||
final avatar = AccountAvatar(
|
if (item.type == 1 && otherside != null) {
|
||||||
|
final avatar = AttachedCircleAvatar(
|
||||||
content: otherside.account.avatar,
|
content: otherside.account.avatar,
|
||||||
radius: widget.isDense ? 12 : 20,
|
radius: 20,
|
||||||
bgColor: Theme.of(context).colorScheme.primary,
|
bgColor: Theme.of(context).colorScheme.primary,
|
||||||
feColor: Theme.of(context).colorScheme.onPrimary,
|
feColor: Theme.of(context).colorScheme.onPrimary,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (widget.isCollapsed) {
|
|
||||||
return Tooltip(
|
|
||||||
message: otherside.account.nick,
|
|
||||||
child: InkWell(
|
|
||||||
child: avatar.paddingSymmetric(vertical: 12),
|
|
||||||
onTap: () => _gotoChannel(item),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: avatar,
|
leading: avatar,
|
||||||
contentPadding: padding,
|
contentPadding: padding,
|
||||||
title: Text(otherside.account.nick),
|
title: _buildTitle(item, otherside),
|
||||||
subtitle: !widget.isDense
|
subtitle: _buildSubtitle(item, otherside),
|
||||||
? _buildDirectMessageDescription(item, otherside)
|
|
||||||
: null,
|
|
||||||
onTap: () => _gotoChannel(item),
|
onTap: () => _gotoChannel(item),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
final avatar = CircleAvatar(
|
final avatar = CircleAvatar(
|
||||||
backgroundColor: item.realmId == null
|
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||||
? Theme.of(context).colorScheme.primary
|
radius: 20,
|
||||||
: Colors.transparent,
|
|
||||||
radius: widget.isDense ? 12 : 20,
|
|
||||||
child: FaIcon(
|
child: FaIcon(
|
||||||
FontAwesomeIcons.hashtag,
|
FontAwesomeIcons.hashtag,
|
||||||
color: item.realmId == null
|
color: Theme.of(context).colorScheme.onPrimary,
|
||||||
? Theme.of(context).colorScheme.onPrimary
|
size: 16,
|
||||||
: Theme.of(context).colorScheme.primary,
|
|
||||||
size: widget.isDense ? 12 : 16,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (widget.isCollapsed) {
|
|
||||||
return Tooltip(
|
|
||||||
message: item.name,
|
|
||||||
child: InkWell(
|
|
||||||
child: avatar.paddingSymmetric(vertical: 12),
|
|
||||||
onTap: () => _gotoChannel(item),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
minTileHeight: widget.isDense ? 48 : null,
|
minTileHeight: null,
|
||||||
leading: avatar,
|
leading: item.realmId == null
|
||||||
|
? avatar
|
||||||
|
: badges.Badge(
|
||||||
|
position: badges.BadgePosition.bottomEnd(bottom: -4, end: -6),
|
||||||
|
badgeStyle: badges.BadgeStyle(
|
||||||
|
badgeColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||||
|
padding: const EdgeInsets.all(2),
|
||||||
|
elevation: 8,
|
||||||
|
),
|
||||||
|
badgeContent: AttachedCircleAvatar(
|
||||||
|
content: item.realm?.avatar,
|
||||||
|
radius: 10,
|
||||||
|
fallbackWidget: const Icon(
|
||||||
|
Icons.workspaces,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: avatar,
|
||||||
|
),
|
||||||
contentPadding: padding,
|
contentPadding: padding,
|
||||||
title: Text(item.name),
|
title: _buildTitle(item, null),
|
||||||
subtitle: !widget.isDense
|
subtitle: _buildSubtitle(item, null),
|
||||||
? Text(
|
|
||||||
item.description,
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
onTap: () => _gotoChannel(item),
|
onTap: () => _gotoChannel(item),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -206,13 +261,12 @@ class _ChannelListWidgetState extends State<ChannelListWidget> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (widget.noCategory) {
|
|
||||||
return CustomScrollView(
|
return CustomScrollView(
|
||||||
slivers: [
|
slivers: [
|
||||||
SliverList.builder(
|
SliverList.builder(
|
||||||
itemCount: _globalChannels.length,
|
itemCount: widget.channels.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final element = _globalChannels[index];
|
final element = widget.channels[index];
|
||||||
return _buildEntry(element);
|
return _buildEntry(element);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -220,36 +274,4 @@ class _ChannelListWidgetState extends State<ChannelListWidget> {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return CustomScrollView(
|
|
||||||
slivers: [
|
|
||||||
SliverList.builder(
|
|
||||||
itemCount: _globalChannels.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final element = _globalChannels[index];
|
|
||||||
return _buildEntry(element);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
SliverList.list(
|
|
||||||
children: _inRealms.entries.map((element) {
|
|
||||||
return ExpansionTile(
|
|
||||||
tilePadding: const EdgeInsets.only(left: 20, right: 24),
|
|
||||||
minTileHeight: 48,
|
|
||||||
title: Text(element.value.first.realm!.name),
|
|
||||||
leading: CircleAvatar(
|
|
||||||
backgroundColor: Colors.teal,
|
|
||||||
radius: widget.isDense ? 12 : 24,
|
|
||||||
child: Icon(
|
|
||||||
Icons.workspaces,
|
|
||||||
color: Colors.white,
|
|
||||||
size: widget.isDense ? 12 : 16,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
children: element.value.map((x) => _buildEntry(x)).toList(),
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -152,7 +152,8 @@ class _ChannelMemberListPopupState extends State<ChannelMemberListPopup> {
|
|||||||
title: Text(element.account.nick),
|
title: Text(element.account.nick),
|
||||||
subtitle: Text(element.account.name),
|
subtitle: Text(element.account.name),
|
||||||
leading: GestureDetector(
|
leading: GestureDetector(
|
||||||
child: AccountAvatar(content: element.account.avatar),
|
child:
|
||||||
|
AttachedCircleAvatar(content: element.account.avatar),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
useRootNavigator: true,
|
useRootNavigator: true,
|
||||||
|
@ -74,7 +74,7 @@ class _NoContentWidgetState extends State<NoContentWidget>
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
child: AccountAvatar(
|
child: AttachedCircleAvatar(
|
||||||
content: widget.userinfo!.avatar,
|
content: widget.userinfo!.avatar,
|
||||||
bgColor: Colors.transparent,
|
bgColor: Colors.transparent,
|
||||||
radius: radius,
|
radius: radius,
|
||||||
|
@ -78,7 +78,7 @@ class ChatEvent extends StatelessWidget {
|
|||||||
child: AttachmentList(
|
child: AttachmentList(
|
||||||
key: Key('m${item.uuid}attachments'),
|
key: Key('m${item.uuid}attachments'),
|
||||||
parentId: item.uuid,
|
parentId: item.uuid,
|
||||||
attachmentsId: attachments,
|
attachmentIds: attachments,
|
||||||
isColumn: true,
|
isColumn: true,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -220,7 +220,7 @@ class ChatEvent extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
AccountAvatar(
|
AttachedCircleAvatar(
|
||||||
content: item.sender.account.avatar,
|
content: item.sender.account.avatar,
|
||||||
radius: 9,
|
radius: 9,
|
||||||
),
|
),
|
||||||
@ -250,7 +250,8 @@ class ChatEvent extends StatelessWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
child: AccountAvatar(content: item.sender.account.avatar),
|
child:
|
||||||
|
AttachedCircleAvatar(content: item.sender.account.avatar),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
useRootNavigator: true,
|
useRootNavigator: true,
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_animate/flutter_animate.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:solian/controllers/chat_events_controller.dart';
|
import 'package:solian/controllers/chat_events_controller.dart';
|
||||||
import 'package:solian/models/channel.dart';
|
import 'package:solian/models/channel.dart';
|
||||||
@ -9,6 +10,7 @@ import 'package:solian/widgets/chat/chat_event_action.dart';
|
|||||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||||
|
|
||||||
class ChatEventList extends StatelessWidget {
|
class ChatEventList extends StatelessWidget {
|
||||||
|
final bool noAnimated;
|
||||||
final String scope;
|
final String scope;
|
||||||
final Channel channel;
|
final Channel channel;
|
||||||
final ChatEventController chatController;
|
final ChatEventController chatController;
|
||||||
@ -23,6 +25,7 @@ class ChatEventList extends StatelessWidget {
|
|||||||
required this.chatController,
|
required this.chatController,
|
||||||
required this.onEdit,
|
required this.onEdit,
|
||||||
required this.onReply,
|
required this.onReply,
|
||||||
|
this.noAnimated = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
bool _checkMessageMergeable(Event? a, Event? b) {
|
bool _checkMessageMergeable(Event? a, Event? b) {
|
||||||
@ -34,6 +37,7 @@ class ChatEventList extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return CustomScrollView(
|
return CustomScrollView(
|
||||||
|
cacheExtent: 100,
|
||||||
reverse: true,
|
reverse: true,
|
||||||
slivers: [
|
slivers: [
|
||||||
Obx(() {
|
Obx(() {
|
||||||
@ -63,7 +67,8 @@ class ChatEventList extends StatelessWidget {
|
|||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
behavior: HitTestBehavior.opaque,
|
behavior: HitTestBehavior.opaque,
|
||||||
child: ChatEvent(
|
child: Builder(builder: (context) {
|
||||||
|
final widget = ChatEvent(
|
||||||
key: Key('m${item!.uuid}'),
|
key: Key('m${item!.uuid}'),
|
||||||
item: item,
|
item: item,
|
||||||
isMerged: isMerged,
|
isMerged: isMerged,
|
||||||
@ -71,7 +76,23 @@ class ChatEventList extends StatelessWidget {
|
|||||||
).paddingOnly(
|
).paddingOnly(
|
||||||
top: !isMerged ? 8 : 0,
|
top: !isMerged ? 8 : 0,
|
||||||
bottom: !hasMerged ? 8 : 0,
|
bottom: !hasMerged ? 8 : 0,
|
||||||
),
|
);
|
||||||
|
|
||||||
|
if (noAnimated) {
|
||||||
|
return widget;
|
||||||
|
} else {
|
||||||
|
return widget
|
||||||
|
.animate(
|
||||||
|
key: Key('animated-m${item.uuid}'),
|
||||||
|
)
|
||||||
|
.slideY(
|
||||||
|
curve: Curves.fastLinearToSlowEaseIn,
|
||||||
|
duration: 250.ms,
|
||||||
|
begin: 0.5,
|
||||||
|
end: 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}),
|
||||||
onLongPress: () {
|
onLongPress: () {
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
useRootNavigator: true,
|
useRootNavigator: true,
|
||||||
@ -79,7 +100,7 @@ class ChatEventList extends StatelessWidget {
|
|||||||
builder: (context) => ChatEventAction(
|
builder: (context) => ChatEventAction(
|
||||||
channel: channel,
|
channel: channel,
|
||||||
realm: channel.realm,
|
realm: channel.realm,
|
||||||
item: item,
|
item: item!,
|
||||||
onEdit: () {
|
onEdit: () {
|
||||||
onEdit(item);
|
onEdit(item);
|
||||||
},
|
},
|
||||||
|
@ -443,7 +443,7 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
|
|||||||
.map(
|
.map(
|
||||||
(x) => ChatMessageSuggestion(
|
(x) => ChatMessageSuggestion(
|
||||||
type: 'users',
|
type: 'users',
|
||||||
leading: AccountAvatar(content: x.avatar),
|
leading: AttachedCircleAvatar(content: x.avatar),
|
||||||
display: x.nick,
|
display: x.nick,
|
||||||
content: '@${x.name}',
|
content: '@${x.name}',
|
||||||
),
|
),
|
||||||
|
@ -2,15 +2,21 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||||
import 'package:flutter_svg/svg.dart';
|
import 'package:flutter_svg/svg.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:solian/models/link.dart';
|
||||||
import 'package:solian/providers/link_expander.dart';
|
import 'package:solian/providers/link_expander.dart';
|
||||||
import 'package:solian/widgets/auto_cache_image.dart';
|
import 'package:solian/widgets/auto_cache_image.dart';
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
class LinkExpansion extends StatelessWidget {
|
class LinkExpansion extends StatefulWidget {
|
||||||
final String content;
|
final String content;
|
||||||
|
|
||||||
const LinkExpansion({super.key, required this.content});
|
const LinkExpansion({super.key, required this.content});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<LinkExpansion> createState() => _LinkExpansionState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LinkExpansionState extends State<LinkExpansion> {
|
||||||
Widget _buildImage(String url, {double? width, double? height}) {
|
Widget _buildImage(String url, {double? width, double? height}) {
|
||||||
if (url.endsWith('svg')) {
|
if (url.endsWith('svg')) {
|
||||||
return SvgPicture.network(url, width: width, height: height);
|
return SvgPicture.network(url, width: width, height: height);
|
||||||
@ -22,61 +28,74 @@ class LinkExpansion extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
List<LinkMeta>? _meta;
|
||||||
Widget build(BuildContext context) {
|
|
||||||
|
Future<void> _doExpand() async {
|
||||||
final linkRegex = RegExp(
|
final linkRegex = RegExp(
|
||||||
r'(?<!\()(?:(?:https?):\/\/|www\.)(?:[-_a-z0-9]+\.)*(?:[-a-z0-9]+\.[-a-z0-9]+)[^\s<]*[^\s<?!.,:*_~]',
|
r'(?<!\()(?:(?:https?):\/\/|www\.)(?:[-_a-z0-9]+\.)*(?:[-a-z0-9]+\.[-a-z0-9]+)[^\s<]*[^\s<?!.,:*_~]',
|
||||||
);
|
);
|
||||||
final matches = linkRegex.allMatches(content);
|
final matches = linkRegex.allMatches(widget.content);
|
||||||
if (matches.isEmpty) {
|
if (matches.isEmpty) return;
|
||||||
return const SizedBox.shrink();
|
|
||||||
}
|
|
||||||
|
|
||||||
final LinkExpandProvider expandController = Get.find();
|
final LinkExpandProvider expandController = Get.find();
|
||||||
|
|
||||||
return Wrap(
|
if (matches.isEmpty) return;
|
||||||
children: matches.map((x) {
|
|
||||||
return Container(
|
List<LinkMeta> out = List.empty(growable: true);
|
||||||
constraints: BoxConstraints(
|
for (final x in matches) {
|
||||||
maxWidth: matches.length == 1 ? 480 : 340,
|
final result = await expandController.expandLink(x.group(0)!);
|
||||||
),
|
if (result != null) out.add(result);
|
||||||
child: FutureBuilder(
|
|
||||||
future: expandController.expandLink(x.group(0)!),
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
if (!snapshot.hasData) {
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setState(() => _meta = out);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_doExpand();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (_meta?.isEmpty ?? true) return const SizedBox.shrink();
|
||||||
|
|
||||||
|
return Wrap(
|
||||||
|
children: _meta!.map((x) {
|
||||||
|
return Container(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
maxWidth: _meta!.length == 1 ? 480 : 340,
|
||||||
|
),
|
||||||
|
child: Builder(
|
||||||
|
builder: (context) {
|
||||||
final isRichDescription = [
|
final isRichDescription = [
|
||||||
'solsynth.dev',
|
'solsynth.dev',
|
||||||
].contains(Uri.parse(snapshot.data!.url).host);
|
].contains(Uri.parse(x.url).host);
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
child: Card(
|
child: Card(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if ([
|
if ([(x.icon?.isNotEmpty ?? false), x.siteName != null]
|
||||||
(snapshot.data!.icon?.isNotEmpty ?? false),
|
.any((x) => x))
|
||||||
snapshot.data!.siteName != null
|
|
||||||
].any((x) => x))
|
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
if (snapshot.data!.icon?.isNotEmpty ?? false)
|
if (x.icon?.isNotEmpty ?? false)
|
||||||
ClipRRect(
|
ClipRRect(
|
||||||
borderRadius: const BorderRadius.all(
|
borderRadius: const BorderRadius.all(
|
||||||
Radius.circular(8),
|
Radius.circular(8),
|
||||||
),
|
),
|
||||||
child: _buildImage(
|
child: _buildImage(
|
||||||
snapshot.data!.icon!,
|
x.icon!,
|
||||||
width: 32,
|
width: 32,
|
||||||
height: 32,
|
height: 32,
|
||||||
),
|
),
|
||||||
).paddingOnly(right: 8),
|
).paddingOnly(right: 8),
|
||||||
if (snapshot.data!.siteName != null)
|
if (x.siteName != null)
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
snapshot.data!.siteName!,
|
x.siteName!,
|
||||||
style: Theme.of(context).textTheme.labelLarge,
|
style: Theme.of(context).textTheme.labelLarge,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
@ -84,32 +103,27 @@ class LinkExpansion extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
).paddingOnly(
|
).paddingOnly(
|
||||||
bottom: (snapshot.data!.icon?.isNotEmpty ?? false)
|
bottom: (x.icon?.isNotEmpty ?? false) ? 8 : 4,
|
||||||
? 8
|
|
||||||
: 4,
|
|
||||||
),
|
),
|
||||||
if (snapshot.data!.image != null &&
|
if (x.image != null &&
|
||||||
(snapshot.data!.image?.startsWith('http') ?? false))
|
(x.image?.startsWith('http') ?? false))
|
||||||
ClipRRect(
|
ClipRRect(
|
||||||
borderRadius: const BorderRadius.all(
|
borderRadius: const BorderRadius.all(
|
||||||
Radius.circular(8),
|
Radius.circular(8),
|
||||||
),
|
),
|
||||||
child: _buildImage(
|
child: _buildImage(x.image!),
|
||||||
snapshot.data!.image!,
|
|
||||||
),
|
|
||||||
).paddingOnly(bottom: 8),
|
).paddingOnly(bottom: 8),
|
||||||
Text(
|
Text(
|
||||||
snapshot.data!.title ?? 'No Title',
|
x.title ?? 'No Title',
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.fade,
|
overflow: TextOverflow.fade,
|
||||||
style: Theme.of(context).textTheme.bodyLarge,
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
),
|
),
|
||||||
if (snapshot.data!.description != null &&
|
if (x.description != null && isRichDescription)
|
||||||
isRichDescription)
|
MarkdownBody(data: x.description!)
|
||||||
MarkdownBody(data: snapshot.data!.description!)
|
else if (x.description != null)
|
||||||
else if (snapshot.data!.description != null)
|
|
||||||
Text(
|
Text(
|
||||||
snapshot.data!.description!,
|
x.description!,
|
||||||
maxLines: 3,
|
maxLines: 3,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
@ -117,7 +131,7 @@ class LinkExpansion extends StatelessWidget {
|
|||||||
).paddingAll(12),
|
).paddingAll(12),
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
launchUrlString(x.group(0)!);
|
launchUrlString(x.url);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
28
lib/widgets/loading_indicator.dart
Normal file
28
lib/widgets/loading_indicator.dart
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
|
class LoadingIndicator extends StatelessWidget {
|
||||||
|
const LoadingIndicator({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 24),
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainerLow.withOpacity(0.5),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const SizedBox(
|
||||||
|
height: 16,
|
||||||
|
width: 16,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2.5),
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
Text('loading'.tr)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,12 +1,16 @@
|
|||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_markdown_selectionarea/flutter_markdown.dart';
|
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
import 'package:markdown/markdown.dart' as markdown;
|
import 'package:markdown/markdown.dart' as markdown;
|
||||||
import 'package:markdown/markdown.dart';
|
import 'package:path/path.dart';
|
||||||
import 'package:solian/providers/stickers.dart';
|
import 'package:solian/providers/stickers.dart';
|
||||||
import 'package:solian/widgets/attachments/attachment_list.dart';
|
import 'package:solian/widgets/attachments/attachment_list.dart';
|
||||||
import 'package:solian/widgets/auto_cache_image.dart';
|
import 'package:solian/widgets/auto_cache_image.dart';
|
||||||
|
import 'package:syntax_highlight/syntax_highlight.dart';
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
import 'account/account_profile_popup.dart';
|
import 'account/account_profile_popup.dart';
|
||||||
@ -39,11 +43,6 @@ class MarkdownTextContent extends StatelessWidget {
|
|||||||
// Getting paragraph
|
// Getting paragraph
|
||||||
var paragraph = paragraphs[idx];
|
var paragraph = paragraphs[idx];
|
||||||
|
|
||||||
// Auto adding new-lines
|
|
||||||
if (isAutoWarp) {
|
|
||||||
paragraph = paragraph.replaceAll('\n', '\\\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Matching stickers
|
// Matching stickers
|
||||||
final stickerMatch = stickerRegex.allMatches(paragraph);
|
final stickerMatch = stickerRegex.allMatches(paragraph);
|
||||||
final isOnlySticker =
|
final isOnlySticker =
|
||||||
@ -58,7 +57,7 @@ class MarkdownTextContent extends StatelessWidget {
|
|||||||
styleSheet: MarkdownStyleSheet.fromTheme(
|
styleSheet: MarkdownStyleSheet.fromTheme(
|
||||||
Theme.of(context),
|
Theme.of(context),
|
||||||
).copyWith(
|
).copyWith(
|
||||||
textScaleFactor: isLargeText ? 1.1 : 1,
|
textScaler: TextScaler.linear(isLargeText ? 1.1 : 1),
|
||||||
blockquote: TextStyle(
|
blockquote: TextStyle(
|
||||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
@ -74,15 +73,32 @@ class MarkdownTextContent extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
codeblockDecoration: BoxDecoration(
|
||||||
|
border: Border.all(
|
||||||
|
color: Theme.of(context).dividerColor,
|
||||||
|
width: 0.3,
|
||||||
),
|
),
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(4)),
|
||||||
|
color: Theme.of(context).colorScheme.surface.withOpacity(0.5),
|
||||||
|
)),
|
||||||
|
builders: {
|
||||||
|
'code': _MarkdownTextCodeElement(),
|
||||||
|
},
|
||||||
|
softLineBreak: true,
|
||||||
extensionSet: markdown.ExtensionSet(
|
extensionSet: markdown.ExtensionSet(
|
||||||
markdown.ExtensionSet.gitHubFlavored.blockSyntaxes,
|
<markdown.BlockSyntax>[
|
||||||
|
markdown.CodeBlockSyntax(),
|
||||||
|
...markdown.ExtensionSet.commonMark.blockSyntaxes,
|
||||||
|
...markdown.ExtensionSet.gitHubFlavored.blockSyntaxes,
|
||||||
|
],
|
||||||
<markdown.InlineSyntax>[
|
<markdown.InlineSyntax>[
|
||||||
|
if (isAutoWarp) markdown.LineBreakSyntax(),
|
||||||
_UserNameCardInlineSyntax(),
|
_UserNameCardInlineSyntax(),
|
||||||
_CustomEmoteInlineSyntax(),
|
_CustomEmoteInlineSyntax(),
|
||||||
markdown.EmojiSyntax(),
|
|
||||||
markdown.AutolinkSyntax(),
|
markdown.AutolinkSyntax(),
|
||||||
markdown.AutolinkExtensionSyntax(),
|
markdown.AutolinkExtensionSyntax(),
|
||||||
|
markdown.CodeSyntax(),
|
||||||
|
...markdown.ExtensionSet.commonMark.inlineSyntaxes,
|
||||||
...markdown.ExtensionSet.gitHubFlavored.inlineSyntaxes
|
...markdown.ExtensionSet.gitHubFlavored.inlineSyntaxes
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -184,7 +200,7 @@ class MarkdownTextContent extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (idx < paragraphs.length - 1) {
|
if (idx < paragraphs.length - 1) {
|
||||||
contentWidgets.add(const Gap(4));
|
contentWidgets.add(isAutoWarp ? const Gap(4) : const Gap(8));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -205,7 +221,7 @@ class MarkdownTextContent extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _UserNameCardInlineSyntax extends InlineSyntax {
|
class _UserNameCardInlineSyntax extends markdown.InlineSyntax {
|
||||||
_UserNameCardInlineSyntax() : super(r'@[a-zA-Z0-9_]+');
|
_UserNameCardInlineSyntax() : super(r'@[a-zA-Z0-9_]+');
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -221,7 +237,7 @@ class _UserNameCardInlineSyntax extends InlineSyntax {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _CustomEmoteInlineSyntax extends InlineSyntax {
|
class _CustomEmoteInlineSyntax extends markdown.InlineSyntax {
|
||||||
_CustomEmoteInlineSyntax() : super(r':([-\w]+):');
|
_CustomEmoteInlineSyntax() : super(r':([-\w]+):');
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -241,3 +257,48 @@ class _CustomEmoteInlineSyntax extends InlineSyntax {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _MarkdownTextCodeElement extends MarkdownElementBuilder {
|
||||||
|
@override
|
||||||
|
Widget? visitElementAfter(
|
||||||
|
markdown.Element element,
|
||||||
|
TextStyle? preferredStyle,
|
||||||
|
) {
|
||||||
|
var language = '';
|
||||||
|
|
||||||
|
if (element.attributes['class'] != null) {
|
||||||
|
String lg = element.attributes['class'] as String;
|
||||||
|
language = lg.substring(9).trim();
|
||||||
|
}
|
||||||
|
return SizedBox(
|
||||||
|
child: FutureBuilder(
|
||||||
|
future: (() async {
|
||||||
|
final docPath = '../../../';
|
||||||
|
final highlightingPath =
|
||||||
|
join(docPath, 'assets/highlighting', language);
|
||||||
|
await Highlighter.initialize([highlightingPath]);
|
||||||
|
return Highlighter(
|
||||||
|
language: highlightingPath,
|
||||||
|
theme: PlatformDispatcher.instance.platformBrightness ==
|
||||||
|
Brightness.light
|
||||||
|
? await HighlighterTheme.loadLightTheme()
|
||||||
|
: await HighlighterTheme.loadDarkTheme(),
|
||||||
|
);
|
||||||
|
})(),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.hasData) {
|
||||||
|
final highlighter = snapshot.data!;
|
||||||
|
return Text.rich(
|
||||||
|
highlighter.highlight(element.textContent.trim()),
|
||||||
|
style: GoogleFonts.robotoMono(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Text(
|
||||||
|
element.textContent.trim(),
|
||||||
|
style: GoogleFonts.robotoMono(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
).paddingAll(8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
80
lib/widgets/navigation/app_account_widget.dart
Normal file
80
lib/widgets/navigation/app_account_widget.dart
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:solian/models/account_status.dart';
|
||||||
|
import 'package:solian/providers/account_status.dart';
|
||||||
|
import 'package:solian/providers/auth.dart';
|
||||||
|
import 'package:solian/providers/relation.dart';
|
||||||
|
import 'package:badges/badges.dart' as badges;
|
||||||
|
import 'package:solian/widgets/account/account_avatar.dart';
|
||||||
|
|
||||||
|
class AppAccountWidget extends StatefulWidget {
|
||||||
|
const AppAccountWidget({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AppAccountWidget> createState() => _AppAccountWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AppAccountWidgetState extends State<AppAccountWidget> {
|
||||||
|
AccountStatus? _accountStatus;
|
||||||
|
|
||||||
|
Future<void> _getStatus() async {
|
||||||
|
final StatusProvider provider = Get.find();
|
||||||
|
|
||||||
|
final resp = await provider.getCurrentStatus();
|
||||||
|
final status = AccountStatus.fromJson(resp.body);
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_accountStatus = status;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_getStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final AuthProvider auth = Get.find();
|
||||||
|
|
||||||
|
return Obx(() {
|
||||||
|
if (auth.isAuthorized.isFalse || auth.userProfile.value == null) {
|
||||||
|
return const Icon(Icons.account_circle);
|
||||||
|
}
|
||||||
|
|
||||||
|
final statusBadgeColor = _accountStatus != null
|
||||||
|
? StatusProvider.determineStatus(_accountStatus!).$2
|
||||||
|
: Colors.grey;
|
||||||
|
|
||||||
|
final RelationshipProvider relations = Get.find();
|
||||||
|
final accountNotifications = relations.friendRequestCount.value;
|
||||||
|
|
||||||
|
return badges.Badge(
|
||||||
|
badgeContent: Text(
|
||||||
|
accountNotifications.toString(),
|
||||||
|
style: const TextStyle(color: Colors.white),
|
||||||
|
),
|
||||||
|
showBadge: accountNotifications > 0,
|
||||||
|
position: badges.BadgePosition.topEnd(
|
||||||
|
top: -10,
|
||||||
|
end: -6,
|
||||||
|
),
|
||||||
|
child: badges.Badge(
|
||||||
|
showBadge: _accountStatus != null,
|
||||||
|
badgeStyle: badges.BadgeStyle(badgeColor: statusBadgeColor),
|
||||||
|
position: badges.BadgePosition.bottomEnd(
|
||||||
|
bottom: 0,
|
||||||
|
end: -2,
|
||||||
|
),
|
||||||
|
child: AttachedCircleAvatar(
|
||||||
|
radius: 14,
|
||||||
|
content: auth.userProfile.value!['avatar'],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -1,27 +1,33 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/utils.dart';
|
import 'package:get/utils.dart';
|
||||||
|
import 'package:solian/widgets/navigation/app_account_widget.dart';
|
||||||
|
|
||||||
abstract class AppNavigation {
|
abstract class AppNavigation {
|
||||||
static List<AppNavigationDestination> destinations = [
|
static List<AppNavigationDestination> destinations = [
|
||||||
AppNavigationDestination(
|
AppNavigationDestination(
|
||||||
icon: Icons.dashboard,
|
icon: const Icon(Icons.dashboard),
|
||||||
label: 'dashboard'.tr,
|
label: 'dashboardNav'.tr,
|
||||||
page: 'dashboard',
|
page: 'dashboard',
|
||||||
),
|
),
|
||||||
AppNavigationDestination(
|
AppNavigationDestination(
|
||||||
icon: Icons.explore,
|
icon: const Icon(Icons.explore),
|
||||||
label: 'explore'.tr,
|
label: 'explore'.tr,
|
||||||
page: 'explore',
|
page: 'explore',
|
||||||
),
|
),
|
||||||
AppNavigationDestination(
|
AppNavigationDestination(
|
||||||
icon: Icons.workspaces,
|
icon: const Icon(Icons.forum),
|
||||||
|
label: 'chat'.tr,
|
||||||
|
page: 'chat',
|
||||||
|
),
|
||||||
|
AppNavigationDestination(
|
||||||
|
icon: const Icon(Icons.workspaces),
|
||||||
label: 'realms'.tr,
|
label: 'realms'.tr,
|
||||||
page: 'realms',
|
page: 'realms',
|
||||||
),
|
),
|
||||||
AppNavigationDestination(
|
AppNavigationDestination(
|
||||||
icon: Icons.forum,
|
icon: const AppAccountWidget(),
|
||||||
label: 'chat'.tr,
|
label: 'accountNav'.tr,
|
||||||
page: 'chat',
|
page: 'account',
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -30,7 +36,7 @@ abstract class AppNavigation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class AppNavigationDestination {
|
class AppNavigationDestination {
|
||||||
final IconData icon;
|
final Widget icon;
|
||||||
final String label;
|
final String label;
|
||||||
final String page;
|
final String page;
|
||||||
|
|
||||||
|
47
lib/widgets/navigation/app_navigation_bottom.dart
Normal file
47
lib/widgets/navigation/app_navigation_bottom.dart
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:solian/router.dart';
|
||||||
|
import 'package:solian/widgets/navigation/app_navigation.dart';
|
||||||
|
|
||||||
|
class AppNavigationBottom extends StatefulWidget {
|
||||||
|
final int initialIndex;
|
||||||
|
|
||||||
|
const AppNavigationBottom({super.key, this.initialIndex = 0});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AppNavigationBottom> createState() => _AppNavigationBottomState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AppNavigationBottomState extends State<AppNavigationBottom> {
|
||||||
|
int _currentIndex = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
if (widget.initialIndex >= 0) {
|
||||||
|
_currentIndex = widget.initialIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BottomNavigationBar(
|
||||||
|
currentIndex: _currentIndex,
|
||||||
|
type: BottomNavigationBarType.fixed,
|
||||||
|
showUnselectedLabels: false,
|
||||||
|
showSelectedLabels: true,
|
||||||
|
landscapeLayout: BottomNavigationBarLandscapeLayout.centered,
|
||||||
|
items: AppNavigation.destinations
|
||||||
|
.map(
|
||||||
|
(x) => BottomNavigationBarItem(
|
||||||
|
icon: x.icon,
|
||||||
|
label: x.label,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
onTap: (idx) {
|
||||||
|
setState(() => _currentIndex = idx);
|
||||||
|
AppRouter.instance.goNamed(AppNavigation.destinations[idx].page);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,330 +0,0 @@
|
|||||||
import 'dart:math' as math;
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:solian/models/account_status.dart';
|
|
||||||
import 'package:solian/providers/account_status.dart';
|
|
||||||
import 'package:solian/providers/auth.dart';
|
|
||||||
import 'package:solian/providers/relation.dart';
|
|
||||||
import 'package:solian/router.dart';
|
|
||||||
import 'package:solian/shells/root_shell.dart';
|
|
||||||
import 'package:solian/theme.dart';
|
|
||||||
import 'package:solian/widgets/account/account_avatar.dart';
|
|
||||||
import 'package:solian/widgets/account/account_status_action.dart';
|
|
||||||
import 'package:solian/widgets/navigation/app_navigation.dart';
|
|
||||||
import 'package:badges/badges.dart' as badges;
|
|
||||||
import 'package:solian/widgets/navigation/app_navigation_region.dart';
|
|
||||||
|
|
||||||
class AppNavigationDrawer extends StatefulWidget {
|
|
||||||
final String? routeName;
|
|
||||||
|
|
||||||
const AppNavigationDrawer({super.key, this.routeName});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<AppNavigationDrawer> createState() => _AppNavigationDrawerState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AppNavigationDrawerState extends State<AppNavigationDrawer>
|
|
||||||
with TickerProviderStateMixin {
|
|
||||||
bool _isCollapsed = true;
|
|
||||||
|
|
||||||
late final AnimationController _drawerAnimationController =
|
|
||||||
AnimationController(
|
|
||||||
duration: const Duration(milliseconds: 500),
|
|
||||||
vsync: this,
|
|
||||||
);
|
|
||||||
late final Animation<double> _drawerAnimation = Tween<double>(
|
|
||||||
begin: 80.0,
|
|
||||||
end: 304.0,
|
|
||||||
).animate(CurvedAnimation(
|
|
||||||
parent: _drawerAnimationController,
|
|
||||||
curve: Curves.easeInOut,
|
|
||||||
));
|
|
||||||
|
|
||||||
AccountStatus? _accountStatus;
|
|
||||||
|
|
||||||
Future<void> _getStatus() async {
|
|
||||||
final StatusProvider provider = Get.find();
|
|
||||||
|
|
||||||
final resp = await provider.getCurrentStatus();
|
|
||||||
final status = AccountStatus.fromJson(resp.body);
|
|
||||||
|
|
||||||
if (mounted) {
|
|
||||||
setState(() {
|
|
||||||
_accountStatus = status;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Color get _unFocusColor =>
|
|
||||||
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
|
|
||||||
|
|
||||||
Widget _buildUserInfo() {
|
|
||||||
return Obx(() {
|
|
||||||
final AuthProvider auth = Get.find();
|
|
||||||
if (auth.isAuthorized.isFalse || auth.userProfile.value == null) {
|
|
||||||
if (_isCollapsed) {
|
|
||||||
return InkWell(
|
|
||||||
child: const Icon(Icons.account_circle).paddingSymmetric(
|
|
||||||
horizontal: 28,
|
|
||||||
vertical: 20,
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
AppRouter.instance.goNamed('account');
|
|
||||||
_closeDrawer();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ListTile(
|
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 28),
|
|
||||||
leading: const Icon(Icons.account_circle),
|
|
||||||
title: !_isCollapsed ? Text('guest'.tr) : null,
|
|
||||||
subtitle: !_isCollapsed ? Text('unsignedIn'.tr) : null,
|
|
||||||
onTap: () {
|
|
||||||
AppRouter.instance.goNamed('account');
|
|
||||||
_closeDrawer();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final leading = Obx(() {
|
|
||||||
final statusBadgeColor = _accountStatus != null
|
|
||||||
? StatusProvider.determineStatus(_accountStatus!).$2
|
|
||||||
: Colors.grey;
|
|
||||||
|
|
||||||
final RelationshipProvider relations = Get.find();
|
|
||||||
final accountNotifications = relations.friendRequestCount.value;
|
|
||||||
|
|
||||||
return badges.Badge(
|
|
||||||
badgeContent: Text(
|
|
||||||
accountNotifications.toString(),
|
|
||||||
style: const TextStyle(color: Colors.white),
|
|
||||||
),
|
|
||||||
showBadge: accountNotifications > 0,
|
|
||||||
position: badges.BadgePosition.topEnd(
|
|
||||||
top: -10,
|
|
||||||
end: -6,
|
|
||||||
),
|
|
||||||
child: badges.Badge(
|
|
||||||
showBadge: _accountStatus != null,
|
|
||||||
badgeStyle: badges.BadgeStyle(badgeColor: statusBadgeColor),
|
|
||||||
position: badges.BadgePosition.bottomEnd(
|
|
||||||
bottom: 0,
|
|
||||||
end: -2,
|
|
||||||
),
|
|
||||||
child: AccountAvatar(
|
|
||||||
content: auth.userProfile.value!['avatar'],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return InkWell(
|
|
||||||
child: !_isCollapsed
|
|
||||||
? Row(
|
|
||||||
children: [
|
|
||||||
leading,
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
auth.userProfile.value!['nick'],
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.fade,
|
|
||||||
style: Theme.of(context).textTheme.bodyLarge,
|
|
||||||
).paddingOnly(left: 16),
|
|
||||||
Builder(
|
|
||||||
builder: (context) {
|
|
||||||
if (_accountStatus == null) {
|
|
||||||
return Text(
|
|
||||||
'loading'.tr,
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.fade,
|
|
||||||
style: TextStyle(
|
|
||||||
color: _unFocusColor,
|
|
||||||
),
|
|
||||||
).paddingOnly(left: 16);
|
|
||||||
}
|
|
||||||
final info = StatusProvider.determineStatus(
|
|
||||||
_accountStatus!,
|
|
||||||
);
|
|
||||||
return Text(
|
|
||||||
info.$3,
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.fade,
|
|
||||||
style: TextStyle(
|
|
||||||
color: _unFocusColor,
|
|
||||||
),
|
|
||||||
).paddingOnly(left: 16);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
).paddingSymmetric(horizontal: 20, vertical: 16)
|
|
||||||
: leading.paddingSymmetric(horizontal: 20, vertical: 16),
|
|
||||||
onTap: () {
|
|
||||||
AppRouter.instance.goNamed('account');
|
|
||||||
_closeDrawer();
|
|
||||||
},
|
|
||||||
onLongPress: () {
|
|
||||||
showModalBottomSheet(
|
|
||||||
useRootNavigator: true,
|
|
||||||
context: context,
|
|
||||||
builder: (context) => AccountStatusAction(
|
|
||||||
currentStatus: _accountStatus!.status,
|
|
||||||
),
|
|
||||||
).then((val) {
|
|
||||||
if (val == true) _getStatus();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void _expandDrawer() {
|
|
||||||
_drawerAnimationController.animateTo(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _collapseDrawer() {
|
|
||||||
_drawerAnimationController.animateTo(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _closeDrawer() {
|
|
||||||
_autoResize();
|
|
||||||
rootScaffoldKey.currentState!.closeDrawer();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _autoResize() {
|
|
||||||
if (AppTheme.isExtraLargeScreen(context)) {
|
|
||||||
_expandDrawer();
|
|
||||||
} else if (AppTheme.isLargeScreen(context)) {
|
|
||||||
_collapseDrawer();
|
|
||||||
} else {
|
|
||||||
_drawerAnimationController.value = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
final AuthProvider auth = Get.find();
|
|
||||||
if (auth.isAuthorized.value) _getStatus();
|
|
||||||
Future.delayed(Duration.zero, () => _autoResize());
|
|
||||||
_drawerAnimationController.addListener(() {
|
|
||||||
if (_drawerAnimation.value > 180 && _isCollapsed) {
|
|
||||||
setState(() => _isCollapsed = false);
|
|
||||||
} else if (_drawerAnimation.value < 180 && !_isCollapsed) {
|
|
||||||
setState(() => _isCollapsed = true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_drawerAnimationController.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return AnimatedBuilder(
|
|
||||||
animation: _drawerAnimation,
|
|
||||||
builder: (context, child) {
|
|
||||||
return Drawer(
|
|
||||||
width: _drawerAnimation.value,
|
|
||||||
backgroundColor:
|
|
||||||
AppTheme.isLargeScreen(context) ? Colors.transparent : null,
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: SafeArea(
|
|
||||||
bottom: false,
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
_buildUserInfo().paddingSymmetric(vertical: 8),
|
|
||||||
const Divider(thickness: 0.3, height: 1),
|
|
||||||
SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: Wrap(
|
|
||||||
runSpacing: 8,
|
|
||||||
spacing: 8,
|
|
||||||
alignment: WrapAlignment.spaceAround,
|
|
||||||
children: AppNavigation.destinations
|
|
||||||
.map(
|
|
||||||
(e) => Tooltip(
|
|
||||||
message: e.label,
|
|
||||||
child: InkWell(
|
|
||||||
borderRadius:
|
|
||||||
const BorderRadius.all(Radius.circular(8)),
|
|
||||||
child: Icon(
|
|
||||||
e.icon,
|
|
||||||
size: 22,
|
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
|
||||||
).paddingAll(16),
|
|
||||||
onTap: () {
|
|
||||||
AppRouter.instance.goNamed(e.page);
|
|
||||||
_closeDrawer();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
).paddingSymmetric(vertical: 8, horizontal: 12),
|
|
||||||
),
|
|
||||||
const Divider(thickness: 0.3, height: 1),
|
|
||||||
Expanded(
|
|
||||||
child: Material(
|
|
||||||
color: Theme.of(context).colorScheme.surface,
|
|
||||||
child: AppNavigationRegion(
|
|
||||||
isCollapsed: _isCollapsed,
|
|
||||||
onSelected: () {
|
|
||||||
_closeDrawer();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Divider(thickness: 0.3, height: 1),
|
|
||||||
Column(
|
|
||||||
children: [
|
|
||||||
if (_isCollapsed)
|
|
||||||
Tooltip(
|
|
||||||
message: 'expand'.tr,
|
|
||||||
child: InkWell(
|
|
||||||
child: const Icon(Icons.chevron_right, size: 20)
|
|
||||||
.paddingSymmetric(
|
|
||||||
horizontal: 28,
|
|
||||||
vertical: 10,
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
_expandDrawer();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
else
|
|
||||||
ListTile(
|
|
||||||
minTileHeight: 0,
|
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 20,
|
|
||||||
),
|
|
||||||
leading:
|
|
||||||
const Icon(Icons.chevron_left, size: 20).paddingAll(2),
|
|
||||||
title: Text('collapse'.tr),
|
|
||||||
onTap: () {
|
|
||||||
_collapseDrawer();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
).paddingOnly(
|
|
||||||
top: 8,
|
|
||||||
bottom: math.max(8, MediaQuery.of(context).padding.bottom),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
69
lib/widgets/navigation/app_navigation_rail.dart
Normal file
69
lib/widgets/navigation/app_navigation_rail.dart
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:solian/router.dart';
|
||||||
|
import 'package:solian/widgets/navigation/app_navigation.dart';
|
||||||
|
|
||||||
|
class AppNavigationRail extends StatefulWidget {
|
||||||
|
final int initialIndex;
|
||||||
|
|
||||||
|
const AppNavigationRail({super.key, this.initialIndex = 0});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AppNavigationRail> createState() => _AppNavigationRailState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AppNavigationRailState extends State<AppNavigationRail> {
|
||||||
|
int? _currentIndex = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
if (widget.initialIndex >= 0) {
|
||||||
|
_currentIndex = widget.initialIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Material(
|
||||||
|
color: Theme.of(context).colorScheme.surface,
|
||||||
|
child: NavigationRail(
|
||||||
|
selectedIndex: _currentIndex,
|
||||||
|
labelType: NavigationRailLabelType.selected,
|
||||||
|
groupAlignment: -1,
|
||||||
|
destinations: AppNavigation.destinations
|
||||||
|
.sublist(0, AppNavigation.destinations.length - 1)
|
||||||
|
.map(
|
||||||
|
(x) => NavigationRailDestination(
|
||||||
|
icon: x.icon,
|
||||||
|
label: Text(x.label),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
trailing: Expanded(
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
child: IconButton(
|
||||||
|
icon: AppNavigation.destinations.last.icon,
|
||||||
|
tooltip: AppNavigation.destinations.last.label,
|
||||||
|
onPressed: () {
|
||||||
|
setState(() => _currentIndex = null);
|
||||||
|
AppRouter.instance
|
||||||
|
.goNamed(AppNavigation.destinations.last.page);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onDestinationSelected: (idx) {
|
||||||
|
setState(() => _currentIndex = idx);
|
||||||
|
AppRouter.instance.goNamed(AppNavigation.destinations[idx].page);
|
||||||
|
},
|
||||||
|
).paddingOnly(
|
||||||
|
top: max(16, MediaQuery.of(context).padding.top),
|
||||||
|
bottom: max(16, MediaQuery.of(context).padding.bottom),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,230 +0,0 @@
|
|||||||
import 'package:animations/animations.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:solian/models/realm.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/navigation.dart';
|
|
||||||
import 'package:solian/services.dart';
|
|
||||||
import 'package:solian/widgets/account/account_avatar.dart';
|
|
||||||
import 'package:solian/widgets/auto_cache_image.dart';
|
|
||||||
import 'package:solian/widgets/channel/channel_list.dart';
|
|
||||||
|
|
||||||
class AppNavigationRegion extends StatefulWidget {
|
|
||||||
final bool isCollapsed;
|
|
||||||
final Function onSelected;
|
|
||||||
|
|
||||||
const AppNavigationRegion({
|
|
||||||
super.key,
|
|
||||||
this.isCollapsed = false,
|
|
||||||
required this.onSelected,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<AppNavigationRegion> createState() => _AppNavigationRegionState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AppNavigationRegionState extends State<AppNavigationRegion> {
|
|
||||||
bool _isTryingExit = false;
|
|
||||||
|
|
||||||
void _focusRealm(Realm item) {
|
|
||||||
setState(
|
|
||||||
() => Get.find<NavigationStateProvider>().focusedRealm.value = item,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _unFocusRealm() {
|
|
||||||
setState(
|
|
||||||
() => Get.find<NavigationStateProvider>().focusedRealm.value = null,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildRealmFocusAvatar() {
|
|
||||||
final focusedRealm = Get.find<NavigationStateProvider>().focusedRealm.value;
|
|
||||||
return GestureDetector(
|
|
||||||
child: MouseRegion(
|
|
||||||
child: AnimatedSwitcher(
|
|
||||||
switchInCurve: Curves.fastOutSlowIn,
|
|
||||||
switchOutCurve: Curves.fastOutSlowIn,
|
|
||||||
duration: const Duration(milliseconds: 300),
|
|
||||||
transitionBuilder: (child, animation) {
|
|
||||||
return ScaleTransition(
|
|
||||||
scale: animation,
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: _isTryingExit
|
|
||||||
? CircleAvatar(
|
|
||||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
|
||||||
child: const Icon(
|
|
||||||
Icons.arrow_back,
|
|
||||||
color: Colors.white,
|
|
||||||
size: 16,
|
|
||||||
),
|
|
||||||
).paddingSymmetric(
|
|
||||||
vertical: 8,
|
|
||||||
)
|
|
||||||
: _buildEntryAvatar(focusedRealm!),
|
|
||||||
),
|
|
||||||
onEnter: (_) => setState(() => _isTryingExit = true),
|
|
||||||
onExit: (_) => setState(() => _isTryingExit = false),
|
|
||||||
),
|
|
||||||
onTap: () => _unFocusRealm(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildEntryAvatar(Realm item) {
|
|
||||||
return Hero(
|
|
||||||
tag: Key('region-realm-avatar-${item.id}'),
|
|
||||||
child: (item.avatar?.isNotEmpty ?? false)
|
|
||||||
? AccountAvatar(content: item.avatar)
|
|
||||||
: CircleAvatar(
|
|
||||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
|
||||||
child: const Icon(
|
|
||||||
Icons.workspaces,
|
|
||||||
color: Colors.white,
|
|
||||||
size: 16,
|
|
||||||
),
|
|
||||||
).paddingSymmetric(
|
|
||||||
vertical: 8,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildEntry(BuildContext context, Realm item) {
|
|
||||||
const padding = EdgeInsets.symmetric(horizontal: 20, vertical: 8);
|
|
||||||
|
|
||||||
if (widget.isCollapsed) {
|
|
||||||
return InkWell(
|
|
||||||
child: _buildEntryAvatar(item).paddingSymmetric(vertical: 8),
|
|
||||||
onTap: () => _focusRealm(item),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ListTile(
|
|
||||||
minTileHeight: 0,
|
|
||||||
leading: _buildEntryAvatar(item),
|
|
||||||
contentPadding: padding,
|
|
||||||
title: Text(item.name),
|
|
||||||
subtitle: Text(
|
|
||||||
item.description,
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
onTap: () => _focusRealm(item),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final RealmProvider realms = Get.find();
|
|
||||||
final ChannelProvider channels = Get.find();
|
|
||||||
final AuthProvider auth = Get.find();
|
|
||||||
final NavigationStateProvider navState = Get.find();
|
|
||||||
|
|
||||||
return Obx(
|
|
||||||
() => PageTransitionSwitcher(
|
|
||||||
transitionBuilder: (child, animation, secondaryAnimation) {
|
|
||||||
return SharedAxisTransition(
|
|
||||||
animation: animation,
|
|
||||||
secondaryAnimation: secondaryAnimation,
|
|
||||||
transitionType: SharedAxisTransitionType.horizontal,
|
|
||||||
child: Material(
|
|
||||||
color: Theme.of(context).colorScheme.surface,
|
|
||||||
child: child,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: navState.focusedRealm.value == null
|
|
||||||
? widget.isCollapsed
|
|
||||||
? CustomScrollView(
|
|
||||||
slivers: [
|
|
||||||
const SliverPadding(padding: EdgeInsets.only(top: 16)),
|
|
||||||
SliverList.builder(
|
|
||||||
itemCount: realms.availableRealms.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final element = realms.availableRealms[index];
|
|
||||||
return Tooltip(
|
|
||||||
message: element.name,
|
|
||||||
child: _buildEntry(context, element),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
: CustomScrollView(
|
|
||||||
slivers: [
|
|
||||||
SliverList.builder(
|
|
||||||
itemCount: realms.availableRealms.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final element = realms.availableRealms[index];
|
|
||||||
return _buildEntry(context, element);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
: Column(
|
|
||||||
children: [
|
|
||||||
if (!widget.isCollapsed &&
|
|
||||||
(navState.focusedRealm.value!.banner?.isNotEmpty ??
|
|
||||||
false))
|
|
||||||
AspectRatio(
|
|
||||||
aspectRatio: 16 / 7,
|
|
||||||
child: AutoCacheImage(
|
|
||||||
ServiceFinder.buildUrl(
|
|
||||||
'uc',
|
|
||||||
'/attachments/${navState.focusedRealm.value!.banner}',
|
|
||||||
),
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (widget.isCollapsed)
|
|
||||||
Tooltip(
|
|
||||||
message: navState.focusedRealm.value!.name,
|
|
||||||
child: _buildRealmFocusAvatar().paddingOnly(
|
|
||||||
top: 24,
|
|
||||||
bottom: 8,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
else
|
|
||||||
ListTile(
|
|
||||||
minTileHeight: 0,
|
|
||||||
tileColor:
|
|
||||||
Theme.of(context).colorScheme.surfaceContainerLow,
|
|
||||||
leading: _buildRealmFocusAvatar(),
|
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 20, vertical: 8),
|
|
||||||
title: Text(navState.focusedRealm.value!.name),
|
|
||||||
subtitle: Text(
|
|
||||||
navState.focusedRealm.value!.description,
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: Obx(
|
|
||||||
() => ChannelListWidget(
|
|
||||||
useReplace: true,
|
|
||||||
channels: channels.availableChannels
|
|
||||||
.where((x) =>
|
|
||||||
x.realm?.id == navState.focusedRealm.value?.id)
|
|
||||||
.toList(),
|
|
||||||
isCollapsed: widget.isCollapsed,
|
|
||||||
selfId: auth.userProfile.value!['id'],
|
|
||||||
noCategory: true,
|
|
||||||
onSelected: (_) => widget.onSelected(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
92
lib/widgets/navigation/realm_switcher.dart
Normal file
92
lib/widgets/navigation/realm_switcher.dart
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:solian/models/realm.dart';
|
||||||
|
import 'package:solian/providers/content/realm.dart';
|
||||||
|
import 'package:solian/providers/navigation.dart';
|
||||||
|
import 'package:solian/widgets/account/account_avatar.dart';
|
||||||
|
|
||||||
|
class RealmSwitcher extends StatelessWidget {
|
||||||
|
const RealmSwitcher({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final realms = Get.find<RealmProvider>();
|
||||||
|
final navState = Get.find<NavigationStateProvider>();
|
||||||
|
|
||||||
|
return Obx(() {
|
||||||
|
return DropdownButtonHideUnderline(
|
||||||
|
child: DropdownButton2<Realm?>(
|
||||||
|
iconStyleData: const IconStyleData(iconSize: 0),
|
||||||
|
isExpanded: true,
|
||||||
|
hint: Text(
|
||||||
|
'Realm Region',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Theme.of(context).hintColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
items: [null, ...realms.availableRealms]
|
||||||
|
.map((Realm? item) => DropdownMenuItem<Realm?>(
|
||||||
|
value: item,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
if (item != null)
|
||||||
|
AttachedCircleAvatar(
|
||||||
|
content: item.avatar,
|
||||||
|
radius: 14,
|
||||||
|
fallbackWidget: const Icon(
|
||||||
|
Icons.workspaces,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
CircleAvatar(
|
||||||
|
backgroundColor:
|
||||||
|
Theme.of(context).colorScheme.primary,
|
||||||
|
radius: 14,
|
||||||
|
child: const Icon(
|
||||||
|
Icons.public,
|
||||||
|
color: Colors.white,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
item?.name ?? 'global'.tr,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
))
|
||||||
|
.toList(),
|
||||||
|
value: navState.focusedRealm.value,
|
||||||
|
onChanged: (Realm? value) {
|
||||||
|
navState.focusedRealm.value = value;
|
||||||
|
},
|
||||||
|
buttonStyleData: ButtonStyleData(
|
||||||
|
height: 48,
|
||||||
|
width: max(200, MediaQuery.of(context).size.width * 0.4),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(16)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
menuItemStyleData: const MenuItemStyleData(
|
||||||
|
height: 48,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -29,7 +29,7 @@ class _PostEditorThumbnailDialogState extends State<PostEditorThumbnailDialog> {
|
|||||||
_attachmentController.text = value.toString();
|
_attachmentController.text = value.toString();
|
||||||
});
|
});
|
||||||
|
|
||||||
widget.controller.thumbnail.value = value;
|
widget.controller.thumbnail.value = value.isEmpty ? null : value;
|
||||||
},
|
},
|
||||||
initialAttachments: const [],
|
initialAttachments: const [],
|
||||||
onRemove: (_) {},
|
onRemove: (_) {},
|
||||||
@ -91,7 +91,8 @@ class _PostEditorThumbnailDialogState extends State<PostEditorThumbnailDialog> {
|
|||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
widget.controller.thumbnail.value = _attachmentController.text;
|
final text = _attachmentController.text;
|
||||||
|
widget.controller.thumbnail.value = text.isEmpty ? null : text;
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
},
|
},
|
||||||
child: Text('confirm'.tr),
|
child: Text('confirm'.tr),
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:io';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:firebase_analytics/firebase_analytics.dart';
|
import 'package:firebase_analytics/firebase_analytics.dart';
|
||||||
@ -5,6 +6,8 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_animate/flutter_animate.dart';
|
import 'package:flutter_animate/flutter_animate.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:screenshot/screenshot.dart';
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
import 'package:solian/exts.dart';
|
import 'package:solian/exts.dart';
|
||||||
import 'package:solian/models/post.dart';
|
import 'package:solian/models/post.dart';
|
||||||
@ -12,6 +15,7 @@ import 'package:solian/platform.dart';
|
|||||||
import 'package:solian/providers/auth.dart';
|
import 'package:solian/providers/auth.dart';
|
||||||
import 'package:solian/router.dart';
|
import 'package:solian/router.dart';
|
||||||
import 'package:solian/screens/posts/post_editor.dart';
|
import 'package:solian/screens/posts/post_editor.dart';
|
||||||
|
import 'package:solian/widgets/posts/post_share.dart';
|
||||||
import 'package:solian/widgets/reports/abuse_report.dart';
|
import 'package:solian/widgets/reports/abuse_report.dart';
|
||||||
|
|
||||||
class PostAction extends StatefulWidget {
|
class PostAction extends StatefulWidget {
|
||||||
@ -84,6 +88,24 @@ class _PostActionState extends State<PostAction> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _shareImage() async {
|
||||||
|
final screenshot = ScreenshotController();
|
||||||
|
final image = await screenshot.captureFromWidget(
|
||||||
|
PostShareImage(item: widget.item),
|
||||||
|
context: context,
|
||||||
|
);
|
||||||
|
final directory = await getApplicationDocumentsDirectory();
|
||||||
|
final imageFile = await File(
|
||||||
|
'${directory.path}/temporary_share_image.png',
|
||||||
|
).create();
|
||||||
|
await imageFile.writeAsBytes(image);
|
||||||
|
|
||||||
|
final file = XFile(imageFile.path);
|
||||||
|
await Share.shareXFiles([file]);
|
||||||
|
|
||||||
|
await imageFile.delete();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@ -135,16 +157,29 @@ class _PostActionState extends State<PostAction> {
|
|||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
leading: const Icon(Icons.share),
|
leading: const Icon(Icons.share),
|
||||||
title: Text('share'.tr),
|
title: Text('share'.tr),
|
||||||
trailing: PlatformInfo.isIOS || PlatformInfo.isAndroid
|
trailing: Row(
|
||||||
? IconButton(
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (PlatformInfo.isIOS || PlatformInfo.isAndroid)
|
||||||
|
IconButton(
|
||||||
icon: const Icon(Icons.link_off),
|
icon: const Icon(Icons.link_off),
|
||||||
tooltip: 'shareNoUri'.tr,
|
tooltip: 'shareNoUri'.tr,
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await _doShare(noUri: true);
|
await _doShare(noUri: true);
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
},
|
},
|
||||||
)
|
),
|
||||||
: null,
|
if (PlatformInfo.isIOS || PlatformInfo.isAndroid)
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.image),
|
||||||
|
tooltip: 'shareImage'.tr,
|
||||||
|
onPressed: () async {
|
||||||
|
await _shareImage();
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
await _doShare();
|
await _doShare();
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
|
108
lib/widgets/posts/post_creation.dart
Normal file
108
lib/widgets/posts/post_creation.dart
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:solian/providers/auth.dart';
|
||||||
|
import 'package:solian/router.dart';
|
||||||
|
|
||||||
|
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,6 +1,5 @@
|
|||||||
import 'package:animations/animations.dart';
|
import 'package:animations/animations.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
|
||||||
import 'package:flutter_animate/flutter_animate.dart';
|
import 'package:flutter_animate/flutter_animate.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
@ -8,11 +7,11 @@ import 'package:get/get.dart';
|
|||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:solian/models/post.dart';
|
import 'package:solian/models/post.dart';
|
||||||
import 'package:solian/providers/content/posts.dart';
|
import 'package:solian/providers/content/posts.dart';
|
||||||
|
import 'package:solian/router.dart';
|
||||||
import 'package:solian/screens/posts/post_detail.dart';
|
import 'package:solian/screens/posts/post_detail.dart';
|
||||||
import 'package:solian/shells/title_shell.dart';
|
import 'package:solian/shells/title_shell.dart';
|
||||||
import 'package:solian/theme.dart';
|
import 'package:solian/theme.dart';
|
||||||
import 'package:solian/widgets/account/account_avatar.dart';
|
import 'package:solian/widgets/account/account_avatar.dart';
|
||||||
import 'package:solian/widgets/account/account_profile_popup.dart';
|
|
||||||
import 'package:solian/widgets/attachments/attachment_list.dart';
|
import 'package:solian/widgets/attachments/attachment_list.dart';
|
||||||
import 'package:solian/widgets/link_expansion.dart';
|
import 'package:solian/widgets/link_expansion.dart';
|
||||||
import 'package:solian/widgets/markdown_text_content.dart';
|
import 'package:solian/widgets/markdown_text_content.dart';
|
||||||
@ -31,11 +30,13 @@ class PostItem extends StatefulWidget {
|
|||||||
final bool isShowEmbed;
|
final bool isShowEmbed;
|
||||||
final bool isOverrideEmbedClickable;
|
final bool isOverrideEmbedClickable;
|
||||||
final bool isFullDate;
|
final bool isFullDate;
|
||||||
final bool isFullContent;
|
|
||||||
final bool isContentSelectable;
|
final bool isContentSelectable;
|
||||||
final bool showFeaturedReply;
|
final bool showFeaturedReply;
|
||||||
final String? attachmentParent;
|
final String? attachmentParent;
|
||||||
final Color? backgroundColor;
|
|
||||||
|
final EdgeInsets? padding;
|
||||||
|
|
||||||
|
final Function? onComment;
|
||||||
|
|
||||||
const PostItem({
|
const PostItem({
|
||||||
super.key,
|
super.key,
|
||||||
@ -47,11 +48,11 @@ class PostItem extends StatefulWidget {
|
|||||||
this.isShowEmbed = true,
|
this.isShowEmbed = true,
|
||||||
this.isOverrideEmbedClickable = false,
|
this.isOverrideEmbedClickable = false,
|
||||||
this.isFullDate = false,
|
this.isFullDate = false,
|
||||||
this.isFullContent = false,
|
|
||||||
this.isContentSelectable = false,
|
this.isContentSelectable = false,
|
||||||
this.showFeaturedReply = false,
|
this.showFeaturedReply = false,
|
||||||
this.attachmentParent,
|
this.attachmentParent,
|
||||||
this.backgroundColor,
|
this.padding,
|
||||||
|
this.onComment,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -64,14 +65,20 @@ class _PostItemState extends State<PostItem> {
|
|||||||
Color get _unFocusColor =>
|
Color get _unFocusColor =>
|
||||||
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
|
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
|
||||||
|
|
||||||
|
static final visibilityIcons = [
|
||||||
|
Icons.public,
|
||||||
|
Icons.group,
|
||||||
|
Icons.visibility,
|
||||||
|
Icons.visibility_off,
|
||||||
|
Icons.lock,
|
||||||
|
];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
item = widget.item;
|
item = widget.item;
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
double _contentHeight = 0;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final List<String> attachments = item.body['attachments'] is List
|
final List<String> attachments = item.body['attachments'] is List
|
||||||
@ -89,20 +96,12 @@ class _PostItemState extends State<PostItem> {
|
|||||||
).paddingOnly(bottom: 8),
|
).paddingOnly(bottom: 8),
|
||||||
_PostHeaderWidget(
|
_PostHeaderWidget(
|
||||||
isCompact: widget.isCompact,
|
isCompact: widget.isCompact,
|
||||||
|
isFullDate: widget.isFullDate,
|
||||||
item: item,
|
item: item,
|
||||||
).paddingSymmetric(horizontal: 12),
|
).paddingSymmetric(horizontal: 12),
|
||||||
_PostHeaderDividerWidget(item: item).paddingSymmetric(horizontal: 12),
|
_PostHeaderDividerWidget(item: item).paddingSymmetric(horizontal: 12),
|
||||||
Stack(
|
|
||||||
children: [
|
|
||||||
SizedContainer(
|
SizedContainer(
|
||||||
maxWidth: 640,
|
maxWidth: 640,
|
||||||
maxHeight: widget.isFullContent ? double.infinity : 80,
|
|
||||||
child: _MeasureSize(
|
|
||||||
onChange: (size) {
|
|
||||||
setState(() => _contentHeight = size.height);
|
|
||||||
},
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
|
||||||
child: MarkdownTextContent(
|
child: MarkdownTextContent(
|
||||||
parentId: 'p${item.id}',
|
parentId: 'p${item.id}',
|
||||||
content: item.body['content'],
|
content: item.body['content'],
|
||||||
@ -110,43 +109,22 @@ class _PostItemState extends State<PostItem> {
|
|||||||
isSelectable: widget.isContentSelectable,
|
isSelectable: widget.isContentSelectable,
|
||||||
),
|
),
|
||||||
).paddingOnly(
|
).paddingOnly(
|
||||||
left: 16,
|
left: 12,
|
||||||
right: 12,
|
right: 12,
|
||||||
top: 2,
|
|
||||||
bottom: hasAttachment ? 4 : 0,
|
bottom: hasAttachment ? 4 : 0,
|
||||||
),
|
),
|
||||||
),
|
if (widget.item.body?['content_truncated'] == true)
|
||||||
),
|
Opacity(
|
||||||
if (_contentHeight >= 80 && !widget.isFullContent)
|
opacity: 0.8,
|
||||||
Align(
|
child: InkWell(child: Text('readMore'.tr)),
|
||||||
alignment: Alignment.bottomCenter,
|
).paddingOnly(
|
||||||
child: IgnorePointer(
|
left: 12,
|
||||||
child: Container(
|
top: 4,
|
||||||
height: 80,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
gradient: LinearGradient(
|
|
||||||
begin: Alignment.bottomCenter,
|
|
||||||
end: Alignment.topCenter,
|
|
||||||
colors: [
|
|
||||||
Theme.of(context).colorScheme.surfaceContainerLow,
|
|
||||||
Theme.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.surface
|
|
||||||
.withOpacity(0),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
LinkExpansion(content: item.body['content']).paddingOnly(
|
LinkExpansion(content: item.body['content']).paddingOnly(
|
||||||
left: 8,
|
left: 8,
|
||||||
right: 8,
|
right: 8,
|
||||||
top: 4,
|
|
||||||
),
|
),
|
||||||
_PostFooterWidget(item: item).paddingOnly(left: 16),
|
|
||||||
if (attachments.isNotEmpty)
|
if (attachments.isNotEmpty)
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
@ -162,105 +140,49 @@ class _PostItemState extends State<PostItem> {
|
|||||||
style: TextStyle(color: _unFocusColor),
|
style: TextStyle(color: _unFocusColor),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
).paddingOnly(left: 16, top: 4),
|
).paddingOnly(left: 14, top: 4),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return OpenContainer(
|
return GestureDetector(
|
||||||
tappable: widget.isClickable,
|
child: Column(
|
||||||
closedBuilder: (_, openContainer) => Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_PostThumbnail(
|
_PostThumbnail(
|
||||||
rid: item.body['thumbnail'],
|
rid: item.body['thumbnail'],
|
||||||
parentId: widget.item.id.toString(),
|
parentId: widget.item.id.toString(),
|
||||||
).paddingOnly(bottom: 4),
|
).paddingOnly(bottom: 4),
|
||||||
Row(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
GestureDetector(
|
|
||||||
child: AccountAvatar(content: item.author.avatar),
|
|
||||||
onTap: () {
|
|
||||||
showModalBottomSheet(
|
|
||||||
useRootNavigator: true,
|
|
||||||
isScrollControlled: true,
|
|
||||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
|
||||||
context: context,
|
|
||||||
builder: (context) => AccountProfilePopup(
|
|
||||||
name: item.author.name,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_PostHeaderWidget(
|
_PostHeaderWidget(
|
||||||
isCompact: widget.isCompact,
|
isCompact: widget.isCompact,
|
||||||
|
isFullDate: widget.isFullDate,
|
||||||
item: item,
|
item: item,
|
||||||
),
|
),
|
||||||
_PostHeaderDividerWidget(item: item),
|
_PostHeaderDividerWidget(item: item),
|
||||||
Stack(
|
|
||||||
children: [
|
|
||||||
SizedContainer(
|
SizedContainer(
|
||||||
maxWidth: 640,
|
maxWidth: 640,
|
||||||
maxHeight:
|
|
||||||
widget.isFullContent ? double.infinity : 320,
|
|
||||||
child: _MeasureSize(
|
|
||||||
onChange: (size) {
|
|
||||||
setState(() => _contentHeight = size.height);
|
|
||||||
},
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
|
||||||
child: MarkdownTextContent(
|
child: MarkdownTextContent(
|
||||||
parentId: 'p${item.id}-embed',
|
parentId: 'p${item.id}-embed',
|
||||||
content: item.body['content'],
|
content: item.body['content'],
|
||||||
isAutoWarp: item.type == 'story',
|
isAutoWarp: item.type == 'story',
|
||||||
isSelectable: widget.isContentSelectable,
|
isSelectable: widget.isContentSelectable,
|
||||||
isLargeText: item.type == 'article' &&
|
|
||||||
widget.isFullContent,
|
|
||||||
).paddingOnly(left: 12, right: 8),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
if (widget.item.body?['content_truncated'] == true)
|
||||||
if (_contentHeight >= 320 && !widget.isFullContent)
|
Opacity(
|
||||||
Align(
|
opacity: 0.8,
|
||||||
alignment: Alignment.bottomCenter,
|
child: InkWell(child: Text('readMore'.tr)),
|
||||||
child: IgnorePointer(
|
).paddingOnly(top: 4),
|
||||||
child: Container(
|
|
||||||
height: 320,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
gradient: LinearGradient(
|
|
||||||
begin: Alignment.bottomCenter,
|
|
||||||
end: Alignment.topCenter,
|
|
||||||
colors: [
|
|
||||||
(widget.backgroundColor ??
|
|
||||||
Theme.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.surface),
|
|
||||||
(widget.backgroundColor ??
|
|
||||||
Theme.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.surface)
|
|
||||||
.withOpacity(0),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
if (widget.item.replyTo != null && widget.isShowEmbed)
|
if (widget.item.replyTo != null && widget.isShowEmbed)
|
||||||
Container(
|
Container(
|
||||||
constraints: const BoxConstraints(maxWidth: 480),
|
constraints: const BoxConstraints(maxWidth: 480),
|
||||||
padding: const EdgeInsets.only(top: 4),
|
padding: const EdgeInsets.only(top: 8),
|
||||||
child: _PostEmbedWidget(
|
child: _PostEmbedWidget(
|
||||||
isClickable: widget.isClickable,
|
isClickable: widget.isClickable,
|
||||||
isOverrideEmbedClickable:
|
isOverrideEmbedClickable: widget.isOverrideEmbedClickable,
|
||||||
widget.isOverrideEmbedClickable,
|
|
||||||
item: widget.item.replyTo!,
|
item: widget.item.replyTo!,
|
||||||
username: widget.item.replyTo!.author.name,
|
username: widget.item.replyTo!.author.name,
|
||||||
hintText: 'postRepliedNotify',
|
hintText: 'postRepliedNotify',
|
||||||
@ -271,11 +193,10 @@ class _PostItemState extends State<PostItem> {
|
|||||||
if (widget.item.repostTo != null && widget.isShowEmbed)
|
if (widget.item.repostTo != null && widget.isShowEmbed)
|
||||||
Container(
|
Container(
|
||||||
constraints: const BoxConstraints(maxWidth: 480),
|
constraints: const BoxConstraints(maxWidth: 480),
|
||||||
padding: const EdgeInsets.only(top: 4),
|
padding: const EdgeInsets.only(top: 8),
|
||||||
child: _PostEmbedWidget(
|
child: _PostEmbedWidget(
|
||||||
isClickable: widget.isClickable,
|
isClickable: widget.isClickable,
|
||||||
isOverrideEmbedClickable:
|
isOverrideEmbedClickable: widget.isOverrideEmbedClickable,
|
||||||
widget.isOverrideEmbedClickable,
|
|
||||||
item: widget.item.repostTo!,
|
item: widget.item.repostTo!,
|
||||||
username: widget.item.repostTo!.author.name,
|
username: widget.item.repostTo!.author.name,
|
||||||
hintText: 'postRepostedNotify',
|
hintText: 'postRepostedNotify',
|
||||||
@ -283,24 +204,21 @@ class _PostItemState extends State<PostItem> {
|
|||||||
id: widget.item.repostTo!.id.toString(),
|
id: widget.item.repostTo!.id.toString(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
_PostFooterWidget(item: item).paddingOnly(left: 12),
|
_PostFooterWidget(item: item),
|
||||||
LinkExpansion(content: item.body['content'])
|
LinkExpansion(content: item.body['content']),
|
||||||
.paddingOnly(top: 4),
|
|
||||||
],
|
],
|
||||||
|
).paddingSymmetric(
|
||||||
|
horizontal: (widget.padding?.horizontal ?? 0) + 16,
|
||||||
),
|
),
|
||||||
|
if (hasAttachment) const Gap(8),
|
||||||
|
_PostAttachmentWidget(
|
||||||
|
item: item,
|
||||||
|
padding: widget.padding,
|
||||||
),
|
),
|
||||||
],
|
if (widget.showFeaturedReply)
|
||||||
).paddingOnly(
|
_PostFeaturedReplyWidget(item: item).paddingSymmetric(
|
||||||
top: 10,
|
horizontal: (widget.padding?.horizontal ?? 0) + 12,
|
||||||
bottom:
|
|
||||||
(attachments.length == 1 && !AppTheme.isLargeScreen(context))
|
|
||||||
? 10
|
|
||||||
: 0,
|
|
||||||
right: 16,
|
|
||||||
left: 16,
|
|
||||||
),
|
),
|
||||||
_PostAttachmentWidget(item: item),
|
|
||||||
if (widget.showFeaturedReply) _PostFeaturedReplyWidget(item: item),
|
|
||||||
if (widget.isShowReply || widget.isReactable)
|
if (widget.isShowReply || widget.isReactable)
|
||||||
PostQuickAction(
|
PostQuickAction(
|
||||||
isShowReply: widget.isShowReply,
|
isShowReply: widget.isShowReply,
|
||||||
@ -312,33 +230,30 @@ class _PostItemState extends State<PostItem> {
|
|||||||
(item.metric!.reactionList[symbol] ?? 0) + changes;
|
(item.metric!.reactionList[symbol] ?? 0) + changes;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
onComment: () {
|
||||||
|
if (widget.onComment != null) {
|
||||||
|
widget.onComment!();
|
||||||
|
}
|
||||||
|
},
|
||||||
).paddingOnly(
|
).paddingOnly(
|
||||||
top: (attachments.length == 1 && !AppTheme.isLargeScreen(context))
|
top: 8,
|
||||||
? 10
|
left: (widget.padding?.left ?? 0) + 14,
|
||||||
: 6,
|
right: (widget.padding?.right ?? 0) + 14,
|
||||||
left:
|
|
||||||
(attachments.length == 1 && !AppTheme.isLargeScreen(context))
|
|
||||||
? 24
|
|
||||||
: 60,
|
|
||||||
right: 16,
|
|
||||||
bottom: 10,
|
|
||||||
)
|
)
|
||||||
else
|
|
||||||
const Gap(10),
|
|
||||||
],
|
],
|
||||||
|
).paddingOnly(
|
||||||
|
top: widget.padding?.top ?? 0,
|
||||||
|
bottom: widget.padding?.bottom ?? 0,
|
||||||
),
|
),
|
||||||
openBuilder: (_, __) => TitleShell(
|
onTap: () {
|
||||||
title: 'postDetail'.tr,
|
if (widget.isClickable) {
|
||||||
child: PostDetailScreen(
|
AppRouter.instance.pushNamed(
|
||||||
id: item.id.toString(),
|
'postDetail',
|
||||||
post: item,
|
pathParameters: {'id': item.id.toString()},
|
||||||
),
|
extra: item,
|
||||||
),
|
);
|
||||||
closedElevation: 0,
|
}
|
||||||
openElevation: 0,
|
},
|
||||||
closedColor:
|
|
||||||
widget.backgroundColor ?? Theme.of(context).colorScheme.surface,
|
|
||||||
openColor: Theme.of(context).colorScheme.surface,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -350,7 +265,6 @@ class _PostFeaturedReplyWidget extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final isLargeScreen = AppTheme.isLargeScreen(context);
|
|
||||||
final unFocusColor =
|
final unFocusColor =
|
||||||
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
|
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
|
||||||
|
|
||||||
@ -358,19 +272,17 @@ class _PostFeaturedReplyWidget extends StatelessWidget {
|
|||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
|
||||||
final List<String> attachments = item.body['attachments'] is List
|
|
||||||
? List.from(item.body['attachments']?.whereType<String>())
|
|
||||||
: List.empty();
|
|
||||||
|
|
||||||
return FutureBuilder(
|
return FutureBuilder(
|
||||||
future:
|
future: Get.find<PostProvider>().listPostFeaturedReply(
|
||||||
Get.find<PostProvider>().listPostFeaturedReply(item.id.toString()),
|
item.id.toString(),
|
||||||
|
),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (!snapshot.hasData || snapshot.data!.isEmpty) {
|
if (!snapshot.hasData || snapshot.data!.isEmpty) {
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
|
padding: EdgeInsets.only(top: 8),
|
||||||
constraints: const BoxConstraints(maxWidth: 480),
|
constraints: const BoxConstraints(maxWidth: 480),
|
||||||
child: Card(
|
child: Card(
|
||||||
margin: EdgeInsets.zero,
|
margin: EdgeInsets.zero,
|
||||||
@ -384,7 +296,7 @@ class _PostFeaturedReplyWidget extends StatelessWidget {
|
|||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
AccountAvatar(
|
AttachedCircleAvatar(
|
||||||
content: reply.author.avatar,
|
content: reply.author.avatar,
|
||||||
radius: 10,
|
radius: 10,
|
||||||
),
|
),
|
||||||
@ -456,16 +368,9 @@ class _PostFeaturedReplyWidget extends StatelessWidget {
|
|||||||
.toList(),
|
.toList(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
).animate().fadeIn(
|
||||||
.animate()
|
|
||||||
.fadeIn(
|
|
||||||
duration: 300.ms,
|
duration: 300.ms,
|
||||||
curve: Curves.easeIn,
|
curve: Curves.easeIn,
|
||||||
)
|
|
||||||
.paddingOnly(
|
|
||||||
top: (attachments.length == 1 && !isLargeScreen) ? 10 : 6,
|
|
||||||
left: (attachments.length == 1 && !isLargeScreen) ? 24 : 60,
|
|
||||||
right: 16,
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -474,8 +379,9 @@ class _PostFeaturedReplyWidget extends StatelessWidget {
|
|||||||
|
|
||||||
class _PostAttachmentWidget extends StatelessWidget {
|
class _PostAttachmentWidget extends StatelessWidget {
|
||||||
final Post item;
|
final Post item;
|
||||||
|
final EdgeInsets? padding;
|
||||||
|
|
||||||
const _PostAttachmentWidget({required this.item});
|
const _PostAttachmentWidget({required this.item, required this.padding});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -485,25 +391,42 @@ class _PostAttachmentWidget extends StatelessWidget {
|
|||||||
? List.from(item.body['attachments']?.whereType<String>())
|
? List.from(item.body['attachments']?.whereType<String>())
|
||||||
: List.empty();
|
: List.empty();
|
||||||
|
|
||||||
if (attachments.length > 3) {
|
if (attachments.isEmpty) return const SizedBox.shrink();
|
||||||
|
|
||||||
|
if (attachments.length == 1 && !isLargeScreen) {
|
||||||
return AttachmentList(
|
return AttachmentList(
|
||||||
parentId: item.id.toString(),
|
parentId: item.id.toString(),
|
||||||
attachmentsId: attachments,
|
attachmentIds: item.preload == null ? attachments : null,
|
||||||
|
attachments: item.preload?.attachments,
|
||||||
autoload: false,
|
autoload: false,
|
||||||
isGrid: true,
|
isFullWidth: true,
|
||||||
).paddingOnly(left: 36, top: 4, bottom: 4);
|
);
|
||||||
} else if (attachments.length > 1 || isLargeScreen) {
|
} else if (attachments.length == 1) {
|
||||||
return AttachmentList(
|
return AttachmentList(
|
||||||
parentId: item.id.toString(),
|
parentId: item.id.toString(),
|
||||||
attachmentsId: attachments,
|
attachmentIds: item.preload == null ? attachments : null,
|
||||||
|
attachments: item.preload?.attachments,
|
||||||
autoload: false,
|
autoload: false,
|
||||||
isColumn: true,
|
isColumn: true,
|
||||||
).paddingOnly(left: 60, right: 24, top: 4, bottom: 4);
|
).paddingSymmetric(horizontal: (padding?.horizontal ?? 0) + 14);
|
||||||
|
} else if (attachments.length > 1 &&
|
||||||
|
attachments.length % 3 == 0 &&
|
||||||
|
!isLargeScreen) {
|
||||||
|
return AttachmentList(
|
||||||
|
parentId: item.id.toString(),
|
||||||
|
attachmentIds: item.preload == null ? attachments : null,
|
||||||
|
attachments: item.preload?.attachments,
|
||||||
|
autoload: false,
|
||||||
|
isGrid: true,
|
||||||
|
).paddingSymmetric(horizontal: (padding?.horizontal ?? 0) + 14);
|
||||||
} else {
|
} else {
|
||||||
return AttachmentList(
|
return AttachmentList(
|
||||||
flatMaxHeight: MediaQuery.of(context).size.width,
|
|
||||||
parentId: item.id.toString(),
|
parentId: item.id.toString(),
|
||||||
attachmentsId: attachments,
|
attachmentIds: item.preload == null ? attachments : null,
|
||||||
|
attachments: item.preload?.attachments,
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
horizontal: (padding?.horizontal ?? 0) + 14,
|
||||||
|
),
|
||||||
autoload: false,
|
autoload: false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -545,16 +468,17 @@ class _PostEmbedWidget extends StatelessWidget {
|
|||||||
size: 16,
|
size: 16,
|
||||||
color: unFocusColor,
|
color: unFocusColor,
|
||||||
),
|
),
|
||||||
|
const Gap(6),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
hintText.trParams(
|
hintText.trParams(
|
||||||
{'username': '@$username'},
|
{'username': '@$username'},
|
||||||
),
|
),
|
||||||
style: TextStyle(color: unFocusColor),
|
style: TextStyle(color: unFocusColor),
|
||||||
).paddingOnly(left: 6),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
).paddingOnly(left: 12),
|
).paddingOnly(left: 2),
|
||||||
Card(
|
Card(
|
||||||
elevation: 1,
|
elevation: 1,
|
||||||
child: PostItem(
|
child: PostItem(
|
||||||
@ -574,7 +498,7 @@ class _PostEmbedWidget extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
closedElevation: 0,
|
closedElevation: 0,
|
||||||
openElevation: 0,
|
openElevation: 0,
|
||||||
closedColor: Theme.of(context).colorScheme.surface,
|
closedColor: Colors.transparent,
|
||||||
openColor: Theme.of(context).colorScheme.surface,
|
openColor: Theme.of(context).colorScheme.surface,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -590,9 +514,7 @@ class _PostHeaderDividerWidget extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (item.body['description'] != null || item.body['title'] != null) {
|
if (item.body['description'] != null || item.body['title'] != null) {
|
||||||
return const Divider(thickness: 0.3, height: 1).paddingSymmetric(
|
return const Gap(8);
|
||||||
vertical: 8,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
@ -648,65 +570,95 @@ class _PostFooterWidget extends StatelessWidget {
|
|||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: widgets,
|
children: widgets,
|
||||||
).paddingOnly(top: 4);
|
).paddingSymmetric(vertical: 4);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _PostHeaderWidget extends StatelessWidget {
|
class _PostHeaderWidget extends StatelessWidget {
|
||||||
final bool isCompact;
|
final bool isCompact;
|
||||||
|
final bool isFullDate;
|
||||||
final Post item;
|
final Post item;
|
||||||
|
|
||||||
const _PostHeaderWidget({
|
const _PostHeaderWidget({
|
||||||
required this.isCompact,
|
required this.isCompact,
|
||||||
|
required this.isFullDate,
|
||||||
required this.item,
|
required this.item,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Row(
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (isCompact)
|
|
||||||
AccountAvatar(
|
AccountAvatar(
|
||||||
content: item.author.avatar,
|
content: item.author.avatar,
|
||||||
radius: 10,
|
username: item.author.name,
|
||||||
).paddingOnly(left: 2, top: 1),
|
radius: isCompact ? 10 : null,
|
||||||
|
),
|
||||||
|
Gap(isCompact ? 6 : 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
item.author.nick,
|
item.author.nick,
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
RelativeDate(item.publishedAt?.toLocal() ?? DateTime.now())
|
if (isCompact) const Gap(4),
|
||||||
.paddingOnly(left: 4),
|
if (isCompact)
|
||||||
|
RelativeDate(
|
||||||
|
item.publishedAt?.toLocal() ?? DateTime.now(),
|
||||||
|
isFull: isFullDate,
|
||||||
|
).paddingOnly(top: 1),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (item.body['title'] != null)
|
if (!isCompact)
|
||||||
Text(
|
Row(
|
||||||
item.body['title'],
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
style: Theme.of(context)
|
children: [
|
||||||
.textTheme
|
RelativeDate(
|
||||||
.bodyMedium!
|
item.publishedAt?.toLocal() ?? DateTime.now(),
|
||||||
.copyWith(fontSize: 15),
|
isFull: isFullDate,
|
||||||
),
|
),
|
||||||
if (item.body['description'] != null)
|
const Gap(4),
|
||||||
Text(
|
Icon(
|
||||||
item.body['description'],
|
_PostItemState.visibilityIcons[item.visibility],
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
size: 16,
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onSurface
|
||||||
|
.withOpacity(0.75),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
).paddingOnly(left: isCompact ? 6 : 12),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
if (item.type == 'article')
|
if (item.type == 'article')
|
||||||
Badge(
|
Badge(
|
||||||
label: Text('article'.tr),
|
label: Text('article'.tr),
|
||||||
).paddingOnly(top: 3),
|
).paddingOnly(top: 3),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
if (item.body['title'] != null)
|
||||||
|
Text(
|
||||||
|
item.body['title'],
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
if (item.body['description'] != null)
|
||||||
|
Text(
|
||||||
|
item.body['description'],
|
||||||
|
style: Theme.of(context).textTheme.titleSmall,
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -736,45 +688,3 @@ class _PostThumbnail extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
typedef _OnWidgetSizeChange = void Function(Size size);
|
|
||||||
|
|
||||||
class _MeasureSizeRenderObject extends RenderProxyBox {
|
|
||||||
Size? oldSize;
|
|
||||||
_OnWidgetSizeChange onChange;
|
|
||||||
|
|
||||||
_MeasureSizeRenderObject(this.onChange);
|
|
||||||
|
|
||||||
@override
|
|
||||||
void performLayout() {
|
|
||||||
super.performLayout();
|
|
||||||
|
|
||||||
Size newSize = child!.size;
|
|
||||||
if (oldSize == newSize) return;
|
|
||||||
|
|
||||||
oldSize = newSize;
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
||||||
onChange(newSize);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _MeasureSize extends SingleChildRenderObjectWidget {
|
|
||||||
final _OnWidgetSizeChange onChange;
|
|
||||||
|
|
||||||
const _MeasureSize({
|
|
||||||
required this.onChange,
|
|
||||||
required Widget super.child,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
RenderObject createRenderObject(BuildContext context) {
|
|
||||||
return _MeasureSizeRenderObject(onChange);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void updateRenderObject(
|
|
||||||
BuildContext context, covariant _MeasureSizeRenderObject renderObject) {
|
|
||||||
renderObject.onChange = onChange;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -3,6 +3,8 @@ import 'package:get/get.dart';
|
|||||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||||
import 'package:solian/models/post.dart';
|
import 'package:solian/models/post.dart';
|
||||||
import 'package:solian/providers/auth.dart';
|
import 'package:solian/providers/auth.dart';
|
||||||
|
import 'package:solian/router.dart';
|
||||||
|
import 'package:solian/screens/posts/post_editor.dart';
|
||||||
import 'package:solian/widgets/posts/post_action.dart';
|
import 'package:solian/widgets/posts/post_action.dart';
|
||||||
import 'package:solian/widgets/posts/post_item.dart';
|
import 'package:solian/widgets/posts/post_item.dart';
|
||||||
|
|
||||||
@ -12,6 +14,7 @@ class PostListWidget extends StatelessWidget {
|
|||||||
final bool isNestedClickable;
|
final bool isNestedClickable;
|
||||||
final PagingController<int, Post> controller;
|
final PagingController<int, Post> controller;
|
||||||
final Color? backgroundColor;
|
final Color? backgroundColor;
|
||||||
|
final EdgeInsets? padding;
|
||||||
|
|
||||||
const PostListWidget({
|
const PostListWidget({
|
||||||
super.key,
|
super.key,
|
||||||
@ -20,6 +23,7 @@ class PostListWidget extends StatelessWidget {
|
|||||||
this.isClickable = true,
|
this.isClickable = true,
|
||||||
this.isNestedClickable = true,
|
this.isNestedClickable = true,
|
||||||
this.backgroundColor,
|
this.backgroundColor,
|
||||||
|
this.padding,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -29,16 +33,18 @@ class PostListWidget extends StatelessWidget {
|
|||||||
pagingController: controller,
|
pagingController: controller,
|
||||||
builderDelegate: PagedChildBuilderDelegate<Post>(
|
builderDelegate: PagedChildBuilderDelegate<Post>(
|
||||||
itemBuilder: (context, item, index) {
|
itemBuilder: (context, item, index) {
|
||||||
return PostListEntryWidget(
|
return Padding(
|
||||||
|
padding: padding ?? EdgeInsets.zero,
|
||||||
|
child: PostListEntryWidget(
|
||||||
isShowEmbed: isShowEmbed,
|
isShowEmbed: isShowEmbed,
|
||||||
isNestedClickable: isNestedClickable,
|
isNestedClickable: isNestedClickable,
|
||||||
isClickable: isClickable,
|
isClickable: isClickable,
|
||||||
showFeaturedReply: true,
|
showFeaturedReply: true,
|
||||||
item: item,
|
item: item,
|
||||||
backgroundColor: backgroundColor,
|
|
||||||
onUpdate: () {
|
onUpdate: () {
|
||||||
controller.refresh();
|
controller.refresh();
|
||||||
},
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -48,25 +54,23 @@ class PostListWidget extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class PostListEntryWidget extends StatelessWidget {
|
class PostListEntryWidget extends StatelessWidget {
|
||||||
final int renderOrder;
|
|
||||||
final bool isShowEmbed;
|
final bool isShowEmbed;
|
||||||
final bool isNestedClickable;
|
final bool isNestedClickable;
|
||||||
final bool isClickable;
|
final bool isClickable;
|
||||||
final bool showFeaturedReply;
|
final bool showFeaturedReply;
|
||||||
final Post item;
|
final Post item;
|
||||||
|
final EdgeInsets? padding;
|
||||||
final Function onUpdate;
|
final Function onUpdate;
|
||||||
final Color? backgroundColor;
|
|
||||||
|
|
||||||
const PostListEntryWidget({
|
const PostListEntryWidget({
|
||||||
super.key,
|
super.key,
|
||||||
this.renderOrder = 0,
|
|
||||||
required this.isShowEmbed,
|
required this.isShowEmbed,
|
||||||
required this.isNestedClickable,
|
required this.isNestedClickable,
|
||||||
required this.isClickable,
|
required this.isClickable,
|
||||||
required this.showFeaturedReply,
|
required this.showFeaturedReply,
|
||||||
required this.item,
|
required this.item,
|
||||||
|
this.padding,
|
||||||
required this.onUpdate,
|
required this.onUpdate,
|
||||||
this.backgroundColor,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -78,7 +82,23 @@ class PostListEntryWidget extends StatelessWidget {
|
|||||||
isShowEmbed: isShowEmbed,
|
isShowEmbed: isShowEmbed,
|
||||||
isClickable: isNestedClickable,
|
isClickable: isNestedClickable,
|
||||||
showFeaturedReply: showFeaturedReply,
|
showFeaturedReply: showFeaturedReply,
|
||||||
backgroundColor: backgroundColor,
|
padding: padding,
|
||||||
|
onComment: () {
|
||||||
|
AppRouter.instance
|
||||||
|
.pushNamed(
|
||||||
|
'postEditor',
|
||||||
|
extra: PostPublishArguments(reply: item),
|
||||||
|
)
|
||||||
|
.then((value) {
|
||||||
|
if (value is Future) {
|
||||||
|
value.then((_) {
|
||||||
|
onUpdate();
|
||||||
|
});
|
||||||
|
} else if (value != null) {
|
||||||
|
onUpdate();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
).paddingSymmetric(vertical: 8),
|
).paddingSymmetric(vertical: 8),
|
||||||
onLongPress: () {
|
onLongPress: () {
|
||||||
final AuthProvider auth = Get.find();
|
final AuthProvider auth = Get.find();
|
||||||
@ -101,3 +121,49 @@ class PostListEntryWidget extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ControlledPostListWidget extends StatelessWidget {
|
||||||
|
final bool isShowEmbed;
|
||||||
|
final bool isClickable;
|
||||||
|
final bool isNestedClickable;
|
||||||
|
final bool isPinned;
|
||||||
|
final PagingController<int, Post> controller;
|
||||||
|
final EdgeInsets? padding;
|
||||||
|
final Function? onUpdate;
|
||||||
|
|
||||||
|
const ControlledPostListWidget({
|
||||||
|
super.key,
|
||||||
|
required this.controller,
|
||||||
|
this.isShowEmbed = true,
|
||||||
|
this.isClickable = true,
|
||||||
|
this.isNestedClickable = true,
|
||||||
|
this.isPinned = true,
|
||||||
|
this.padding,
|
||||||
|
this.onUpdate,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return PagedSliverList<int, Post>.separated(
|
||||||
|
addRepaintBoundaries: true,
|
||||||
|
pagingController: controller,
|
||||||
|
builderDelegate: PagedChildBuilderDelegate<Post>(
|
||||||
|
itemBuilder: (context, item, index) {
|
||||||
|
if (item.pinnedAt != null && !isPinned) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
return PostListEntryWidget(
|
||||||
|
isShowEmbed: isShowEmbed,
|
||||||
|
isNestedClickable: isNestedClickable,
|
||||||
|
isClickable: isClickable,
|
||||||
|
showFeaturedReply: true,
|
||||||
|
padding: padding,
|
||||||
|
item: item,
|
||||||
|
onUpdate: onUpdate ?? () {},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
separatorBuilder: (_, __) => const Divider(thickness: 0.3, height: 0.3),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -31,8 +31,6 @@ class PostOwnedListEntry extends StatelessWidget {
|
|||||||
isClickable: false,
|
isClickable: false,
|
||||||
isShowReply: false,
|
isShowReply: false,
|
||||||
isReactable: false,
|
isReactable: false,
|
||||||
isFullContent: isFullContent,
|
|
||||||
backgroundColor: backgroundColor,
|
|
||||||
).paddingSymmetric(vertical: 8),
|
).paddingSymmetric(vertical: 8),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -11,6 +11,7 @@ class PostQuickAction extends StatefulWidget {
|
|||||||
final Post item;
|
final Post item;
|
||||||
final bool isReactable;
|
final bool isReactable;
|
||||||
final bool isShowReply;
|
final bool isShowReply;
|
||||||
|
final Function onComment;
|
||||||
final void Function(String symbol, int num) onReact;
|
final void Function(String symbol, int num) onReact;
|
||||||
|
|
||||||
const PostQuickAction({
|
const PostQuickAction({
|
||||||
@ -18,6 +19,7 @@ class PostQuickAction extends StatefulWidget {
|
|||||||
required this.item,
|
required this.item,
|
||||||
this.isShowReply = true,
|
this.isShowReply = true,
|
||||||
this.isReactable = true,
|
this.isReactable = true,
|
||||||
|
required this.onComment,
|
||||||
required this.onReact,
|
required this.onReact,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -106,7 +108,11 @@ class _PostQuickActionState extends State<PostQuickAction> {
|
|||||||
builder: (context) {
|
builder: (context) {
|
||||||
return PostReplyListPopup(item: widget.item);
|
return PostReplyListPopup(item: widget.item);
|
||||||
},
|
},
|
||||||
);
|
).then((signal) {
|
||||||
|
if (signal == true) {
|
||||||
|
widget.onComment();
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -8,11 +8,13 @@ import 'package:solian/widgets/posts/post_list.dart';
|
|||||||
|
|
||||||
class PostReplyList extends StatefulWidget {
|
class PostReplyList extends StatefulWidget {
|
||||||
final Post item;
|
final Post item;
|
||||||
|
final EdgeInsets? padding;
|
||||||
final Color? backgroundColor;
|
final Color? backgroundColor;
|
||||||
|
|
||||||
const PostReplyList({
|
const PostReplyList({
|
||||||
super.key,
|
super.key,
|
||||||
required this.item,
|
required this.item,
|
||||||
|
this.padding,
|
||||||
this.backgroundColor,
|
this.backgroundColor,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -53,6 +55,7 @@ class _PostReplyListState extends State<PostReplyList> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return PostListWidget(
|
return PostListWidget(
|
||||||
|
padding: widget.padding,
|
||||||
isShowEmbed: false,
|
isShowEmbed: false,
|
||||||
controller: _pagingController,
|
controller: _pagingController,
|
||||||
backgroundColor: widget.backgroundColor,
|
backgroundColor: widget.backgroundColor,
|
||||||
@ -70,16 +73,31 @@ class PostReplyListPopup extends StatelessWidget {
|
|||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
'postReplies'.tr,
|
'postReplies'.tr,
|
||||||
style: Theme.of(context).textTheme.headlineSmall,
|
style: Theme.of(context).textTheme.headlineSmall,
|
||||||
).paddingOnly(left: 24, right: 24, top: 32, bottom: 16),
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.add_comment),
|
||||||
|
visualDensity: const VisualDensity(horizontal: -4),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(context, true);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).paddingOnly(left: 24, right: 24, top: 24, bottom: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: CustomScrollView(
|
child: CustomScrollView(
|
||||||
slivers: [
|
slivers: [
|
||||||
PostReplyList(
|
PostReplyList(
|
||||||
item: item,
|
item: item,
|
||||||
backgroundColor: Theme.of(context).colorScheme.surfaceContainerLow,
|
padding: EdgeInsets.symmetric(horizontal: 10),
|
||||||
|
backgroundColor:
|
||||||
|
Theme.of(context).colorScheme.surfaceContainerLow,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
92
lib/widgets/posts/post_share.dart
Normal file
92
lib/widgets/posts/post_share.dart
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:qr_flutter/qr_flutter.dart';
|
||||||
|
import 'package:solian/models/post.dart';
|
||||||
|
import 'package:solian/widgets/posts/post_item.dart';
|
||||||
|
import 'package:solian/widgets/root_container.dart';
|
||||||
|
|
||||||
|
class PostShareImage extends StatelessWidget {
|
||||||
|
final Post item;
|
||||||
|
|
||||||
|
const PostShareImage({super.key, required this.item});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final textColor = Theme.of(context).colorScheme.onSurface.withOpacity(0.3);
|
||||||
|
return RootContainer(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Gap(24),
|
||||||
|
Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: Card(
|
||||||
|
child: PostItem(
|
||||||
|
item: item,
|
||||||
|
isShowEmbed: true,
|
||||||
|
isClickable: false,
|
||||||
|
showFeaturedReply: false,
|
||||||
|
isReactable: false,
|
||||||
|
isShowReply: false,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 8),
|
||||||
|
onComment: () {},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
|
child: Image.asset(
|
||||||
|
'assets/logo.png',
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(16),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'shareImageFooter'.tr,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: textColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'Solsynth LLC © ${DateTime.now().year}',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
color: textColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
|
child: Material(
|
||||||
|
color: Theme.of(context).colorScheme.surface,
|
||||||
|
child: QrImageView(
|
||||||
|
data: 'https://solsynth.dev/posts/${item.id}',
|
||||||
|
version: QrVersions.auto,
|
||||||
|
padding: const EdgeInsets.all(4),
|
||||||
|
size: 48,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).paddingSymmetric(horizontal: 36, vertical: 24),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user