Compare commits

...

60 Commits

Author SHA1 Message Date
a04bfe4cf9 🚀 Launch 1.3.7+8 2024-10-12 00:56:30 +08:00
7b7988e6cb ♻️ Refactored post layout 2024-10-12 00:41:03 +08:00
81a616157e 🚀 Launch v1.3.7+7 2024-10-11 01:10:23 +08:00
52312662fb 🐛 Fix post visibility issue 2024-10-11 00:28:09 +08:00
ca18d6ade4 💄 Chat channels loading indicator 2024-10-10 23:48:37 +08:00
af7cc8dab0 💄 Optimize the explore page app bar behavior 2024-10-10 23:36:07 +08:00
382e3c4a4c Optimize post attachment loading 2024-10-10 22:52:05 +08:00
1e37c6ddae 🚀 Launch 1.3.6+6 (to App Store) 2024-10-08 00:52:13 +08:00
442ef06147 💄 Optimize post editor 2024-10-08 00:06:08 +08:00
606a0d708a 🍱 Add cpp highlight file 2024-10-07 23:35:06 +08:00
558828f3e0 🚀 Launch 1.3.6+5 2024-10-07 16:54:29 +08:00
09dc7d2a0d 💄 Brightness of code block 2024-10-07 16:29:36 +08:00
6876d2e7c0 Syntax highlighting in markdown
💄 Optimize content rendering
2024-10-07 16:23:25 +08:00
3a5964730c 🚀 1.3.6+4 2024-10-07 02:12:50 +08:00
271c722df3 🐛 Bug fixes on background image 2024-10-07 01:47:34 +08:00
97656249f2 ⬆️ Upgrade deps 2024-10-06 23:23:11 +08:00
d7e6fe2d8f 💄 More transparency 2024-10-06 23:06:33 +08:00
2e9c4d166e 💄 Optimize designs and bug fixes with background image 2024-10-06 22:38:37 +08:00
c5258cb9ca 🚀 Launch 1.3.6+3 2024-10-06 19:57:17 +08:00
47c535910d 💄 Optimize the style with background image 2024-10-06 19:54:32 +08:00
66f2f33394 🐛 Bug fixes with background image 2024-10-06 19:41:44 +08:00
f5fbe1f483 Better theme & background image 2024-10-06 19:29:47 +08:00
fcf4dc7a2d ♻️ Use unified root container 2024-10-06 17:37:07 +08:00
43b7059957 🐛 Bug fixes and optimization 2024-10-06 17:31:44 +08:00
11c913af60 🚀 Launch 1.3.6+1 2024-10-06 11:34:12 +08:00
db8f0d63e1 🐛 Fix responsive chat issue 2024-10-06 11:12:54 +08:00
4036a79995 🐛 Fix some building time problem 2024-10-06 01:53:36 +08:00
859bbd09e0 🚀 Launch 1.3.0+1 2024-10-06 01:43:10 +08:00
60033fdef3 🐛 Fix platform specific bugs & crashes 2024-10-06 01:42:51 +08:00
9c3d181deb 📱 Optimize the call experience on landscape device 2024-10-06 01:25:10 +08:00
9e6829bd5a 📱 New layout for the landscape device 2024-10-06 01:17:49 +08:00
f50461a7f7 💄 Better chat list 2024-10-05 23:12:23 +08:00
147879e4d8 Better last message preview 2024-10-05 15:11:48 +08:00
f353c05cb5 💄 Better way to switch focused realm 2024-10-05 14:25:57 +08:00
ac60043ca7 🐛 Bug fixes 2024-10-05 03:38:30 +08:00
8d79274b0c 🐛 Fix dm channel display error with deleted user 2024-10-05 03:21:53 +08:00
ad4e4071fa ♻️ Use bottom navigation bar instead 2024-10-05 03:14:52 +08:00
c59f77c877 🐛 Fix windows rendering lack 2024-09-28 18:41:56 +08:00
16047a7d57 🚀 Launch 1.2.5+1 2024-09-27 00:20:04 +08:00
fdc68fc5e1 💄 Optimize attachment editor controls 2024-09-27 00:12:30 +08:00
bbee825cf4 ♻️ Refactor profile page code 2024-09-27 00:02:08 +08:00
2673c11046 Able to block anyone
💄 Optimize user profile page
2024-09-26 23:47:19 +08:00
3ac6822ab6 🚀 Launch 1.2.4+1 2024-09-24 22:40:54 +08:00
7a5fd2e468 In app rating 2024-09-24 22:10:45 +08:00
e1ddd22e4e 🚀 Launch 1.2.3+2 2024-09-23 23:34:40 +08:00
22b2ae32e9 Featured replies clickable 2024-09-23 23:34:25 +08:00
9d5c452eae 🐛 Fix overflow in content 2024-09-23 23:20:01 +08:00
0fdb1e4ead 💫 Improve loading image animation 2024-09-23 23:19:52 +08:00
724bd6592e 💄 Improvements and optimize UX 2024-09-23 22:43:13 +08:00
2d347e0d41 ♻️ Refactored post item widget 2024-09-23 22:43:02 +08:00
de39799301 🚀 Launch 1.2.3 2024-09-22 22:57:00 +08:00
4b921602a2 🐛 Bug fixes 2024-09-22 22:56:28 +08:00
6cde218393 💄 Optimization of post item style 2024-09-21 23:28:14 +08:00
c896185af0 See other user recent fortune 2024-09-21 23:10:20 +08:00
4cbeafd447 Account deletion 2024-09-21 22:44:08 +08:00
91a32e6736 Report abuse 2024-09-21 22:10:59 +08:00
befc647b03 💄 Improved about page 2024-09-19 20:39:09 +08:00
16b2e3a0c7 Terms that show up let user accept 2024-09-19 20:34:04 +08:00
0cc842c030 🐛 Fix upgrade detection method 2024-09-18 20:27:13 +08:00
fb370a484d 🐛 Fix english localization update message placeholder issue 2024-09-18 19:55:42 +08:00
109 changed files with 6896 additions and 3829 deletions

13
.roadsignrc Normal file
View File

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

View File

@ -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

View 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": ";"
}
]
}
}
}

View 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": "\\."
}
]
}
}
}

View 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"
}

View 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": "\\\\[\"']" }
]
}
}
}

View 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"
}

View 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"
}
]
}
}
}

View File

@ -22,9 +22,9 @@
"explore": "Explore", "explore": "Explore",
"posts": "Posts", "posts": "Posts",
"unlink": "Unlink", "unlink": "Unlink",
"feedSearch": "Search Feed", "postSearch": "Search Post",
"feedSearchWithTag": "Searching with tag #@key", "postSearchWithTag": "Searching with tag #@key",
"feedSearchWithCategory": "Searching in category @category", "postSearchWithCategory": "Searching in category @category",
"feedUnreadCount": "@count posts you may missed", "feedUnreadCount": "@count posts you may missed",
"messages": "Messages", "messages": "Messages",
"messagesUnreadCount": "@count messages unread", "messagesUnreadCount": "@count messages unread",
@ -98,6 +98,8 @@
"accountFriendBlocked": "Friend blocklist", "accountFriendBlocked": "Friend blocklist",
"accountFriendListHint": "Swipe left to decline, right to approve", "accountFriendListHint": "Swipe left to decline, right to approve",
"accountFriendRequestSent": "Friend request sent, waiting for processing...", "accountFriendRequestSent": "Friend request sent, waiting for processing...",
"accountBlocked": "Account has been blocked",
"accountUnblocked": "Account has been unblocked",
"accountSuspended": "Account was suspended", "accountSuspended": "Account was suspended",
"accountSuspendedAt": "Account was suspended since @date", "accountSuspendedAt": "Account was suspended since @date",
"aspectRatio": "Aspect Ratio", "aspectRatio": "Aspect Ratio",
@ -428,9 +430,52 @@
"preferencesApplied": "Preferences has been applied.", "preferencesApplied": "Preferences has been applied.",
"save": "Save", "save": "Save",
"updateAvailable": "Update available", "updateAvailable": "Update available",
"updateAvailableDesc": "There is an update available (@version). Do you want to download and install it now? You can still use the app normally while waiting for the download to complete.", "updateAvailableDesc": "There is an update available (@from to @to). Do you want to download and install it now? You can still use the app normally while waiting for the download to complete.",
"update": "Update", "update": "Update",
"updateCheckStrictly": "Strict mode", "updateCheckStrictly": "Strict mode",
"updateCheckStrictlyDesc": "If enabled, the app will ask for updating once the local version is different from remote one.", "updateCheckStrictlyDesc": "If enabled, the app will ask for updating once the local version is different from remote one.",
"updateMayAvailable": "App version @version is available, you can update from app store or our website." "updateMayAvailable": "App version @version is available, you can update from app store or our website.",
"updateNow": "Update now",
"termAccept": "I've read and agree to Solar Network's Terms",
"termAcceptDesc": "Including but not limited to \"User Agreement\" and \"Privacy Policy\"",
"termAcceptLink": "View terms",
"termAcceptNextWithAgree": "By clicking the \"Next\", it means you agree to our terms and its updates. You should already agreed with them while you sign up.",
"termRelated": "Related Terms",
"appDetails": "App Details",
"projectWebsite": "Project Website",
"iAmNotRobot": "I'm not a Robot",
"report": "Report",
"reportAbuse": "Report abuse",
"reportAbuseDesc": "Report any violation of service terms",
"reportAbuseResource": "Resource identifier",
"reportAbuseReason": "Report reason",
"reportSubmitted": "Report submitted, thank you for your contribution. We will send a notification about the result of the report within 24 hours for you.",
"accountDeletion": "Request account deletion",
"accountDeletionDesc": "Delete the current account and all its data. Note that this action is irreversible!",
"accountDeletionConfirm": "Confirm request account deletion",
"accountDeletionConfirmDesc": "Are you sure to delete account @account? You will receive a confirmation email with a link to confirm the deletion of the account within 24 hours. Note that this action is irreversible, and all data associated with the account will be deleted, and you should be careful about it.",
"accountDeletionRequested": "Account deletion requested, check your inbox to confirm the request.",
"slideToConfirm": "Slide to confirm",
"serviceStatus": "Status of Service",
"firstBootTime": "First boot at @time",
"rateTheApp": "Rate the app",
"rateTheAppDesc": "Rate Solar Network on the App Store to let us serve you better!",
"friendAdd": "Add as friend",
"blockUser": "Block user",
"unblockUser": "Unblock user",
"learnMoreAboutPerson": "Learn more about that person",
"global": "Global",
"all": "All",
"unablePreview": "Unable to preview",
"dashboardNav": "Dash",
"accountNav": "You",
"performance": "Performance",
"animatedMessageList": "Non-animated message list",
"animatedMessageListDesc": "Remove animation effects in message list, to reduce cause lag",
"theme": "Theme",
"globalTheme": "Global theme",
"agedTheme": "Old school style theme",
"agedThemeDesc": "Downgrade the global theme to Material Design 2. Unexpected issues may occur. For experimental use only.",
"appBackgroundImage": "Global background image",
"appBackgroundImageDesc": "The global background image will be displayed on all pages"
} }

View File

@ -32,9 +32,9 @@
"dashboard": "仪表盘", "dashboard": "仪表盘",
"today": "今日", "today": "今日",
"yesterday": "昨日", "yesterday": "昨日",
"feedSearch": "搜索资讯", "postSearch": "搜索帖子",
"feedSearchWithTag": "检索带有 #@key 标签的资讯", "postSearchWithTag": "检索带有 #@key 标签的资讯",
"feedSearchWithCategory": "检索位于分类 @category 的资讯", "postSearchWithCategory": "检索位于分类 @category 的资讯",
"feedUnreadCount": "@count 条你可能错过的帖子", "feedUnreadCount": "@count 条你可能错过的帖子",
"messages": "消息", "messages": "消息",
"messagesUnreadCount": "@count 条未读的消息", "messagesUnreadCount": "@count 条未读的消息",
@ -98,6 +98,8 @@
"accountFriendBlocked": "好友黑名单", "accountFriendBlocked": "好友黑名单",
"accountFriendListHint": "左滑来拒绝,右滑来接受", "accountFriendListHint": "左滑来拒绝,右滑来接受",
"accountFriendRequestSent": "好友请求已发送,等待处理对方中……", "accountFriendRequestSent": "好友请求已发送,等待处理对方中……",
"accountBlocked": "已屏蔽账号",
"accountUnblocked": "已解除屏蔽账号",
"accountSuspended": "帐号被停用", "accountSuspended": "帐号被停用",
"accountSuspendedAt": "该帐号自 @date 起被停用", "accountSuspendedAt": "该帐号自 @date 起被停用",
"aspectRatio": "纵横比", "aspectRatio": "纵横比",
@ -264,7 +266,7 @@
"channelMembersAddHint": "到 @channel", "channelMembersAddHint": "到 @channel",
"channelType": "频道类型", "channelType": "频道类型",
"channelTypeCommon": "普通频道", "channelTypeCommon": "普通频道",
"channelTypeDirect": "私信聊天", "channelTypeDirect": "私信",
"channelAdjust": "调整频道", "channelAdjust": "调整频道",
"channelDetail": "频道详情", "channelDetail": "频道详情",
"channelSettings": "频道设置", "channelSettings": "频道设置",
@ -428,5 +430,48 @@
"update": "更新", "update": "更新",
"updateCheckStrictly": "严格模式", "updateCheckStrictly": "严格模式",
"updateCheckStrictlyDesc": "如果启用,应用程序将会在本地版本与远程版本不同时询问更新,而不会检查版本号大小。", "updateCheckStrictlyDesc": "如果启用,应用程序将会在本地版本与远程版本不同时询问更新,而不会检查版本号大小。",
"updateMayAvailable": "版本 @version 现已可用,你可以前往应用商店或是我们的官网下载更新" "updateNow": "立即更新",
"updateMayAvailable": "版本 @version 现已可用,你可以前往应用商店或是我们的官网下载更新。",
"termAccept": "我已阅读并同意 Solar Network 各项条款",
"termAcceptDesc": "包括但不限于《用户守则》和《隐私政策》",
"termAcceptLink": "浏览条款",
"termAcceptNextWithAgree": "点击 “下一步”,即表示你同意我们的各项条款,包括其之后的更新。你应该在注册时已经同意过了。",
"termRelated": "相关条款",
"projectWebsite": "项目网站",
"appDetails": "应用详情",
"iAmNotRobot": "我不是机器人",
"report": "举报",
"reportAbuse": "举报滥用",
"reportAbuseDesc": "举报任何违反服务条款的行为",
"reportAbuseResource": "举报的资源",
"reportAbuseReason": "举报的原因",
"reportSubmitted": "举报已提交,感谢你的贡献。我们将通过通知在 24 小时内通知该举报的处理结果。",
"accountDeletion": "请求删除账号",
"accountDeletionDesc": "删除目前登陆的账号,及其所有的数据。注意,该操作不可撤销!",
"accountDeletionConfirm": "确认账号删除请求",
"accountDeletionConfirmDesc": "你确定要删除账号 @account 吗?你将会在其绑定的主要邮件地址收到一封包含着确认删除账号连接的邮件,在二十四小时内使用该连接即可完成删除账号。注意,本操作不可撤销,并且账号创建或关联的所有数据都将被删除,请三思而后行。",
"accountDeletionRequested": "已请求删除账号,检查你的收件箱来确认请求。",
"slideToConfirm": "滑动来确认",
"serviceStatus": "服务状态",
"firstBootTime": "首次启动于 @time",
"rateTheApp": "给应用评分",
"rateTheAppDesc": "在 App Store 上给 Solar Network 评分,让我们更好地为您服务吧!",
"friendAdd": "添加好友",
"blockUser": "屏蔽用户",
"unblockUser": "解除屏蔽用户",
"learnMoreAboutPerson": "了解关于 TA 的更多",
"global": "全局",
"all": "全部",
"unablePreview": "无法预览",
"dashboardNav": "仪表盘",
"accountNav": "您",
"performance": "性能",
"animatedMessageList": "无动画消息列表",
"animatedMessageListDesc": "在消息列表中禁用动画效果",
"theme": "主题",
"globalTheme": "全局应用主题",
"agedTheme": "过时主题",
"agedThemeDesc": "将全局主题降级为 Material Design 2可能发生意料之外的问题仅供实验使用",
"appBackgroundImage": "全局背景图片",
"appBackgroundImageDesc": "全局背景图片将会在所有页面中展示"
} }

View File

@ -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
@ -172,21 +172,21 @@ PODS:
- 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)
@ -227,7 +227,9 @@ PODS:
- TOCropViewController (~> 2.7.4) - TOCropViewController (~> 2.7.4)
- image_picker_ios (0.0.1): - image_picker_ios (0.0.1):
- Flutter - Flutter
- livekit_client (2.2.5): - in_app_review (0.2.0):
- Flutter
- livekit_client (2.2.6):
- Flutter - Flutter
- WebRTC-SDK (= 125.6422.04) - WebRTC-SDK (= 125.6422.04)
- media_kit_libs_ios_video (1.0.4): - media_kit_libs_ios_video (1.0.4):
@ -267,7 +269,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)":
@ -318,6 +320,7 @@ DEPENDENCIES:
- 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`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- in_app_review (from `.symlinks/plugins/in_app_review/ios`)
- livekit_client (from `.symlinks/plugins/livekit_client/ios`) - livekit_client (from `.symlinks/plugins/livekit_client/ios`)
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`) - media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
- media_kit_native_event_loop (from `.symlinks/plugins/media_kit_native_event_loop/ios`) - media_kit_native_event_loop (from `.symlinks/plugins/media_kit_native_event_loop/ios`)
@ -331,7 +334,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`)
@ -406,6 +409,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/image_cropper/ios" :path: ".symlinks/plugins/image_cropper/ios"
image_picker_ios: image_picker_ios:
:path: ".symlinks/plugins/image_picker_ios/ios" :path: ".symlinks/plugins/image_picker_ios/ios"
in_app_review:
:path: ".symlinks/plugins/in_app_review/ios"
livekit_client: livekit_client:
:path: ".symlinks/plugins/livekit_client/ios" :path: ".symlinks/plugins/livekit_client/ios"
media_kit_libs_ios_video: media_kit_libs_ios_video:
@ -432,8 +437,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:
@ -449,25 +454,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
@ -477,12 +482,13 @@ SPEC CHECKSUMS:
flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12 flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
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
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1 image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
livekit_client: 9c8080879256a0fb16da13c9be4845248209d896 in_app_review: 318597b3a06c22bb46dc454d56828c85f444f99d
livekit_client: 20e01637431bc108dad451c8a11c1d206e1dd2cd
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1 media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
@ -499,7 +505,7 @@ SPEC CHECKSUMS:
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

View File

@ -1,87 +1,91 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>CFBundleURLTypes</key> <key>CFBundleURLTypes</key>
<array> <array>
<dict> <dict>
<key>CFBundleURLName</key> <key>CFBundleURLName</key>
<string></string> <string></string>
<key>CFBundleTypeRole</key> <key>CFBundleTypeRole</key>
<string>Editor</string> <string>Editor</string>
<key>CFBundleURLSchemes</key> <key>CFBundleURLSchemes</key>
<array> <array>
<string>solink</string> <string>solink</string>
</array> </array>
</dict> </dict>
</array> </array>
<key>FirebaseMessagingAutoInitEnabled</key> <key>FirebaseMessagingAutoInitEnabled</key>
<false/> <false/>
<key>CADisableMinimumFrameDurationOnPhone</key> <key>CADisableMinimumFrameDurationOnPhone</key>
<true/> <true/>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key> <key>CFBundleDisplayName</key>
<string>Solian</string> <string>Solian</string>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string> <string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key> <key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string> <string>6.0</string>
<key>CFBundleName</key> <key>CFBundleName</key>
<string>solian</string> <string>solian</string>
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string> <string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string> <string>$(FLUTTER_BUILD_NUMBER)</string>
<key>ITSAppUsesNonExemptEncryption</key> <key>ITSAppUsesNonExemptEncryption</key>
<false/> <false/>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true/>
<key>NSCameraUsageDescription</key> <key>NSCameraUsageDescription</key>
<string>Allow you take photo/video for your message or post</string> <string>Allow you take photo/video for your message or post</string>
<key>NSMicrophoneUsageDescription</key> <key>NSMicrophoneUsageDescription</key>
<string>Allow you record audio for your message or post</string> <string>Allow you record audio for your message or post</string>
<key>NSPhotoLibraryUsageDescription</key> <key>NSPhotoLibraryUsageDescription</key>
<string>Allow you add photo to your message or post</string> <string>Allow you add photo to your message or post</string>
<key>UIApplicationSupportsIndirectInputEvents</key> <key>UIApplicationSupportsIndirectInputEvents</key>
<true/> <true/>
<key>UIBackgroundModes</key> <key>UIBackgroundModes</key>
<array> <array>
<string>audio</string> <string>audio</string>
<string>fetch</string> <string>fetch</string>
<string>remote-notification</string> <string>remote-notification</string>
<string>voip</string> <string>voip</string>
</array> </array>
<key>UILaunchStoryboardName</key> <key>UILaunchStoryboardName</key>
<string>LaunchScreen</string> <string>LaunchScreen</string>
<key>UIMainStoryboardFile</key> <key>UIMainStoryboardFile</key>
<string>Main</string> <string>Main</string>
<key>UISupportedInterfaceOrientations</key> <key>UISupportedInterfaceOrientations</key>
<array> <array>
<string>UIInterfaceOrientationPortrait</string> <string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string> <string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string> <string>UIInterfaceOrientationLandscapeRight</string>
</array> </array>
<key>FlutterDeepLinkingEnabled</key> <key>FlutterDeepLinkingEnabled</key>
<true/> <true/>
<key>NSUserActivityTypes</key> <key>NSUserActivityTypes</key>
<array> <array>
<string>INSendMessageIntent</string> <string>INSendMessageIntent</string>
</array> </array>
<key>UISupportedInterfaceOrientations~ipad</key> <key>UISupportedInterfaceOrientations~ipad</key>
<array> <array>
<string>UIInterfaceOrientationPortrait</string> <string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string> <string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string> <string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string> <string>UIInterfaceOrientationLandscapeRight</string>
</array> </array>
<key>UIStatusBarHidden</key> <key>CFBundleLocalizations</key>
<false/> <array>
</dict> <string>en</string>
</array>
<key>UIStatusBarHidden</key>
<false/>
</dict>
</plist> </plist>

View File

@ -1,20 +1,23 @@
import 'dart:async'; import 'dart:async';
import 'dart:developer';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:in_app_review/in_app_review.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.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/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';
@ -41,6 +44,106 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
final Completer _bootCompleter = Completer(); final Completer _bootCompleter = Completer();
void _requestRating() async {
final prefs = await SharedPreferences.getInstance();
if (prefs.containsKey('first_boot_time')) {
final rawTime = prefs.getString('first_boot_time');
final time = DateTime.tryParse(rawTime ?? '');
if (time != null &&
time.isBefore(DateTime.now().subtract(const Duration(days: 3)))) {
final inAppReview = InAppReview.instance;
if (prefs.getBool('rating_requested') == true) return;
if (await inAppReview.isAvailable()) {
await inAppReview.requestReview();
prefs.setBool('rating_requested', true);
} else {
log('Unable request app review, unavailable');
}
}
} else {
prefs.setString('first_boot_time', DateTime.now().toIso8601String());
}
}
void _updateNow(String localVersionString, String remoteVersionString) {
context
.showConfirmDialog(
'updateAvailable'.tr,
'updateAvailableDesc'.trParams({
'from': localVersionString,
'to': remoteVersionString,
}),
)
.then((result) {
if (result) {
final model = UpdateModel(
'https://files.solsynth.dev/d/production01/solian/app-arm64-v8a-release.apk',
'solian-app-arm64-v8a-release.apk',
'ic_launcher',
'https://testflight.apple.com/join/YJ0lmN6O',
);
AzhonAppUpdate.update(model);
}
});
}
Future<void> _checkForUpdate() async {
if (PlatformInfo.isWeb) return;
try {
final prefs = await SharedPreferences.getInstance();
final info = await PackageInfo.fromPlatform();
final localVersionString = '${info.version}+${info.buildNumber}';
final resp = await GetConnect(
timeout: const Duration(seconds: 60),
).get(
'https://git.solsynth.dev/api/v1/repos/hydrogen/solian/tags?page=1&limit=1',
);
if (resp.statusCode != 200) {
throw RequestException(resp);
}
final remoteVersionString =
(resp.body as List).firstOrNull?['name'] ?? '0.0.0+0';
final remoteVersion = Version.parse(remoteVersionString.split('+').first);
final localVersion = Version.parse(localVersionString.split('+').first);
final remoteBuildNumber =
int.tryParse(remoteVersionString.split('+').last) ?? 0;
final localBuildNumber =
int.tryParse(localVersionString.split('+').last) ?? 0;
final strictUpdate = prefs.getBool('check_update_strictly') ?? false;
if (remoteVersion > localVersion ||
(remoteVersion == localVersion &&
remoteBuildNumber > localBuildNumber) ||
(remoteVersionString != localVersionString && strictUpdate)) {
if (PlatformInfo.isAndroid) {
_updateNow(localVersionString, remoteVersionString);
} else {
context.showInfoDialog(
'updateAvailable'.tr,
'bsCheckForUpdateDesc'.tr,
);
}
} else if (remoteVersionString != localVersionString) {
_bootCompleter.future.then((_) {
context.showSnackbar(
'updateMayAvailable'.trParams({
'version': remoteVersionString,
}),
action: PlatformInfo.isAndroid
? SnackBarAction(
label: 'updateNow'.tr,
onPressed: () {
_updateNow(localVersionString, remoteVersionString);
},
)
: null,
);
});
}
} catch (e) {
context.showErrorDialog('Unable to check update: $e');
}
}
late final List<({String label, Future<void> Function() action})> _periods = [ late final List<({String label, Future<void> Function() action})> _periods = [
( (
label: 'bsLoadingTheme', label: 'bsLoadingTheme',
@ -48,68 +151,6 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
await context.read<ThemeSwitcher>().restoreTheme(); await context.read<ThemeSwitcher>().restoreTheme();
}, },
), ),
(
label: 'bsCheckForUpdate',
action: () async {
if (PlatformInfo.isWeb) return;
try {
final prefs = await SharedPreferences.getInstance();
final info = await PackageInfo.fromPlatform();
final localVersionString = '${info.version}+${info.buildNumber}';
final resp = await GetConnect().get(
'https://git.solsynth.dev/api/v1/repos/hydrogen/solian/tags?page=1&limit=1',
);
final remoteVersionString =
(resp.body as List).firstOrNull?['name'] ?? '0.0.0';
final remoteVersion = Version.parse(remoteVersionString ?? '0.0.0');
final localVersion =
Version.parse(localVersionString.split('+').first);
final strictUpdate = prefs.getBool('check_update_strictly') ?? false;
if (remoteVersion > localVersion ||
(remoteVersionString != localVersionString && strictUpdate)) {
setState(() {
_isErrored = true;
_subtitle = 'bsCheckForUpdateDesc'.tr;
});
if (PlatformInfo.isAndroid) {
context
.showConfirmDialog(
'updateAvailable'.tr,
'updateAvailableDesc'.trParams({
'from': localVersionString,
'to': remoteVersionString,
}),
)
.then((result) {
if (result) {
final model = UpdateModel(
'https://files.solsynth.dev/d/production01/solian/app-arm64-v8a-release.apk',
'solian-app-arm64-v8a-release.apk',
'ic_launcher',
'https://testflight.apple.com/join/YJ0lmN6O',
);
AzhonAppUpdate.update(model);
}
});
} else {
setState(() {
_isErrored = true;
_subtitle = 'bsCheckForUpdateDesc'.tr;
});
}
} else if (remoteVersionString != localVersionString) {
_bootCompleter.future.then((_) {
context.showSnackbar('updateMayAvailable'.trParams({
'version': remoteVersionString,
}));
});
}
} catch (e) {
context.showErrorDialog('Unable to check update: $e');
}
},
),
( (
label: 'bsCheckingServer', label: 'bsCheckingServer',
action: () async { action: () async {
@ -157,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)
@ -207,14 +246,17 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
void initState() { void initState() {
super.initState(); super.initState();
_runPeriods(); _runPeriods();
_checkForUpdate();
_bootCompleter.future.then((_) {
_requestRating();
});
} }
@override @override
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,

View File

@ -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;

View File

@ -1,6 +1,8 @@
import 'dart:math' as math; import 'dart:math' as math;
import 'package:action_slider/action_slider.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:solian/exceptions/request.dart'; import 'package:solian/exceptions/request.dart';
import 'package:solian/exceptions/unauthorized.dart'; import 'package:solian/exceptions/unauthorized.dart';
@ -73,6 +75,47 @@ extension AppExtensions on BuildContext {
false; false;
} }
Future<bool> showSlideToConfirmDialog(String title, body) async {
return await showDialog<bool>(
useRootNavigator: true,
context: this,
builder: (ctx) => AlertDialog(
title: Text(title, textAlign: TextAlign.center),
content: SizedBox(
width: double.maxFinite,
child: ListView(
shrinkWrap: true,
children: [
Text(body, textAlign: TextAlign.center),
const Gap(28),
ActionSlider.standard(
icon: const Icon(Icons.send),
iconAlignment: Alignment.center,
sliderBehavior: SliderBehavior.move,
actionThresholdType: ThresholdType.release,
toggleColor: Colors.red,
action: (controller) async {
controller.success();
await Future.delayed(const Duration(milliseconds: 500));
Navigator.pop(ctx, true);
},
child: Text('slideToConfirm'.tr),
),
],
),
),
actionsAlignment: MainAxisAlignment.center,
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
child: Text('cancel'.tr),
)
],
),
) ??
false;
}
Future<void> showErrorDialog(dynamic exception) { Future<void> showErrorDialog(dynamic exception) {
Widget content = Text(exception.toString().capitalize!); Widget content = Text(exception.toString().capitalize!);
if (exception is UnauthorizedException) { if (exception is UnauthorizedException) {

View File

@ -57,13 +57,16 @@ void main() async {
Future<void> _initializeFirebase() async { Future<void> _initializeFirebase() async {
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
FlutterError.onError = (errorDetails) { if (PlatformInfo.isIOS || PlatformInfo.isAndroid || PlatformInfo.isMacOS) {
FirebaseCrashlytics.instance.recordFlutterFatalError(errorDetails); // Initialize firebase crashlytics for the platform that supported
}; FlutterError.onError = (errorDetails) {
PlatformDispatcher.instance.onError = (error, stack) { FirebaseCrashlytics.instance.recordFlutterFatalError(errorDetails);
FirebaseCrashlytics.instance.recordError(error, stack, fatal: true); };
return true; PlatformDispatcher.instance.onError = (error, stack) {
}; FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
return true;
};
}
} }
Future<void> _initializeBackgroundNotificationService() async { Future<void> _initializeBackgroundNotificationService() async {

View File

@ -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;
@ -33,6 +42,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,

50
lib/models/theme.dart Normal file
View 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
View 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,
};

View File

@ -27,6 +27,10 @@ abstract class PlatformInfo {
static bool get canCacheImage => isAndroid || isIOS || isMacOS; static bool get canCacheImage => isAndroid || isIOS || isMacOS;
static bool get canRateTheApp => isIOS || isMacOS;
static bool get canCropImage => isIOS || isAndroid || isWeb;
static bool get canRecord => (isMobile || isMacOS); static bool get canRecord => (isMobile || isMacOS);
static bool get canPushNotification => isAndroid || isIOS || isMacOS; static bool get canPushNotification => isAndroid || isIOS || isMacOS;

View File

@ -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;

View File

@ -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()),
); );
} }

View File

@ -41,25 +41,27 @@ class AttachmentProvider extends GetConnect {
} }
} }
final resp = await get( if (pendingQuery.isNotEmpty) {
'/attachments?take=${pendingQuery.length}&id=${pendingQuery.join(',')}', final resp = await get(
); '/attachments?take=${pendingQuery.length}&id=${pendingQuery.join(',')}',
if (resp.statusCode != 200) return result; );
if (resp.statusCode != 200) return result;
final rawOut = PaginationResult.fromJson(resp.body); final rawOut = PaginationResult.fromJson(resp.body);
if (rawOut.data == null) return result; if (rawOut.data == null) return result;
final List<Attachment> out = final List<Attachment> out =
rawOut.data!.map((x) => Attachment.fromJson(x)).toList(); rawOut.data!.map((x) => Attachment.fromJson(x)).toList();
for (final item in out) { for (final item in out) {
if (item.destination != 0 && item.isAnalyzed) { if (item.destination != 0 && item.isAnalyzed) {
_cachedResponses[item.rid] = item; _cachedResponses[item.rid] = item;
}
} }
} for (var i = 0; i < out.length; i++) {
for (var i = 0; i < out.length; i++) { for (var j = 0; j < rid.length; j++) {
for (var j = 0; j < rid.length; j++) { if (out[i].rid == rid[j]) {
if (out[i].rid == rid[j]) { result[j] = out[i];
result[j] = out[i]; }
} }
} }
} }

View File

@ -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 {

View File

@ -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('&')}'
@ -71,16 +55,18 @@ class PostProvider extends GetConnect {
} }
Future<Response> listPost(int page, Future<Response> listPost(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}', '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?${queries.join('&')}');
if (resp.statusCode != 200) { if (resp.statusCode != 200) {
throw RequestException(resp); throw RequestException(resp);
} }
@ -89,7 +75,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 +87,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 +98,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);
} }

View File

@ -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);
}
} }

View File

@ -26,6 +26,19 @@ class RelationshipProvider extends GetxController {
return _friends.any((x) => x.relatedId == account.id); return _friends.any((x) => x.relatedId == account.id);
} }
Future<Relationship?> getRelationship(int relatedId) async {
final AuthProvider auth = Get.find();
final client = await auth.configureClient('auth');
final resp = await client.get('/users/me/relations/$relatedId');
if (resp.statusCode == 404) {
return null;
} else if (resp.statusCode != 200) {
throw RequestException(resp);
}
return Relationship.fromJson(resp.body);
}
Future<Response> listRelation() async { Future<Response> listRelation() async {
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
final client = await auth.configureClient('auth'); final client = await auth.configureClient('auth');
@ -38,7 +51,19 @@ class RelationshipProvider extends GetxController {
return client.get('/users/me/relations?status=$status'); return client.get('/users/me/relations?status=$status');
} }
Future<Response> makeFriend(String username) async { Future<Relationship?> blockUser(String username) async {
final AuthProvider auth = Get.find();
final client = await auth.configureClient('auth');
final resp =
await client.post('/users/me/relations/block?related=$username', {});
if (resp.statusCode != 200) {
throw RequestException(resp);
}
return Relationship.fromJson(resp.body);
}
Future<Relationship?> makeFriend(String username) async {
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
final client = await auth.configureClient('auth'); final client = await auth.configureClient('auth');
final resp = await client.post('/users/me/relations?related=$username', {}); final resp = await client.post('/users/me/relations?related=$username', {});
@ -46,7 +71,7 @@ class RelationshipProvider extends GetxController {
throw RequestException(resp); throw RequestException(resp);
} }
return resp; return Relationship.fromJson(resp.body);
} }
Future<Response> handleRelation( Future<Response> handleRelation(
@ -64,17 +89,17 @@ class RelationshipProvider extends GetxController {
return resp; return resp;
} }
Future<Response> editRelation(Relationship relationship, int status) async { Future<Relationship?> editRelation(int relatedId, int status) async {
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
final client = await auth.configureClient('auth'); final client = await auth.configureClient('auth');
final resp = await client.patch( final resp = await client.put(
'/users/me/relations/${relationship.relatedId}', '/users/me/relations/$relatedId',
{'status': status}, {'status': status},
); );
if (resp.statusCode != 200) { if (resp.statusCode != 200) {
throw RequestException(resp); throw RequestException(resp);
} }
return resp; return Relationship.fromJson(resp.body);
} }
} }

View File

@ -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();
}
} }

View File

@ -23,11 +23,13 @@ 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';
import 'package:solian/screens/realms/realm_view.dart'; import 'package:solian/screens/realms/realm_view.dart';
import 'package:solian/screens/feed.dart'; import 'package:solian/screens/explore.dart';
import 'package:solian/screens/posts/post_editor.dart'; import 'package:solian/screens/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(
@ -78,13 +80,18 @@ abstract class AppRouter {
builder: (context, state, child) => child, builder: (context, state, child) => child,
routes: [ routes: [
GoRoute( GoRoute(
path: '/feed', path: '/explore',
name: 'feed', name: 'explore',
builder: (context, state) => const FeedScreen(), builder: (context, state) => const ExploreScreen(),
), ),
GoRoute( GoRoute(
path: '/feed/search', path: '/drafts',
name: 'feedSearch', name: 'draftBox',
builder: (context, state) => const DraftBoxScreen(),
),
GoRoute(
path: '/posts/search',
name: 'postSearch',
builder: (context, state) => TitleShell( builder: (context, state) => TitleShell(
state: state, state: state,
child: FeedSearchScreen( child: FeedSearchScreen(
@ -93,11 +100,6 @@ abstract class AppRouter {
), ),
), ),
), ),
GoRoute(
path: '/drafts',
name: 'draftBox',
builder: (context, state) => const DraftBoxScreen(),
),
GoRoute( GoRoute(
path: '/posts/view/:id', path: '/posts/view/:id',
name: 'postDetail', name: 'postDetail',
@ -137,12 +139,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 +178,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,

View File

@ -1,6 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:solian/widgets/sized_container.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
class AboutScreen extends StatelessWidget { class AboutScreen extends StatelessWidget {
@ -11,78 +15,130 @@ 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, width: double.infinity,
child: SizedBox( child: Column(
width: double.infinity, mainAxisAlignment: MainAxisAlignment.center,
child: Column( children: [
mainAxisAlignment: MainAxisAlignment.center, ClipRRect(
children: [ borderRadius: const BorderRadius.all(Radius.circular(16)),
ClipRRect( child: Image.asset('assets/logo.png', width: 120, height: 120),
borderRadius: const BorderRadius.all(Radius.circular(16)), ),
child: Image.asset('assets/logo.png', width: 120, height: 120), const Gap(8),
), Text(
const Gap(8), 'Solian',
Text( style: Theme.of(context).textTheme.headlineMedium,
'Solian', ),
style: Theme.of(context).textTheme.headlineMedium, const Text(
), 'The Solar Network',
const Text( style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
'The Solar Network', ),
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), const Gap(8),
), FutureBuilder(
const Gap(8), future: PackageInfo.fromPlatform(),
FutureBuilder( builder: (context, snapshot) {
future: PackageInfo.fromPlatform(), if (!snapshot.hasData) {
builder: (context, snapshot) { return const SizedBox.shrink();
if (!snapshot.hasData) { }
return const SizedBox.shrink();
}
return Text( return Text(
'v${snapshot.data!.version} · ${snapshot.data!.buildNumber}', 'v${snapshot.data!.version} · ${snapshot.data!.buildNumber}',
style: const TextStyle(fontFamily: 'monospace'), style: const TextStyle(fontFamily: 'monospace'),
); );
}, },
), ),
Text('Copyright © ${DateTime.now().year} Solsynth LLC'), Text('Copyright © ${DateTime.now().year} Solsynth LLC'),
const Gap(16), const Gap(16),
TextButton( CenteredContainer(
style: denseButtonStyle, maxWidth: 280,
child: const Text('App Details'), child: Wrap(
onPressed: () async { spacing: 4,
final info = await PackageInfo.fromPlatform(); runSpacing: 4,
alignment: WrapAlignment.center,
children: [
TextButton(
style: denseButtonStyle,
child: Text('appDetails'.tr),
onPressed: () async {
final info = await PackageInfo.fromPlatform();
showAboutDialog( showAboutDialog(
context: context, context: context,
applicationVersion: '${info.version} (${info.buildNumber})', applicationVersion:
applicationLegalese: '${info.version} (${info.buildNumber})',
'The Solar Network App is an intuitive and self-hostable social network and computing platform. Experience the freedom of a user-friendly design that empowers you to create and connect with communities on your own terms. Embrace the future of social networking with a platform that prioritizes your independence and privacy.', applicationLegalese:
applicationIcon: ClipRRect( 'The Solar Network App is an intuitive and open-source social network and computing platform. Experience the freedom of a user-friendly design that empowers you to create and connect with communities on your own terms. Embrace the future of social networking with a platform that prioritizes your independence and privacy.',
borderRadius: const BorderRadius.all(Radius.circular(16)), applicationIcon: ClipRRect(
child: borderRadius:
Image.asset('assets/logo.png', width: 60, height: 60), const BorderRadius.all(Radius.circular(16)),
), child: Image.asset('assets/logo.png',
); width: 60, height: 60),
}, ),
);
},
),
TextButton(
style: denseButtonStyle,
child: Text('projectWebsite'.tr),
onPressed: () {
launchUrlString(
'https://solsynth.dev/products/solar-network');
},
),
TextButton(
style: denseButtonStyle,
child: Text('termRelated'.tr),
onPressed: () {
launchUrlString('https://solsynth.dev/terms');
},
),
TextButton(
style: denseButtonStyle,
child: Text('serviceStatus'.tr),
onPressed: () {
launchUrlString('https://status.solsynth.dev');
},
),
],
), ),
TextButton( ),
style: denseButtonStyle, const Gap(16),
child: const Text('Project Website'), const Text(
onPressed: () { 'Open-sourced under AGPLv3',
launchUrlString('https://solsynth.dev/products/solar-network'); style: TextStyle(
}, fontWeight: FontWeight.w300,
fontSize: 12,
), ),
const Gap(16), ),
const Text( FutureBuilder(
'Open-sourced under AGPLv3', future: SharedPreferences.getInstance(),
style: TextStyle( builder: (context, snapshot) {
const textStyle = TextStyle(
fontWeight: FontWeight.w300, fontWeight: FontWeight.w300,
fontSize: 12, fontSize: 12,
), );
), if (!snapshot.hasData ||
], !snapshot.data!.containsKey('first_boot_time')) {
), return Text(
'firstBootTime'.trParams({'time': 'unknown'.tr}),
style: textStyle,
);
} else {
return Text(
'firstBootTime'.trParams({
'time': DateFormat('yyyy-MM-dd').format(
DateTime.tryParse(
snapshot.data!.getString('first_boot_time')!,
)?.toLocal() ??
DateTime.now(),
),
}),
style: textStyle,
);
}
},
),
],
), ),
); );
} }

View File

@ -49,113 +49,110 @@ 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: Obx(() {
child: SafeArea( if (auth.isAuthorized.isFalse) {
child: Obx(() { return Center(
if (auth.isAuthorized.isFalse) { child: Column(
return Center( mainAxisSize: MainAxisSize.min,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_ActionCard(
icon: Icon(
Icons.login,
color: Theme.of(context).colorScheme.onPrimary,
),
title: 'signin'.tr,
caption: 'signinCaption'.tr,
onTap: () {
AppRouter.instance.pushNamed('signin').then((val) async {
if (val == true) {
await auth.refreshUserProfile();
}
});
},
),
_ActionCard(
icon: Icon(
Icons.add,
color: Theme.of(context).colorScheme.onPrimary,
),
title: 'signup'.tr,
caption: 'signupCaption'.tr,
onTap: () {
AppRouter.instance.pushNamed('signup').then((_) {
setState(() {});
});
},
),
const Gap(4),
TextButton(
style: const ButtonStyle(
visualDensity: VisualDensity(
horizontal: -4,
vertical: -2,
),
),
onPressed: () {
AppRouter.instance.pushNamed('settings');
},
child: Text('settings'.tr),
),
],
),
);
}
return CenteredContainer(
child: ListView(
children: [ children: [
if (auth.userProfile.value != null) _ActionCard(
const AccountHeading().paddingOnly(bottom: 8, top: 8), icon: Icon(
...(actionItems.map( Icons.login,
(x) => ListTile( color: Theme.of(context).colorScheme.onPrimary,
contentPadding: const EdgeInsets.symmetric(horizontal: 34),
leading: x.$1,
title: Text(x.$2),
onTap: () {
AppRouter.instance
.pushNamed(x.$3)
.then((_) => setState(() {}));
},
), ),
)), title: 'signin'.tr,
const Divider(thickness: 0.3, height: 1) caption: 'signinCaption'.tr,
.paddingSymmetric(vertical: 4),
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 34),
leading: const Icon(Icons.settings),
title: Text('settings'.tr),
onTap: () { onTap: () {
AppRouter.instance.pushNamed('settings'); AppRouter.instance.pushNamed('signin').then((val) async {
if (val == true) {
await auth.refreshUserProfile();
}
});
}, },
), ),
if (auth.isAuthorized.value) _ActionCard(
ListTile( icon: Icon(
contentPadding: const EdgeInsets.symmetric(horizontal: 34), Icons.add,
leading: const Icon(Icons.edit_notifications), color: Theme.of(context).colorScheme.onPrimary,
title: Text('notificationPreferences'.tr),
onTap: () {
AppRouter.instance.pushNamed('notificationPreferences');
},
), ),
const Divider(thickness: 0.3, height: 1) title: 'signup'.tr,
.paddingSymmetric(vertical: 4), caption: 'signupCaption'.tr,
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 34),
leading: const Icon(Icons.logout),
title: Text('signout'.tr),
onTap: () { onTap: () {
auth.signout(); AppRouter.instance.pushNamed('signup').then((_) {
setState(() {}); setState(() {});
});
}, },
), ),
const Gap(4),
TextButton(
style: const ButtonStyle(
visualDensity: VisualDensity(
horizontal: -4,
vertical: -2,
),
),
onPressed: () {
AppRouter.instance.pushNamed('settings');
},
child: Text('settings'.tr),
),
], ],
), ),
); );
}), }
),
return CenteredContainer(
child: ListView(
children: [
if (auth.userProfile.value != null)
const AccountHeading().paddingOnly(bottom: 8, top: 16),
...(actionItems.map(
(x) => ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 34),
leading: x.$1,
title: Text(x.$2),
onTap: () {
AppRouter.instance
.pushNamed(x.$3)
.then((_) => setState(() {}));
},
),
)),
const Divider(thickness: 0.3, height: 1)
.paddingSymmetric(vertical: 4),
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 34),
leading: const Icon(Icons.settings),
title: Text('settings'.tr),
onTap: () {
AppRouter.instance.pushNamed('settings');
},
),
if (auth.isAuthorized.value)
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 34),
leading: const Icon(Icons.edit_notifications),
title: Text('notificationPreferences'.tr),
onTap: () {
AppRouter.instance.pushNamed('notificationPreferences');
},
),
const Divider(thickness: 0.3, height: 1)
.paddingSymmetric(vertical: 4),
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 34),
leading: const Icon(Icons.logout),
title: Text('signout'.tr),
onTap: () {
auth.signout();
setState(() {});
},
),
],
),
);
}),
); );
} }
} }

View File

@ -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,

View File

@ -74,45 +74,42 @@ class _NotificationPreferencesScreenState
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Material( return Column(
color: Theme.of(context).colorScheme.surface, children: [
child: Column( if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
children: [ ListTile(
if (_isBusy) const LinearProgressIndicator().animate().scaleX(), tileColor: Theme.of(context).colorScheme.surfaceContainer,
ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 24),
tileColor: Theme.of(context).colorScheme.surfaceContainer, leading: const Icon(Icons.save),
contentPadding: const EdgeInsets.symmetric(horizontal: 24), title: Text('save'.tr),
leading: const Icon(Icons.save), enabled: !_isBusy,
title: Text('save'.tr), onTap: () {
enabled: !_isBusy, _savePreferences();
onTap: () { },
_savePreferences(); ),
Expanded(
child: ListView.builder(
itemCount: _topicMap.length,
itemBuilder: (context, index) {
final element = _topicMap.entries.elementAt(index);
return CheckboxListTile(
title: Text(element.value),
subtitle: Text(
element.key,
style: GoogleFonts.robotoMono(fontSize: 12),
),
value: _config[element.key] ?? true,
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
onChanged: (value) {
setState(() {
_config[element.key] = value ?? false;
});
},
);
}, },
), ),
Expanded( ),
child: ListView.builder( ],
itemCount: _topicMap.length,
itemBuilder: (context, index) {
final element = _topicMap.entries.elementAt(index);
return CheckboxListTile(
title: Text(element.value),
subtitle: Text(
element.key,
style: GoogleFonts.robotoMono(fontSize: 12),
),
value: _config[element.key] ?? true,
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
onChanged: (value) {
setState(() {
_config[element.key] = value ?? false;
});
},
);
},
),
),
],
),
); );
} }
} }

View File

@ -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,36 +76,42 @@ 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;
CroppedFile? croppedFile = await ImageCropper().cropImage( if (PlatformInfo.canCropImage) {
sourcePath: image.path, CroppedFile? croppedFile = await ImageCropper().cropImage(
uiSettings: [ sourcePath: image.path,
AndroidUiSettings( uiSettings: [
toolbarTitle: 'cropImage'.tr, AndroidUiSettings(
toolbarColor: Theme.of(context).colorScheme.primary, toolbarTitle: 'cropImage'.tr,
toolbarWidgetColor: Theme.of(context).colorScheme.onPrimary, toolbarColor: Theme.of(context).colorScheme.primary,
aspectRatioPresets: [ toolbarWidgetColor: Theme.of(context).colorScheme.onPrimary,
if (position == 'avatar') CropAspectRatioPreset.square, aspectRatioPresets: [
if (position == 'banner') _BannerCropAspectRatioPreset(), if (position == 'avatar') CropAspectRatioPreset.square,
], if (position == 'banner') _BannerCropAspectRatioPreset(),
), ],
IOSUiSettings( ),
title: 'cropImage'.tr, IOSUiSettings(
aspectRatioPresets: [ title: 'cropImage'.tr,
if (position == 'avatar') CropAspectRatioPreset.square, aspectRatioPresets: [
if (position == 'banner') _BannerCropAspectRatioPreset(), if (position == 'avatar') CropAspectRatioPreset.square,
], if (position == 'banner') _BannerCropAspectRatioPreset(),
), ],
WebUiSettings( ),
context: context, WebUiSettings(
), context: context,
], ),
); ],
);
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,164 +186,161 @@ 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, children: [
child: ListView( if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
children: [ const Gap(24),
if (_isBusy) const LinearProgressIndicator().animate().scaleX(), Stack(
const Gap(24), children: [
Stack( AttachedCircleAvatar(content: _avatar, radius: 40),
children: [ Positioned(
AccountAvatar(content: _avatar, radius: 40), bottom: 0,
Positioned( left: 40,
bottom: 0, child: FloatingActionButton.small(
left: 40, heroTag: const Key('avatar-editor'),
child: FloatingActionButton.small( onPressed: () => _editImage('avatar'),
heroTag: const Key('avatar-editor'), child: const Icon(
onPressed: () => _editImage('avatar'), Icons.camera,
child: const Icon(
Icons.camera,
),
), ),
), ),
],
).paddingSymmetric(horizontal: padding),
const Gap(16),
Stack(
children: [
ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: AspectRatio(
aspectRatio: 16 / 9,
child: Container(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
child: _banner != null
? Image.network(
ServiceFinder.buildUrl(
'files', '/attachments/$_banner'),
fit: BoxFit.cover,
loadingBuilder: (BuildContext context, Widget child,
ImageChunkEvent? loadingProgress) {
if (loadingProgress == null) return child;
return Center(
child: CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes !=
null
? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null,
),
);
},
)
: Container(),
),
),
),
Positioned(
bottom: 16,
right: 16,
child: FloatingActionButton(
heroTag: const Key('banner-editor'),
onPressed: () => _editImage('banner'),
child: const Icon(
Icons.camera_alt,
),
),
),
],
).paddingSymmetric(horizontal: padding),
const Gap(24),
Row(
children: [
Flexible(
flex: 1,
child: TextField(
readOnly: true,
controller: _usernameController,
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: 'username'.tr,
prefixText: '@',
),
),
),
const Gap(16),
Flexible(
flex: 1,
child: TextField(
controller: _nicknameController,
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: 'nickname'.tr,
),
),
),
],
).paddingSymmetric(horizontal: padding),
const Gap(16),
Row(
children: [
Flexible(
flex: 1,
child: TextField(
controller: _firstNameController,
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: 'firstName'.tr,
),
),
),
const Gap(16),
Flexible(
flex: 1,
child: TextField(
controller: _lastNameController,
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: 'lastName'.tr,
),
),
),
],
).paddingSymmetric(horizontal: padding),
const Gap(16),
TextField(
controller: _descriptionController,
keyboardType: TextInputType.multiline,
maxLines: null,
minLines: 3,
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: 'description'.tr,
), ),
).paddingSymmetric(horizontal: padding), ],
const Gap(16), ).paddingSymmetric(horizontal: padding),
TextField( const Gap(16),
controller: _birthdayController, Stack(
readOnly: true, children: [
decoration: InputDecoration( ClipRRect(
border: const OutlineInputBorder(), borderRadius: const BorderRadius.all(Radius.circular(8)),
labelText: 'birthday'.tr, child: AspectRatio(
aspectRatio: 16 / 9,
child: Container(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
child: _banner != null
? Image.network(
ServiceFinder.buildUrl(
'files', '/attachments/$_banner'),
fit: BoxFit.cover,
loadingBuilder: (BuildContext context, Widget child,
ImageChunkEvent? loadingProgress) {
if (loadingProgress == null) return child;
return Center(
child: CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes !=
null
? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null,
),
);
},
)
: Container(),
),
),
), ),
onTap: () => _selectBirthday(), Positioned(
).paddingSymmetric(horizontal: padding), bottom: 16,
const Gap(16), right: 16,
Row( child: FloatingActionButton(
mainAxisAlignment: MainAxisAlignment.end, heroTag: const Key('banner-editor'),
children: [ onPressed: () => _editImage('banner'),
TextButton( child: const Icon(
onPressed: _isBusy ? null : () => _syncWidget(), Icons.camera_alt,
child: Text('reset'.tr), ),
), ),
ElevatedButton( ),
onPressed: _isBusy ? null : () => _editUserInfo(), ],
child: Text('apply'.tr), ).paddingSymmetric(horizontal: padding),
const Gap(24),
Row(
children: [
Flexible(
flex: 1,
child: TextField(
readOnly: true,
controller: _usernameController,
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: 'username'.tr,
prefixText: '@',
),
), ),
], ),
).paddingSymmetric(horizontal: padding), const Gap(16),
], Flexible(
), flex: 1,
child: TextField(
controller: _nicknameController,
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: 'nickname'.tr,
),
),
),
],
).paddingSymmetric(horizontal: padding),
const Gap(16),
Row(
children: [
Flexible(
flex: 1,
child: TextField(
controller: _firstNameController,
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: 'firstName'.tr,
),
),
),
const Gap(16),
Flexible(
flex: 1,
child: TextField(
controller: _lastNameController,
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: 'lastName'.tr,
),
),
),
],
).paddingSymmetric(horizontal: padding),
const Gap(16),
TextField(
controller: _descriptionController,
keyboardType: TextInputType.multiline,
maxLines: null,
minLines: 3,
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: 'description'.tr,
),
).paddingSymmetric(horizontal: padding),
const Gap(16),
TextField(
controller: _birthdayController,
readOnly: true,
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: 'birthday'.tr,
),
onTap: () => _selectBirthday(),
).paddingSymmetric(horizontal: padding),
const Gap(16),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: _isBusy ? null : () => _syncWidget(),
child: Text('reset'.tr),
),
ElevatedButton(
onPressed: _isBusy ? null : () => _editUserInfo(),
child: Text('apply'.tr),
),
],
).paddingSymmetric(horizontal: padding),
],
); );
} }

View File

@ -1,13 +1,19 @@
import 'dart:math';
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
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:intl/intl.dart';
import 'package:solian/controllers/post_list_controller.dart'; import 'package:solian/controllers/post_list_controller.dart';
import 'package:solian/exts.dart'; import 'package:solian/exts.dart';
import 'package:solian/models/account.dart'; import 'package:solian/models/account.dart';
import 'package:solian/models/attachment.dart'; import 'package:solian/models/attachment.dart';
import 'package:solian/models/daily_sign.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/models/relations.dart';
import 'package:solian/models/subscription.dart'; import 'package:solian/models/subscription.dart';
import 'package:solian/providers/account_status.dart'; import 'package:solian/providers/account_status.dart';
import 'package:solian/providers/relation.dart'; import 'package:solian/providers/relation.dart';
@ -18,8 +24,10 @@ import 'package:solian/widgets/account/account_avatar.dart';
import 'package:solian/widgets/account/account_heading.dart'; import 'package:solian/widgets/account/account_heading.dart';
import 'package:solian/widgets/app_bar_leading.dart'; 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/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/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 {
@ -44,7 +52,9 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
Account? _userinfo; Account? _userinfo;
Subscription? _subscription; Subscription? _subscription;
Relationship? _relationship;
List<Post> _pinnedPosts = List.empty(); List<Post> _pinnedPosts = List.empty();
List<DailySignRecord> _dailySignRecords = List.empty();
int _totalUpvote = 0, _totalDownvote = 0; int _totalUpvote = 0, _totalDownvote = 0;
Future<void> _getSubscription() async { Future<void> _getSubscription() async {
@ -54,10 +64,19 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
setState(() => _isSubscribing = false); setState(() => _isSubscribing = false);
} }
Future<void> _getRelationship() async {
setState(() => _isBusy = true);
final relations = Get.find<RelationshipProvider>();
_relationship = await relations.getRelationship(_userinfo!.id);
setState(() => _isBusy = false);
}
Future<void> _getUserinfo() async { Future<void> _getUserinfo() async {
setState(() => _isBusy = true); setState(() => _isBusy = true);
var client = await ServiceFinder.configureClient('auth'); var client = await ServiceFinder.configureClient('id');
var resp = await client.get('/users/${widget.name}'); var resp = await client.get('/users/${widget.name}');
if (resp.statusCode != 200) { if (resp.statusCode != 200) {
context.showErrorDialog(resp.bodyString).then((_) { context.showErrorDialog(resp.bodyString).then((_) {
@ -67,7 +86,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
_userinfo = Account.fromJson(resp.body); _userinfo = Account.fromJson(resp.body);
} }
client = await ServiceFinder.configureClient('interactive'); client = await ServiceFinder.configureClient('co');
resp = await client.get('/users/${widget.name}'); resp = await client.get('/users/${widget.name}');
if (resp.statusCode != 200) { if (resp.statusCode != 200) {
context.showErrorDialog(resp.bodyString).then((_) { context.showErrorDialog(resp.bodyString).then((_) {
@ -82,7 +101,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
} }
Future<void> _getPinnedPosts() async { Future<void> _getPinnedPosts() async {
final client = await ServiceFinder.configureClient('interactive'); final client = await ServiceFinder.configureClient('co');
final resp = await client.get('/users/${widget.name}/pin'); final resp = await client.get('/users/${widget.name}/pin');
if (resp.statusCode != 200) { if (resp.statusCode != 200) {
context.showErrorDialog(resp.bodyString).then((_) { context.showErrorDialog(resp.bodyString).then((_) {
@ -96,6 +115,80 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
} }
} }
Future<void> _getDailySignRecords() async {
final client = await ServiceFinder.configureClient('id');
final resp = await client.get('/users/${widget.name}/daily?take=14');
if (resp.statusCode != 200) {
context.showErrorDialog(resp.bodyString).then((_) {
Navigator.pop(context);
});
} else {
final result = PaginationResult.fromJson(resp.body);
setState(() {
_dailySignRecords = List.from(
result.data?.map((x) => DailySignRecord.fromJson(x)) ?? [],
);
});
}
}
Future<void> _subscribeToUser() async {
setState(() => _isSubscribing = true);
_subscription =
await Get.find<SubscriptionProvider>().subscribeToUser(_userinfo!.id);
setState(() => _isSubscribing = false);
}
Future<void> _unsubscribeFromUser() async {
setState(() => _isSubscribing = true);
await Get.find<SubscriptionProvider>().unsubscribeFromUser(_userinfo!.id);
_subscription = null;
setState(() => _isSubscribing = false);
}
Future<void> _makeFriend() async {
setState(() => _isMakingFriend = true);
try {
_relationship = await _relationshipProvider.makeFriend(widget.name);
context.showSnackbar(
'accountFriendRequestSent'.tr,
);
} catch (e) {
context.showErrorDialog(e);
} finally {
setState(() => _isMakingFriend = false);
}
}
Future<void> _blockUser() async {
setState(() => _isMakingFriend = true);
try {
_relationship = await _relationshipProvider.blockUser(widget.name);
context.showSnackbar(
'accountBlocked'.tr,
);
} catch (e) {
context.showErrorDialog(e);
} finally {
setState(() => _isMakingFriend = false);
}
}
Future<void> _unblockUser() async {
setState(() => _isMakingFriend = true);
try {
_relationship =
await _relationshipProvider.editRelation(_userinfo!.id, 1);
context.showSnackbar(
'accountUnblocked'.tr,
);
} catch (e) {
context.showErrorDialog(e);
} finally {
setState(() => _isMakingFriend = false);
}
}
int get _userSocialCreditPoints { int get _userSocialCreditPoints {
return _totalUpvote * 2 - _totalDownvote + _postController.postTotal.value; return _totalUpvote * 2 - _totalDownvote + _postController.postTotal.value;
} }
@ -127,36 +220,24 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
}); });
_getUserinfo().then((_) { _getUserinfo().then((_) {
_getRelationship();
_getSubscription(); _getSubscription();
_getPinnedPosts(); _getPinnedPosts();
_getDailySignRecords();
}); });
} }
Widget _buildStatisticsEntry(String label, String content) {
return Expanded(
child: Column(
children: [
Text(
label,
style: Theme.of(context).textTheme.bodySmall,
),
Text(
content,
style: Theme.of(context).textTheme.bodyLarge,
),
],
),
);
}
@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(
@ -168,96 +249,76 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
toolbarHeight: AppTheme.toolbarHeight(context), toolbarHeight: AppTheme.toolbarHeight(context),
leadingWidth: 24, leadingWidth: 24,
automaticallyImplyLeading: false, automaticallyImplyLeading: false,
flexibleSpace: Row( flexibleSpace: SizedBox(
children: [ height: 56,
AppBarLeadingButton.adaptive(context) ?? const Gap(8), child: Row(
const Gap(8), children: [
if (_userinfo != null) AppBarLeadingButton.adaptive(
AccountAvatar(content: _userinfo!.avatar, radius: 16), context,
const Gap(12), forceBack: true,
Expanded( ) ??
child: Column( const Gap(8),
mainAxisAlignment: MainAxisAlignment.center, const Gap(8),
crossAxisAlignment: CrossAxisAlignment.start, if (_userinfo != null)
children: [ AttachedCircleAvatar(
if (_userinfo != null) content: _userinfo!.avatar, radius: 16),
Text( const Gap(12),
_userinfo!.nick, Expanded(
style: Theme.of(context).textTheme.bodyLarge, child: Column(
), mainAxisAlignment: MainAxisAlignment.center,
if (_userinfo != null) crossAxisAlignment: CrossAxisAlignment.start,
Text( children: [
'@${_userinfo!.name}', if (_userinfo != null)
style: Theme.of(context).textTheme.bodySmall, Text(
), _userinfo!.nick,
], style: Theme.of(context).textTheme.bodyLarge,
), ),
), if (_userinfo != null)
if (_userinfo != null && _subscription == null) Text(
OutlinedButton( '@${_userinfo!.name}',
style: const ButtonStyle( style: Theme.of(context).textTheme.bodySmall,
visualDensity: ),
VisualDensity(horizontal: -4, vertical: -2), ],
), ),
onPressed: _isSubscribing ),
? null if (_userinfo != null && _subscription == null)
: () async { IconButton(
setState(() => _isSubscribing = true); style: const ButtonStyle(
_subscription = visualDensity:
await Get.find<SubscriptionProvider>() VisualDensity(horizontal: -4, vertical: -2),
.subscribeToUser(_userinfo!.id); ),
setState(() => _isSubscribing = false); onPressed: _isSubscribing ? null : _subscribeToUser,
}, icon: const Icon(Icons.add_circle_outline),
child: Text('subscribe'.tr), tooltip: 'subscribe'.tr,
) )
else if (_userinfo != null) else if (_userinfo != null)
OutlinedButton( IconButton(
style: const ButtonStyle( style: const ButtonStyle(
visualDensity: visualDensity:
VisualDensity(horizontal: -4, vertical: -2), VisualDensity(horizontal: -4, vertical: -2),
),
onPressed:
_isSubscribing ? null : _unsubscribeFromUser,
icon: const Icon(Icons.remove_circle_outline),
tooltip: 'unsubscribe'.tr,
), ),
onPressed: _isSubscribing if (_userinfo != null && _relationship == null)
? null IconButton(
: () async { icon: const Icon(Icons.person_add),
setState(() => _isSubscribing = true); onPressed: _isMakingFriend ? null : _makeFriend,
await Get.find<SubscriptionProvider>() tooltip: 'friendAdd'.tr,
.unsubscribeFromUser(_userinfo!.id); )
_subscription = null; else
setState(() => _isSubscribing = false); const IconButton(
}, icon: Icon(Icons.handshake),
child: Text('unsubscribe'.tr), onPressed: null,
),
SizedBox(
width: AppTheme.isLargeScreen(context) ? 8 : 16,
), ),
if (_userinfo != null && ],
!_relationshipProvider.hasFriend(_userinfo!)) ),
IconButton( ).paddingOnly(top: MediaQuery.of(context).padding.top),
icon: const Icon(Icons.person_add),
onPressed: _isMakingFriend
? null
: () async {
setState(() => _isMakingFriend = true);
try {
await _relationshipProvider
.makeFriend(widget.name);
context.showSnackbar(
'accountFriendRequestSent'.tr,
);
} catch (e) {
context.showErrorDialog(e);
} finally {
setState(() => _isMakingFriend = false);
}
},
)
else
const IconButton(
icon: Icon(Icons.handshake),
onPressed: null,
),
SizedBox(
width: AppTheme.isLargeScreen(context) ? 8 : 16,
),
],
),
bottom: TabBar( bottom: TabBar(
tabs: [ tabs: [
Tab(text: 'profilePage'.tr), Tab(text: 'profilePage'.tr),
@ -271,21 +332,205 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
body: TabBarView( body: TabBarView(
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
children: [ children: [
Column( ListView(
padding: const EdgeInsets.only(top: 16, bottom: 16),
children: [ children: [
const Gap(16), CenteredContainer(
AccountHeadingWidget( child: AccountHeadingWidget(
name: _userinfo!.name, name: _userinfo!.name,
nick: _userinfo!.nick, nick: _userinfo!.nick,
desc: _userinfo!.description, desc: _userinfo!.description,
badges: _userinfo!.badges, badges: _userinfo!.badges,
banner: _userinfo!.banner, banner: _userinfo!.banner,
avatar: _userinfo!.avatar, avatar: _userinfo!.avatar,
status: Get.find<StatusProvider>() status: Get.find<StatusProvider>()
.getSomeoneStatus(_userinfo!.name), .getSomeoneStatus(_userinfo!.name),
detail: _userinfo, detail: _userinfo,
profile: _userinfo!.profile, profile: _userinfo!.profile,
extraWidgets: const [], extraWidgets: [
if (_dailySignRecords.isNotEmpty)
Card(
child: SizedBox(
height: 180,
width:
max(640, MediaQuery.of(context).size.width),
child: LineChart(
LineChartData(
lineBarsData: [
LineChartBarData(
isCurved: true,
isStrokeCapRound: true,
isStrokeJoinRound: true,
color:
Theme.of(context).colorScheme.primary,
belowBarData: BarAreaData(
show: true,
gradient: LinearGradient(
colors: List.filled(
_dailySignRecords.length,
Theme.of(context)
.colorScheme
.primary
.withOpacity(0.3),
).toList(),
),
),
spots: _dailySignRecords
.map(
(x) => FlSpot(
x.createdAt
.copyWith(
hour: 0,
minute: 0,
second: 0,
millisecond: 0,
microsecond: 0,
)
.millisecondsSinceEpoch
.toDouble(),
x.resultTier.toDouble(),
),
)
.toList(),
)
],
lineTouchData: LineTouchData(
touchTooltipData: LineTouchTooltipData(
getTooltipItems: (spots) => spots
.map((spot) => LineTooltipItem(
'${DailySignHistoryChartDialog.signSymbols[spot.y.toInt()]}\n${DateFormat('MM/dd').format(DateTime.fromMillisecondsSinceEpoch(spot.x.toInt()))}',
TextStyle(
color: Theme.of(context)
.colorScheme
.onSurface,
),
))
.toList(),
getTooltipColor: (_) => Theme.of(context)
.colorScheme
.surfaceContainerHigh,
),
),
titlesData: FlTitlesData(
topTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
rightTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 40,
interval: 1,
getTitlesWidget: (value, _) => Align(
alignment: Alignment.centerRight,
child: Text(
DailySignHistoryChartDialog
.signSymbols[value.toInt()],
textAlign: TextAlign.right,
).paddingOnly(right: 8),
),
),
),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 28,
interval: 86400000,
getTitlesWidget: (value, _) => Text(
DateFormat('dd').format(
DateTime.fromMillisecondsSinceEpoch(
value.toInt(),
),
),
textAlign: TextAlign.center,
).paddingOnly(top: 8),
),
),
),
gridData: const FlGridData(show: false),
borderData: FlBorderData(show: false),
),
),
).marginOnly(
right: 24,
left: 12,
bottom: 8,
top: 24,
),
)
],
appendWidgets: [
Card(
child: Container(
padding: const EdgeInsets.symmetric(
vertical: 4,
horizontal: 8,
),
width: double.maxFinite,
child: Wrap(
alignment: WrapAlignment.spaceAround,
children: [
TextButton.icon(
style: const ButtonStyle(
visualDensity: VisualDensity(
horizontal: -4,
vertical: -2,
),
),
onPressed: () {
showDialog(
context: context,
builder: (context) => AbuseReportDialog(
resourceId: 'user:${_userinfo!.id}',
),
);
},
icon: const Icon(
Icons.flag,
size: 16,
),
label: Text('reportAbuse'.tr),
),
if (_relationship?.status != 2)
TextButton.icon(
style: const ButtonStyle(
visualDensity: VisualDensity(
horizontal: -4,
vertical: -2,
),
),
onPressed:
_isMakingFriend ? null : _blockUser,
icon: const Icon(
Icons.block,
size: 16,
),
label: Text('blockUser'.tr),
)
else
TextButton.icon(
style: const ButtonStyle(
visualDensity: VisualDensity(
horizontal: -4,
vertical: -2,
),
),
onPressed:
_isMakingFriend ? null : _unblockUser,
icon: const Icon(
Icons.add_circle_outline,
size: 16,
),
label: Text('unblockUser'.tr),
),
],
),
),
),
],
),
), ),
], ],
), ),
@ -301,7 +546,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceAround, mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [ children: [
_buildStatisticsEntry( _StatsWidget(
'totalSocialCreditPoints'.tr, 'totalSocialCreditPoints'.tr,
_userinfo != null _userinfo != null
? _userSocialCreditPoints.toString() ? _userSocialCreditPoints.toString()
@ -314,16 +559,16 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
mainAxisAlignment: MainAxisAlignment.spaceAround, mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [ children: [
Obx( Obx(
() => _buildStatisticsEntry( () => _StatsWidget(
'totalPostCount'.tr, 'totalPostCount'.tr,
_postController.postTotal.value.toString(), _postController.postTotal.value.toString(),
), ),
), ),
_buildStatisticsEntry( _StatsWidget(
'totalUpvote'.tr, 'totalUpvote'.tr,
_totalUpvote.toString(), _totalUpvote.toString(),
), ),
_buildStatisticsEntry( _StatsWidget(
'totalDownvote'.tr, 'totalDownvote'.tr,
_totalDownvote.toString(), _totalDownvote.toString(),
), ),
@ -364,7 +609,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(),
@ -373,8 +618,9 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
), ),
CenteredContainer( CenteredContainer(
child: RefreshIndicator( child: RefreshIndicator(
onRefresh: () => onRefresh: () => Future.sync(
Future.sync(() => _albumPagingController.refresh()), () => _albumPagingController.refresh(),
),
child: PagedGridView<int, Attachment>( child: PagedGridView<int, Attachment>(
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
pagingController: _albumPagingController, pagingController: _albumPagingController,
@ -420,3 +666,28 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
); );
} }
} }
class _StatsWidget extends StatelessWidget {
final String label;
final String content;
const _StatsWidget(this.label, this.content);
@override
Widget build(BuildContext context) {
return Expanded(
child: Column(
children: [
Text(
label,
style: Theme.of(context).textTheme.bodySmall,
),
Text(
content,
style: Theme.of(context).textTheme.bodyLarge,
),
],
),
);
}
}

View File

@ -7,12 +7,12 @@ 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';
import 'package:solian/services.dart'; import 'package:solian/services.dart';
import 'package:solian/widgets/sized_container.dart'; import 'package:solian/widgets/sized_container.dart';
import 'package:url_launcher/url_launcher_string.dart';
class SignInScreen extends StatefulWidget { class SignInScreen extends StatefulWidget {
const SignInScreen({super.key}); const SignInScreen({super.key});
@ -167,7 +167,6 @@ class _SignInScreenState extends State<SignInScreen> {
final result = AuthResult.fromJson(resp.body); final result = AuthResult.fromJson(resp.body);
_currentTicket = result.ticket; _currentTicket = result.ticket;
_passwordController.clear();
// Finish sign in if possible // Finish sign in if possible
if (result.isFinished) { if (result.isFinished) {
@ -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();
@ -185,11 +183,13 @@ class _SignInScreenState extends State<SignInScreen> {
autoStartBackgroundNotificationService(); autoStartBackgroundNotificationService();
Navigator.pop(context, true); Navigator.pop(context, true);
_passwordController.clear();
}); });
} else { } else {
// Skip the first step // Skip the first step
_factorPicked = null; _factorPicked = null;
_factorPickedType = null; _factorPickedType = null;
_passwordController.clear();
setState(() => _period += 2); setState(() => _period += 2);
} }
} catch (e) { } catch (e) {
@ -210,17 +210,16 @@ class _SignInScreenState extends State<SignInScreen> {
case 2: case 2:
_passwordController.clear(); _passwordController.clear();
_factorPickedType = null; _factorPickedType = null;
default:
setState(() => _period--);
} }
setState(() => _period--);
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Material( return CenteredContainer(
color: Theme.of(context).colorScheme.surface, maxWidth: 360,
child: CenteredContainer( child: Theme(
maxWidth: 360, data: Theme.of(context).copyWith(canvasColor: Colors.transparent),
child: PageTransitionSwitcher( child: PageTransitionSwitcher(
transitionBuilder: ( transitionBuilder: (
Widget child, Widget child,
@ -235,16 +234,18 @@ class _SignInScreenState extends State<SignInScreen> {
); );
}, },
child: switch (_period % 3) { child: switch (_period % 3) {
1 => Column( 1 => ListView(
shrinkWrap: true,
key: const ValueKey<int>(1), key: const ValueKey<int>(1),
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
ClipRRect( Align(
borderRadius: const BorderRadius.all(Radius.circular(8)), alignment: Alignment.centerLeft,
child: child: ClipRRect(
Image.asset('assets/logo.png', width: 64, height: 64), borderRadius: const BorderRadius.all(Radius.circular(8)),
).paddingOnly(bottom: 8, left: 4), child:
Image.asset('assets/logo.png', width: 64, height: 64),
).paddingOnly(bottom: 8, left: 4),
),
Text( Text(
'signinPickFactor'.tr, 'signinPickFactor'.tr,
style: const TextStyle( style: const TextStyle(
@ -323,16 +324,18 @@ class _SignInScreenState extends State<SignInScreen> {
), ),
], ],
), ),
2 => Column( 2 => ListView(
key: const ValueKey<int>(2), key: const ValueKey<int>(2),
mainAxisAlignment: MainAxisAlignment.center, shrinkWrap: true,
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
ClipRRect( Align(
borderRadius: const BorderRadius.all(Radius.circular(8)), alignment: Alignment.centerLeft,
child: child: ClipRRect(
Image.asset('assets/logo.png', width: 64, height: 64), borderRadius: const BorderRadius.all(Radius.circular(8)),
).paddingOnly(bottom: 8, left: 4), child:
Image.asset('assets/logo.png', width: 64, height: 64),
).paddingOnly(bottom: 8, left: 4),
),
Text( Text(
'signinEnterPassword'.tr, 'signinEnterPassword'.tr,
style: const TextStyle( style: const TextStyle(
@ -396,16 +399,18 @@ class _SignInScreenState extends State<SignInScreen> {
), ),
], ],
), ),
_ => Column( _ => ListView(
key: const ValueKey<int>(0), key: const ValueKey<int>(0),
mainAxisAlignment: MainAxisAlignment.center, shrinkWrap: true,
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
ClipRRect( Align(
borderRadius: const BorderRadius.all(Radius.circular(8)), alignment: Alignment.centerLeft,
child: child: ClipRRect(
Image.asset('assets/logo.png', width: 64, height: 64), borderRadius: const BorderRadius.all(Radius.circular(8)),
).paddingOnly(bottom: 8, left: 4), child:
Image.asset('assets/logo.png', width: 64, height: 64),
).paddingOnly(bottom: 8, left: 4),
),
Text( Text(
'signinGreeting'.tr, 'signinGreeting'.tr,
style: const TextStyle( style: const TextStyle(
@ -451,11 +456,50 @@ class _SignInScreenState extends State<SignInScreen> {
), ),
], ],
), ),
const Gap(12),
Align(
alignment: Alignment.centerRight,
child: Container(
constraints: const BoxConstraints(maxWidth: 290),
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
'termAcceptNextWithAgree'.tr,
textAlign: TextAlign.end,
style:
Theme.of(context).textTheme.bodySmall!.copyWith(
color: Theme.of(context)
.colorScheme
.onSurface
.withOpacity(0.75),
),
),
Material(
color: Colors.transparent,
child: InkWell(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text('termAcceptLink'.tr),
const Gap(4),
const Icon(Icons.launch, size: 14),
],
),
onTap: () {
launchUrlString('https://solsynth.dev/terms');
},
),
),
],
),
).paddingSymmetric(horizontal: 16),
),
], ],
), ),
}, },
), ),
), ).paddingAll(24),
); );
} }
} }

View File

@ -4,6 +4,7 @@ import 'package:get/get.dart';
import 'package:solian/exts.dart'; import 'package:solian/exts.dart';
import 'package:solian/services.dart'; import 'package:solian/services.dart';
import 'package:solian/widgets/sized_container.dart'; import 'package:solian/widgets/sized_container.dart';
import 'package:url_launcher/url_launcher_string.dart';
class SignUpScreen extends StatefulWidget { class SignUpScreen extends StatefulWidget {
const SignUpScreen({super.key}); const SignUpScreen({super.key});
@ -18,7 +19,7 @@ class _SignUpScreenState extends State<SignUpScreen> {
final _nicknameController = TextEditingController(); final _nicknameController = TextEditingController();
final _passwordController = TextEditingController(); final _passwordController = TextEditingController();
void performAction(BuildContext context) async { void _performAction(BuildContext context) async {
final email = _emailController.value.text; final email = _emailController.value.text;
final username = _usernameController.value.text; final username = _usernameController.value.text;
final nickname = _nicknameController.value.text; final nickname = _nicknameController.value.text;
@ -60,101 +61,146 @@ class _SignUpScreenState extends State<SignUpScreen> {
} }
} }
bool _isTermAccepted = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Material( return CenteredContainer(
color: Theme.of(context).colorScheme.surface, maxWidth: 360,
child: CenteredContainer( child: ListView(
maxWidth: 360, shrinkWrap: true,
child: Column( children: [
mainAxisSize: MainAxisSize.min, Align(
crossAxisAlignment: CrossAxisAlignment.start, alignment: Alignment.centerLeft,
children: [ child: ClipRRect(
ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)), borderRadius: const BorderRadius.all(Radius.circular(8)),
child: Image.asset('assets/logo.png', width: 64, height: 64), child: Image.asset('assets/logo.png', width: 64, height: 64),
).paddingOnly(bottom: 8, left: 4), ).paddingOnly(bottom: 8, left: 4),
Text( ),
'signupGreeting'.tr, Text(
style: const TextStyle( 'signupGreeting'.tr,
fontSize: 28, style: const TextStyle(
fontWeight: FontWeight.w900, fontSize: 28,
), fontWeight: FontWeight.w900,
).paddingOnly(left: 4, bottom: 16),
TextField(
autocorrect: false,
enableSuggestions: false,
controller: _usernameController,
autofillHints: const [AutofillHints.username],
decoration: InputDecoration(
isDense: true,
border: const OutlineInputBorder(),
labelText: 'username'.tr,
),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
), ),
const Gap(12), ).paddingOnly(left: 4, bottom: 16),
TextField( TextField(
autocorrect: false, autocorrect: false,
enableSuggestions: false, enableSuggestions: false,
controller: _nicknameController, controller: _usernameController,
autofillHints: const [AutofillHints.nickname], autofillHints: const [AutofillHints.username],
decoration: InputDecoration( decoration: InputDecoration(
isDense: true, isDense: true,
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
labelText: 'nickname'.tr, labelText: 'username'.tr,
),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
), ),
const Gap(12), onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
TextField( ),
autocorrect: false, const Gap(12),
enableSuggestions: false, TextField(
controller: _emailController, autocorrect: false,
autofillHints: const [AutofillHints.email], enableSuggestions: false,
decoration: InputDecoration( controller: _nicknameController,
isDense: true, autofillHints: const [AutofillHints.nickname],
border: const OutlineInputBorder(), decoration: InputDecoration(
labelText: 'email'.tr, isDense: true,
), border: const OutlineInputBorder(),
onTapOutside: (_) => labelText: 'nickname'.tr,
FocusManager.instance.primaryFocus?.unfocus(),
), ),
const Gap(12), onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
TextField( ),
obscureText: true, const Gap(12),
autocorrect: false, TextField(
enableSuggestions: false, autocorrect: false,
autofillHints: const [AutofillHints.password], enableSuggestions: false,
controller: _passwordController, controller: _emailController,
decoration: InputDecoration( autofillHints: const [AutofillHints.email],
isDense: true, decoration: InputDecoration(
border: const OutlineInputBorder(), isDense: true,
labelText: 'password'.tr, border: const OutlineInputBorder(),
), labelText: 'email'.tr,
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
onSubmitted: (_) => performAction(context),
), ),
const Gap(16), onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
Align( ),
alignment: Alignment.centerRight, const Gap(12),
child: TextButton( TextField(
child: Row( obscureText: true,
mainAxisSize: MainAxisSize.min, autocorrect: false,
children: [ enableSuggestions: false,
Text('next'.tr), autofillHints: const [AutofillHints.password],
const Icon(Icons.chevron_right), controller: _passwordController,
], decoration: InputDecoration(
), isDense: true,
onPressed: () => performAction(context), border: const OutlineInputBorder(),
labelText: 'password'.tr,
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
onSubmitted: (_) => _performAction(context),
),
const Gap(8),
CheckboxListTile(
value: _isTermAccepted,
title: Text(
'termAccept'.tr,
style: const TextStyle(height: 1.2),
).paddingOnly(bottom: 4),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(8),
), ),
) ),
], subtitle: RichText(
), text: TextSpan(
), style: Theme.of(context).textTheme.bodySmall!.copyWith(
color: Theme.of(context)
.colorScheme
.onSurface
.withOpacity(0.75),
),
children: [
TextSpan(text: 'termAcceptDesc'.tr),
WidgetSpan(
child: Material(
color: Colors.transparent,
child: InkWell(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text('termAcceptLink'.tr),
const Gap(4),
const Icon(Icons.launch, size: 14),
],
),
onTap: () {
launchUrlString('https://solsynth.dev/terms');
},
),
),
),
],
),
),
onChanged: (value) {
setState(() => _isTermAccepted = value ?? false);
},
),
const Gap(16),
Align(
alignment: Alignment.centerRight,
child: TextButton(
onPressed:
!_isTermAccepted ? null : () => _performAction(context),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text('next'.tr),
const Icon(Icons.chevron_right),
],
),
),
)
],
).paddingAll(24),
); );
} }
} }

View File

@ -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

View File

@ -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,10 +193,13 @@ class _ChannelChatScreenState extends State<ChannelChatScreen>
_chatController = ChatEventController(); _chatController = ChatEventController();
_chatController.initialize(); _chatController.initialize();
_getOngoingCall(); SharedPreferences.getInstance().then((inst) {
_getChannel().then((_) { _prefs = inst;
_chatController.getInitialEvents(_channel!, widget.realm); _getOngoingCall();
_listenMessages(); _getChannel().then((_) {
_chatController.getInitialEvents(_channel!, widget.realm);
_listenMessages();
});
}); });
} }
@ -201,151 +208,159 @@ 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).firstOrNull;
_channel!.members!.where((e) => e.account.id != _accountId).first;
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(
appBar: AppBar( child: Scaffold(
leading: AppBarLeadingButton.adaptive(context), appBar: AppBar(
title: AppBarTitle(title), leading: AppBarLeadingButton.adaptive(context),
centerTitle: false, title: AppBarTitle(title),
titleSpacing: AppTheme.titleSpacing(context), centerTitle: false,
toolbarHeight: AppTheme.toolbarHeight(context), titleSpacing: AppTheme.titleSpacing(context),
actions: [ toolbarHeight: AppTheme.toolbarHeight(context),
const BackgroundStateWidget(), actions: [
Builder(builder: (context) { const BackgroundStateWidget(),
if (_isBusy || _channel == null) return const SizedBox.shrink(); Builder(builder: (context) {
if (_isBusy || _channel == null) return const SizedBox.shrink();
return ChatCallButton( return ChatCallButton(
realm: _channel!.realm, realm: _channel!.realm,
channel: _channel!, channel: _channel!,
ongoingCall: _ongoingCall, ongoingCall: _ongoingCall,
); );
}), }),
IconButton( IconButton(
icon: const Icon(Icons.more_vert), icon: const Icon(Icons.more_vert),
onPressed: () { onPressed: () {
if (_channel == null) return; if (_channel == null) return;
AppRouter.instance AppRouter.instance
.pushNamed( .pushNamed(
'channelDetail', 'channelDetail',
pathParameters: {'alias': widget.alias}, pathParameters: {'alias': widget.alias},
queryParameters: {'realm': widget.realm}, queryParameters: {'realm': widget.realm},
extra: ChannelDetailArguments( extra: ChannelDetailArguments(
profile: _channelProfile!, profile: _channelProfile!,
channel: _channel!, channel: _channel!,
),
)
.then((value) {
if (value == false) AppRouter.instance.pop();
if (value != null) {
final resp = Channel.fromJson(value as Map<String, dynamic>);
_getChannel(alias: resp.alias);
}
});
},
),
SizedBox(
width: AppTheme.isLargeScreen(context) ? 8 : 16,
),
],
),
body: Builder(builder: (context) {
if (_isBusy || _channel == null) {
return const Center(
child: CircularProgressIndicator(),
);
}
return Row(
children: [
Expanded(
child: Column(
children: [
if (_ongoingCall != null)
ChannelCallIndicator(
channel: _channel!,
ongoingCall: _ongoingCall!,
onJoin: () {
if (!AppTheme.isLargeScreen(context)) {
final ChatCallProvider call = Get.find();
call.gotoScreen(context);
}
},
),
Expanded(
child: ChatEventList(
scope: widget.realm,
channel: _channel!,
chatController: _chatController,
onEdit: (item) {
setState(() => _messageToEditing = item);
},
onReply: (item) {
setState(() => _messageToReplying = item);
},
),
), ),
ClipRect( )
child: BackdropFilter( .then((value) {
filter: ImageFilter.blur(sigmaX: 50, sigmaY: 50), if (value == false) AppRouter.instance.pop();
child: SafeArea( if (value != null) {
child: Column( final resp =
children: [ Channel.fromJson(value as Map<String, dynamic>);
ChatTypingIndicator(users: _typingUsers), _getChannel(alias: resp.alias);
ChatMessageInput( }
edit: _messageToEditing, });
reply: _messageToReplying, },
realm: widget.realm, ),
placeholder: placeholder, SizedBox(
channel: _channel!, width: AppTheme.isLargeScreen(context) ? 8 : 16,
onSent: (Event item) { ),
setState(() { ],
_chatController.addPendingEvent(item); ),
}); body: Builder(builder: (context) {
}, if (_isBusy || _channel == null) {
onReset: () { return const Center(
setState(() { child: CircularProgressIndicator(),
_messageToReplying = null; );
_messageToEditing = null; }
});
}, return Row(
), children: [
], Expanded(
child: Column(
children: [
if (_ongoingCall != null)
ChannelCallIndicator(
channel: _channel!,
ongoingCall: _ongoingCall!,
onJoin: () {
if (!AppTheme.isUltraLargeScreen(context)) {
final ChatCallProvider call = Get.find();
call.gotoScreen(context);
}
},
),
Expanded(
child: ChatEventList(
noAnimated:
_prefs.getBool('non_animated_message_list') ??
false,
scope: widget.realm,
channel: _channel!,
chatController: _chatController,
onEdit: (item) {
setState(() => _messageToEditing = item);
},
onReply: (item) {
setState(() => _messageToReplying = item);
},
),
),
ClipRect(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 50, sigmaY: 50),
child: SafeArea(
child: Column(
children: [
ChatTypingIndicator(users: _typingUsers),
ChatMessageInput(
edit: _messageToEditing,
reply: _messageToReplying,
realm: widget.realm,
placeholder: placeholder,
channel: _channel!,
onSent: (Event item) {
setState(() {
_chatController.addPendingEvent(item);
});
},
onReset: () {
setState(() {
_messageToReplying = null;
_messageToEditing = null;
});
},
),
],
),
), ),
), ),
), ),
), ],
], ),
), ),
), Obx(() {
Obx(() { final ChatCallProvider call = Get.find();
final ChatCallProvider call = Get.find(); if (call.isMounted.value &&
if (call.isMounted.value && AppTheme.isLargeScreen(context)) { 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),
Expanded( Expanded(
child: CallScreen( child: CallScreen(
hideAppBar: true, hideAppBar: true,
isExpandable: true, isExpandable: true,
),
), ),
), ]),
]), );
); }
} return const SizedBox.shrink();
return const SizedBox.shrink(); }),
}), ],
], );
); }),
}), ),
); );
} }

View File

@ -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),

View File

@ -1,145 +1,346 @@
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/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(
child: Scaffold( length: 2 + realms.availableRealms.length,
appBar: AppBar( child: ResponsiveRootContainer(
leading: AppBarLeadingButton.adaptive(context), child: Scaffold(
title: AppBarTitle('chat'.tr), appBar: AppBar(
centerTitle: true, leading: AppBarLeadingButton.adaptive(context),
toolbarHeight: AppTheme.toolbarHeight(context), title: AppBarTitle('chat'.tr),
actions: [ centerTitle: true,
const BackgroundStateWidget(), toolbarHeight: AppTheme.toolbarHeight(context),
const NotificationButton(), actions: [
PopupMenuButton( const BackgroundStateWidget(),
icon: const Icon(Icons.add_circle), const NotificationButton(),
itemBuilder: (BuildContext context) => [ PopupMenuButton(
PopupMenuItem( icon: const Icon(Icons.add_circle),
child: ListTile( itemBuilder: (BuildContext context) => [
title: Text('channelOrganizeCommon'.tr), PopupMenuItem(
leading: const Icon(Icons.tag), child: ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 8), title: Text('channelOrganizeCommon'.tr),
), leading: const Icon(Icons.tag),
onTap: () { contentPadding:
AppRouter.instance.pushNamed('channelOrganizing').then( const EdgeInsets.symmetric(horizontal: 8),
(value) { ),
if (value != null) { onTap: () {
_channels.refreshAvailableChannel(); AppRouter.instance.pushNamed('channelOrganizing').then(
} (value) {
if (value != null) {
_loadAllChannels();
}
},
);
}, },
);
},
),
PopupMenuItem(
child: ListTile(
title: Text('channelOrganizeDirect'.tr),
leading: const FaIcon(
FontAwesomeIcons.userGroup,
size: 16,
), ),
contentPadding: const EdgeInsets.symmetric(horizontal: 8), PopupMenuItem(
), child: ListTile(
onTap: () { title: Text('channelOrganizeDirect'.tr),
final ChannelProvider channels = Get.find(); leading: const FaIcon(
channels FontAwesomeIcons.userGroup,
.createDirectChannel(context, 'global') size: 16,
.then((resp) { ),
if (resp != null) { contentPadding:
_channels.refreshAvailableChannel(); const EdgeInsets.symmetric(horizontal: 8),
} ),
}).catchError((e) { onTap: () {
context.showErrorDialog(e); final ChannelProvider channels = Get.find();
}); channels
}, .createDirectChannel(context, 'global')
.then((resp) {
if (resp != null) {
_loadAllChannels();
}
}).catchError((e) {
context.showErrorDialog(e);
});
},
),
],
),
SizedBox(
width: AppTheme.isLargeScreen(context) ? 8 : 16,
), ),
], ],
), bottom: TabBar(
SizedBox( isScrollable: true,
width: AppTheme.isLargeScreen(context) ? 8 : 16, dividerHeight: 0.3,
), tabAlignment: TabAlignment.startOffset,
], tabs: [
), Tab(
body: Obx(() { child: Row(
if (auth.isAuthorized.isFalse) { mainAxisSize: MainAxisSize.min,
return SigninRequiredOverlay( children: [
onDone: () => _channels.refreshAvailableChannel(), CircleAvatar(
); radius: 14,
} backgroundColor:
Theme.of(context).colorScheme.primary,
final selfId = auth.userProfile.value!['id']; child: const Icon(
Icons.forum,
return Column( size: 16,
children: [ color: Colors.white,
Obx(() { ),
if (_channels.isLoading.isFalse) { ),
return const SizedBox.shrink(); const Gap(8),
} else { Text('all'.tr),
return const LinearProgressIndicator(); ],
}
}),
const ChatCallCurrentIndicator(),
Expanded(
child: CenteredContainer(
child: RefreshIndicator(
onRefresh: _channels.refreshAvailableChannel,
child: Obx(
() => ChannelListWidget(
noCategory: true,
channels: List.from([
..._channels.groupChannels
.where((x) => x.realmId == null),
..._channels.directChannels
]),
selfId: selfId,
useReplace: true,
),
), ),
), ),
), Tab(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const CircleAvatar(
radius: 14,
child: Icon(
Icons.chat_bubble,
size: 16,
),
),
const Gap(8),
Text('channelTypeDirect'.tr),
],
),
),
...realms.availableRealms.map((x) => Tab(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
AttachedCircleAvatar(
content: x.avatar,
radius: 14,
fallbackWidget: const Icon(
Icons.workspaces,
size: 16,
),
),
const Gap(8),
Text(x.name),
],
),
)),
],
), ),
], ),
); body: Obx(() {
}), if (auth.isAuthorized.isFalse) {
return SigninRequiredOverlay(
onDone: () => _loadAllChannels(),
);
}
final selfId = auth.userProfile.value!['id'];
return Column(
children: [
const ChatCallCurrentIndicator(),
if (_isBusy)
Container(
color: Theme.of(context)
.colorScheme
.surfaceContainerLow
.withOpacity(0.8),
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)
],
).paddingSymmetric(vertical: 8),
),
Expanded(
child: TabBarView(
children: [
RefreshIndicator(
onRefresh: _loadNormalChannels,
child: ChannelListWidget(
channels: _sortChannels([
..._normalChannels,
..._directChannels,
..._realmChannels.values.expand((x) => x),
]),
selfId: selfId,
useReplace: AppTheme.isLargeScreen(context),
),
),
RefreshIndicator(
onRefresh: _loadDirectChannels,
child: ChannelListWidget(
channels: _directChannels,
selfId: selfId,
useReplace: AppTheme.isLargeScreen(context),
),
),
...realms.availableRealms.map(
(x) => RefreshIndicator(
onRefresh: () => _loadRealmChannels(x.alias),
child: ChannelListWidget(
channels: _realmChannels[x.alias] ?? [],
selfId: selfId,
useReplace: AppTheme.isLargeScreen(context),
),
),
),
],
),
),
],
);
}),
),
),
), ),
); );
} }

View File

@ -75,10 +75,12 @@ 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;
setState(() { if (mounted) {
_currentMessages = out.$1; setState(() {
_currentMessagesCount = out.$2; _currentMessages = out.$1;
}); _currentMessagesCount = out.$2;
});
}
} }
bool _signingDaily = true; bool _signingDaily = true;
@ -88,14 +90,18 @@ class _DashboardScreenState extends State<DashboardScreen> {
Future<void> _pullDaily() async { Future<void> _pullDaily() async {
try { try {
_signRecord = await _dailySign.getToday(); _signRecord = await _dailySign.getToday();
_dailySign.listLastRecord(30).then((value) { _dailySign.listLastRecord(14).then((value) {
setState(() => _signRecordHistory = value); if (mounted) {
setState(() => _signRecordHistory = value);
}
}); });
} catch (e) { } catch (e) {
context.showErrorDialog(e); context.showErrorDialog(e);
} }
setState(() => _signingDaily = false); if (mounted) {
setState(() => _signingDaily = false);
}
} }
Future<void> _signDaily() async { Future<void> _signDaily() async {
@ -103,7 +109,7 @@ class _DashboardScreenState extends State<DashboardScreen> {
try { try {
_signRecord = await _dailySign.signToday(); _signRecord = await _dailySign.signToday();
_dailySign.listLastRecord(30).then((value) { _dailySign.listLastRecord(14).then((value) {
setState(() => _signRecordHistory = value); setState(() => _signRecordHistory = value);
}); });
} catch (e) { } catch (e) {
@ -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');
}, },
), ),
], ],
@ -386,7 +392,7 @@ class _DashboardScreenState extends State<DashboardScreen> {
backgroundColor: Theme.of(context) backgroundColor: Theme.of(context)
.colorScheme .colorScheme
.surfaceContainerLow, .surfaceContainerLow,
), ).paddingAll(8),
), ),
), ),
).paddingSymmetric(horizontal: 8), ).paddingSymmetric(horizontal: 8),

View File

@ -6,24 +6,25 @@ import 'package:get/get.dart';
import 'package:solian/controllers/post_list_controller.dart'; import 'package:solian/controllers/post_list_controller.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
import 'package:solian/providers/navigation.dart'; import 'package:solian/providers/navigation.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/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 FeedScreen extends StatefulWidget { class ExploreScreen extends StatefulWidget {
const FeedScreen({super.key}); const ExploreScreen({super.key});
@override @override
State<FeedScreen> createState() => _FeedScreenState(); State<ExploreScreen> createState() => _ExploreScreenState();
} }
class _FeedScreenState extends State<FeedScreen> class _ExploreScreenState extends State<ExploreScreen>
with SingleTickerProviderStateMixin { with SingleTickerProviderStateMixin {
late final PostListController _postController; late final PostListController _postController;
late final TabController _tabController; late final TabController _tabController;
@ -55,10 +56,8 @@ class _FeedScreenState extends State<FeedScreen>
@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 +80,85 @@ class _FeedScreenState extends State<FeedScreen>
body: NestedScrollView( body: NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return [ return [
SliverAppBar( SliverLayoutBuilder(
title: AppBarTitle('feed'.tr), builder: (context, constraints) {
centerTitle: false, final scrollOffset = constraints.scrollOffset;
floating: true, final colorChangeOffset = 120;
toolbarHeight: AppTheme.toolbarHeight(context),
leading: AppBarLeadingButton.adaptive(context), final scrollProgress =
actions: [ (scrollOffset / colorChangeOffset).clamp(0.0, 1.0);
const BackgroundStateWidget(), final backgroundColor = Color.lerp(
const NotificationButton(), Theme.of(context)
SizedBox( .colorScheme
width: AppTheme.isLargeScreen(context) ? 8 : 16, .surfaceContainerLow
), .withOpacity(0),
], Theme.of(context)
bottom: TabBar( .colorScheme
controller: _tabController, .surfaceContainerLow
tabs: [ .withOpacity(0.9),
Tab(text: 'postListNews'.tr), scrollProgress,
Tab(text: 'postListFriends'.tr), );
Tab(text: 'postListShuffle'.tr),
], return SliverAppBar(
), backgroundColor: backgroundColor,
flexibleSpace: SizedBox(
height: 48,
child: const Row(
children: [
RealmSwitcher(),
],
).paddingSymmetric(horizontal: 8),
).paddingOnly(top: MediaQuery.of(context).padding.top),
snap: true,
floating: true,
toolbarHeight: AppTheme.toolbarHeight(context),
leading: AppBarLeadingButton.adaptive(context),
actions: [
const BackgroundStateWidget(),
const NotificationButton(),
SizedBox(
width: AppTheme.isLargeScreen(context) ? 8 : 16,
),
],
bottom: TabBar(
controller: _tabController,
dividerHeight: 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),
],
),
),
],
),
);
},
) )
]; ];
}, },
@ -114,16 +171,6 @@ class _FeedScreenState extends State<FeedScreen>
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 +179,7 @@ class _FeedScreenState extends State<FeedScreen>
RefreshIndicator( RefreshIndicator(
onRefresh: () => _postController.reloadAllOver(), onRefresh: () => _postController.reloadAllOver(),
child: CustomScrollView(slivers: [ child: CustomScrollView(slivers: [
PostWarpedListWidget( ControlledPostListWidget(
controller: _postController.pagingController, controller: _postController.pagingController,
onUpdate: () => _postController.reloadAllOver(), onUpdate: () => _postController.reloadAllOver(),
), ),
@ -143,7 +190,7 @@ class _FeedScreenState extends State<FeedScreen>
return RefreshIndicator( return RefreshIndicator(
onRefresh: () => _postController.reloadAllOver(), onRefresh: () => _postController.reloadAllOver(),
child: CustomScrollView(slivers: [ child: CustomScrollView(slivers: [
PostWarpedListWidget( ControlledPostListWidget(
controller: _postController.pagingController, controller: _postController.pagingController,
onUpdate: () => _postController.reloadAllOver(), onUpdate: () => _postController.reloadAllOver(),
), ),
@ -178,106 +225,3 @@ class _FeedScreenState extends State<FeedScreen>
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),
),
],
),
);
}
}

View File

@ -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),

View File

@ -3,7 +3,7 @@ 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/pagination.dart'; import 'package:solian/models/pagination.dart';
import 'package:solian/providers/content/posts.dart'; import 'package:solian/providers/content/posts.dart';
import 'package:solian/widgets/posts/post_warped_list.dart'; import 'package:solian/widgets/posts/post_list.dart';
import '../../models/post.dart'; import '../../models/post.dart';
@ -63,13 +63,13 @@ class _FeedSearchScreenState extends State<FeedSearchScreen> {
ListTile( ListTile(
leading: const Icon(Icons.label), leading: const Icon(Icons.label),
tileColor: Theme.of(context).colorScheme.surfaceContainer, tileColor: Theme.of(context).colorScheme.surfaceContainer,
title: Text('feedSearchWithTag'.trParams({'key': widget.tag!})), title: Text('postSearchWithTag'.trParams({'key': widget.tag!})),
), ),
if (widget.category != null) if (widget.category != null)
ListTile( ListTile(
leading: const Icon(Icons.category), leading: const Icon(Icons.category),
tileColor: Theme.of(context).colorScheme.surfaceContainer, tileColor: Theme.of(context).colorScheme.surfaceContainer,
title: Text('feedSearchWithCategory' title: Text('postSearchWithCategory'
.trParams({'key': widget.category!})), .trParams({'key': widget.category!})),
), ),
Expanded( Expanded(
@ -77,7 +77,7 @@ class _FeedSearchScreenState extends State<FeedSearchScreen> {
onRefresh: () => Future.sync(() => _pagingController.refresh()), onRefresh: () => Future.sync(() => _pagingController.refresh()),
child: CustomScrollView( child: CustomScrollView(
slivers: [ slivers: [
PostWarpedListWidget( ControlledPostListWidget(
controller: _pagingController, controller: _pagingController,
onUpdate: () => _pagingController.refresh(), onUpdate: () => _pagingController.refresh(),
), ),

View File

@ -24,11 +24,11 @@ class PostDetailScreen extends StatefulWidget {
class _PostDetailScreenState extends State<PostDetailScreen> { class _PostDetailScreenState extends State<PostDetailScreen> {
Post? item; Post? item;
Future<Post?> getDetail() async { Future<Post?> _getDetail() async {
if (widget.post != null) { if (widget.post != null) {
item = widget.post; setState(() {
Get.find<LastReadProvider>().feedLastReadAt = item?.id; item = widget.post;
return widget.post; });
} }
final PostProvider provider = Get.find(); final PostProvider provider = Get.find();
@ -47,51 +47,48 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Material( return FutureBuilder(
color: Theme.of(context).colorScheme.surface, future: _getDetail(),
child: FutureBuilder( builder: (context, snapshot) {
future: getDetail(), if (!snapshot.hasData || snapshot.data == null) {
builder: (context, snapshot) { return const Center(
if (!snapshot.hasData || snapshot.data == null) { child: CircularProgressIndicator(),
return const Center(
child: CircularProgressIndicator(),
);
}
return CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: PostItem(
item: item!,
isClickable: false,
isOverrideEmbedClickable: true,
isFullDate: true,
isFullContent: true,
isShowReply: false,
isContentSelectable: true,
),
),
SliverToBoxAdapter(
child: const Divider(thickness: 0.3, height: 1)
.paddingOnly(top: 4),
),
SliverToBoxAdapter(
child: Align(
alignment: Alignment.centerLeft,
child: Text(
'postReplies'.tr,
style: Theme.of(context).textTheme.headlineSmall,
).paddingOnly(left: 24, right: 24, top: 16),
),
),
PostReplyList(item: item!),
SliverToBoxAdapter(
child: SizedBox(height: MediaQuery.of(context).padding.bottom),
),
],
); );
}, }
),
return CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: PostItem(
item: item!,
isClickable: false,
isOverrideEmbedClickable: true,
isFullDate: true,
isFullContent: true,
isShowReply: false,
isContentSelectable: true,
),
),
SliverToBoxAdapter(
child:
const Divider(thickness: 0.3, height: 1).paddingOnly(top: 4),
),
SliverToBoxAdapter(
child: Align(
alignment: Alignment.centerLeft,
child: Text(
'postReplies'.tr,
style: Theme.of(context).textTheme.headlineSmall,
).paddingOnly(left: 24, right: 24, top: 16),
),
),
PostReplyList(item: item!),
SliverToBoxAdapter(
child: SizedBox(height: MediaQuery.of(context).padding.bottom),
),
],
);
},
); );
} }
} }

View File

@ -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),
@ -273,115 +273,69 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
), ),
if (_isBusy) const LinearProgressIndicator().animate().scaleX(), if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
Expanded( Expanded(
child: Row( child: DefaultTabController(
crossAxisAlignment: CrossAxisAlignment.start, length: 2,
children: [ child: AppTheme.isLargeScreen(context)
Expanded( ? Row(
child: Column( crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Expanded( Expanded(
child: ListView( child: _PostEditorTextField(
children: [ focusNode: _contentFocusNode,
Container( controller: _editorController,
padding: const EdgeInsets.symmetric( onUpdate: () => setState(() {}),
horizontal: 16, ),
vertical: 8, ),
), const VerticalDivider(width: 0.3, thickness: 0.3)
child: TextField( .paddingSymmetric(horizontal: 16),
maxLines: null, Expanded(
autofocus: true, child: SingleChildScrollView(
autocorrect: true, padding:
keyboardType: TextInputType.multiline, const EdgeInsets.only(top: 12, bottom: 64),
controller: child: MarkdownTextContent(
_editorController.contentController, isAutoWarp: _editorController.mode.value == 0,
focusNode: _contentFocusNode, content:
decoration: InputDecoration.collapsed( _editorController.contentController.text,
hintText: 'postContentPlaceholder'.tr, parentId: 'post-editor-preview',
), ).paddingOnly(right: 16),
onTapOutside: (_) => FocusManager ),
.instance.primaryFocus ),
?.unfocus(), ],
), )
), : Column(
const Gap(120) children: [
TabBar(
tabs: [
const Tab(icon: Icon(Icons.edit)),
const Tab(icon: Icon(Icons.preview)),
], ],
), ),
), Expanded(
Obx(() { child: TabBarView(
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: [ children: [
if (showFactors[0]) _PostEditorTextField(
Text('postRestoreFromLocal'.tr, focusNode: _contentFocusNode,
style: textStyle) controller: _editorController,
.paddingOnly(right: 4), onUpdate: () => setState(() {}),
if (showFactors[0]) ),
InkWell( SingleChildScrollView(
child: Text('clear'.tr, style: textStyle), padding: const EdgeInsets.only(
onTap: () { top: 12,
_editorController.localClear(); bottom: 64,
_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,
), ),
child: MarkdownTextContent(
isAutoWarp:
_editorController.mode.value == 0,
content: _editorController
.contentController.text,
parentId: 'post-editor-preview',
).paddingOnly(left: 16, right: 16),
)
], ],
), ),
) ),
.animate( ],
key: const Key('post-editor-hint-animation'),
target: doShow ? 1 : 0,
)
.fade(curve: Curves.easeInOut, duration: 300.ms);
}),
],
),
),
if (AppTheme.isLargeScreen(context))
const VerticalDivider(width: 0.3, thickness: 0.3)
.paddingSymmetric(
horizontal: 16,
),
if (AppTheme.isLargeScreen(context))
Expanded(
child: SingleChildScrollView(
child: MarkdownTextContent(
content: _editorController.contentController.text,
parentId: 'post-editor-preview',
).paddingOnly(top: 12, right: 16),
), ),
),
],
), ),
), ),
Material( Material(
@ -390,6 +344,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 +493,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 +520,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 +539,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);
}),
],
);
}
}

View File

@ -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,
), ),

View File

@ -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,61 +87,63 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> {
), ),
]; ];
return Column( return RootContainer(
children: [ child: Column(
Padding( children: [
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), Padding(
child: Row( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
children: [ child: Row(
const CircleAvatar( children: [
radius: 28, const CircleAvatar(
backgroundColor: Colors.teal, radius: 28,
child: Icon(Icons.group, color: Colors.white), backgroundColor: Colors.teal,
), child: Icon(Icons.group, color: Colors.white),
const Gap(16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(widget.realm.name,
style: Theme.of(context).textTheme.bodyLarge),
Text(widget.realm.description,
style: Theme.of(context).textTheme.bodySmall),
Text(
'#${widget.realm.id.toString().padLeft(8, '0')} · ${widget.realm.alias}',
style: const TextStyle(fontSize: 11),
),
],
), ),
) const Gap(16),
], Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(widget.realm.name,
style: Theme.of(context).textTheme.bodyLarge),
Text(widget.realm.description,
style: Theme.of(context).textTheme.bodySmall),
Text(
'#${widget.realm.id.toString().padLeft(8, '0')} · ${widget.realm.alias}',
style: const TextStyle(fontSize: 11),
),
],
),
)
],
),
), ),
), const Divider(thickness: 0.3),
const Divider(thickness: 0.3), Expanded(
Expanded( child: ListView(
child: ListView( children: [
children: [ ListTile(
ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 24),
contentPadding: const EdgeInsets.symmetric(horizontal: 24), leading: const Icon(Icons.supervisor_account),
leading: const Icon(Icons.supervisor_account), trailing: const Icon(Icons.chevron_right),
trailing: const Icon(Icons.chevron_right), title: Text('realmMembers'.tr),
title: Text('realmMembers'.tr), onTap: () => showMemberList(),
onTap: () => showMemberList(), ),
), ...(_isOwned ? ownerActions : List.empty()),
...(_isOwned ? ownerActions : List.empty()), const Divider(thickness: 0.3),
const Divider(thickness: 0.3), ListTile(
ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 24),
contentPadding: const EdgeInsets.symmetric(horizontal: 24), leading: _isOwned
leading: _isOwned ? const Icon(Icons.delete)
? const Icon(Icons.delete) : const Icon(Icons.exit_to_app),
: const Icon(Icons.exit_to_app), title: Text(_isOwned ? 'delete'.tr : 'leave'.tr),
title: Text(_isOwned ? 'delete'.tr : 'leave'.tr), onTap: () => promptLeaveChannel(),
onTap: () => promptLeaveChannel(), ),
), ],
], ),
), ),
), ],
], ),
); );
} }
} }

View File

@ -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,36 +84,42 @@ 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;
CroppedFile? croppedFile = await ImageCropper().cropImage( if (PlatformInfo.canCropImage) {
sourcePath: image.path, CroppedFile? croppedFile = await ImageCropper().cropImage(
uiSettings: [ sourcePath: image.path,
AndroidUiSettings( uiSettings: [
toolbarTitle: 'cropImage'.tr, AndroidUiSettings(
toolbarColor: Theme.of(context).colorScheme.primary, toolbarTitle: 'cropImage'.tr,
toolbarWidgetColor: Theme.of(context).colorScheme.onPrimary, toolbarColor: Theme.of(context).colorScheme.primary,
aspectRatioPresets: [ toolbarWidgetColor: Theme.of(context).colorScheme.onPrimary,
if (position == 'avatar') CropAspectRatioPreset.square, aspectRatioPresets: [
if (position == 'banner') _BannerCropAspectRatioPreset(), if (position == 'avatar') CropAspectRatioPreset.square,
], if (position == 'banner') _BannerCropAspectRatioPreset(),
), ],
IOSUiSettings( ),
title: 'cropImage'.tr, IOSUiSettings(
aspectRatioPresets: [ title: 'cropImage'.tr,
if (position == 'avatar') CropAspectRatioPreset.square, aspectRatioPresets: [
if (position == 'banner') _BannerCropAspectRatioPreset(), if (position == 'avatar') CropAspectRatioPreset.square,
], if (position == 'banner') _BannerCropAspectRatioPreset(),
), ],
WebUiSettings( ),
context: context, WebUiSettings(
), context: context,
], ),
); ],
);
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),

View File

@ -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,
), ),
) )
], ],

View File

@ -1,13 +1,24 @@
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: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/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/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';
class SettingScreen extends StatefulWidget { class SettingScreen extends StatefulWidget {
const SettingScreen({super.key}); const SettingScreen({super.key});
@ -18,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(
@ -28,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( SolianThemeData(
AppTheme.build( id: 'themeColorBlue',
Brightness.light, seedColor: const Color.fromRGBO(103, 96, 193, 1),
seedColor: color, ),
), SolianThemeData(
AppTheme.build( id: 'themeColorMiku',
Brightness.dark, seedColor: const Color.fromRGBO(56, 120, 126, 1),
seedColor: color, ),
), SolianThemeData(
); id: 'themeColorKagamine',
_prefs?.setInt('global_theme_color', color.value); seedColor: const Color.fromRGBO(244, 183, 63, 1),
context.clearSnackbar(); ),
context.showSnackbar('themeColorApplied'.tr); SolianThemeData(
}, id: 'themeColorLuka',
); seedColor: const Color.fromRGBO(243, 174, 218, 1),
} ),
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) {
@ -71,100 +82,258 @@ 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, children: [
child: ListView( _buildCaptionHeader('theme'.tr),
children: [ ListTile(
_buildCaptionHeader('themeColor'.tr), leading: const Icon(Icons.palette),
SizedBox( contentPadding: const EdgeInsets.symmetric(horizontal: 22),
height: 56, title: Text('globalTheme'.tr),
child: ListView( trailing: DropdownButtonHideUnderline(
scrollDirection: Axis.horizontal, child: DropdownButton2<SolianThemeData>(
children: _presentTheme isExpanded: true,
.map((x) => _buildThemeColorButton(x.$1, x.$2)) hint: Text(
.toList(), 'theme'.tr,
).paddingSymmetric(horizontal: 12, vertical: 8), style: TextStyle(
), fontSize: 14,
_buildCaptionHeader('notification'.tr), color: Theme.of(context).hintColor,
Tooltip( ),
message: 'settingsNotificationBgServiceDesc'.tr,
child: CheckboxListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
secondary: const Icon(Icons.system_security_update_warning),
enabled: PlatformInfo.isAndroid,
title: Text('settingsNotificationBgService'.tr),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('holdToSeeDetail'.tr),
Text(
'needRestartToApply'.tr,
style: const TextStyle(fontWeight: FontWeight.bold),
)
],
), ),
value: items: _presentTheme
_prefs?.getBool('service_background_notification') ?? false, .map((SolianThemeData item) =>
onChanged: (value) { DropdownMenuItem<SolianThemeData>(
_prefs value: item,
?.setBool('service_background_notification', value ?? false) child: Row(
.then((_) { crossAxisAlignment: CrossAxisAlignment.center,
setState(() {}); children: [
}); Icon(Icons.circle, color: item.seedColor),
const Gap(8),
Expanded(
child: Text(
item.id.tr,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 14,
),
),
),
],
),
))
.toList(),
value: (_prefs?.containsKey('global_theme') ?? false)
? SolianThemeData.fromJson(
jsonDecode(_prefs!.getString('global_theme')!),
)
: null,
onChanged: (SolianThemeData? value) {
context.read<ThemeSwitcher>().setThemeData(value);
setState(() {});
}, },
buttonStyleData: const ButtonStyleData(
padding: EdgeInsets.symmetric(horizontal: 8),
height: 40,
width: 140,
),
menuItemStyleData: const MenuItemStyleData(
height: 40,
),
), ),
), ),
_buildCaptionHeader('update'.tr), ),
CheckboxListTile( 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),
Tooltip(
message: 'settingsNotificationBgServiceDesc'.tr,
child: CheckboxListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 22), contentPadding: const EdgeInsets.symmetric(horizontal: 22),
secondary: const Icon(Icons.sync_alt), secondary: const Icon(Icons.system_security_update_warning),
title: Text('updateCheckStrictly'.tr), enabled: PlatformInfo.isAndroid,
subtitle: Text('updateCheckStrictlyDesc'.tr), title: Text('settingsNotificationBgService'.tr),
value: _prefs?.getBool('check_update_strictly') ?? false, subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('holdToSeeDetail'.tr),
Text(
'needRestartToApply'.tr,
style: const TextStyle(fontWeight: FontWeight.bold),
)
],
),
value: _prefs?.getBool('service_background_notification') ?? false,
onChanged: (value) { onChanged: (value) {
_prefs _prefs
?.setBool('check_update_strictly', value ?? false) ?.setBool('service_background_notification', value ?? false)
.then((_) { .then((_) {
setState(() {}); setState(() {});
}); });
}, },
), ),
_buildCaptionHeader('more'.tr), ),
ListTile( _buildCaptionHeader('update'.tr),
leading: const Icon(Icons.delete_sweep), CheckboxListTile(
trailing: const Icon(Icons.chevron_right), contentPadding: const EdgeInsets.symmetric(horizontal: 22),
subtitle: FutureBuilder( secondary: const Icon(Icons.sync_alt),
future: AppDatabase.getDatabaseSize(), title: Text('updateCheckStrictly'.tr),
builder: (context, snapshot) { subtitle: Text('updateCheckStrictlyDesc'.tr),
if (!snapshot.hasData) { value: _prefs?.getBool('check_update_strictly') ?? false,
return Text('localDatabaseSize'.trParams( onChanged: (value) {
{'size': 'unknown'.tr}, _prefs?.setBool('check_update_strictly', value ?? false).then((_) {
)); setState(() {});
} });
},
),
Obx(() {
final AuthProvider auth = Get.find<AuthProvider>();
if (!auth.isAuthorized.value) return const SizedBox.shrink();
return Column(
children: [
_buildCaptionHeader('account'.tr),
ListTile(
leading: const Icon(Icons.flag),
trailing: const Icon(Icons.chevron_right),
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
title: Text('reportAbuse'.tr),
subtitle: Text('reportAbuseDesc'.tr),
onTap: () {
showDialog(
context: context,
builder: (context) => const AbuseReportDialog(),
);
},
),
ListTile(
leading: const Icon(Icons.person_remove),
trailing: const Icon(Icons.chevron_right),
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
title: Text('accountDeletion'.tr),
subtitle: Text('accountDeletionDesc'.tr),
onTap: () {
context
.showSlideToConfirmDialog(
'accountDeletionConfirm'.tr,
'accountDeletionConfirmDesc'.trParams({
'account': '@${auth.userProfile.value!['name']}',
}),
)
.then((value) async {
if (value != true) return;
final client = await auth.configureClient('id');
final resp = await client.post('/users/me/deletion', {});
if (resp.statusCode != 200) {
context.showErrorDialog(RequestException(resp));
} else {
context.showSnackbar('accountDeletionRequested'.tr);
}
});
},
),
],
);
}),
_buildCaptionHeader('performance'.tr),
CheckboxListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
secondary: const Icon(Icons.message),
title: Text('animatedMessageList'.tr),
subtitle: Text('animatedMessageListDesc'.tr),
value: _prefs?.getBool('non_animated_message_list') ?? false,
onChanged: (value) {
_prefs
?.setBool('non_animated_message_list', value ?? false)
.then((_) {
setState(() {});
});
},
),
_buildCaptionHeader('more'.tr),
ListTile(
leading: const Icon(Icons.delete_sweep),
trailing: const Icon(Icons.chevron_right),
subtitle: FutureBuilder(
future: AppDatabase.getDatabaseSize(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Text('localDatabaseSize'.trParams( return Text('localDatabaseSize'.trParams(
{'size': snapshot.data!.formatBytes()}, {'size': 'unknown'.tr},
)); ));
}, }
), return Text('localDatabaseSize'.trParams(
contentPadding: const EdgeInsets.symmetric(horizontal: 22), {'size': snapshot.data!.formatBytes()},
title: Text('localDatabaseWipe'.tr), ));
onTap: () {
AppDatabase.removeDatabase().then((_) {
setState(() {});
});
}, },
), ),
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
title: Text('localDatabaseWipe'.tr),
onTap: () {
AppDatabase.removeDatabase().then((_) {
setState(() {});
});
},
),
if (PlatformInfo.canRateTheApp)
ListTile( ListTile(
leading: const Icon(Icons.info_outline), leading: const Icon(Icons.star),
trailing: const Icon(Icons.chevron_right), trailing: const Icon(Icons.chevron_right),
contentPadding: const EdgeInsets.symmetric(horizontal: 22), contentPadding: const EdgeInsets.symmetric(horizontal: 22),
title: Text('about'.tr), title: Text('rateTheApp'.tr),
subtitle: Text('rateTheAppDesc'.tr),
onTap: () { onTap: () {
AppRouter.instance.pushNamed('about'); final inAppReview = InAppReview.instance;
inAppReview.openStoreListing(
appStoreId: '6499032345',
);
}, },
), ),
], ListTile(
), leading: const Icon(Icons.info_outline),
trailing: const Icon(Icons.chevron_right),
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
title: Text('about'.tr),
onTap: () {
AppRouter.instance.pushNamed('about');
},
),
],
); );
} }
} }

View File

@ -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),
], ],
) )

View File

@ -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,
);
}
}

View File

@ -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);
}
} }
} }

View File

@ -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);
@ -35,6 +39,13 @@ abstract class AppTheme {
brightness: brightness, brightness: brightness,
seedColor: seedColor ?? const Color.fromRGBO(154, 98, 91, 1), seedColor: seedColor ?? const Color.fromRGBO(154, 98, 91, 1),
), ),
snackBarTheme: const SnackBarThemeData(
behavior: SnackBarBehavior.floating,
),
scaffoldBackgroundColor: Colors.transparent,
appBarTheme: const AppBarTheme(
backgroundColor: Colors.transparent,
),
fontFamily: 'Comfortaa', fontFamily: 'Comfortaa',
fontFamilyFallback: [ fontFamilyFallback: [
'NotoSansSC', 'NotoSansSC',
@ -49,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(),
),
);
}
} }

View File

@ -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,
),
);
},
); );
} }
} }

View File

@ -23,6 +23,7 @@ class AccountHeadingWidget extends StatelessWidget {
final AccountProfile? profile; final AccountProfile? profile;
final List<AccountBadge>? badges; final List<AccountBadge>? badges;
final List<Widget>? extraWidgets; final List<Widget>? extraWidgets;
final List<Widget>? appendWidgets;
final Future<Response>? status; final Future<Response>? status;
final Function? onEditStatus; final Function? onEditStatus;
@ -39,6 +40,7 @@ class AccountHeadingWidget extends StatelessWidget {
this.profile, this.profile,
this.status, this.status,
this.extraWidgets, this.extraWidgets,
this.appendWidgets,
this.onEditStatus, this.onEditStatus,
}); });
@ -82,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),
), ),
], ],
), ),
@ -257,6 +259,7 @@ class AccountHeadingWidget extends StatelessWidget {
), ),
), ),
).paddingSymmetric(horizontal: 16), ).paddingSymmetric(horizontal: 16),
...?appendWidgets?.map((x) => x.paddingSymmetric(horizontal: 16)),
], ],
), ),
); );

View File

@ -106,15 +106,19 @@ class _AccountProfilePopupState extends State<AccountProfilePopup> {
extraWidgets: [ extraWidgets: [
Card( Card(
child: ListTile( child: ListTile(
leading: const Icon(
Icons.contact_page_outlined,
),
shape: const RoundedRectangleBorder( shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(8)), borderRadius: BorderRadius.all(Radius.circular(8)),
), ),
title: Text('visitProfilePage'.tr), title: Text('visitProfilePage'.tr),
subtitle: Text('learnMoreAboutPerson'.tr),
visualDensity: visualDensity:
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},
); );

View File

@ -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)

View File

@ -23,47 +23,51 @@ 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,
isScrollControlled: true, isScrollControlled: true,
backgroundColor: Theme backgroundColor: Theme.of(context).colorScheme.surface,
.of(context)
.colorScheme
.surface,
context: context, context: context,
builder: (context) => builder: (context) => AccountProfilePopup(
AccountProfilePopup( name: element.related.name,
name: element.related.name, ),
),
); );
}, },
), ),
trailing: Row( trailing: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
if(element.status != 1 && element.status != 3) if (element.status != 1 && element.status != 3)
IconButton( IconButton(
icon: const Icon(Icons.check), icon: const Icon(Icons.check),
onPressed: () { onPressed: () {
final RelationshipProvider provider = Get.find(); final RelationshipProvider provider = Get.find();
if (element.status == 0) { if (element.status == 0) {
provider.handleRelation(element, true).then((_) => onUpdate()); provider
.handleRelation(element, true)
.then((_) => onUpdate());
} else { } else {
provider.editRelation(element, 1).then((_) => onUpdate()); provider
.editRelation(element.relatedId, 1)
.then((_) => onUpdate());
} }
}, },
), ),
if(element.status != 2 && element.status != 3) if (element.status != 2 && element.status != 3)
IconButton( IconButton(
icon: const Icon(Icons.close), icon: const Icon(Icons.close),
onPressed: () { onPressed: () {
final RelationshipProvider provider = Get.find(); final RelationshipProvider provider = Get.find();
if (element.status == 0) { if (element.status == 0) {
provider.handleRelation(element, false).then((_) => onUpdate()); provider
.handleRelation(element, false)
.then((_) => onUpdate());
} else { } else {
provider.editRelation(element, 2).then((_) => onUpdate()); provider
.editRelation(element.relatedId, 2)
.then((_) => onUpdate());
} }
}, },
), ),

View File

@ -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,

View File

@ -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 const SizedBox.shrink();
return DrawerButton(
onPressed: () => rootScaffoldKey.currentState!.openDrawer(),
);
} else {
return const SizedBox.shrink();
}
} }
} }

View File

@ -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,
@ -744,8 +745,8 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
return IgnorePointer( return IgnorePointer(
ignoring: _uploadController.isUploading.value, ignoring: _uploadController.isUploading.value,
child: Container( child: Container(
height: 64,
width: MediaQuery.of(context).size.width, width: MediaQuery.of(context).size.width,
padding: const EdgeInsets.all(8),
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border( border: Border(
top: BorderSide( top: BorderSide(
@ -754,67 +755,72 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
), ),
), ),
), ),
child: SingleChildScrollView( child: Wrap(
scrollDirection: Axis.horizontal, spacing: 8,
child: Wrap( runSpacing: 8,
spacing: 8, alignment: WrapAlignment.center,
runSpacing: 0, runAlignment: WrapAlignment.center,
alignment: WrapAlignment.center, children: [
runAlignment: WrapAlignment.center, if ((PlatformInfo.isDesktop ||
children: [ PlatformInfo.isIOS ||
if ((PlatformInfo.isDesktop || PlatformInfo.isWeb) &&
PlatformInfo.isIOS || !widget.imageOnly)
PlatformInfo.isWeb) && IconButton(
!widget.imageOnly) icon: const Icon(Icons.paste),
ElevatedButton.icon( tooltip: 'attachmentAddClipboard'.tr,
icon: const Icon(Icons.paste),
label: Text('attachmentAddClipboard'.tr),
style: const ButtonStyle(visualDensity: density),
onPressed: () => _pasteFileToUpload(),
),
ElevatedButton.icon(
icon: const Icon(Icons.add_photo_alternate),
label: Text('attachmentAddGalleryPhoto'.tr),
style: const ButtonStyle(visualDensity: density), style: const ButtonStyle(visualDensity: density),
onPressed: () => _pickPhotoToUpload(), color: Theme.of(context).colorScheme.primary,
onPressed: () => _pasteFileToUpload(),
), ),
if (!widget.imageOnly) IconButton(
ElevatedButton.icon( icon: const Icon(Icons.add_photo_alternate),
icon: const Icon(Icons.add_road), tooltip: 'attachmentAddGalleryPhoto'.tr,
label: Text('attachmentAddGalleryVideo'.tr), style: const ButtonStyle(visualDensity: density),
style: const ButtonStyle(visualDensity: density), color: Theme.of(context).colorScheme.primary,
onPressed: () => _pickVideoToUpload(), onPressed: () => _pickPhotoToUpload(),
), ),
ElevatedButton.icon( if (!widget.imageOnly)
icon: const Icon(Icons.photo_camera_back), IconButton(
label: Text('attachmentAddCameraPhoto'.tr), icon: const Icon(Icons.add_road),
tooltip: 'attachmentAddGalleryVideo'.tr,
style: const ButtonStyle(visualDensity: density), style: const ButtonStyle(visualDensity: density),
color: Theme.of(context).colorScheme.primary,
onPressed: () => _pickVideoToUpload(),
),
if (PlatformInfo.isMobile)
IconButton(
icon: const Icon(Icons.photo_camera_back),
tooltip: 'attachmentAddCameraPhoto'.tr,
style: const ButtonStyle(visualDensity: density),
color: Theme.of(context).colorScheme.primary,
onPressed: () => _takeMediaToUpload(false), onPressed: () => _takeMediaToUpload(false),
), ),
if (!widget.imageOnly) if (!widget.imageOnly && PlatformInfo.isMobile)
ElevatedButton.icon( IconButton(
icon: const Icon(Icons.video_camera_back_outlined), icon: const Icon(Icons.video_camera_back_outlined),
label: Text('attachmentAddCameraVideo'.tr), tooltip: 'attachmentAddCameraVideo'.tr,
style: const ButtonStyle(visualDensity: density), style: const ButtonStyle(visualDensity: density),
onPressed: () => _takeMediaToUpload(true), color: Theme.of(context).colorScheme.primary,
), onPressed: () => _takeMediaToUpload(true),
if (!widget.imageOnly) ),
ElevatedButton.icon( if (!widget.imageOnly)
icon: const Icon(Icons.file_present_rounded), IconButton(
label: Text('attachmentAddFile'.tr), icon: const Icon(Icons.file_present_rounded),
style: const ButtonStyle(visualDensity: density), tooltip: 'attachmentAddFile'.tr,
onPressed: () => _pickFileToUpload(), style: const ButtonStyle(visualDensity: density),
), color: Theme.of(context).colorScheme.primary,
if (!widget.imageOnly) onPressed: () => _pickFileToUpload(),
ElevatedButton.icon( ),
icon: const Icon(Icons.link), if (!widget.imageOnly)
label: Text('attachmentAddFile'.tr), IconButton(
style: const ButtonStyle(visualDensity: density), icon: const Icon(Icons.link),
onPressed: () => _linkAttachments(), tooltip: 'attachmentAddLink'.tr,
), style: const ButtonStyle(visualDensity: density),
], color: Theme.of(context).colorScheme.primary,
).paddingSymmetric(horizontal: 12), onPressed: () => _linkAttachments(),
), ),
],
).paddingSymmetric(horizontal: 12),
) )
.animate( .animate(
target: _uploadController.isUploading.value ? 0 : 1, target: _uploadController.isUploading.value ? 0 : 1,

View File

@ -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,
), ),

View File

@ -21,6 +21,7 @@ class AttachmentItem extends StatefulWidget {
final bool showBadge; final bool showBadge;
final bool showHideButton; final bool showHideButton;
final bool autoload; final bool autoload;
final bool isDense;
final BoxFit fit; final BoxFit fit;
final String? badge; final String? badge;
final Function? onHide; final Function? onHide;
@ -34,6 +35,7 @@ class AttachmentItem extends StatefulWidget {
this.showBadge = true, this.showBadge = true,
this.showHideButton = true, this.showHideButton = true,
this.autoload = false, this.autoload = false,
this.isDense = false,
this.onHide, this.onHide,
}); });
@ -53,6 +55,7 @@ class _AttachmentItemState extends State<AttachmentItem> {
fit: widget.fit, fit: widget.fit,
showBadge: widget.showBadge, showBadge: widget.showBadge,
showHideButton: widget.showHideButton, showHideButton: widget.showHideButton,
isDense: widget.isDense,
onHide: widget.onHide, onHide: widget.onHide,
); );
case 'video': case 'video':
@ -120,6 +123,7 @@ class _AttachmentItemImage extends StatelessWidget {
final bool showBadge; final bool showBadge;
final bool showHideButton; final bool showHideButton;
final BoxFit fit; final BoxFit fit;
final bool isDense;
final String? badge; final String? badge;
final Function? onHide; final Function? onHide;
@ -128,6 +132,7 @@ class _AttachmentItemImage extends StatelessWidget {
required this.item, required this.item,
required this.showBadge, required this.showBadge,
required this.showHideButton, required this.showHideButton,
required this.isDense,
required this.fit, required this.fit,
this.badge, this.badge,
this.onHide, this.onHide,
@ -146,6 +151,7 @@ class _AttachmentItemImage extends StatelessWidget {
'/attachments/${item.rid}', '/attachments/${item.rid}',
), ),
fit: fit, fit: fit,
isDense: isDense,
), ),
if (showBadge && badge != null) if (showBadge && badge != null)
Positioned( Positioned(

View File

@ -15,12 +15,12 @@ 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 double? width; final double? width;
@ -29,12 +29,12 @@ class AttachmentList extends StatefulWidget {
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.width, this.width,
this.viewport, this.viewport,
@ -50,21 +50,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 +76,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 +117,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,7 +131,16 @@ class _AttachmentListState extends State<AttachmentList> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_getMetadataList(); assert(widget.attachmentIds != null || widget.attachments != null);
if (widget.attachments == null) {
_getMetadataList();
} else {
setState(() {
_attachments = widget.attachments!;
_isLoading = false;
});
_calculateAspectRatio();
}
} }
Color get _unFocusColor => Color get _unFocusColor =>
@ -140,7 +148,7 @@ class _AttachmentListState extends State<AttachmentList> {
@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();
} }
@ -154,7 +162,7 @@ class _AttachmentListState extends State<AttachmentList> {
).paddingOnly(right: 5), ).paddingOnly(right: 5),
Text( Text(
'attachmentHint'.trParams( 'attachmentHint'.trParams(
{'count': widget.attachmentsId.length.toString()}, {'count': _attachments.toString()},
), ),
style: TextStyle(color: _unFocusColor, fontSize: 12), style: TextStyle(color: _unFocusColor, fontSize: 12),
) )
@ -165,21 +173,79 @@ 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;
double ratio = element!.metadata?['ratio']?.toDouble() ?? 16 / 9;
return Container(
constraints: BoxConstraints(
maxWidth: widget.columnMaxWidth,
maxHeight: 640,
),
child: AspectRatio(
aspectRatio: ratio,
child: Container(
decoration: BoxDecoration(
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.surfaceContainerHigh,
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; double ratio = element.metadata?['ratio']?.toDouble() ?? 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,
@ -205,67 +271,44 @@ class _AttachmentListState extends State<AttachmentList> {
); );
} }
final isNotPureImage = _attachmentsMeta.any( return SizedBox(
(x) => x?.mimetype.split('/').firstOrNull != 'image', width: math.min(MediaQuery.of(context).size.width, widget.columnMaxWidth),
);
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(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
border: Border.all(
color: Theme.of(context).dividerColor,
width: 1,
),
borderRadius: radius,
),
child: ClipRRect(
borderRadius: radius,
child: _buildEntry(element, idx),
),
);
},
).paddingSymmetric(horizontal: 24);
}
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( child: CarouselSlider.builder(
options: CarouselOptions( options: CarouselOptions(
disableCenter: true,
animateToClosest: true,
aspectRatio: _aspectRatio, aspectRatio: _aspectRatio,
viewportFraction: enlargeCenterPage: true,
widget.viewport ?? (widget.attachmentsId.length > 1 ? 0.95 : 1), viewportFraction: widget.viewport ?? 0.95,
enableInfiniteScroll: false, enableInfiniteScroll: false,
), ),
itemCount: _attachmentsMeta.length, itemCount: _attachments.length,
itemBuilder: (context, idx, _) { itemBuilder: (context, idx, _) {
final element = _attachmentsMeta[idx]; final element = _attachments[idx];
return _buildEntry(element, idx); if (element == null) const SizedBox.shrink();
double ratio = element!.metadata?['ratio']?.toDouble() ?? 16 / 9;
return Container(
constraints: BoxConstraints(
maxWidth: widget.columnMaxWidth,
maxHeight: 640,
),
child: AspectRatio(
aspectRatio: ratio,
child: Container(
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).dividerColor,
width: 1,
),
borderRadius: radius,
),
child: ClipRRect(
borderRadius: radius,
child: _buildEntry(element, idx),
),
),
),
);
}, },
), ),
); );
@ -319,6 +362,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(
@ -338,6 +382,7 @@ class AttachmentListEntry extends StatelessWidget {
badge: showBadge ? badgeContent : null, badge: showBadge ? badgeContent : null,
showHideButton: !item!.isMature || showMature, showHideButton: !item!.isMature || showMature,
autoload: autoload, autoload: autoload,
isDense: isDense,
onHide: () { onHide: () {
onReveal(false); onReveal(false);
}, },

View File

@ -34,8 +34,17 @@ class AutoCacheImage extends StatelessWidget {
progressIndicatorBuilder: noProgressIndicator progressIndicatorBuilder: noProgressIndicator
? null ? null
: (context, url, downloadProgress) => Center( : (context, url, downloadProgress) => Center(
child: CircularProgressIndicator( child: TweenAnimationBuilder(
value: downloadProgress.progress, tween: Tween(
begin: 0,
end: downloadProgress.progress ?? 0,
),
duration: const Duration(milliseconds: 300),
builder: (context, value, _) => CircularProgressIndicator(
value: downloadProgress.progress != null
? value.toDouble()
: null,
),
), ),
), ),
errorWidget: noErrorWidget errorWidget: noErrorWidget
@ -74,11 +83,20 @@ class AutoCacheImage extends StatelessWidget {
ImageChunkEvent? loadingProgress) { ImageChunkEvent? loadingProgress) {
if (loadingProgress == null) return child; if (loadingProgress == null) return child;
return Center( return Center(
child: CircularProgressIndicator( child: TweenAnimationBuilder(
value: loadingProgress.expectedTotalBytes != null tween: Tween(
? loadingProgress.cumulativeBytesLoaded / begin: 0,
loadingProgress.expectedTotalBytes! end: loadingProgress.expectedTotalBytes != null
: null, ? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: 0,
),
duration: const Duration(milliseconds: 300),
builder: (context, value, _) => CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null
? value.toDouble()
: null,
),
), ),
); );
}, },

View File

@ -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),

View File

@ -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 (otherside != null) {
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(child: Text(otherside.account.nick)),
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),
),
),
],
);
}
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),
),
),
],
);
}
Widget _buildSubtitle(Channel item, ChannelMember? otherside) {
if (PlatformInfo.isWeb) { if (PlatformInfo.isWeb) {
return Text('channelDirectDescription'.trParams( return otherside != null
{'username': '@${otherside.account.name}'}, ? Text(
)); 'channelDirectDescription'.trParams(
{'username': '@${otherside.account.name}'},
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
)
: Text(
item.description,
maxLines: 1,
overflow: TextOverflow.ellipsis,
);
} }
return FutureBuilder( return AnimatedSwitcher(
future: Future.delayed( switchInCurve: Curves.easeIn,
const Duration(milliseconds: 500), switchOutCurve: Curves.easeOut,
() => _eventController.src.getLastInChannel(item), transitionBuilder: (child, animation) {
), return FadeTransition(opacity: animation, child: child);
builder: (context, snapshot) { },
if (!snapshot.hasData && snapshot.data == null) { duration: const Duration(milliseconds: 300),
return Text('channelDirectDescription'.trParams( child: (_lastMessages == null || _lastMessages![item.id] == null)
{'username': '@${otherside.account.name}'}, ? Builder(
)); builder: (context) {
} return otherside != null
? Text(
final data = snapshot.data!.data!; 'channelDirectDescription'.trParams(
return Text( {'username': '@${otherside.account.name}'},
'${data.sender.account.nick}: ${data.body['text'] ?? 'Unsupported message to preview'}', ),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, 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).firstOrNull;
item.members!.where((e) => e.account.id != widget.selfId).first;
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,49 +261,16 @@ class _ChannelListWidgetState extends State<ChannelListWidget> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (widget.noCategory) {
return CustomScrollView(
slivers: [
SliverList.builder(
itemCount: _globalChannels.length,
itemBuilder: (context, index) {
final element = _globalChannels[index];
return _buildEntry(element);
},
),
SliverGap(max(16, MediaQuery.of(context).padding.bottom)),
],
);
}
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);
}, },
), ),
SliverList.list( SliverGap(max(16, MediaQuery.of(context).padding.bottom)),
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(),
),
], ],
); );
} }

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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) {
@ -63,15 +66,32 @@ class ChatEventList extends StatelessWidget {
return GestureDetector( return GestureDetector(
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
child: ChatEvent( child: Builder(builder: (context) {
key: Key('m${item!.uuid}'), final widget = ChatEvent(
item: item, key: Key('m${item!.uuid}'),
isMerged: isMerged, item: item,
chatController: chatController, isMerged: isMerged,
).paddingOnly( chatController: chatController,
top: !isMerged ? 8 : 0, ).paddingOnly(
bottom: !hasMerged ? 8 : 0, top: !isMerged ? 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 +99,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);
}, },

View File

@ -49,6 +49,7 @@ class ChatEventMessage extends StatelessWidget {
return MarkdownTextContent( return MarkdownTextContent(
parentId: 'm${item.id}', parentId: 'm${item.id}',
isSelectable: true, isSelectable: true,
isAutoWarp: true,
content: body.text, content: body.text,
); );
} }

View File

@ -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}',
), ),

View File

@ -12,7 +12,7 @@ class DailySignHistoryChartDialog extends StatelessWidget {
const DailySignHistoryChartDialog({super.key, required this.data}); const DailySignHistoryChartDialog({super.key, required this.data});
static List<String> signSymbols = ['大凶', '', '中平', '', '大吉']; static final List<String> signSymbols = ['大凶', '', '中平', '', '大吉'];
DateTime? get _firstRecordDate => data?.map((x) => x.createdAt).reduce( DateTime? get _firstRecordDate => data?.map((x) => x.createdAt).reduce(
(a, b) => DateTime.fromMillisecondsSinceEpoch( (a, b) => DateTime.fromMillisecondsSinceEpoch(
@ -42,215 +42,222 @@ class DailySignHistoryChartDialog extends StatelessWidget {
child: CircularProgressIndicator(), child: CircularProgressIndicator(),
), ),
) )
: Column( : SizedBox(
mainAxisSize: MainAxisSize.min, width: double.maxFinite,
crossAxisAlignment: CrossAxisAlignment.start, child: ListView(
children: [ shrinkWrap: true,
Text( children: [
'dailySignHistoryRecent'.tr, Text(
style: Theme.of(context).textTheme.titleMedium, 'dailySignHistoryRecent'.tr,
).paddingOnly(bottom: 18), style: Theme.of(context).textTheme.titleMedium,
SizedBox( ).paddingOnly(bottom: 18),
height: 180, SizedBox(
width: max(640, MediaQuery.of(context).size.width), height: 180,
child: LineChart( width: max(640, MediaQuery.of(context).size.width),
LineChartData( child: LineChart(
lineBarsData: [ LineChartData(
LineChartBarData( lineBarsData: [
isCurved: true, LineChartBarData(
isStrokeCapRound: true, isCurved: true,
isStrokeJoinRound: true, isStrokeCapRound: true,
color: Theme.of(context).colorScheme.primary, isStrokeJoinRound: true,
belowBarData: BarAreaData( color: Theme.of(context).colorScheme.primary,
show: true, belowBarData: BarAreaData(
gradient: LinearGradient( show: true,
colors: List.filled( gradient: LinearGradient(
data!.length, colors: List.filled(
Theme.of(context) data!.length,
.colorScheme Theme.of(context)
.primary .colorScheme
.withOpacity(0.3), .primary
).toList(), .withOpacity(0.3),
), ).toList(),
),
spots: data!
.map(
(x) => FlSpot(
x.createdAt
.copyWith(
hour: 0,
minute: 0,
second: 0,
millisecond: 0,
microsecond: 0,
)
.millisecondsSinceEpoch
.toDouble(),
x.resultTier.toDouble(),
),
)
.toList(),
)
],
lineTouchData: LineTouchData(
touchTooltipData: LineTouchTooltipData(
getTooltipItems: (spots) => spots
.map((spot) => LineTooltipItem(
'${signSymbols[spot.y.toInt()]}\n${DateFormat('MM/dd').format(DateTime.fromMillisecondsSinceEpoch(spot.x.toInt()))}',
TextStyle(
color:
Theme.of(context).colorScheme.onSurface,
),
))
.toList(),
getTooltipColor: (_) =>
Theme.of(context).colorScheme.surfaceContainerHigh,
)),
titlesData: FlTitlesData(
topTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
rightTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 40,
interval: 1,
getTitlesWidget: (value, _) => Align(
alignment: Alignment.centerRight,
child: Text(
signSymbols[value.toInt()],
textAlign: TextAlign.right,
).paddingOnly(right: 8),
),
),
),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 28,
interval: 86400000,
getTitlesWidget: (value, _) => Text(
DateFormat('dd').format(
DateTime.fromMillisecondsSinceEpoch(
value.toInt(),
),
), ),
textAlign: TextAlign.center,
).paddingOnly(top: 8),
),
),
),
gridData: const FlGridData(show: false),
borderData: FlBorderData(show: false),
),
),
).marginOnly(right: 24, bottom: 8, top: 8),
const Gap(16),
Text(
'dailySignHistoryReward'.tr,
style: Theme.of(context).textTheme.titleMedium,
).paddingOnly(bottom: 18),
SizedBox(
height: 180,
width: max(640, MediaQuery.of(context).size.width),
child: LineChart(
LineChartData(
lineBarsData: [
LineChartBarData(
isCurved: true,
isStrokeCapRound: true,
isStrokeJoinRound: true,
color: Theme.of(context).colorScheme.primary,
belowBarData: BarAreaData(
show: true,
gradient: LinearGradient(
colors: List.filled(
data!.length,
Theme.of(context)
.colorScheme
.primary
.withOpacity(0.3),
).toList(),
), ),
), spots: data!
spots: data! .map(
.map( (x) => FlSpot(
(x) => FlSpot( x.createdAt
x.createdAt .copyWith(
.copyWith( hour: 0,
hour: 0, minute: 0,
minute: 0, second: 0,
second: 0, millisecond: 0,
millisecond: 0, microsecond: 0,
microsecond: 0, )
) .millisecondsSinceEpoch
.millisecondsSinceEpoch .toDouble(),
.toDouble(), x.resultTier.toDouble(),
x.resultExperience.toDouble(),
),
)
.toList(),
)
],
lineTouchData: LineTouchData(
touchTooltipData: LineTouchTooltipData(
getTooltipItems: (spots) => spots
.map((spot) => LineTooltipItem(
'+${spot.y.toStringAsFixed(0)} EXP\n${DateFormat('MM/dd').format(DateTime.fromMillisecondsSinceEpoch(spot.x.toInt()))}',
TextStyle(
color:
Theme.of(context).colorScheme.onSurface,
), ),
)) )
.toList(), .toList(),
getTooltipColor: (_) => )
Theme.of(context).colorScheme.surfaceContainerHigh, ],
)), lineTouchData: LineTouchData(
titlesData: FlTitlesData( touchTooltipData: LineTouchTooltipData(
topTitles: const AxisTitles( getTooltipItems: (spots) => spots
sideTitles: SideTitles(showTitles: false), .map((spot) => LineTooltipItem(
'${signSymbols[spot.y.toInt()]}\n${DateFormat('MM/dd').format(DateTime.fromMillisecondsSinceEpoch(spot.x.toInt()))}',
TextStyle(
color: Theme.of(context)
.colorScheme
.onSurface,
),
))
.toList(),
getTooltipColor: (_) => Theme.of(context)
.colorScheme
.surfaceContainerHigh,
),
), ),
rightTitles: const AxisTitles( titlesData: FlTitlesData(
sideTitles: SideTitles(showTitles: false), topTitles: const AxisTitles(
), sideTitles: SideTitles(showTitles: false),
leftTitles: AxisTitles( ),
sideTitles: SideTitles( rightTitles: const AxisTitles(
showTitles: true, sideTitles: SideTitles(showTitles: false),
reservedSize: 40, ),
getTitlesWidget: (value, _) => Align( leftTitles: AxisTitles(
alignment: Alignment.centerRight, sideTitles: SideTitles(
child: Text( showTitles: true,
value.toStringAsFixed(0), reservedSize: 40,
textAlign: TextAlign.right, interval: 1,
).paddingOnly(right: 8), getTitlesWidget: (value, _) => Align(
alignment: Alignment.centerRight,
child: Text(
signSymbols[value.toInt()],
textAlign: TextAlign.right,
).paddingOnly(right: 8),
),
),
),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 28,
interval: 86400000,
getTitlesWidget: (value, _) => Text(
DateFormat('dd').format(
DateTime.fromMillisecondsSinceEpoch(
value.toInt(),
),
),
textAlign: TextAlign.center,
).paddingOnly(top: 8),
), ),
), ),
), ),
bottomTitles: AxisTitles( gridData: const FlGridData(show: false),
sideTitles: SideTitles( borderData: FlBorderData(show: false),
showTitles: true, ),
reservedSize: 28, ),
interval: 86400000, ).marginOnly(right: 24, bottom: 8, top: 8),
getTitlesWidget: (value, _) => Text( const Gap(16),
DateFormat('dd').format( Text(
DateTime.fromMillisecondsSinceEpoch( 'dailySignHistoryReward'.tr,
value.toInt(), style: Theme.of(context).textTheme.titleMedium,
), ).paddingOnly(bottom: 18),
SizedBox(
height: 180,
width: max(640, MediaQuery.of(context).size.width),
child: LineChart(
LineChartData(
lineBarsData: [
LineChartBarData(
isCurved: true,
isStrokeCapRound: true,
isStrokeJoinRound: true,
color: Theme.of(context).colorScheme.primary,
belowBarData: BarAreaData(
show: true,
gradient: LinearGradient(
colors: List.filled(
data!.length,
Theme.of(context)
.colorScheme
.primary
.withOpacity(0.3),
).toList(),
), ),
textAlign: TextAlign.center, ),
).paddingOnly(top: 8), spots: data!
.map(
(x) => FlSpot(
x.createdAt
.copyWith(
hour: 0,
minute: 0,
second: 0,
millisecond: 0,
microsecond: 0,
)
.millisecondsSinceEpoch
.toDouble(),
x.resultExperience.toDouble(),
),
)
.toList(),
)
],
lineTouchData: LineTouchData(
touchTooltipData: LineTouchTooltipData(
getTooltipItems: (spots) => spots
.map((spot) => LineTooltipItem(
'+${spot.y.toStringAsFixed(0)} EXP\n${DateFormat('MM/dd').format(DateTime.fromMillisecondsSinceEpoch(spot.x.toInt()))}',
TextStyle(
color: Theme.of(context)
.colorScheme
.onSurface,
),
))
.toList(),
getTooltipColor: (_) => Theme.of(context)
.colorScheme
.surfaceContainerHigh,
)),
titlesData: FlTitlesData(
topTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
rightTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 40,
getTitlesWidget: (value, _) => Align(
alignment: Alignment.centerRight,
child: Text(
value.toStringAsFixed(0),
textAlign: TextAlign.right,
).paddingOnly(right: 8),
),
),
),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 28,
interval: 86400000,
getTitlesWidget: (value, _) => Text(
DateFormat('dd').format(
DateTime.fromMillisecondsSinceEpoch(
value.toInt(),
),
),
textAlign: TextAlign.center,
).paddingOnly(top: 8),
),
), ),
), ),
gridData: const FlGridData(show: false),
borderData: FlBorderData(show: false),
), ),
gridData: const FlGridData(show: false),
borderData: FlBorderData(show: false),
), ),
), ).marginOnly(right: 24, bottom: 8, top: 8),
).marginOnly(right: 24, bottom: 8, top: 8), ],
], ),
), ),
); );
} }

View File

@ -74,9 +74,13 @@ class LinkExpansion extends StatelessWidget {
), ),
).paddingOnly(right: 8), ).paddingOnly(right: 8),
if (snapshot.data!.siteName != null) if (snapshot.data!.siteName != null)
Text( Expanded(
snapshot.data!.siteName!, child: Text(
style: Theme.of(context).textTheme.labelLarge, snapshot.data!.siteName!,
style: Theme.of(context).textTheme.labelLarge,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
), ),
], ],
).paddingOnly( ).paddingOnly(

View File

@ -1,11 +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: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';
@ -15,6 +20,7 @@ class MarkdownTextContent extends StatelessWidget {
final String parentId; final String parentId;
final bool isSelectable; final bool isSelectable;
final bool isLargeText; final bool isLargeText;
final bool isAutoWarp;
const MarkdownTextContent({ const MarkdownTextContent({
super.key, super.key,
@ -22,139 +28,187 @@ class MarkdownTextContent extends StatelessWidget {
required this.parentId, required this.parentId,
this.isSelectable = false, this.isSelectable = false,
this.isLargeText = false, this.isLargeText = false,
this.isAutoWarp = false,
}); });
Widget _buildContent(BuildContext context) { Widget _buildContent(BuildContext context) {
final emojiRegex = RegExp(r':([-\w]+):'); final stickerRegex = RegExp(r':([-\w]+):');
final emojiMatch = emojiRegex.allMatches(content);
final isOnlyEmoji = content.replaceAll(emojiRegex, '').trim().isEmpty;
return Markdown( // Split the content into paragraphs
shrinkWrap: true, final paragraphs = content.split(RegExp(r'\n\s*\n'));
physics: const NeverScrollableScrollPhysics(),
data: content, // Iterate over each paragraph to process stickers individually
padding: EdgeInsets.zero, List<Widget> contentWidgets = [];
styleSheet: MarkdownStyleSheet.fromTheme( for (var idx = 0; idx < paragraphs.length; idx++) {
Theme.of(context), // Getting paragraph
).copyWith( var paragraph = paragraphs[idx];
textScaleFactor: isLargeText ? 1.1 : 1,
blockquote: TextStyle( // Matching stickers
color: Theme.of(context).colorScheme.onSurfaceVariant, final stickerMatch = stickerRegex.allMatches(paragraph);
), final isOnlySticker =
blockquoteDecoration: BoxDecoration( paragraph.replaceAll(stickerRegex, '').trim().isEmpty;
color: Theme.of(context).colorScheme.surfaceContainerHigh,
borderRadius: const BorderRadius.all(Radius.circular(4)), contentWidgets.add(
), Markdown(
horizontalRuleDecoration: BoxDecoration( shrinkWrap: true,
border: Border( physics: const NeverScrollableScrollPhysics(),
top: BorderSide( data: paragraph,
width: 1.0, padding: EdgeInsets.zero,
color: Theme.of(context).dividerColor, styleSheet: MarkdownStyleSheet.fromTheme(
), Theme.of(context),
).copyWith(
textScaler: TextScaler.linear(isLargeText ? 1.1 : 1),
blockquote: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
blockquoteDecoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
borderRadius: const BorderRadius.all(Radius.circular(4)),
),
horizontalRuleDecoration: BoxDecoration(
border: Border(
top: BorderSide(
width: 1.0,
color: Theme.of(context).dividerColor,
),
),
),
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(
<markdown.BlockSyntax>[
markdown.CodeBlockSyntax(),
...markdown.ExtensionSet.commonMark.blockSyntaxes,
...markdown.ExtensionSet.gitHubFlavored.blockSyntaxes,
],
<markdown.InlineSyntax>[
if (isAutoWarp) markdown.LineBreakSyntax(),
_UserNameCardInlineSyntax(),
_CustomEmoteInlineSyntax(),
markdown.AutolinkSyntax(),
markdown.AutolinkExtensionSyntax(),
markdown.CodeSyntax(),
...markdown.ExtensionSet.commonMark.inlineSyntaxes,
...markdown.ExtensionSet.gitHubFlavored.inlineSyntaxes
],
), ),
), onTapLink: (text, href, title) async {
), if (href == null) return;
extensionSet: markdown.ExtensionSet( if (href.startsWith('solink://')) {
markdown.ExtensionSet.gitHubFlavored.blockSyntaxes, final segments = href.replaceFirst('solink://', '').split('/');
<markdown.InlineSyntax>[ switch (segments[0]) {
_UserNameCardInlineSyntax(), case 'users':
_CustomEmoteInlineSyntax(), showModalBottomSheet(
markdown.EmojiSyntax(), useRootNavigator: true,
markdown.AutolinkSyntax(), isScrollControlled: true,
markdown.AutolinkExtensionSyntax(), backgroundColor: Theme.of(context).colorScheme.surface,
...markdown.ExtensionSet.gitHubFlavored.inlineSyntaxes context: context,
], builder: (context) => AccountProfilePopup(
), name: segments[1],
onTapLink: (text, href, title) async { ),
if (href == null) return; );
if (href.startsWith('solink://')) {
final segments = href.replaceFirst('solink://', '').split('/');
switch (segments[0]) {
case 'users':
showModalBottomSheet(
useRootNavigator: true,
isScrollControlled: true,
backgroundColor: Theme.of(context).colorScheme.surface,
context: context,
builder: (context) => AccountProfilePopup(
name: segments[1],
),
);
}
return;
}
await launchUrlString(
href,
mode: LaunchMode.externalApplication,
);
},
imageBuilder: (uri, title, alt) {
var url = uri.toString();
double? width, height;
BoxFit? fit;
if (url.startsWith('solink://')) {
final segments = url.replaceFirst('solink://', '').split('/');
switch (segments[0]) {
case 'stickers':
double radius = 8;
final StickerProvider sticker = Get.find();
if (emojiMatch.length <= 1 && isOnlyEmoji) {
width = 128;
height = 128;
} else if (emojiMatch.length <= 3 && isOnlyEmoji) {
width = 32;
height = 32;
} else {
radius = 4;
width = 16;
height = 16;
} }
fit = BoxFit.contain; return;
return ClipRRect( }
borderRadius: BorderRadius.all(Radius.circular(radius)),
child: Container(
color: Theme.of(context).colorScheme.surfaceContainer,
child: FutureBuilder(
future: sticker.getStickerByAlias(segments[1]),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Center(child: CircularProgressIndicator());
}
return AutoCacheImage(
snapshot.data!.imageUrl,
width: width,
height: height,
fit: fit,
noErrorWidget: true,
);
},
),
),
).paddingSymmetric(vertical: 4);
case 'attachments':
const radius = BorderRadius.all(Radius.circular(8));
return LimitedBox(
maxHeight: MediaQuery.of(context).size.width,
child: ClipRRect(
borderRadius: radius,
child: AttachmentSelfContainedEntry(
isDense: true,
parentId: parentId,
rid: segments[1],
),
),
).paddingSymmetric(vertical: 4);
}
}
return AutoCacheImage( await launchUrlString(
url, href,
width: width, mode: LaunchMode.externalApplication,
height: height, );
fit: fit, },
); imageBuilder: (uri, title, alt) {
}, var url = uri.toString();
double? width, height;
BoxFit? fit;
if (url.startsWith('solink://')) {
final segments = url.replaceFirst('solink://', '').split('/');
switch (segments[0]) {
case 'stickers':
double radius = 8;
final StickerProvider sticker = Get.find();
// Adjust sticker size based on the sticker count in this paragraph
if (stickerMatch.length <= 1 && isOnlySticker) {
width = 128;
height = 128;
} else if (stickerMatch.length <= 3 && isOnlySticker) {
width = 32;
height = 32;
} else {
radius = 4;
width = 16;
height = 16;
}
fit = BoxFit.contain;
return ClipRRect(
borderRadius: BorderRadius.all(Radius.circular(radius)),
child: Container(
width: width,
height: height,
color: Theme.of(context).colorScheme.surfaceContainer,
child: FutureBuilder(
future: sticker.getStickerByAlias(segments[1]),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Center(
child: CircularProgressIndicator());
}
return AutoCacheImage(
snapshot.data!.imageUrl,
width: width,
height: height,
fit: fit,
noErrorWidget: true,
);
},
),
),
).paddingSymmetric(vertical: 4);
case 'attachments':
const radius = BorderRadius.all(Radius.circular(8));
return LimitedBox(
maxHeight: MediaQuery.of(context).size.width,
child: ClipRRect(
borderRadius: radius,
child: AttachmentSelfContainedEntry(
isDense: true,
parentId: parentId,
rid: segments[1],
),
),
).paddingSymmetric(vertical: 4);
}
}
return AutoCacheImage(
url,
width: width,
height: height,
fit: fit,
);
},
),
);
if (idx < paragraphs.length - 1) {
contentWidgets.add(isAutoWarp ? const Gap(4) : const Gap(8));
}
}
// Return the list of widgets for the paragraphs
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: contentWidgets,
); );
} }
@ -167,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
@ -183,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
@ -203,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);
}
}

View 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'],
),
),
);
});
}
}

View File

@ -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.newspaper, icon: const Icon(Icons.explore),
label: 'feed'.tr, label: 'explore'.tr,
page: 'feed', 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;

View 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);
},
);
}
}

View File

@ -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),
),
],
),
),
);
}
}

View 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),
),
);
}
}

View File

@ -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(),
),
),
),
],
),
),
);
}
}

View 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,
),
),
);
});
}
}

View File

@ -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),

View File

@ -12,6 +12,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/reports/abuse_report.dart';
class PostAction extends StatefulWidget { class PostAction extends StatefulWidget {
final Post item; final Post item;
@ -149,6 +150,23 @@ class _PostActionState extends State<PostAction> {
Navigator.pop(context); Navigator.pop(context);
}, },
), ),
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Icons.flag),
title: Text('report'.tr),
onTap: () {
showDialog(
context: context,
builder: (context) => AbuseReportDialog(
resourceId: 'post:${widget.item.id}',
),
).then((status) {
if (status == true) {
Navigator.pop(context);
}
});
},
),
if (!widget.noReact) if (!widget.noReact)
ListTile( ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 24), contentPadding: const EdgeInsets.symmetric(horizontal: 24),

View 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),
),
],
),
);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -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,19 @@ 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(
isShowEmbed: isShowEmbed, padding: padding ?? EdgeInsets.zero,
isNestedClickable: isNestedClickable, child: PostListEntryWidget(
isClickable: isClickable, isShowEmbed: isShowEmbed,
showFeaturedReply: true, isNestedClickable: isNestedClickable,
item: item, isClickable: isClickable,
backgroundColor: backgroundColor, showFeaturedReply: true,
onUpdate: () { item: item,
controller.refresh(); backgroundColor: backgroundColor,
}, onUpdate: () {
controller.refresh();
},
),
); );
}, },
), ),
@ -48,7 +55,6 @@ 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;
@ -59,7 +65,6 @@ class PostListEntryWidget extends StatelessWidget {
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,
@ -79,6 +84,22 @@ class PostListEntryWidget extends StatelessWidget {
isClickable: isNestedClickable, isClickable: isNestedClickable,
showFeaturedReply: showFeaturedReply, showFeaturedReply: showFeaturedReply,
backgroundColor: backgroundColor, backgroundColor: backgroundColor,
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 +122,46 @@ 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 Function? onUpdate;
const ControlledPostListWidget({
super.key,
required this.controller,
this.isShowEmbed = true,
this.isClickable = true,
this.isNestedClickable = true,
this.isPinned = true,
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,
item: item,
onUpdate: onUpdate ?? () {},
);
},
),
separatorBuilder: (_, __) => const Divider(thickness: 0.3, height: 0.3),
);
}
}

View File

@ -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();
}
});
}, },
), ),
), ),

View File

@ -53,6 +53,7 @@ class _PostReplyListState extends State<PostReplyList> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return PostListWidget( return PostListWidget(
padding: EdgeInsets.symmetric(horizontal: 10),
isShowEmbed: false, isShowEmbed: false,
controller: _pagingController, controller: _pagingController,
backgroundColor: widget.backgroundColor, backgroundColor: widget.backgroundColor,
@ -70,16 +71,30 @@ class PostReplyListPopup extends StatelessWidget {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Row(
'postReplies'.tr, children: [
style: Theme.of(context).textTheme.headlineSmall, Expanded(
).paddingOnly(left: 24, right: 24, top: 32, bottom: 16), child: Text(
'postReplies'.tr,
style: Theme.of(context).textTheme.headlineSmall,
),
),
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, backgroundColor:
Theme.of(context).colorScheme.surfaceContainerLow,
), ),
], ],
), ),

View File

@ -27,7 +27,7 @@ class PostTagsList extends StatelessWidget {
), ),
), ),
onTap: () { onTap: () {
AppRouter.instance.pushNamed('feedSearch', queryParameters: { AppRouter.instance.pushNamed('postSearch', queryParameters: {
'tag': x.alias, 'tag': x.alias,
}); });
}, },

View File

@ -1,48 +0,0 @@
import 'package:flutter/material.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:solian/models/post.dart';
import 'package:solian/widgets/posts/post_list.dart';
class PostWarpedListWidget extends StatelessWidget {
final bool isShowEmbed;
final bool isClickable;
final bool isNestedClickable;
final bool isPinned;
final PagingController<int, Post> controller;
final Function? onUpdate;
const PostWarpedListWidget({
super.key,
required this.controller,
this.isShowEmbed = true,
this.isClickable = true,
this.isNestedClickable = true,
this.isPinned = true,
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(
renderOrder: index,
isShowEmbed: isShowEmbed,
isNestedClickable: isNestedClickable,
isClickable: isClickable,
showFeaturedReply: true,
item: item,
onUpdate: onUpdate ?? () {},
);
},
),
separatorBuilder: (_, __) => const Divider(thickness: 0.3, height: 0.3),
);
}
}

View File

@ -149,7 +149,8 @@ class _RealmMemberListPopupState extends State<RealmMemberListPopup> {
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,

View File

@ -0,0 +1,23 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:timeago/timeago.dart';
class RelativeDate extends StatelessWidget {
final DateTime date;
final bool isFull;
const RelativeDate(this.date, {super.key, this.isFull = false});
@override
Widget build(BuildContext context) {
if (isFull) {
return Text(DateFormat('y/M/d HH:mm').format(date));
}
return Text(
format(
date,
locale: 'en_short',
),
);
}
}

View File

@ -0,0 +1,107 @@
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:get/get.dart';
import 'package:solian/exceptions/request.dart';
import 'package:solian/exts.dart';
import 'package:solian/providers/auth.dart';
class AbuseReportDialog extends StatefulWidget {
final String? resourceId;
const AbuseReportDialog({super.key, this.resourceId});
@override
State<AbuseReportDialog> createState() => _AbuseReportDialogState();
}
class _AbuseReportDialogState extends State<AbuseReportDialog> {
final TextEditingController _resourceController = TextEditingController();
final TextEditingController _reasonController = TextEditingController();
bool _isBusy = false;
Future<void> _submit() async {
final auth = Get.find<AuthProvider>();
if (!auth.isAuthorized.value) return;
final client = await auth.configureClient('id');
setState(() => _isBusy = true);
final resp = await client.post('/reports/abuse', {
'resource': _resourceController.text,
'reason': _reasonController.text,
});
setState(() => _isBusy = false);
if (resp.statusCode != 200) {
context.showErrorDialog(RequestException(resp));
} else {
context.showSnackbar('reportSubmitted'.tr);
Navigator.pop(context, true);
}
}
@override
void initState() {
if (widget.resourceId != null) {
_resourceController.text = widget.resourceId!;
}
super.initState();
}
@override
void dispose() {
_resourceController.dispose();
_reasonController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text('reportAbuse'.tr),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Gap(4),
TextField(
controller: _resourceController,
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: 'reportAbuseResource'.tr,
enabled: widget.resourceId == null,
isDense: true,
),
),
const Gap(12),
TextField(
controller: _reasonController,
textInputAction: TextInputAction.newline,
minLines: 1,
maxLines: 3,
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: 'reportAbuseReason'.tr,
isDense: true,
),
),
],
),
actions: [
TextButton(
onPressed: _isBusy
? null
: () {
Navigator.pop(context);
},
child: Text('cancel'.tr),
),
TextButton(
onPressed: _isBusy ? null : () => _submit(),
child: Text('okay'.tr),
),
],
);
}
}

View File

@ -0,0 +1,64 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
import 'package:solian/platform.dart';
import 'package:solian/theme.dart';
class RootContainer extends StatelessWidget {
final Widget? child;
const RootContainer({super.key, this.child});
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: PlatformInfo.isWeb
? Future.value(null)
: getApplicationDocumentsDirectory(),
builder: (context, snapshot) {
if (snapshot.hasData) {
final path = '${snapshot.data!.path}/app_background_image';
final file = File(path);
if (file.existsSync()) {
return Material(
color: Theme.of(context).colorScheme.surface,
child: Container(
decoration: BoxDecoration(
backgroundBlendMode: BlendMode.darken,
color: Theme.of(context).colorScheme.surface,
image: DecorationImage(
opacity: 0.2,
image: FileImage(file),
fit: BoxFit.cover,
),
),
child: child,
),
);
}
}
return Material(
color: Theme.of(context).colorScheme.surface,
child: child,
);
},
);
}
}
class ResponsiveRootContainer extends StatelessWidget {
final Widget? child;
const ResponsiveRootContainer({super.key, this.child});
@override
Widget build(BuildContext context) {
if (AppTheme.isLargeScreen(context)) {
return child ?? SizedBox.shrink();
} else {
return RootContainer(child: child);
}
}
}

View File

@ -1,14 +1,17 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:solian/widgets/root_container.dart';
class EmptyPagePlaceholder extends StatelessWidget { class EmptyPagePlaceholder extends StatelessWidget {
const EmptyPagePlaceholder({super.key}); const EmptyPagePlaceholder({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Material( return ResponsiveRootContainer(
color: Theme.of(context).colorScheme.surface,
child: Center( child: Center(
child: Image.asset('assets/logo.png', width: 80, height: 80), child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(12)),
child: Image.asset('assets/logo.png', width: 80, height: 80),
),
), ),
); );
} }

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