Compare commits

...

34 Commits

Author SHA1 Message Date
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
77 changed files with 4179 additions and 2487 deletions

View File

@ -4,3 +4,4 @@ android.enableJetifier=true
android.defaults.buildfeatures.buildconfig=true
android.nonTransitiveRClass=false
android.nonFinalResIds=false
kotlin.jvm.target.validation.mode = IGNORE

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

@ -98,6 +98,8 @@
"accountFriendBlocked": "Friend blocklist",
"accountFriendListHint": "Swipe left to decline, right to approve",
"accountFriendRequestSent": "Friend request sent, waiting for processing...",
"accountBlocked": "Account has been blocked",
"accountUnblocked": "Account has been unblocked",
"accountSuspended": "Account was suspended",
"accountSuspendedAt": "Account was suspended since @date",
"aspectRatio": "Aspect Ratio",
@ -453,5 +455,27 @@
"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"
"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

@ -98,6 +98,8 @@
"accountFriendBlocked": "好友黑名单",
"accountFriendListHint": "左滑来拒绝,右滑来接受",
"accountFriendRequestSent": "好友请求已发送,等待处理对方中……",
"accountBlocked": "已屏蔽账号",
"accountUnblocked": "已解除屏蔽账号",
"accountSuspended": "帐号被停用",
"accountSuspendedAt": "该帐号自 @date 起被停用",
"aspectRatio": "纵横比",
@ -264,7 +266,7 @@
"channelMembersAddHint": "到 @channel",
"channelType": "频道类型",
"channelTypeCommon": "普通频道",
"channelTypeDirect": "私信聊天",
"channelTypeDirect": "私信",
"channelAdjust": "调整频道",
"channelDetail": "频道详情",
"channelSettings": "频道设置",
@ -449,5 +451,27 @@
"accountDeletionConfirm": "确认账号删除请求",
"accountDeletionConfirmDesc": "你确定要删除账号 @account 吗?你将会在其绑定的主要邮件地址收到一封包含着确认删除账号连接的邮件,在二十四小时内使用该连接即可完成删除账号。注意,本操作不可撤销,并且账号创建或关联的所有数据都将被删除,请三思而后行。",
"accountDeletionRequested": "已请求删除账号,检查你的收件箱来确认请求。",
"slideToConfirm": "滑动来确认"
"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):
- DKImagePickerController/PhotoGallery
- Flutter
- Firebase/Analytics (11.0.0):
- Firebase/Analytics (11.2.0):
- Firebase/Core
- Firebase/Core (11.0.0):
- Firebase/Core (11.2.0):
- Firebase/CoreOnly
- FirebaseAnalytics (~> 11.0.0)
- Firebase/CoreOnly (11.0.0):
- FirebaseCore (= 11.0.0)
- Firebase/Crashlytics (11.0.0):
- FirebaseAnalytics (~> 11.2.0)
- Firebase/CoreOnly (11.2.0):
- FirebaseCore (= 11.2.0)
- Firebase/Crashlytics (11.2.0):
- Firebase/CoreOnly
- FirebaseCrashlytics (~> 11.0.0)
- Firebase/Messaging (11.0.0):
- FirebaseCrashlytics (~> 11.2.0)
- Firebase/Messaging (11.2.0):
- Firebase/CoreOnly
- FirebaseMessaging (~> 11.0.0)
- Firebase/Performance (11.0.0):
- FirebaseMessaging (~> 11.2.0)
- Firebase/Performance (11.2.0):
- Firebase/CoreOnly
- FirebasePerformance (~> 11.0.0)
- firebase_analytics (11.3.2):
- Firebase/Analytics (= 11.0.0)
- FirebasePerformance (~> 11.2.0)
- firebase_analytics (11.3.3):
- Firebase/Analytics (= 11.2.0)
- firebase_core
- Flutter
- firebase_core (3.5.0):
- Firebase/CoreOnly (= 11.0.0)
- firebase_core (3.6.0):
- Firebase/CoreOnly (= 11.2.0)
- Flutter
- firebase_crashlytics (4.1.2):
- Firebase/Crashlytics (= 11.0.0)
- firebase_crashlytics (4.1.3):
- Firebase/Crashlytics (= 11.2.0)
- firebase_core
- Flutter
- firebase_messaging (15.1.2):
- Firebase/Messaging (= 11.0.0)
- firebase_messaging (15.1.3):
- Firebase/Messaging (= 11.2.0)
- firebase_core
- Flutter
- firebase_performance (0.10.0-7):
- Firebase/Performance (= 11.0.0)
- firebase_performance (0.10.0-8):
- Firebase/Performance (= 11.2.0)
- firebase_core
- Flutter
- FirebaseABTesting (11.2.0):
- FirebaseABTesting (11.3.0):
- FirebaseCore (~> 11.0)
- FirebaseAnalytics (11.0.0):
- FirebaseAnalytics/AdIdSupport (= 11.0.0)
- FirebaseAnalytics (11.2.0):
- FirebaseAnalytics/AdIdSupport (= 11.2.0)
- FirebaseCore (~> 11.0)
- FirebaseInstallations (~> 11.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
@ -84,24 +84,24 @@ PODS:
- GoogleUtilities/Network (~> 8.0)
- "GoogleUtilities/NSData+zlib (~> 8.0)"
- nanopb (~> 3.30910.0)
- FirebaseAnalytics/AdIdSupport (11.0.0):
- FirebaseAnalytics/AdIdSupport (11.2.0):
- FirebaseCore (~> 11.0)
- FirebaseInstallations (~> 11.0)
- GoogleAppMeasurement (= 11.0.0)
- GoogleAppMeasurement (= 11.2.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
- GoogleUtilities/MethodSwizzler (~> 8.0)
- GoogleUtilities/Network (~> 8.0)
- "GoogleUtilities/NSData+zlib (~> 8.0)"
- nanopb (~> 3.30910.0)
- FirebaseCore (11.0.0):
- FirebaseCore (11.2.0):
- FirebaseCoreInternal (~> 11.0)
- GoogleUtilities/Environment (~> 8.0)
- GoogleUtilities/Logger (~> 8.0)
- FirebaseCoreExtension (11.2.0):
- FirebaseCoreExtension (11.3.0):
- FirebaseCore (~> 11.0)
- FirebaseCoreInternal (11.2.0):
- FirebaseCoreInternal (11.3.0):
- "GoogleUtilities/NSData+zlib (~> 8.0)"
- FirebaseCrashlytics (11.0.0):
- FirebaseCrashlytics (11.2.0):
- FirebaseCore (~> 11.0)
- FirebaseInstallations (~> 11.0)
- FirebaseRemoteConfigInterop (~> 11.0)
@ -110,12 +110,12 @@ PODS:
- GoogleUtilities/Environment (~> 8.0)
- nanopb (~> 3.30910.0)
- PromisesObjC (~> 2.4)
- FirebaseInstallations (11.2.0):
- FirebaseInstallations (11.3.0):
- FirebaseCore (~> 11.0)
- GoogleUtilities/Environment (~> 8.0)
- GoogleUtilities/UserDefaults (~> 8.0)
- PromisesObjC (~> 2.4)
- FirebaseMessaging (11.0.0):
- FirebaseMessaging (11.2.0):
- FirebaseCore (~> 11.0)
- FirebaseInstallations (~> 11.0)
- GoogleDataTransport (~> 10.0)
@ -124,7 +124,7 @@ PODS:
- GoogleUtilities/Reachability (~> 8.0)
- GoogleUtilities/UserDefaults (~> 8.0)
- nanopb (~> 3.30910.0)
- FirebasePerformance (11.0.0):
- FirebasePerformance (11.2.0):
- FirebaseCore (~> 11.0)
- FirebaseInstallations (~> 11.0)
- FirebaseRemoteConfig (~> 11.0)
@ -134,7 +134,7 @@ PODS:
- GoogleUtilities/MethodSwizzler (~> 8.0)
- GoogleUtilities/UserDefaults (~> 8.0)
- nanopb (~> 3.30910.0)
- FirebaseRemoteConfig (11.2.0):
- FirebaseRemoteConfig (11.3.0):
- FirebaseABTesting (~> 11.0)
- FirebaseCore (~> 11.0)
- FirebaseInstallations (~> 11.0)
@ -142,8 +142,8 @@ PODS:
- FirebaseSharedSwift (~> 11.0)
- GoogleUtilities/Environment (~> 8.0)
- "GoogleUtilities/NSData+zlib (~> 8.0)"
- FirebaseRemoteConfigInterop (11.2.0)
- FirebaseSessions (11.2.0):
- FirebaseRemoteConfigInterop (11.3.0)
- FirebaseSessions (11.3.0):
- FirebaseCore (~> 11.0)
- FirebaseCoreExtension (~> 11.0)
- FirebaseInstallations (~> 11.0)
@ -152,7 +152,7 @@ PODS:
- GoogleUtilities/UserDefaults (~> 8.0)
- nanopb (~> 3.30910.0)
- PromisesSwift (~> 2.1)
- FirebaseSharedSwift (11.2.0)
- FirebaseSharedSwift (11.3.0)
- Flutter (1.0.0)
- flutter_app_update (0.0.1):
- Flutter
@ -172,21 +172,21 @@ PODS:
- gal (1.0.0):
- Flutter
- FlutterMacOS
- GoogleAppMeasurement (11.0.0):
- GoogleAppMeasurement/AdIdSupport (= 11.0.0)
- GoogleAppMeasurement (11.2.0):
- GoogleAppMeasurement/AdIdSupport (= 11.2.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
- GoogleUtilities/MethodSwizzler (~> 8.0)
- GoogleUtilities/Network (~> 8.0)
- "GoogleUtilities/NSData+zlib (~> 8.0)"
- nanopb (~> 3.30910.0)
- GoogleAppMeasurement/AdIdSupport (11.0.0):
- GoogleAppMeasurement/WithoutAdIdSupport (= 11.0.0)
- GoogleAppMeasurement/AdIdSupport (11.2.0):
- GoogleAppMeasurement/WithoutAdIdSupport (= 11.2.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
- GoogleUtilities/MethodSwizzler (~> 8.0)
- GoogleUtilities/Network (~> 8.0)
- "GoogleUtilities/NSData+zlib (~> 8.0)"
- nanopb (~> 3.30910.0)
- GoogleAppMeasurement/WithoutAdIdSupport (11.0.0):
- GoogleAppMeasurement/WithoutAdIdSupport (11.2.0):
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
- GoogleUtilities/MethodSwizzler (~> 8.0)
- GoogleUtilities/Network (~> 8.0)
@ -227,6 +227,8 @@ PODS:
- TOCropViewController (~> 2.7.4)
- image_picker_ios (0.0.1):
- Flutter
- in_app_review (0.2.0):
- Flutter
- livekit_client (2.2.6):
- Flutter
- WebRTC-SDK (= 125.6422.04)
@ -318,6 +320,7 @@ DEPENDENCIES:
- gal (from `.symlinks/plugins/gal/darwin`)
- image_cropper (from `.symlinks/plugins/image_cropper/ios`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- in_app_review (from `.symlinks/plugins/in_app_review/ios`)
- livekit_client (from `.symlinks/plugins/livekit_client/ios`)
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
- media_kit_native_event_loop (from `.symlinks/plugins/media_kit_native_event_loop/ios`)
@ -406,6 +409,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/image_cropper/ios"
image_picker_ios:
:path: ".symlinks/plugins/image_picker_ios/ios"
in_app_review:
:path: ".symlinks/plugins/in_app_review/ios"
livekit_client:
:path: ".symlinks/plugins/livekit_client/ios"
media_kit_libs_ios_video:
@ -449,25 +454,25 @@ SPEC CHECKSUMS:
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
Firebase: 9f574c08c2396885b5e7e100ed4293d956218af9
firebase_analytics: 4fd10182fd08bb8358f26ac8aca8dad7b6d0f592
firebase_core: 2ec6b789859c7c24766344ec71fdf78639402d56
firebase_crashlytics: 60630a0f91ee432275fa1660fd8593079761448a
firebase_messaging: a18e1e02b2e8e69097c8173e0c851be223b21c50
firebase_performance: 12d45fdf120992fa879d990929bf73d4a5ced053
FirebaseABTesting: 2104d957ce33888a3d6f3bde298cdee376dde8f1
FirebaseAnalytics: 27eb78b97880ea4a004839b9bac0b58880f5a92a
FirebaseCore: 3cf438f431f18c12cdf2aaf64434648b63f7e383
FirebaseCoreExtension: cda74ddfb001224bd8fd1d6e74698b4ed07803de
FirebaseCoreInternal: 0c569513412da9f3b31bd0b340013bbee8f295c5
FirebaseCrashlytics: 745d8f0221fe49c62865391d1bf56f5a12eeec0b
FirebaseInstallations: 771177d89d6c451dc6e50085ec82e2fc77ed0a4a
FirebaseMessaging: d2d1d9c62c46dd2db49a952f7deb5b16ad2c9742
FirebasePerformance: efdc02bacb1b4710588c9f867011605c081cdf79
FirebaseRemoteConfig: fca0b2d017fc1de52b28a4e5bcf2007c1a840457
FirebaseRemoteConfigInterop: 477b26fdeb8fb5fbaf22fa9db5343b42289dc7db
FirebaseSessions: adcec8b72d0066a385e3affcd1bcb1ebb3908ce6
FirebaseSharedSwift: 7a0d78d155ede78407f0fdc89fbc914014c7c540
Firebase: 98e6bf5278170668a7983e12971a66b2cd57fc8c
firebase_analytics: fbc57838bdb94eef1e0ff504f127d974ff2981ad
firebase_core: 2bedc3136ec7c7b8561c6123ed0239387b53f2af
firebase_crashlytics: 37d104d457b51760b48504a93a12b3bf70995d77
firebase_messaging: 15d114e1a41fc31e4fbabcd48d765a19eec94a38
firebase_performance: 26ad47755d3e8d7b04b9bb36bdfbf1cec8d8dfcc
FirebaseABTesting: c4559fcd2eba9f6bdaf0599e2c37ded01c343e4c
FirebaseAnalytics: c36efd5710c60c17558650fa58c2066eca7e9265
FirebaseCore: a282032ae9295c795714ded2ec9c522fc237f8da
FirebaseCoreExtension: 30bb063476ef66cd46925243d64ad8b2c8ac3264
FirebaseCoreInternal: ac26d09a70c730e497936430af4e60fb0c68ec4e
FirebaseCrashlytics: cfc69af5b53565dc6a5e563788809b5778ac4eac
FirebaseInstallations: 58cf94dabf1e2bb2fa87725a9be5c2249171cda0
FirebaseMessaging: c9ec7b90c399c7a6100297e9d16f8a27fc7f7152
FirebasePerformance: c39138c0700b8ef6040f0b80b5707320808e2862
FirebaseRemoteConfig: 5be2ca4f9870d475b39214210955fdaeecf7e5ca
FirebaseRemoteConfigInterop: c3a5c31b3c22079f41ba1dc645df889d9ce38cb9
FirebaseSessions: 655ff17f3cc1a635cbdc2d69b953878001f9e25b
FirebaseSharedSwift: d39c2ad64a11a8d936ce25a42b00df47078bb59c
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_app_update: 65f61da626cb111d1b24674abc4b01728d7723bc
flutter_background_service_ios: e30e0d3ee69e4cee66272d0c78eacd48c2e94aac
@ -477,11 +482,12 @@ SPEC CHECKSUMS:
flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
flutter_webrtc: 75b868e4f9e817c7a9a42ca4b6169063de4eec9f
gal: 61e868295d28fe67ffa297fae6dacebf56fd53e1
GoogleAppMeasurement: 6e49ffac7d3f2c3ded9cc663f912a13b67bbd0de
GoogleAppMeasurement: 76d4f8b36b03bd8381fa9a7fe2cc7f99c0a2e93a
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
image_cropper: 37d40f62177c101ff4c164906d259ea2c3aa70cf
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
in_app_review: 318597b3a06c22bb46dc454d56828c85f444f99d
livekit_client: 20e01637431bc108dad451c8a11c1d206e1dd2cd
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a

View File

@ -81,6 +81,11 @@
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>CFBundleLocalizations</key>
<array>
<string>zh_CN</string>
<string>en</string>
</array>
<key>UIStatusBarHidden</key>
<false/>
</dict>

View File

@ -1,8 +1,10 @@
import 'dart:async';
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:get/get.dart';
import 'package:in_app_review/in_app_review.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
@ -10,12 +12,12 @@ import 'package:solian/exceptions/request.dart';
import 'package:solian/exts.dart';
import 'package:solian/platform.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/providers/content/channel.dart';
import 'package:solian/providers/content/realm.dart';
import 'package:solian/providers/relation.dart';
import 'package:solian/providers/theme_switcher.dart';
import 'package:solian/providers/websocket.dart';
import 'package:solian/services.dart';
import 'package:solian/widgets/root_container.dart';
import 'package:solian/widgets/sized_container.dart';
import 'package:flutter_app_update/flutter_app_update.dart';
import 'package:version/version.dart';
@ -42,6 +44,27 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
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(
@ -175,8 +198,6 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
final AuthProvider auth = Get.find();
try {
await Future.wait([
if (auth.isAuthorized.isTrue)
Get.find<ChannelProvider>().refreshAvailableChannel(),
if (auth.isAuthorized.isTrue)
Get.find<RelationshipProvider>().refreshRelativeList(),
if (auth.isAuthorized.isTrue)
@ -226,14 +247,16 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
super.initState();
_runPeriods();
_checkForUpdate();
_bootCompleter.future.then((_) {
_requestRating();
});
}
@override
Widget build(BuildContext context) {
if (_isBusy || _isErrored) {
return GestureDetector(
child: Material(
color: Theme.of(context).colorScheme.surface,
child: RootContainer(
child: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceAround,

View File

@ -57,6 +57,8 @@ void main() async {
Future<void> _initializeFirebase() async {
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
if (PlatformInfo.isIOS || PlatformInfo.isAndroid || PlatformInfo.isMacOS) {
// Initialize firebase crashlytics for the platform that supported
FlutterError.onError = (errorDetails) {
FirebaseCrashlytics.instance.recordFlutterFatalError(errorDetails);
};
@ -65,6 +67,7 @@ Future<void> _initializeFirebase() async {
return true;
};
}
}
Future<void> _initializeBackgroundNotificationService() async {
autoConfigureBackgroundNotificationService();

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 canRateTheApp => isIOS || isMacOS;
static bool get canCropImage => isIOS || isAndroid || isWeb;
static bool get canRecord => (isMobile || isMacOS);
static bool get canPushNotification => isAndroid || isIOS || isMacOS;

View File

@ -392,7 +392,7 @@ class ChatCallProvider extends GetxController {
}
Future gotoScreen(BuildContext context) {
return Navigator.of(context, rootNavigator: true).push(
return Navigator.of(context).push(
MaterialPageRoute(builder: (context) => const CallScreen()),
);
}

View File

@ -9,25 +9,6 @@ import 'package:uuid/uuid.dart';
class ChannelProvider extends GetxController {
RxBool isLoading = false.obs;
RxList<Channel> availableChannels = RxList.empty(growable: true);
List<Channel> get groupChannels =>
availableChannels.where((x) => x.type == 0).toList();
List<Channel> get directChannels =>
availableChannels.where((x) => x.type == 1).toList();
Future<void> refreshAvailableChannel() async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
isLoading.value = true;
final resp = await listAvailableChannel();
isLoading.value = false;
availableChannels.value =
resp.body.map((x) => Channel.fromJson(x)).toList().cast<Channel>();
availableChannels.refresh();
}
Future<Response> getChannel(String alias, {String realm = 'global'}) async {
final AuthProvider auth = Get.find();
@ -89,18 +70,22 @@ class ChannelProvider extends GetxController {
return resp;
}
Future<Response> listAvailableChannel({String scope = 'global'}) async {
Future<List<Channel>> listAvailableChannel({
String scope = 'global',
bool isDirect = false,
}) async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
final client = await auth.configureClient('messaging');
final resp = await client.get('/channels/$scope/me/available');
final resp =
await client.get('/channels/$scope/me/available?direct=$isDirect');
if (resp.statusCode != 200) {
throw RequestException(resp);
}
return resp;
return List.from(resp.body.map((x) => Channel.fromJson(x)));
}
Future<Response> createChannel(String scope, dynamic payload) async {

View File

@ -1,3 +1,6 @@
import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:drift/drift.dart';
import 'package:get/get.dart' hide Value;
import 'package:solian/exceptions/request.dart';
@ -182,4 +185,26 @@ class MessagesFetchingProvider extends GetxController {
..orderBy([(t) => OrderingTerm.desc(t.id)]))
.getSingleOrNull();
}
Future<Map<int, List<LocalMessageEventTableData>>>
getLastInAllChannels() async {
final database = Get.find<DatabaseProvider>().database;
final rows = await database.customSelect('''
SELECT id, channel_id, data, created_at
FROM ${database.localMessageEventTable.actualTableName}
WHERE (channel_id, created_at) IN (
SELECT channel_id, MAX(created_at)
FROM ${database.localMessageEventTable.actualTableName}
GROUP BY channel_id
)
''', readsFrom: {database.localMessageEventTable}).get();
return rows.map((row) {
return LocalMessageEventTableData(
id: row.read<int>('id'),
channelId: row.read<int>('channel_id'),
data: Event.fromJson(jsonDecode(row.read<String>('data'))),
createdAt: row.read<DateTime>('created_at'),
);
}).groupListsBy((x) => x.channelId);
}
}

View File

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

View File

@ -1,5 +1,8 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:solian/models/theme.dart';
import 'package:solian/theme.dart';
class ThemeSwitcher extends ChangeNotifier {
@ -13,11 +16,21 @@ class ThemeSwitcher extends ChangeNotifier {
Future<void> restoreTheme() async {
final prefs = await SharedPreferences.getInstance();
if (prefs.containsKey('global_theme_color')) {
final value = prefs.getInt('global_theme_color')!;
final color = Color(value);
lightThemeData = AppTheme.build(Brightness.light, seedColor: color);
darkThemeData = AppTheme.build(Brightness.dark, seedColor: color);
if (prefs.containsKey('global_theme')) {
final value = SolianThemeData.fromJson(
jsonDecode(prefs.getString('global_theme')!),
);
final agedTheme = prefs.getBool('aged_theme');
lightThemeData = AppTheme.buildFromData(
Brightness.light,
value,
useMaterial3: agedTheme == null ? true : !agedTheme,
);
darkThemeData = AppTheme.buildFromData(
Brightness.dark,
value,
useMaterial3: agedTheme == null ? true : !agedTheme,
);
notifyListeners();
}
}
@ -27,4 +40,25 @@ class ThemeSwitcher extends ChangeNotifier {
darkThemeData = dark;
notifyListeners();
}
Future<void> setThemeData(SolianThemeData? data) async {
final prefs = await SharedPreferences.getInstance();
if (data == null) {
prefs.remove('global_theme');
} else {
prefs.setString(
'global_theme',
jsonEncode(data.toJson()),
);
lightThemeData = AppTheme.buildFromData(Brightness.light, data);
darkThemeData = AppTheme.buildFromData(Brightness.dark, data);
notifyListeners();
}
}
Future<void> setAgedTheme(bool enabled) async {
final prefs = await SharedPreferences.getInstance();
prefs.setBool('aged_theme', enabled);
await restoreTheme();
}
}

View File

@ -28,6 +28,8 @@ import 'package:solian/screens/posts/post_editor.dart';
import 'package:solian/screens/settings.dart';
import 'package:solian/shells/root_shell.dart';
import 'package:solian/shells/title_shell.dart';
import 'package:solian/theme.dart';
import 'package:solian/widgets/sidebar/empty_placeholder.dart';
abstract class AppRouter {
static GoRouter instance = GoRouter(
@ -137,12 +139,15 @@ abstract class AppRouter {
);
static final ShellRoute _chatRoute = ShellRoute(
builder: (context, state, child) => child,
builder: (context, state, child) =>
AppTheme.isLargeScreen(context) ? ChatListShell(child: child) : child,
routes: [
GoRoute(
path: '/chat',
name: 'chat',
builder: (context, state) => const ChatScreen(),
builder: (context, state) => AppTheme.isLargeScreen(context)
? const EmptyPagePlaceholder()
: const ChatScreen(),
),
GoRoute(
path: '/chat/organize',
@ -173,6 +178,7 @@ abstract class AppRouter {
final arguments = state.extra as ChannelDetailArguments;
return TitleShell(
state: state,
isResponsive: true,
child: ChannelDetailScreen(
channel: arguments.channel,
profile: arguments.profile,

View File

@ -1,7 +1,9 @@
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:solian/widgets/sized_container.dart';
import 'package:url_launcher/url_launcher_string.dart';
@ -13,9 +15,7 @@ class AboutScreen extends StatelessWidget {
const denseButtonStyle =
ButtonStyle(visualDensity: VisualDensity(vertical: -4));
return Material(
color: Theme.of(context).colorScheme.surface,
child: SizedBox(
return SizedBox(
width: double.infinity,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
@ -52,8 +52,8 @@ class AboutScreen extends StatelessWidget {
CenteredContainer(
maxWidth: 280,
child: Wrap(
spacing: 8,
runSpacing: 8,
spacing: 4,
runSpacing: 4,
alignment: WrapAlignment.center,
children: [
TextButton(
@ -92,6 +92,13 @@ class AboutScreen extends StatelessWidget {
launchUrlString('https://solsynth.dev/terms');
},
),
TextButton(
style: denseButtonStyle,
child: Text('serviceStatus'.tr),
onPressed: () {
launchUrlString('https://status.solsynth.dev');
},
),
],
),
),
@ -103,8 +110,35 @@ class AboutScreen extends StatelessWidget {
fontSize: 12,
),
),
],
FutureBuilder(
future: SharedPreferences.getInstance(),
builder: (context, snapshot) {
const textStyle = TextStyle(
fontWeight: FontWeight.w300,
fontSize: 12,
);
if (!snapshot.hasData ||
!snapshot.data!.containsKey('first_boot_time')) {
return Text(
'firstBootTime'.trParams({'time': 'unknown'.tr}),
style: textStyle,
);
} else {
return Text(
'firstBootTime'.trParams({
'time': DateFormat('yyyy-MM-dd').format(
DateTime.tryParse(
snapshot.data!.getString('first_boot_time')!,
)?.toLocal() ??
DateTime.now(),
),
}),
style: textStyle,
);
}
},
),
],
),
);
}

View File

@ -49,9 +49,7 @@ class _AccountScreenState extends State<AccountScreen> {
final AuthProvider auth = Get.find();
return Material(
color: Theme.of(context).colorScheme.surface,
child: SafeArea(
return SafeArea(
child: Obx(() {
if (auth.isAuthorized.isFalse) {
return Center(
@ -108,7 +106,7 @@ class _AccountScreenState extends State<AccountScreen> {
child: ListView(
children: [
if (auth.userProfile.value != null)
const AccountHeading().paddingOnly(bottom: 8, top: 8),
const AccountHeading().paddingOnly(bottom: 8, top: 16),
...(actionItems.map(
(x) => ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 34),
@ -155,7 +153,6 @@ class _AccountScreenState extends State<AccountScreen> {
),
);
}),
),
);
}
}

View File

@ -6,6 +6,7 @@ import 'package:solian/models/relations.dart';
import 'package:solian/providers/relation.dart';
import 'package:solian/theme.dart';
import 'package:solian/widgets/account/relative_list.dart';
import 'package:solian/widgets/root_container.dart';
class FriendScreen extends StatefulWidget {
const FriendScreen({super.key});
@ -117,8 +118,7 @@ class _FriendScreenState extends State<FriendScreen>
@override
Widget build(BuildContext context) {
return Material(
color: Theme.of(context).colorScheme.surface,
return RootContainer(
child: Scaffold(
appBar: AppBar(
centerTitle: false,

View File

@ -74,9 +74,7 @@ class _NotificationPreferencesScreenState
@override
Widget build(BuildContext context) {
return Material(
color: Theme.of(context).colorScheme.surface,
child: Column(
return Column(
children: [
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
ListTile(
@ -112,7 +110,6 @@ class _NotificationPreferencesScreenState
),
),
],
),
);
}
}

View File

@ -1,5 +1,3 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:gap/gap.dart';
@ -9,6 +7,7 @@ import 'package:image_picker/image_picker.dart';
import 'package:intl/intl.dart';
import 'package:solian/exts.dart';
import 'package:solian/models/attachment.dart';
import 'package:solian/platform.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/providers/content/attachment.dart';
import 'package:solian/services.dart';
@ -77,9 +76,12 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) return;
XFile file;
final image = await _imagePicker.pickImage(source: ImageSource.gallery);
if (image == null) return;
if (PlatformInfo.canCropImage) {
CroppedFile? croppedFile = await ImageCropper().cropImage(
sourcePath: image.path,
uiSettings: [
@ -106,7 +108,10 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
);
if (croppedFile == null) return;
final file = File(croppedFile.path);
file = XFile(croppedFile.path);
} else {
file = XFile(image.path);
}
setState(() => _isBusy = true);
@ -181,9 +186,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
Widget build(BuildContext context) {
const double padding = 32;
return Material(
color: Theme.of(context).colorScheme.surface,
child: ListView(
return ListView(
children: [
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
const Gap(24),
@ -338,7 +341,6 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
],
).paddingSymmetric(horizontal: padding),
],
),
);
}

View File

@ -13,6 +13,7 @@ import 'package:solian/models/attachment.dart';
import 'package:solian/models/daily_sign.dart';
import 'package:solian/models/pagination.dart';
import 'package:solian/models/post.dart';
import 'package:solian/models/relations.dart';
import 'package:solian/models/subscription.dart';
import 'package:solian/providers/account_status.dart';
import 'package:solian/providers/relation.dart';
@ -26,6 +27,8 @@ import 'package:solian/widgets/attachments/attachment_list.dart';
import 'package:solian/widgets/daily_sign/history_chart.dart';
import 'package:solian/widgets/posts/post_list.dart';
import 'package:solian/widgets/posts/post_warped_list.dart';
import 'package:solian/widgets/reports/abuse_report.dart';
import 'package:solian/widgets/root_container.dart';
import 'package:solian/widgets/sized_container.dart';
class AccountProfilePage extends StatefulWidget {
@ -50,6 +53,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
Account? _userinfo;
Subscription? _subscription;
Relationship? _relationship;
List<Post> _pinnedPosts = List.empty();
List<DailySignRecord> _dailySignRecords = List.empty();
int _totalUpvote = 0, _totalDownvote = 0;
@ -61,6 +65,15 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
setState(() => _isSubscribing = false);
}
Future<void> _getRelationship() async {
setState(() => _isBusy = true);
final relations = Get.find<RelationshipProvider>();
_relationship = await relations.getRelationship(_userinfo!.id);
setState(() => _isBusy = false);
}
Future<void> _getUserinfo() async {
setState(() => _isBusy = true);
@ -120,6 +133,63 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
}
}
Future<void> _subscribeToUser() async {
setState(() => _isSubscribing = true);
_subscription =
await Get.find<SubscriptionProvider>().subscribeToUser(_userinfo!.id);
setState(() => _isSubscribing = false);
}
Future<void> _unsubscribeFromUser() async {
setState(() => _isSubscribing = true);
await Get.find<SubscriptionProvider>().unsubscribeFromUser(_userinfo!.id);
_subscription = null;
setState(() => _isSubscribing = false);
}
Future<void> _makeFriend() async {
setState(() => _isMakingFriend = true);
try {
_relationship = await _relationshipProvider.makeFriend(widget.name);
context.showSnackbar(
'accountFriendRequestSent'.tr,
);
} catch (e) {
context.showErrorDialog(e);
} finally {
setState(() => _isMakingFriend = false);
}
}
Future<void> _blockUser() async {
setState(() => _isMakingFriend = true);
try {
_relationship = await _relationshipProvider.blockUser(widget.name);
context.showSnackbar(
'accountBlocked'.tr,
);
} catch (e) {
context.showErrorDialog(e);
} finally {
setState(() => _isMakingFriend = false);
}
}
Future<void> _unblockUser() async {
setState(() => _isMakingFriend = true);
try {
_relationship =
await _relationshipProvider.editRelation(_userinfo!.id, 1);
context.showSnackbar(
'accountUnblocked'.tr,
);
} catch (e) {
context.showErrorDialog(e);
} finally {
setState(() => _isMakingFriend = false);
}
}
int get _userSocialCreditPoints {
return _totalUpvote * 2 - _totalDownvote + _postController.postTotal.value;
}
@ -151,37 +221,24 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
});
_getUserinfo().then((_) {
_getRelationship();
_getSubscription();
_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
Widget build(BuildContext context) {
if (_isBusy || _userinfo == null) {
return RootContainer(
child: const Center(
child: CircularProgressIndicator(),
),
);
}
@override
Widget build(BuildContext context) {
if (_isBusy || _userinfo == null) {
return const Center(child: CircularProgressIndicator());
}
return Material(
color: Theme.of(context).colorScheme.surface,
return RootContainer(
child: DefaultTabController(
length: 3,
child: NestedScrollView(
@ -197,7 +254,11 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
height: 56,
child: Row(
children: [
AppBarLeadingButton.adaptive(context) ?? const Gap(8),
AppBarLeadingButton.adaptive(
context,
forceBack: true,
) ??
const Gap(8),
const Gap(8),
if (_userinfo != null)
AccountAvatar(content: _userinfo!.avatar, radius: 16),
@ -221,59 +282,31 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
),
),
if (_userinfo != null && _subscription == null)
OutlinedButton(
IconButton(
style: const ButtonStyle(
visualDensity:
VisualDensity(horizontal: -4, vertical: -2),
),
onPressed: _isSubscribing
? null
: () async {
setState(() => _isSubscribing = true);
_subscription =
await Get.find<SubscriptionProvider>()
.subscribeToUser(_userinfo!.id);
setState(() => _isSubscribing = false);
},
child: Text('subscribe'.tr),
onPressed: _isSubscribing ? null : _subscribeToUser,
icon: const Icon(Icons.add_circle_outline),
tooltip: 'subscribe'.tr,
)
else if (_userinfo != null)
OutlinedButton(
IconButton(
style: const ButtonStyle(
visualDensity:
VisualDensity(horizontal: -4, vertical: -2),
),
onPressed: _isSubscribing
? null
: () async {
setState(() => _isSubscribing = true);
await Get.find<SubscriptionProvider>()
.unsubscribeFromUser(_userinfo!.id);
_subscription = null;
setState(() => _isSubscribing = false);
},
child: Text('unsubscribe'.tr),
onPressed:
_isSubscribing ? null : _unsubscribeFromUser,
icon: const Icon(Icons.remove_circle_outline),
tooltip: 'unsubscribe'.tr,
),
if (_userinfo != null &&
!_relationshipProvider.hasFriend(_userinfo!))
if (_userinfo != null && _relationship == null)
IconButton(
icon: const Icon(Icons.person_add),
onPressed: _isMakingFriend
? null
: () async {
setState(() => _isMakingFriend = true);
try {
await _relationshipProvider
.makeFriend(widget.name);
context.showSnackbar(
'accountFriendRequestSent'.tr,
);
} catch (e) {
context.showErrorDialog(e);
} finally {
setState(() => _isMakingFriend = false);
}
},
onPressed: _isMakingFriend ? null : _makeFriend,
tooltip: 'friendAdd'.tr,
)
else
const IconButton(
@ -300,8 +333,8 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
physics: const NeverScrollableScrollPhysics(),
children: [
ListView(
padding: const EdgeInsets.only(top: 16, bottom: 16),
children: [
const Gap(16),
CenteredContainer(
child: AccountHeadingWidget(
name: _userinfo!.name,
@ -421,9 +454,82 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
),
),
).marginOnly(
right: 24, left: 12, bottom: 8, top: 24),
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),
),
],
),
),
),
],
),
),
],
@ -440,7 +546,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildStatisticsEntry(
_StatsWidget(
'totalSocialCreditPoints'.tr,
_userinfo != null
? _userSocialCreditPoints.toString()
@ -453,16 +559,16 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Obx(
() => _buildStatisticsEntry(
() => _StatsWidget(
'totalPostCount'.tr,
_postController.postTotal.value.toString(),
),
),
_buildStatisticsEntry(
_StatsWidget(
'totalUpvote'.tr,
_totalUpvote.toString(),
),
_buildStatisticsEntry(
_StatsWidget(
'totalDownvote'.tr,
_totalDownvote.toString(),
),
@ -560,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,7 +7,6 @@ import 'package:solian/exceptions/request.dart';
import 'package:solian/exts.dart';
import 'package:solian/models/auth.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/providers/content/channel.dart';
import 'package:solian/providers/content/realm.dart';
import 'package:solian/providers/relation.dart';
import 'package:solian/providers/websocket.dart';
@ -177,7 +176,6 @@ class _SignInScreenState extends State<SignInScreen> {
await auth.refreshAuthorizeStatus();
await auth.refreshUserProfile();
Get.find<ChannelProvider>().refreshAvailableChannel();
Get.find<RealmProvider>().refreshAvailableRealms();
Get.find<RelationshipProvider>().refreshRelativeList();
Get.find<WebSocketProvider>().registerPushNotifications();
@ -218,10 +216,10 @@ class _SignInScreenState extends State<SignInScreen> {
@override
Widget build(BuildContext context) {
return Material(
color: Theme.of(context).colorScheme.surface,
child: CenteredContainer(
return CenteredContainer(
maxWidth: 360,
child: Theme(
data: Theme.of(context).copyWith(canvasColor: Colors.transparent),
child: PageTransitionSwitcher(
transitionBuilder: (
Widget child,

View File

@ -65,9 +65,7 @@ class _SignUpScreenState extends State<SignUpScreen> {
@override
Widget build(BuildContext context) {
return Material(
color: Theme.of(context).colorScheme.surface,
child: CenteredContainer(
return CenteredContainer(
maxWidth: 360,
child: ListView(
shrinkWrap: true,
@ -96,8 +94,7 @@ class _SignUpScreenState extends State<SignUpScreen> {
border: const OutlineInputBorder(),
labelText: 'username'.tr,
),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
const Gap(12),
TextField(
@ -110,8 +107,7 @@ class _SignUpScreenState extends State<SignUpScreen> {
border: const OutlineInputBorder(),
labelText: 'nickname'.tr,
),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
const Gap(12),
TextField(
@ -124,8 +120,7 @@ class _SignUpScreenState extends State<SignUpScreen> {
border: const OutlineInputBorder(),
labelText: 'email'.tr,
),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
const Gap(12),
TextField(
@ -139,8 +134,7 @@ class _SignUpScreenState extends State<SignUpScreen> {
border: const OutlineInputBorder(),
labelText: 'password'.tr,
),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
onSubmitted: (_) => _performAction(context),
),
const Gap(8),
@ -206,7 +200,6 @@ class _SignUpScreenState extends State<SignUpScreen> {
),
)
],
),
).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_participant.dart';
import 'package:livekit_client/livekit_client.dart' as livekit;
import 'package:solian/widgets/root_container.dart';
class CallScreen extends StatefulWidget {
final bool hideAppBar;
@ -197,8 +198,7 @@ class _CallScreenState extends State<CallScreen> with TickerProviderStateMixin {
Widget build(BuildContext context) {
final ChatCallProvider ctrl = Get.find();
return Material(
color: Theme.of(context).colorScheme.surface,
return RootContainer(
child: Scaffold(
appBar: widget.hideAppBar
? null

View File

@ -3,6 +3,7 @@ import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:solian/controllers/chat_events_controller.dart';
import 'package:solian/exts.dart';
import 'package:solian/models/call.dart';
@ -25,6 +26,7 @@ import 'package:solian/widgets/chat/chat_event_list.dart';
import 'package:solian/widgets/chat/chat_message_input.dart';
import 'package:solian/widgets/chat/chat_typing_indicator.dart';
import 'package:solian/widgets/current_state_action.dart';
import 'package:solian/widgets/root_container.dart';
class ChannelChatScreen extends StatefulWidget {
final String alias;
@ -179,6 +181,8 @@ class _ChannelChatScreenState extends State<ChannelChatScreen>
}
}
late SharedPreferences _prefs;
@override
void initState() {
super.initState();
@ -189,11 +193,14 @@ class _ChannelChatScreenState extends State<ChannelChatScreen>
_chatController = ChatEventController();
_chatController.initialize();
SharedPreferences.getInstance().then((inst) {
_prefs = inst;
_getOngoingCall();
_getChannel().then((_) {
_chatController.getInitialEvents(_channel!, widget.realm);
_listenMessages();
});
});
}
@override
@ -201,16 +208,18 @@ class _ChannelChatScreenState extends State<ChannelChatScreen>
String title = _channel?.name ?? 'loading'.tr;
String? placeholder;
if (_channel?.type == 1) {
final otherside =
_channel!.members!.where((e) => e.account.id != _accountId).first;
_channel?.members!.where((e) => e.account.id != _accountId).firstOrNull;
if (_channel?.type == 1 && otherside != null) {
title = otherside.account.nick;
placeholder = 'messageInputPlaceholder'.trParams(
{'channel': '@${otherside.account.name}'},
);
}
return Scaffold(
return ResponsiveRootContainer(
child: Scaffold(
appBar: AppBar(
leading: AppBarLeadingButton.adaptive(context),
title: AppBarTitle(title),
@ -246,7 +255,8 @@ class _ChannelChatScreenState extends State<ChannelChatScreen>
.then((value) {
if (value == false) AppRouter.instance.pop();
if (value != null) {
final resp = Channel.fromJson(value as Map<String, dynamic>);
final resp =
Channel.fromJson(value as Map<String, dynamic>);
_getChannel(alias: resp.alias);
}
});
@ -274,7 +284,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen>
channel: _channel!,
ongoingCall: _ongoingCall!,
onJoin: () {
if (!AppTheme.isLargeScreen(context)) {
if (!AppTheme.isUltraLargeScreen(context)) {
final ChatCallProvider call = Get.find();
call.gotoScreen(context);
}
@ -282,6 +292,9 @@ class _ChannelChatScreenState extends State<ChannelChatScreen>
),
Expanded(
child: ChatEventList(
noAnimated:
_prefs.getBool('non_animated_message_list') ??
false,
scope: widget.realm,
channel: _channel!,
chatController: _chatController,
@ -328,7 +341,8 @@ class _ChannelChatScreenState extends State<ChannelChatScreen>
),
Obx(() {
final ChatCallProvider call = Get.find();
if (call.isMounted.value && AppTheme.isLargeScreen(context)) {
if (call.isMounted.value &&
AppTheme.isUltraLargeScreen(context)) {
return const Expanded(
child: Row(children: [
VerticalDivider(width: 0.3, thickness: 0.3),
@ -346,6 +360,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen>
],
);
}),
),
);
}

View File

@ -9,6 +9,7 @@ import 'package:solian/providers/content/channel.dart';
import 'package:solian/router.dart';
import 'package:solian/theme.dart';
import 'package:solian/widgets/app_bar_title.dart';
import 'package:solian/widgets/root_container.dart';
import 'package:uuid/uuid.dart';
class ChannelOrganizeArguments {
@ -114,8 +115,7 @@ class _ChannelOrganizeScreenState extends State<ChannelOrganizeScreen> {
),
];
return Material(
color: Theme.of(context).colorScheme.surface,
return ResponsiveRootContainer(
child: Scaffold(
appBar: AppBar(
title: AppBarTitle('channelOrganizing'.tr),

View File

@ -1,50 +1,158 @@
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:gap/gap.dart';
import 'package:get/get.dart';
import 'package:solian/controllers/chat_events_controller.dart';
import 'package:solian/exts.dart';
import 'package:solian/models/channel.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/providers/content/channel.dart';
import 'package:solian/providers/content/realm.dart';
import 'package:solian/providers/database/database.dart';
import 'package:solian/router.dart';
import 'package:solian/screens/account/notification.dart';
import 'package:solian/theme.dart';
import 'package:solian/widgets/account/account_avatar.dart';
import 'package:solian/widgets/account/signin_required_overlay.dart';
import 'package:solian/widgets/app_bar_leading.dart';
import 'package:solian/widgets/app_bar_title.dart';
import 'package:solian/widgets/channel/channel_list.dart';
import 'package:solian/widgets/chat/call/chat_call_indicator.dart';
import 'package:solian/widgets/current_state_action.dart';
import 'package:solian/widgets/sized_container.dart';
import 'package:solian/widgets/root_container.dart';
import 'package:solian/widgets/sidebar/empty_placeholder.dart';
class ChatScreen extends StatefulWidget {
class ChatScreen extends StatelessWidget {
const ChatScreen({super.key});
@override
State<ChatScreen> createState() => _ChatScreenState();
Widget build(BuildContext context) {
return const ResponsiveRootContainer(
child: ChatList(),
);
}
}
class _ChatScreenState extends State<ChatScreen> {
late final ChannelProvider _channels;
class ChatListShell extends StatelessWidget {
final Widget? child;
const ChatListShell({super.key, required this.child});
@override
Widget build(BuildContext context) {
return RootContainer(
child: 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();
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();
setState(() {
_lastMessages = messages
.map((k, v) => MapEntry(k, v.firstOrNull))
.cast<int, LocalMessageEventTableData>();
});
}
@override
void initState() {
super.initState();
try {
_channels = Get.find();
_channels.refreshAvailableChannel();
} catch (e) {
context.showErrorDialog(e);
}
_loadLastMessages().then((_) {
_loadAllChannels();
});
}
@override
Widget build(BuildContext context) {
final AuthProvider auth = Get.find();
final RealmProvider realms = Get.find();
return Material(
color: Theme.of(context).colorScheme.surface,
return Obx(
() => DefaultTabController(
length: 2 + realms.availableRealms.length,
child: ResponsiveRootContainer(
child: Scaffold(
appBar: AppBar(
leading: AppBarLeadingButton.adaptive(context),
leading: Obx(() {
final adaptive = AppBarLeadingButton.adaptive(context);
if (adaptive != null) return adaptive;
if (_channels.isLoading.value) {
return const CircularProgressIndicator(
strokeWidth: 3,
).paddingAll(18);
}
return const SizedBox.shrink();
}),
title: AppBarTitle('chat'.tr),
centerTitle: true,
toolbarHeight: AppTheme.toolbarHeight(context),
@ -58,13 +166,14 @@ class _ChatScreenState extends State<ChatScreen> {
child: ListTile(
title: Text('channelOrganizeCommon'.tr),
leading: const Icon(Icons.tag),
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
contentPadding:
const EdgeInsets.symmetric(horizontal: 8),
),
onTap: () {
AppRouter.instance.pushNamed('channelOrganizing').then(
(value) {
if (value != null) {
_channels.refreshAvailableChannel();
_loadAllChannels();
}
},
);
@ -77,7 +186,8 @@ class _ChatScreenState extends State<ChatScreen> {
FontAwesomeIcons.userGroup,
size: 16,
),
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
contentPadding:
const EdgeInsets.symmetric(horizontal: 8),
),
onTap: () {
final ChannelProvider channels = Get.find();
@ -85,7 +195,7 @@ class _ChatScreenState extends State<ChatScreen> {
.createDirectChannel(context, 'global')
.then((resp) {
if (resp != null) {
_channels.refreshAvailableChannel();
_loadAllChannels();
}
}).catchError((e) {
context.showErrorDialog(e);
@ -98,11 +208,70 @@ class _ChatScreenState extends State<ChatScreen> {
width: AppTheme.isLargeScreen(context) ? 8 : 16,
),
],
bottom: TabBar(
isScrollable: true,
dividerHeight: 0.3,
tabAlignment: TabAlignment.startOffset,
tabs: [
Tab(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
CircleAvatar(
radius: 14,
backgroundColor:
Theme.of(context).colorScheme.primary,
child: const Icon(
Icons.forum,
size: 16,
color: Colors.white,
),
),
const Gap(8),
Text('all'.tr),
],
),
),
Tab(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const CircleAvatar(
radius: 14,
child: Icon(
Icons.chat_bubble,
size: 16,
),
),
const Gap(8),
Text('channelTypeDirect'.tr),
],
),
),
...realms.availableRealms.map((x) => Tab(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
AccountAvatar(
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: () => _channels.refreshAvailableChannel(),
onDone: () => _loadAllChannels(),
);
}
@ -110,37 +279,49 @@ class _ChatScreenState extends State<ChatScreen> {
return Column(
children: [
Obx(() {
if (_channels.isLoading.isFalse) {
return const SizedBox.shrink();
} else {
return const LinearProgressIndicator();
}
}),
const ChatCallCurrentIndicator(),
Expanded(
child: CenteredContainer(
child: RefreshIndicator(
onRefresh: _channels.refreshAvailableChannel,
child: Obx(
() => ChannelListWidget(
noCategory: true,
channels: List.from([
..._channels.groupChannels
.where((x) => x.realmId == null),
..._channels.directChannels
child: TabBarView(
children: [
RefreshIndicator(
onRefresh: _loadNormalChannels,
child: ChannelListWidget(
channels: _sortChannels([
..._normalChannels,
..._directChannels,
..._realmChannels.values.expand((x) => x),
]),
selfId: selfId,
useReplace: true,
useReplace: AppTheme.isLargeScreen(context),
),
),
RefreshIndicator(
onRefresh: _loadDirectChannels,
child: ChannelListWidget(
channels: _directChannels,
selfId: selfId,
useReplace: AppTheme.isLargeScreen(context),
),
),
...realms.availableRealms.map(
(x) => RefreshIndicator(
onRefresh: () => _loadRealmChannels(x.alias),
child: ChannelListWidget(
channels: _realmChannels[x.alias] ?? [],
selfId: selfId,
useReplace: AppTheme.isLargeScreen(context),
),
),
),
],
),
),
],
);
}),
),
),
),
);
}
}

View File

@ -89,14 +89,18 @@ class _DashboardScreenState extends State<DashboardScreen> {
try {
_signRecord = await _dailySign.getToday();
_dailySign.listLastRecord(14).then((value) {
if (mounted) {
setState(() => _signRecordHistory = value);
}
});
} catch (e) {
context.showErrorDialog(e);
}
if (mounted) {
setState(() => _signingDaily = false);
}
}
Future<void> _signDaily() async {
setState(() => _signingDaily = true);
@ -147,7 +151,7 @@ class _DashboardScreenState extends State<DashboardScreen> {
),
Text(DateFormat('yyyy/MM/dd').format(DateTime.now().toUtc())),
],
).paddingOnly(top: 8, left: 18, right: 18, bottom: 12),
).paddingOnly(top: 16, left: 18, right: 18, bottom: 12),
Card(
child: Column(
children: [
@ -354,7 +358,7 @@ class _DashboardScreenState extends State<DashboardScreen> {
IconButton(
icon: const Icon(Icons.arrow_forward),
onPressed: () {
AppRouter.instance.goNamed('feed');
AppRouter.instance.goNamed('explore');
},
),
],

View File

@ -10,11 +10,12 @@ import 'package:solian/router.dart';
import 'package:solian/screens/account/notification.dart';
import 'package:solian/theme.dart';
import 'package:solian/widgets/account/signin_required_overlay.dart';
import 'package:solian/widgets/app_bar_title.dart';
import 'package:solian/widgets/current_state_action.dart';
import 'package:solian/widgets/app_bar_leading.dart';
import 'package:solian/widgets/navigation/realm_switcher.dart';
import 'package:solian/widgets/posts/post_shuffle_swiper.dart';
import 'package:solian/widgets/posts/post_warped_list.dart';
import 'package:solian/widgets/root_container.dart';
class ExploreScreen extends StatefulWidget {
const ExploreScreen({super.key});
@ -55,10 +56,8 @@ class _ExploreScreenState extends State<ExploreScreen>
@override
Widget build(BuildContext context) {
final AuthProvider auth = Get.find();
final NavigationStateProvider navState = Get.find();
return Material(
color: Theme.of(context).colorScheme.surface,
return RootContainer(
child: Scaffold(
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add),
@ -82,8 +81,14 @@ class _ExploreScreenState extends State<ExploreScreen>
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return [
SliverAppBar(
title: AppBarTitle('explore'.tr),
centerTitle: false,
flexibleSpace: SizedBox(
height: 48,
child: const Row(
children: [
RealmSwitcher(),
],
).paddingSymmetric(horizontal: 8),
).paddingOnly(top: MediaQuery.of(context).padding.top),
floating: true,
toolbarHeight: AppTheme.toolbarHeight(context),
leading: AppBarLeadingButton.adaptive(context),
@ -96,10 +101,39 @@ class _ExploreScreenState extends State<ExploreScreen>
],
bottom: TabBar(
controller: _tabController,
dividerHeight: 0.3,
tabAlignment: TabAlignment.fill,
tabs: [
Tab(text: 'postListNews'.tr),
Tab(text: 'postListFriends'.tr),
Tab(text: 'postListShuffle'.tr),
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 +148,6 @@ class _ExploreScreenState extends State<ExploreScreen>
return Column(
children: [
if (navState.focusedRealm.value != null)
MaterialBanner(
leading: const Icon(Icons.layers),
content: Text(
'postBrowsingIn'.trParams({
'region': '#${navState.focusedRealm.value!.alias}',
}),
),
actions: const [SizedBox.shrink()],
),
Expanded(
child: TabBarView(
physics: const NeverScrollableScrollPhysics(),

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/posts/post_action.dart';
import 'package:solian/widgets/posts/post_owned_list.dart';
import 'package:solian/widgets/root_container.dart';
class DraftBoxScreen extends StatefulWidget {
const DraftBoxScreen({super.key});
@ -54,8 +55,7 @@ class _DraftBoxScreenState extends State<DraftBoxScreen> {
@override
Widget build(BuildContext context) {
return Material(
color: Theme.of(context).colorScheme.surface,
return RootContainer(
child: Scaffold(
appBar: AppBar(
leading: AppBarLeadingButton.adaptive(context),

View File

@ -47,9 +47,7 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
@override
Widget build(BuildContext context) {
return Material(
color: Theme.of(context).colorScheme.surface,
child: FutureBuilder(
return FutureBuilder(
future: getDetail(),
builder: (context, snapshot) {
if (!snapshot.hasData || snapshot.data == null) {
@ -72,8 +70,8 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
),
),
SliverToBoxAdapter(
child: const Divider(thickness: 0.3, height: 1)
.paddingOnly(top: 4),
child:
const Divider(thickness: 0.3, height: 1).paddingOnly(top: 4),
),
SliverToBoxAdapter(
child: Align(
@ -91,7 +89,6 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
],
);
},
),
);
}
}

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/posts/post_item.dart';
import 'package:badges/badges.dart' as badges;
import 'package:solian/widgets/root_container.dart';
class PostPublishArguments {
final Post? edit;
@ -151,8 +152,7 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
)
];
return Material(
color: Theme.of(context).colorScheme.surface,
return RootContainer(
child: Scaffold(
appBar: AppBar(
leading: AppBarLeadingButton.adaptive(context),
@ -376,6 +376,7 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
Expanded(
child: SingleChildScrollView(
child: MarkdownTextContent(
isAutoWarp: _editorController.mode.value == 0,
content: _editorController.contentController.text,
parentId: 'post-editor-preview',
).paddingOnly(top: 12, right: 16),

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/auto_cache_image.dart';
import 'package:solian/widgets/current_state_action.dart';
import 'package:solian/widgets/root_container.dart';
import 'package:solian/widgets/sized_container.dart';
class RealmListScreen extends StatefulWidget {
@ -58,8 +59,7 @@ class _RealmListScreenState extends State<RealmListScreen> {
Widget build(BuildContext context) {
final AuthProvider auth = Get.find();
return Material(
color: Theme.of(context).colorScheme.surface,
return RootContainer(
child: Scaffold(
appBar: AppBar(
leading: AppBarLeadingButton.adaptive(context),
@ -99,6 +99,7 @@ class _RealmListScreenState extends State<RealmListScreen> {
child: RefreshIndicator(
onRefresh: () => _getRealms(),
child: ListView.builder(
padding: const EdgeInsets.symmetric(vertical: 16),
itemCount: _realms.length,
itemBuilder: (context, index) {
final element = _realms[index];

View File

@ -7,6 +7,7 @@ import 'package:solian/router.dart';
import 'package:solian/screens/realms/realm_organize.dart';
import 'package:solian/widgets/realms/realm_deletion.dart';
import 'package:solian/widgets/realms/realm_member.dart';
import 'package:solian/widgets/root_container.dart';
class RealmDetailScreen extends StatefulWidget {
final String alias;
@ -86,7 +87,8 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> {
),
];
return Column(
return RootContainer(
child: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
@ -141,6 +143,7 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> {
),
),
],
),
);
}
}

View File

@ -1,5 +1,3 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.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/models/attachment.dart';
import 'package:solian/models/realm.dart';
import 'package:solian/platform.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/providers/content/attachment.dart';
import 'package:solian/router.dart';
import 'package:solian/theme.dart';
import 'package:solian/widgets/app_bar_leading.dart';
import 'package:solian/widgets/app_bar_title.dart';
import 'package:solian/widgets/root_container.dart';
import 'package:uuid/uuid.dart';
class RealmOrganizeArguments {
@ -84,9 +84,12 @@ class _RealmOrganizeScreenState extends State<RealmOrganizeScreen> {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) return;
XFile file;
final image = await _imagePicker.pickImage(source: ImageSource.gallery);
if (image == null) return;
if (PlatformInfo.canCropImage) {
CroppedFile? croppedFile = await ImageCropper().cropImage(
sourcePath: image.path,
uiSettings: [
@ -113,7 +116,10 @@ class _RealmOrganizeScreenState extends State<RealmOrganizeScreen> {
);
if (croppedFile == null) return;
final file = File(croppedFile.path);
file = XFile(croppedFile.path);
} else {
file = XFile(image.path);
}
setState(() => _isBusy = true);
@ -184,8 +190,7 @@ class _RealmOrganizeScreenState extends State<RealmOrganizeScreen> {
@override
Widget build(BuildContext context) {
return Material(
color: Theme.of(context).colorScheme.surface,
return RootContainer(
child: Scaffold(
appBar: AppBar(
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/channel/channel_list.dart';
import 'package:solian/widgets/posts/post_list.dart';
import 'package:solian/widgets/root_container.dart';
class RealmViewScreen extends StatefulWidget {
final String alias;
@ -68,12 +69,7 @@ class _RealmViewScreenState extends State<RealmViewScreen> {
_channels.addAll(
resp.body.map((e) => Channel.fromJson(e)).toList().cast<Channel>(),
);
_channels.addAll(
availableResp.body
.map((e) => Channel.fromJson(e))
.toList()
.cast<Channel>(),
);
_channels.addAll(availableResp);
_channels.retainWhere((x) => channelIdx.add(x.id));
});
@ -91,8 +87,7 @@ class _RealmViewScreenState extends State<RealmViewScreen> {
@override
Widget build(BuildContext context) {
return Material(
color: Theme.of(context).colorScheme.surface,
return RootContainer(
child: DefaultTabController(
length: 2,
child: NestedScrollView(
@ -260,7 +255,6 @@ class RealmChannelListWidget extends StatelessWidget {
child: ChannelListWidget(
channels: channels,
selfId: auth.userProfile.value!['id'],
noCategory: true,
),
)
],

View File

@ -1,15 +1,23 @@
import 'dart:convert';
import 'dart:io';
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:get/get.dart';
import 'package:image_picker/image_picker.dart';
import 'package:in_app_review/in_app_review.dart';
import 'package:path_provider/path_provider.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:solian/exceptions/request.dart';
import 'package:solian/exts.dart';
import 'package:solian/models/theme.dart';
import 'package:solian/platform.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/providers/database/database.dart';
import 'package:solian/providers/theme_switcher.dart';
import 'package:solian/router.dart';
import 'package:solian/theme.dart';
import 'package:solian/widgets/reports/abuse_report.dart';
class SettingScreen extends StatefulWidget {
@ -21,6 +29,7 @@ class SettingScreen extends StatefulWidget {
class _SettingScreenState extends State<SettingScreen> {
SharedPreferences? _prefs;
String _docBasepath = '/';
Widget _buildCaptionHeader(String title) {
return Container(
@ -31,39 +40,38 @@ class _SettingScreenState extends State<SettingScreen> {
);
}
Widget _buildThemeColorButton(String label, Color color) {
return IconButton(
icon: Icon(Icons.circle, color: color),
tooltip: label,
onPressed: () {
context.read<ThemeSwitcher>().setTheme(
AppTheme.build(
Brightness.light,
seedColor: color,
static final List<SolianThemeData> _presentTheme = [
SolianThemeData(
id: 'themeColorRed',
seedColor: const Color.fromRGBO(154, 98, 91, 1),
),
AppTheme.build(
Brightness.dark,
seedColor: color,
SolianThemeData(
id: 'themeColorBlue',
seedColor: const Color.fromRGBO(103, 96, 193, 1),
),
SolianThemeData(
id: 'themeColorMiku',
seedColor: const Color.fromRGBO(56, 120, 126, 1),
),
SolianThemeData(
id: 'themeColorKagamine',
seedColor: const Color.fromRGBO(244, 183, 63, 1),
),
SolianThemeData(
id: 'themeColorLuka',
seedColor: const Color.fromRGBO(243, 174, 218, 1),
),
);
_prefs?.setInt('global_theme_color', color.value);
context.clearSnackbar();
context.showSnackbar('themeColorApplied'.tr);
},
);
}
static final List<(String, Color)> _presentTheme = [
('themeColorRed', const Color.fromRGBO(154, 98, 91, 1)),
('themeColorBlue', const Color.fromRGBO(103, 96, 193, 1)),
('themeColorMiku', const Color.fromRGBO(56, 120, 126, 1)),
('themeColorKagamine', const Color.fromRGBO(244, 183, 63, 1)),
('themeColorLuka', const Color.fromRGBO(243, 174, 218, 1)),
];
@override
void initState() {
super.initState();
getApplicationDocumentsDirectory().then((dir) {
_docBasepath = dir.path;
if (mounted) {
setState(() {});
}
});
SharedPreferences.getInstance().then((inst) {
_prefs = inst;
if (mounted) {
@ -74,19 +82,103 @@ class _SettingScreenState extends State<SettingScreen> {
@override
Widget build(BuildContext context) {
return Material(
color: Theme.of(context).colorScheme.surface,
child: ListView(
return ListView(
children: [
_buildCaptionHeader('themeColor'.tr),
SizedBox(
height: 56,
child: ListView(
scrollDirection: Axis.horizontal,
children: _presentTheme
.map((x) => _buildThemeColorButton(x.$1, x.$2))
_buildCaptionHeader('theme'.tr),
ListTile(
leading: const Icon(Icons.palette),
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
title: Text('globalTheme'.tr),
trailing: DropdownButtonHideUnderline(
child: DropdownButton2<SolianThemeData>(
isExpanded: true,
hint: Text(
'theme'.tr,
style: TextStyle(
fontSize: 14,
color: Theme.of(context).hintColor,
),
),
items: _presentTheme
.map((SolianThemeData item) =>
DropdownMenuItem<SolianThemeData>(
value: item,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(Icons.circle, color: item.seedColor),
const Gap(8),
Expanded(
child: Text(
item.id.tr,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 14,
),
),
),
],
),
))
.toList(),
).paddingSymmetric(horizontal: 12, vertical: 8),
value: (_prefs?.containsKey('global_theme') ?? false)
? SolianThemeData.fromJson(
jsonDecode(_prefs!.getString('global_theme')!),
)
: null,
onChanged: (SolianThemeData? value) {
context.read<ThemeSwitcher>().setThemeData(value);
setState(() {});
},
buttonStyleData: const ButtonStyleData(
padding: EdgeInsets.symmetric(horizontal: 8),
height: 40,
width: 140,
),
menuItemStyleData: const MenuItemStyleData(
height: 40,
),
),
),
),
CheckboxListTile(
secondary: const Icon(Icons.military_tech),
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
title: Text('agedTheme'.tr),
subtitle: Text('agedThemeDesc'.tr),
value: _prefs?.getBool('aged_theme') ?? false,
onChanged: (value) {
if (value != null) {
context.read<ThemeSwitcher>().setAgedTheme(value);
}
setState(() {});
},
),
if (!PlatformInfo.isWeb)
ListTile(
leading: const Icon(Icons.wallpaper),
contentPadding: const EdgeInsets.only(left: 22, right: 31),
title: Text('appBackgroundImage'.tr),
subtitle: Text('appBackgroundImageDesc'.tr),
trailing: File('$_docBasepath/app_background_image').existsSync()
? const Icon(Icons.check_box)
: const Icon(Icons.check_box_outline_blank),
onTap: () async {
if (File('$_docBasepath/app_background_image').existsSync()) {
File('$_docBasepath/app_background_image').deleteSync();
} else {
final image = await ImagePicker().pickImage(
source: ImageSource.gallery,
);
if (image == null) return;
await File(image.path)
.copy('$_docBasepath/app_background_image');
}
setState(() {});
},
),
_buildCaptionHeader('notification'.tr),
Tooltip(
@ -106,8 +198,7 @@ class _SettingScreenState extends State<SettingScreen> {
)
],
),
value:
_prefs?.getBool('service_background_notification') ?? false,
value: _prefs?.getBool('service_background_notification') ?? false,
onChanged: (value) {
_prefs
?.setBool('service_background_notification', value ?? false)
@ -125,9 +216,7 @@ class _SettingScreenState extends State<SettingScreen> {
subtitle: Text('updateCheckStrictlyDesc'.tr),
value: _prefs?.getBool('check_update_strictly') ?? false,
onChanged: (value) {
_prefs
?.setBool('check_update_strictly', value ?? false)
.then((_) {
_prefs?.setBool('check_update_strictly', value ?? false).then((_) {
setState(() {});
});
},
@ -180,6 +269,21 @@ class _SettingScreenState extends State<SettingScreen> {
],
);
}),
_buildCaptionHeader('performance'.tr),
CheckboxListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
secondary: const Icon(Icons.message),
title: Text('animatedMessageList'.tr),
subtitle: Text('animatedMessageListDesc'.tr),
value: _prefs?.getBool('non_animated_message_list') ?? false,
onChanged: (value) {
_prefs
?.setBool('non_animated_message_list', value ?? false)
.then((_) {
setState(() {});
});
},
),
_buildCaptionHeader('more'.tr),
ListTile(
leading: const Icon(Icons.delete_sweep),
@ -205,6 +309,21 @@ class _SettingScreenState extends State<SettingScreen> {
});
},
),
if (PlatformInfo.canRateTheApp)
ListTile(
leading: const Icon(Icons.star),
trailing: const Icon(Icons.chevron_right),
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
title: Text('rateTheApp'.tr),
subtitle: Text('rateTheAppDesc'.tr),
onTap: () {
final inAppReview = InAppReview.instance;
inAppReview.openStoreListing(
appStoreId: '6499032345',
);
},
),
ListTile(
leading: const Icon(Icons.info_outline),
trailing: const Icon(Icons.chevron_right),
@ -215,7 +334,6 @@ class _SettingScreenState extends State<SettingScreen> {
},
),
],
),
);
}
}

View File

@ -2,7 +2,9 @@ import 'package:firebase_analytics/firebase_analytics.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:solian/theme.dart';
import 'package:solian/widgets/navigation/app_navigation_drawer.dart';
import 'package:solian/widgets/navigation/app_navigation.dart';
import 'package:solian/widgets/navigation/app_navigation_bottom.dart';
import 'package:solian/widgets/navigation/app_navigation_rail.dart';
final GlobalKey<ScaffoldState> rootScaffoldKey = GlobalKey<ScaffoldState>();
@ -39,17 +41,28 @@ 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(
key: rootScaffoldKey,
drawer: AppTheme.isLargeScreen(context)
? null
: AppNavigationDrawer(routeName: routeName),
bottomNavigationBar: showBottomNavigation
? AppNavigationBottom(
initialIndex: destNames.indexOf(routeName ?? 'page'),
)
: null,
body: AppTheme.isLargeScreen(context)
? Row(
children: [
if (showNavigation) AppNavigationDrawer(routeName: routeName),
if (showNavigation)
const VerticalDivider(thickness: 0.3, width: 1),
if (showRailNavigation) const AppNavigationRail(),
if (showRailNavigation)
const VerticalDivider(
width: 0.3,
thickness: 0.3,
),
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_leading.dart';
import 'package:solian/widgets/current_state_action.dart';
import 'package:solian/widgets/root_container.dart';
class TitleShell extends StatelessWidget {
final bool showAppBar;
final bool isCenteredTitle;
final bool isResponsive;
final String? title;
final GoRouterState? state;
final Widget child;
@ -20,13 +22,14 @@ class TitleShell extends StatelessWidget {
this.state,
this.showAppBar = true,
this.isCenteredTitle = false,
this.isResponsive = false,
});
@override
Widget build(BuildContext context) {
assert(state != null || title != null);
return Scaffold(
final widget = Scaffold(
appBar: showAppBar
? AppBar(
leading: AppBarLeadingButton.adaptive(context),
@ -45,5 +48,11 @@ class TitleShell extends StatelessWidget {
: null,
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:solian/models/theme.dart';
import 'package:solian/platform.dart';
abstract class AppTheme {
@ -6,7 +7,10 @@ abstract class AppTheme {
MediaQuery.of(context).size.width > 640;
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) =>
PlatformInfo.isMacOS && !AppTheme.isLargeScreen(context);
@ -38,6 +42,10 @@ abstract class AppTheme {
snackBarTheme: const SnackBarThemeData(
behavior: SnackBarBehavior.floating,
),
scaffoldBackgroundColor: Colors.transparent,
appBarTheme: const AppBarTheme(
backgroundColor: Colors.transparent,
),
fontFamily: 'Comfortaa',
fontFamilyFallback: [
'NotoSansSC',
@ -52,4 +60,37 @@ abstract class AppTheme {
),
);
}
static ThemeData buildFromData(
Brightness brightness,
SolianThemeData data, {
bool useMaterial3 = true,
}) {
return ThemeData(
brightness: brightness,
useMaterial3: useMaterial3,
colorScheme: ColorScheme.fromSeed(
brightness: brightness,
seedColor: data.seedColor,
),
snackBarTheme: const SnackBarThemeData(
behavior: SnackBarBehavior.floating,
),
scaffoldBackgroundColor: Colors.transparent,
appBarTheme: const AppBarTheme(backgroundColor: Colors.transparent),
fontFamily: data.fontFamily ?? 'Comfortaa',
fontFamilyFallback: data.fontFamilyFallback ??
[
'NotoSansSC',
'NotoSansHK',
'NotoSansJP',
if (PlatformInfo.isWeb) 'NotoSansEmoji',
],
typography: Typography.material2021(
colorScheme: brightness == Brightness.light
? const ColorScheme.light()
: const ColorScheme.dark(),
),
);
}
}

View File

@ -7,6 +7,7 @@ class AccountAvatar extends StatelessWidget {
final Color? bgColor;
final Color? feColor;
final double? radius;
final Widget? fallbackWidget;
const AccountAvatar({
super.key,
@ -14,6 +15,7 @@ class AccountAvatar extends StatelessWidget {
this.bgColor,
this.feColor,
this.radius,
this.fallbackWidget,
});
@override
@ -35,11 +37,12 @@ class AccountAvatar extends StatelessWidget {
backgroundColor: bgColor,
backgroundImage: !isEmpty ? AutoCacheImage.provider(url) : null,
child: isEmpty
? Icon(
? (fallbackWidget ??
Icon(
Icons.account_circle,
size: radius != null ? radius! * 1.2 : 24,
color: feColor,
)
))
: null,
);
}

View File

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

View File

@ -106,15 +106,19 @@ class _AccountProfilePopupState extends State<AccountProfilePopup> {
extraWidgets: [
Card(
child: ListTile(
leading: const Icon(
Icons.contact_page_outlined,
),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(8)),
),
title: Text('visitProfilePage'.tr),
subtitle: Text('learnMoreAboutPerson'.tr),
visualDensity:
const VisualDensity(horizontal: -4, vertical: -2),
trailing: const Icon(Icons.chevron_right),
onTap: () {
AppRouter.instance.goNamed(
AppRouter.instance.pushNamed(
'accountProfilePage',
pathParameters: {'name': _userinfo!.name},
);

View File

@ -28,13 +28,9 @@ class SilverRelativeList extends StatelessWidget {
showModalBottomSheet(
useRootNavigator: true,
isScrollControlled: true,
backgroundColor: Theme
.of(context)
.colorScheme
.surface,
backgroundColor: Theme.of(context).colorScheme.surface,
context: context,
builder: (context) =>
AccountProfilePopup(
builder: (context) => AccountProfilePopup(
name: element.related.name,
),
);
@ -49,9 +45,13 @@ class SilverRelativeList extends StatelessWidget {
onPressed: () {
final RelationshipProvider provider = Get.find();
if (element.status == 0) {
provider.handleRelation(element, true).then((_) => onUpdate());
provider
.handleRelation(element, true)
.then((_) => onUpdate());
} else {
provider.editRelation(element, 1).then((_) => onUpdate());
provider
.editRelation(element.relatedId, 1)
.then((_) => onUpdate());
}
},
),
@ -61,9 +61,13 @@ class SilverRelativeList extends StatelessWidget {
onPressed: () {
final RelationshipProvider provider = Get.find();
if (element.status == 0) {
provider.handleRelation(element, false).then((_) => onUpdate());
provider
.handleRelation(element, false)
.then((_) => onUpdate());
} else {
provider.editRelation(element, 2).then((_) => onUpdate());
provider
.editRelation(element.relatedId, 2)
.then((_) => onUpdate());
}
},
),

View File

@ -1,28 +1,22 @@
import 'package:flutter/material.dart';
import 'package:solian/shells/root_shell.dart';
class AppBarLeadingButton extends StatelessWidget {
const AppBarLeadingButton({super.key});
final bool forceBack;
static Widget? adaptive(BuildContext context) {
final hasContent =
Navigator.canPop(context) || rootScaffoldKey.currentState!.hasDrawer;
return hasContent ? const AppBarLeadingButton() : null;
const AppBarLeadingButton({super.key, this.forceBack = false});
static Widget? adaptive(BuildContext context, {bool forceBack = false}) {
final hasContent = Navigator.canPop(context) || forceBack;
return hasContent ? AppBarLeadingButton(forceBack: forceBack) : null;
}
@override
Widget build(BuildContext context) {
if (Navigator.canPop(context)) {
if (Navigator.canPop(context) || forceBack) {
return BackButton(
onPressed: () => Navigator.pop(context),
);
}
if (rootScaffoldKey.currentState!.hasDrawer) {
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 &&
element.error == null &&
canBeCrop)
canBeCrop &&
PlatformInfo.canCropImage)
Obx(
() => IconButton(
color: Colors.teal,
@ -744,8 +745,8 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
return IgnorePointer(
ignoring: _uploadController.isUploading.value,
child: Container(
height: 64,
width: MediaQuery.of(context).size.width,
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
border: Border(
top: BorderSide(
@ -754,11 +755,9 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
),
),
),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Wrap(
spacing: 8,
runSpacing: 0,
runSpacing: 8,
alignment: WrapAlignment.center,
runAlignment: WrapAlignment.center,
children: [
@ -766,55 +765,62 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
PlatformInfo.isIOS ||
PlatformInfo.isWeb) &&
!widget.imageOnly)
ElevatedButton.icon(
IconButton(
icon: const Icon(Icons.paste),
label: Text('attachmentAddClipboard'.tr),
tooltip: 'attachmentAddClipboard'.tr,
style: const ButtonStyle(visualDensity: density),
color: Theme.of(context).colorScheme.primary,
onPressed: () => _pasteFileToUpload(),
),
ElevatedButton.icon(
IconButton(
icon: const Icon(Icons.add_photo_alternate),
label: Text('attachmentAddGalleryPhoto'.tr),
tooltip: 'attachmentAddGalleryPhoto'.tr,
style: const ButtonStyle(visualDensity: density),
color: Theme.of(context).colorScheme.primary,
onPressed: () => _pickPhotoToUpload(),
),
if (!widget.imageOnly)
ElevatedButton.icon(
IconButton(
icon: const Icon(Icons.add_road),
label: Text('attachmentAddGalleryVideo'.tr),
tooltip: 'attachmentAddGalleryVideo'.tr,
style: const ButtonStyle(visualDensity: density),
color: Theme.of(context).colorScheme.primary,
onPressed: () => _pickVideoToUpload(),
),
ElevatedButton.icon(
if (PlatformInfo.isMobile)
IconButton(
icon: const Icon(Icons.photo_camera_back),
label: Text('attachmentAddCameraPhoto'.tr),
tooltip: 'attachmentAddCameraPhoto'.tr,
style: const ButtonStyle(visualDensity: density),
color: Theme.of(context).colorScheme.primary,
onPressed: () => _takeMediaToUpload(false),
),
if (!widget.imageOnly)
ElevatedButton.icon(
if (!widget.imageOnly && PlatformInfo.isMobile)
IconButton(
icon: const Icon(Icons.video_camera_back_outlined),
label: Text('attachmentAddCameraVideo'.tr),
tooltip: 'attachmentAddCameraVideo'.tr,
style: const ButtonStyle(visualDensity: density),
color: Theme.of(context).colorScheme.primary,
onPressed: () => _takeMediaToUpload(true),
),
if (!widget.imageOnly)
ElevatedButton.icon(
IconButton(
icon: const Icon(Icons.file_present_rounded),
label: Text('attachmentAddFile'.tr),
tooltip: 'attachmentAddFile'.tr,
style: const ButtonStyle(visualDensity: density),
color: Theme.of(context).colorScheme.primary,
onPressed: () => _pickFileToUpload(),
),
if (!widget.imageOnly)
ElevatedButton.icon(
IconButton(
icon: const Icon(Icons.link),
label: Text('attachmentAddFile'.tr),
tooltip: 'attachmentAddLink'.tr,
style: const ButtonStyle(visualDensity: density),
color: Theme.of(context).colorScheme.primary,
onPressed: () => _linkAttachments(),
),
],
).paddingSymmetric(horizontal: 12),
),
)
.animate(
target: _uploadController.isUploading.value ? 0 : 1,

View File

@ -177,9 +177,6 @@ class _AttachmentListState extends State<AttachmentList> {
if (element == null) return const SizedBox.shrink();
double ratio = element.metadata?['ratio']?.toDouble() ?? 16 / 9;
return Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
),
constraints: BoxConstraints(
maxWidth: widget.columnMaxWidth,
maxHeight: 640,
@ -247,7 +244,7 @@ class _AttachmentListState extends State<AttachmentList> {
maxHeight: widget.flatMaxHeight,
),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
color: Colors.transparent,
border: Border.symmetric(
horizontal: BorderSide(
width: 0.3,
@ -257,6 +254,7 @@ class _AttachmentListState extends State<AttachmentList> {
),
child: CarouselSlider.builder(
options: CarouselOptions(
animateToClosest: true,
aspectRatio: _aspectRatio,
viewportFraction:
widget.viewport ?? (widget.attachmentsId.length > 1 ? 0.95 : 1),
@ -319,6 +317,7 @@ class AttachmentListEntry extends StatelessWidget {
width: width ?? MediaQuery.of(context).size.width,
height: height,
decoration: BoxDecoration(
color: Colors.transparent,
border: showBorder
? Border.symmetric(
vertical: BorderSide(

View File

@ -98,12 +98,12 @@ class ChannelCallIndicator extends StatelessWidget {
child: Text('callJoin'.tr),
);
} else if (call.channel.value?.id == channel.id &&
!AppTheme.isLargeScreen(context)) {
!AppTheme.isUltraLargeScreen(context)) {
return TextButton(
onPressed: () => onJoin(),
child: Text('callResume'.tr),
);
} else if (!AppTheme.isLargeScreen(context)) {
} else if (!AppTheme.isUltraLargeScreen(context)) {
return TextButton(
onPressed: null,
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:gap/gap.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:solian/controllers/chat_events_controller.dart';
import 'package:solian/models/channel.dart';
import 'package:solian/platform.dart';
import 'package:solian/providers/database/database.dart';
import 'package:solian/router.dart';
import 'package:solian/widgets/account/account_avatar.dart';
import 'package:badges/badges.dart' as badges;
class ChannelListWidget extends StatefulWidget {
final List<Channel> channels;
final int selfId;
final bool isDense;
final bool isCollapsed;
final bool noCategory;
final bool useReplace;
final Function(Channel)? onSelected;
@ -23,9 +23,6 @@ class ChannelListWidget extends StatefulWidget {
super.key,
required this.channels,
required this.selfId,
this.isDense = false,
this.isCollapsed = false,
this.noCategory = false,
this.useReplace = false,
this.onSelected,
});
@ -35,43 +32,25 @@ class ChannelListWidget extends StatefulWidget {
}
class _ChannelListWidgetState extends State<ChannelListWidget> {
final List<Channel> _globalChannels = List.empty(growable: true);
final Map<String, List<Channel>> _inRealms = {};
Map<int, LocalMessageEventTableData>? _lastMessages;
final ChatEventController _eventController = ChatEventController();
void _mapChannels() {
_inRealms.clear();
_globalChannels.clear();
if (widget.noCategory) {
_globalChannels.addAll(widget.channels);
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());
Future<void> _loadLastMessages() async {
final messages = await _eventController.src.getLastInAllChannels();
setState(() {
_lastMessages = messages
.map((k, v) => MapEntry(k, v.firstOrNull))
.cast<int, LocalMessageEventTableData>();
});
}
@override
void initState() {
super.initState();
_mapChannels();
_eventController.initialize();
_eventController.initialize().then((_) {
_loadLastMessages();
});
}
void _gotoChannel(Channel item) {
@ -98,107 +77,183 @@ class _ChannelListWidgetState extends State<ChannelListWidget> {
}
}
Widget _buildDirectMessageDescription(Channel item, ChannelMember otherside) {
if (PlatformInfo.isWeb) {
return Text('channelDirectDescription'.trParams(
{'username': '@${otherside.account.name}'},
));
}
return FutureBuilder(
future: Future.delayed(
const Duration(milliseconds: 500),
() => _eventController.src.getLastInChannel(item),
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(),
),
builder: (context, snapshot) {
if (!snapshot.hasData && snapshot.data == null) {
return Text('channelDirectDescription'.trParams(
{'username': '@${otherside.account.name}'},
));
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),
),
),
],
);
}
final data = snapshot.data!.data!;
return Text(
'${data.sender.account.nick}: ${data.body['text'] ?? 'Unsupported message to preview'}',
Widget _buildSubtitle(Channel item, ChannelMember? otherside) {
if (PlatformInfo.isWeb) {
return otherside != null
? Text(
'channelDirectDescription'.trParams(
{'username': '@${otherside.account.name}'},
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
)
: Text(
item.description,
maxLines: 1,
overflow: TextOverflow.ellipsis,
);
}
return AnimatedSwitcher(
switchInCurve: Curves.easeIn,
switchOutCurve: Curves.easeOut,
transitionBuilder: (child, animation) {
return FadeTransition(opacity: animation, child: child);
},
duration: const Duration(milliseconds: 300),
child: (_lastMessages == null || _lastMessages![item.id] == null)
? Builder(
builder: (context) {
return otherside != null
? Text(
'channelDirectDescription'.trParams(
{'username': '@${otherside.account.name}'},
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
)
: Text(
item.description,
maxLines: 1,
overflow: TextOverflow.ellipsis,
);
},
)
: Builder(
builder: (context) {
final data = _lastMessages![item.id]!.data!;
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (item.type == 0)
Badge(
label: Text(data.sender.account.nick),
backgroundColor:
Theme.of(context).colorScheme.secondaryContainer,
textColor:
Theme.of(context).colorScheme.onSecondaryContainer,
),
if (item.type == 0) const Gap(6),
if (data.body['text'] != null)
Expanded(
child: Text(
data.body['text'],
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
)
else
Badge(label: Text('unablePreview'.tr)),
],
);
},
),
layoutBuilder: (currentChild, previousChildren) {
return Stack(
alignment: Alignment.centerLeft,
children: <Widget>[
...previousChildren,
if (currentChild != null) currentChild,
],
);
},
);
}
Widget _buildEntry(Channel item) {
final padding = widget.isDense
? const EdgeInsets.symmetric(horizontal: 20)
: const EdgeInsets.symmetric(horizontal: 16);
const padding = EdgeInsets.symmetric(horizontal: 20);
if (item.type == 1) {
final otherside =
item.members!.where((e) => e.account.id != widget.selfId).first;
item.members!.where((e) => e.account.id != widget.selfId).firstOrNull;
if (item.type == 1 && otherside != null) {
final avatar = AccountAvatar(
content: otherside.account.avatar,
radius: widget.isDense ? 12 : 20,
radius: 20,
bgColor: Theme.of(context).colorScheme.primary,
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(
leading: avatar,
contentPadding: padding,
title: Text(otherside.account.nick),
subtitle: !widget.isDense
? _buildDirectMessageDescription(item, otherside)
: null,
title: _buildTitle(item, otherside),
subtitle: _buildSubtitle(item, otherside),
onTap: () => _gotoChannel(item),
);
} else {
final avatar = CircleAvatar(
backgroundColor: item.realmId == null
? Theme.of(context).colorScheme.primary
: Colors.transparent,
radius: widget.isDense ? 12 : 20,
backgroundColor: Theme.of(context).colorScheme.primary,
radius: 20,
child: FaIcon(
FontAwesomeIcons.hashtag,
color: item.realmId == null
? Theme.of(context).colorScheme.onPrimary
: Theme.of(context).colorScheme.primary,
size: widget.isDense ? 12 : 16,
color: Theme.of(context).colorScheme.onPrimary,
size: 16,
),
);
if (widget.isCollapsed) {
return Tooltip(
message: item.name,
child: InkWell(
child: avatar.paddingSymmetric(vertical: 12),
onTap: () => _gotoChannel(item),
),
);
}
return ListTile(
minTileHeight: widget.isDense ? 48 : null,
leading: avatar,
minTileHeight: null,
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: AccountAvatar(
content: item.realm?.avatar,
radius: 10,
fallbackWidget: const Icon(
Icons.workspaces,
size: 16,
),
),
child: avatar,
),
contentPadding: padding,
title: Text(item.name),
subtitle: !widget.isDense
? Text(
item.description,
maxLines: 1,
overflow: TextOverflow.ellipsis,
)
: null,
title: _buildTitle(item, null),
subtitle: _buildSubtitle(item, null),
onTap: () => _gotoChannel(item),
);
}
@ -206,13 +261,12 @@ class _ChannelListWidgetState extends State<ChannelListWidget> {
@override
Widget build(BuildContext context) {
if (widget.noCategory) {
return CustomScrollView(
slivers: [
SliverList.builder(
itemCount: _globalChannels.length,
itemCount: widget.channels.length,
itemBuilder: (context, index) {
final element = _globalChannels[index];
final element = widget.channels[index];
return _buildEntry(element);
},
),
@ -220,36 +274,4 @@ class _ChannelListWidgetState extends State<ChannelListWidget> {
],
);
}
return CustomScrollView(
slivers: [
SliverList.builder(
itemCount: _globalChannels.length,
itemBuilder: (context, index) {
final element = _globalChannels[index];
return _buildEntry(element);
},
),
SliverList.list(
children: _inRealms.entries.map((element) {
return ExpansionTile(
tilePadding: const EdgeInsets.only(left: 20, right: 24),
minTileHeight: 48,
title: Text(element.value.first.realm!.name),
leading: CircleAvatar(
backgroundColor: Colors.teal,
radius: widget.isDense ? 12 : 24,
child: Icon(
Icons.workspaces,
color: Colors.white,
size: widget.isDense ? 12 : 16,
),
),
children: element.value.map((x) => _buildEntry(x)).toList(),
);
}).toList(),
),
],
);
}
}

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:get/get.dart';
import 'package:solian/controllers/chat_events_controller.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';
class ChatEventList extends StatelessWidget {
final bool noAnimated;
final String scope;
final Channel channel;
final ChatEventController chatController;
@ -23,6 +25,7 @@ class ChatEventList extends StatelessWidget {
required this.chatController,
required this.onEdit,
required this.onReply,
this.noAnimated = false,
});
bool _checkMessageMergeable(Event? a, Event? b) {
@ -63,7 +66,8 @@ class ChatEventList extends StatelessWidget {
return GestureDetector(
behavior: HitTestBehavior.opaque,
child: ChatEvent(
child: Builder(builder: (context) {
final widget = ChatEvent(
key: Key('m${item!.uuid}'),
item: item,
isMerged: isMerged,
@ -71,7 +75,23 @@ class ChatEventList extends StatelessWidget {
).paddingOnly(
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: () {
showModalBottomSheet(
useRootNavigator: true,
@ -79,7 +99,7 @@ class ChatEventList extends StatelessWidget {
builder: (context) => ChatEventAction(
channel: channel,
realm: channel.realm,
item: item,
item: item!,
onEdit: () {
onEdit(item);
},

View File

@ -1,12 +1,16 @@
import 'dart:ui';
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:google_fonts/google_fonts.dart';
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/widgets/attachments/attachment_list.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 'account/account_profile_popup.dart';
@ -39,11 +43,6 @@ class MarkdownTextContent extends StatelessWidget {
// Getting paragraph
var paragraph = paragraphs[idx];
// Auto adding new-lines
if (isAutoWarp) {
paragraph = paragraph.replaceAll('\n', '\\\n');
}
// Matching stickers
final stickerMatch = stickerRegex.allMatches(paragraph);
final isOnlySticker =
@ -58,7 +57,7 @@ class MarkdownTextContent extends StatelessWidget {
styleSheet: MarkdownStyleSheet.fromTheme(
Theme.of(context),
).copyWith(
textScaleFactor: isLargeText ? 1.1 : 1,
textScaler: TextScaler.linear(isLargeText ? 1.1 : 1),
blockquote: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
@ -74,15 +73,32 @@ class MarkdownTextContent extends StatelessWidget {
),
),
),
codeblockDecoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).dividerColor,
width: 0.3,
),
borderRadius: const BorderRadius.all(Radius.circular(4)),
color: Theme.of(context).colorScheme.surface.withOpacity(0.5),
)),
builders: {
'code': _MarkdownTextCodeElement(),
},
softLineBreak: true,
extensionSet: markdown.ExtensionSet(
markdown.ExtensionSet.gitHubFlavored.blockSyntaxes,
<markdown.BlockSyntax>[
markdown.CodeBlockSyntax(),
...markdown.ExtensionSet.commonMark.blockSyntaxes,
...markdown.ExtensionSet.gitHubFlavored.blockSyntaxes,
],
<markdown.InlineSyntax>[
if (isAutoWarp) markdown.LineBreakSyntax(),
_UserNameCardInlineSyntax(),
_CustomEmoteInlineSyntax(),
markdown.EmojiSyntax(),
markdown.AutolinkSyntax(),
markdown.AutolinkExtensionSyntax(),
markdown.CodeSyntax(),
...markdown.ExtensionSet.commonMark.inlineSyntaxes,
...markdown.ExtensionSet.gitHubFlavored.inlineSyntaxes
],
),
@ -184,7 +200,7 @@ class MarkdownTextContent extends StatelessWidget {
);
if (idx < paragraphs.length - 1) {
contentWidgets.add(const Gap(4));
contentWidgets.add(isAutoWarp ? const Gap(4) : const Gap(8));
}
}
@ -205,7 +221,7 @@ class MarkdownTextContent extends StatelessWidget {
}
}
class _UserNameCardInlineSyntax extends InlineSyntax {
class _UserNameCardInlineSyntax extends markdown.InlineSyntax {
_UserNameCardInlineSyntax() : super(r'@[a-zA-Z0-9_]+');
@override
@ -221,7 +237,7 @@ class _UserNameCardInlineSyntax extends InlineSyntax {
}
}
class _CustomEmoteInlineSyntax extends InlineSyntax {
class _CustomEmoteInlineSyntax extends markdown.InlineSyntax {
_CustomEmoteInlineSyntax() : super(r':([-\w]+):');
@override
@ -241,3 +257,48 @@ class _CustomEmoteInlineSyntax extends InlineSyntax {
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: AccountAvatar(
radius: 14,
content: auth.userProfile.value!['avatar'],
),
),
);
});
}
}

View File

@ -1,27 +1,33 @@
import 'package:flutter/material.dart';
import 'package:get/utils.dart';
import 'package:solian/widgets/navigation/app_account_widget.dart';
abstract class AppNavigation {
static List<AppNavigationDestination> destinations = [
AppNavigationDestination(
icon: Icons.dashboard,
label: 'dashboard'.tr,
icon: const Icon(Icons.dashboard),
label: 'dashboardNav'.tr,
page: 'dashboard',
),
AppNavigationDestination(
icon: Icons.explore,
icon: const Icon(Icons.explore),
label: 'explore'.tr,
page: 'explore',
),
AppNavigationDestination(
icon: Icons.workspaces,
icon: const Icon(Icons.forum),
label: 'chat'.tr,
page: 'chat',
),
AppNavigationDestination(
icon: const Icon(Icons.workspaces),
label: 'realms'.tr,
page: 'realms',
),
AppNavigationDestination(
icon: Icons.forum,
label: 'chat'.tr,
page: 'chat',
icon: const AppAccountWidget(),
label: 'accountNav'.tr,
page: 'account',
),
];
@ -30,7 +36,7 @@ abstract class AppNavigation {
}
class AppNavigationDestination {
final IconData icon;
final Widget icon;
final String label;
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)
AccountAvatar(
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();
});
widget.controller.thumbnail.value = value;
widget.controller.thumbnail.value = value.isEmpty ? null : value;
},
initialAttachments: const [],
onRemove: (_) {},
@ -91,7 +91,8 @@ class _PostEditorThumbnailDialogState extends State<PostEditorThumbnailDialog> {
actions: [
TextButton(
onPressed: () {
widget.controller.thumbnail.value = _attachmentController.text;
final text = _attachmentController.text;
widget.controller.thumbnail.value = text.isEmpty ? null : text;
Navigator.pop(context);
},
child: Text('confirm'.tr),

View File

@ -117,29 +117,15 @@ class _PostItemState extends State<PostItem> {
),
),
),
],
),
if (_contentHeight >= 80 && !widget.isFullContent)
Align(
alignment: Alignment.bottomCenter,
child: IgnorePointer(
child: Container(
height: 80,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
Theme.of(context).colorScheme.surfaceContainerLow,
Theme.of(context)
.colorScheme
.surface
.withOpacity(0),
],
),
),
),
),
),
],
Opacity(
opacity: 0.8,
child: InkWell(child: Text('readMore'.tr)),
).paddingOnly(
left: 12,
top: 4,
),
LinkExpansion(content: item.body['content']).paddingOnly(
left: 8,
@ -225,33 +211,15 @@ class _PostItemState extends State<PostItem> {
),
),
),
],
),
if (_contentHeight >= 320 && !widget.isFullContent)
Align(
alignment: Alignment.bottomCenter,
child: IgnorePointer(
child: Container(
height: 320,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
(widget.backgroundColor ??
Theme.of(context)
.colorScheme
.surface),
(widget.backgroundColor ??
Theme.of(context)
.colorScheme
.surface)
.withOpacity(0),
],
),
),
),
),
),
],
Opacity(
opacity: 0.8,
child: InkWell(child: Text('readMore'.tr)),
).paddingOnly(
left: 12,
top: 4,
),
if (widget.item.replyTo != null && widget.isShowEmbed)
Container(
@ -336,8 +304,7 @@ class _PostItemState extends State<PostItem> {
),
closedElevation: 0,
openElevation: 0,
closedColor:
widget.backgroundColor ?? Theme.of(context).colorScheme.surface,
closedColor: Colors.transparent,
openColor: Theme.of(context).colorScheme.surface,
);
}
@ -574,7 +541,7 @@ class _PostEmbedWidget extends StatelessWidget {
),
closedElevation: 0,
openElevation: 0,
closedColor: Theme.of(context).colorScheme.surface,
closedColor: Colors.transparent,
openColor: Theme.of(context).colorScheme.surface,
);
}

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

View File

@ -1,15 +0,0 @@
import 'package:flutter/material.dart';
class SidebarPlaceholder extends StatelessWidget {
const SidebarPlaceholder({super.key});
@override
Widget build(BuildContext context) {
return Material(
color: Theme.of(context).colorScheme.surface,
child: const Center(
child: Icon(Icons.menu_open, size: 50),
),
);
}
}

View File

@ -17,6 +17,7 @@ import flutter_local_notifications
import flutter_secure_storage_macos
import flutter_webrtc
import gal
import in_app_review
import livekit_client
import macos_window_utils
import media_kit_libs_macos_video
@ -46,6 +47,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin"))
GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin"))
InAppReviewPlugin.register(with: registry.registrar(forPlugin: "InAppReviewPlugin"))
LiveKitPlugin.register(with: registry.registrar(forPlugin: "LiveKitPlugin"))
MacOSWindowUtilsPlugin.register(with: registry.registrar(forPlugin: "MacOSWindowUtilsPlugin"))
MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin"))

View File

@ -8,38 +8,38 @@ PODS:
- FlutterMacOS
- file_selector_macos (0.0.1):
- FlutterMacOS
- Firebase/Analytics (11.0.0):
- Firebase/Analytics (11.2.0):
- Firebase/Core
- Firebase/Core (11.0.0):
- Firebase/Core (11.2.0):
- Firebase/CoreOnly
- FirebaseAnalytics (~> 11.0.0)
- Firebase/CoreOnly (11.0.0):
- FirebaseCore (= 11.0.0)
- Firebase/Crashlytics (11.0.0):
- FirebaseAnalytics (~> 11.2.0)
- Firebase/CoreOnly (11.2.0):
- FirebaseCore (= 11.2.0)
- Firebase/Crashlytics (11.2.0):
- Firebase/CoreOnly
- FirebaseCrashlytics (~> 11.0.0)
- Firebase/Messaging (11.0.0):
- FirebaseCrashlytics (~> 11.2.0)
- Firebase/Messaging (11.2.0):
- Firebase/CoreOnly
- FirebaseMessaging (~> 11.0.0)
- firebase_analytics (11.3.2):
- Firebase/Analytics (= 11.0.0)
- FirebaseMessaging (~> 11.2.0)
- firebase_analytics (11.3.3):
- Firebase/Analytics (= 11.2.0)
- firebase_core
- FlutterMacOS
- firebase_core (3.5.0):
- Firebase/CoreOnly (~> 11.0.0)
- firebase_core (3.6.0):
- Firebase/CoreOnly (~> 11.2.0)
- FlutterMacOS
- firebase_crashlytics (4.1.2):
- Firebase/CoreOnly (~> 11.0.0)
- Firebase/Crashlytics (~> 11.0.0)
- firebase_crashlytics (4.1.3):
- Firebase/CoreOnly (~> 11.2.0)
- Firebase/Crashlytics (~> 11.2.0)
- firebase_core
- FlutterMacOS
- firebase_messaging (15.1.2):
- Firebase/CoreOnly (~> 11.0.0)
- Firebase/Messaging (~> 11.0.0)
- firebase_messaging (15.1.3):
- Firebase/CoreOnly (~> 11.2.0)
- Firebase/Messaging (~> 11.2.0)
- firebase_core
- FlutterMacOS
- FirebaseAnalytics (11.0.0):
- FirebaseAnalytics/AdIdSupport (= 11.0.0)
- FirebaseAnalytics (11.2.0):
- FirebaseAnalytics/AdIdSupport (= 11.2.0)
- FirebaseCore (~> 11.0)
- FirebaseInstallations (~> 11.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
@ -47,24 +47,24 @@ PODS:
- GoogleUtilities/Network (~> 8.0)
- "GoogleUtilities/NSData+zlib (~> 8.0)"
- nanopb (~> 3.30910.0)
- FirebaseAnalytics/AdIdSupport (11.0.0):
- FirebaseAnalytics/AdIdSupport (11.2.0):
- FirebaseCore (~> 11.0)
- FirebaseInstallations (~> 11.0)
- GoogleAppMeasurement (= 11.0.0)
- GoogleAppMeasurement (= 11.2.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
- GoogleUtilities/MethodSwizzler (~> 8.0)
- GoogleUtilities/Network (~> 8.0)
- "GoogleUtilities/NSData+zlib (~> 8.0)"
- nanopb (~> 3.30910.0)
- FirebaseCore (11.0.0):
- FirebaseCore (11.2.0):
- FirebaseCoreInternal (~> 11.0)
- GoogleUtilities/Environment (~> 8.0)
- GoogleUtilities/Logger (~> 8.0)
- FirebaseCoreExtension (11.2.0):
- FirebaseCoreExtension (11.3.0):
- FirebaseCore (~> 11.0)
- FirebaseCoreInternal (11.2.0):
- FirebaseCoreInternal (11.3.0):
- "GoogleUtilities/NSData+zlib (~> 8.0)"
- FirebaseCrashlytics (11.0.0):
- FirebaseCrashlytics (11.2.0):
- FirebaseCore (~> 11.0)
- FirebaseInstallations (~> 11.0)
- FirebaseRemoteConfigInterop (~> 11.0)
@ -73,12 +73,12 @@ PODS:
- GoogleUtilities/Environment (~> 8.0)
- nanopb (~> 3.30910.0)
- PromisesObjC (~> 2.4)
- FirebaseInstallations (11.2.0):
- FirebaseInstallations (11.3.0):
- FirebaseCore (~> 11.0)
- GoogleUtilities/Environment (~> 8.0)
- GoogleUtilities/UserDefaults (~> 8.0)
- PromisesObjC (~> 2.4)
- FirebaseMessaging (11.0.0):
- FirebaseMessaging (11.2.0):
- FirebaseCore (~> 11.0)
- FirebaseInstallations (~> 11.0)
- GoogleDataTransport (~> 10.0)
@ -87,8 +87,8 @@ PODS:
- GoogleUtilities/Reachability (~> 8.0)
- GoogleUtilities/UserDefaults (~> 8.0)
- nanopb (~> 3.30910.0)
- FirebaseRemoteConfigInterop (11.2.0)
- FirebaseSessions (11.2.0):
- FirebaseRemoteConfigInterop (11.3.0)
- FirebaseSessions (11.3.0):
- FirebaseCore (~> 11.0)
- FirebaseCoreExtension (~> 11.0)
- FirebaseInstallations (~> 11.0)
@ -108,21 +108,21 @@ PODS:
- gal (1.0.0):
- Flutter
- FlutterMacOS
- GoogleAppMeasurement (11.0.0):
- GoogleAppMeasurement/AdIdSupport (= 11.0.0)
- GoogleAppMeasurement (11.2.0):
- GoogleAppMeasurement/AdIdSupport (= 11.2.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
- GoogleUtilities/MethodSwizzler (~> 8.0)
- GoogleUtilities/Network (~> 8.0)
- "GoogleUtilities/NSData+zlib (~> 8.0)"
- nanopb (~> 3.30910.0)
- GoogleAppMeasurement/AdIdSupport (11.0.0):
- GoogleAppMeasurement/WithoutAdIdSupport (= 11.0.0)
- GoogleAppMeasurement/AdIdSupport (11.2.0):
- GoogleAppMeasurement/WithoutAdIdSupport (= 11.2.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
- GoogleUtilities/MethodSwizzler (~> 8.0)
- GoogleUtilities/Network (~> 8.0)
- "GoogleUtilities/NSData+zlib (~> 8.0)"
- nanopb (~> 3.30910.0)
- GoogleAppMeasurement/WithoutAdIdSupport (11.0.0):
- GoogleAppMeasurement/WithoutAdIdSupport (11.2.0):
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
- GoogleUtilities/MethodSwizzler (~> 8.0)
- GoogleUtilities/Network (~> 8.0)
@ -158,6 +158,8 @@ PODS:
- GoogleUtilities/UserDefaults (8.0.2):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- in_app_review (0.2.0):
- FlutterMacOS
- livekit_client (2.2.6):
- FlutterMacOS
- WebRTC-SDK (= 125.6422.04)
@ -234,6 +236,7 @@ DEPENDENCIES:
- flutter_webrtc (from `Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos`)
- FlutterMacOS (from `Flutter/ephemeral`)
- gal (from `Flutter/ephemeral/.symlinks/plugins/gal/darwin`)
- in_app_review (from `Flutter/ephemeral/.symlinks/plugins/in_app_review/macos`)
- livekit_client (from `Flutter/ephemeral/.symlinks/plugins/livekit_client/macos`)
- macos_window_utils (from `Flutter/ephemeral/.symlinks/plugins/macos_window_utils/macos`)
- media_kit_libs_macos_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos`)
@ -299,6 +302,8 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral
gal:
:path: Flutter/ephemeral/.symlinks/plugins/gal/darwin
in_app_review:
:path: Flutter/ephemeral/.symlinks/plugins/in_app_review/macos
livekit_client:
:path: Flutter/ephemeral/.symlinks/plugins/livekit_client/macos
macos_window_utils:
@ -336,29 +341,30 @@ SPEC CHECKSUMS:
connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db
desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898
device_info_plus: ce1b7762849d3ec103d0e0517299f2db7ad60720
file_selector_macos: 54fdab7caa3ac3fc43c9fac4d7d8d231277f8cf2
Firebase: 9f574c08c2396885b5e7e100ed4293d956218af9
firebase_analytics: a2d0d907566e4a48e27745317f05b4b7db85edd9
firebase_core: c55630cdb8a01cf49eae741dd4bc8c93bdd546b8
firebase_crashlytics: a359f1f75a23e560c8c97b743ab684ba795d7688
firebase_messaging: 12726b352752420d073ff075e328cc2f0ca14c47
FirebaseAnalytics: 27eb78b97880ea4a004839b9bac0b58880f5a92a
FirebaseCore: 3cf438f431f18c12cdf2aaf64434648b63f7e383
FirebaseCoreExtension: cda74ddfb001224bd8fd1d6e74698b4ed07803de
FirebaseCoreInternal: 0c569513412da9f3b31bd0b340013bbee8f295c5
FirebaseCrashlytics: 745d8f0221fe49c62865391d1bf56f5a12eeec0b
FirebaseInstallations: 771177d89d6c451dc6e50085ec82e2fc77ed0a4a
FirebaseMessaging: d2d1d9c62c46dd2db49a952f7deb5b16ad2c9742
FirebaseRemoteConfigInterop: 477b26fdeb8fb5fbaf22fa9db5343b42289dc7db
FirebaseSessions: adcec8b72d0066a385e3affcd1bcb1ebb3908ce6
file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d
Firebase: 98e6bf5278170668a7983e12971a66b2cd57fc8c
firebase_analytics: 30ff72f6d4847ff0b479d8edd92fc8582e719072
firebase_core: e88f946a4601cb1854178cb07da241bba5a6508e
firebase_crashlytics: e09adc8a2db53e71b3ef3d085deb688a0c17467a
firebase_messaging: e1b1c1504659e13d66131f62ec22919293cd0d11
FirebaseAnalytics: c36efd5710c60c17558650fa58c2066eca7e9265
FirebaseCore: a282032ae9295c795714ded2ec9c522fc237f8da
FirebaseCoreExtension: 30bb063476ef66cd46925243d64ad8b2c8ac3264
FirebaseCoreInternal: ac26d09a70c730e497936430af4e60fb0c68ec4e
FirebaseCrashlytics: cfc69af5b53565dc6a5e563788809b5778ac4eac
FirebaseInstallations: 58cf94dabf1e2bb2fa87725a9be5c2249171cda0
FirebaseMessaging: c9ec7b90c399c7a6100297e9d16f8a27fc7f7152
FirebaseRemoteConfigInterop: c3a5c31b3c22079f41ba1dc645df889d9ce38cb9
FirebaseSessions: 655ff17f3cc1a635cbdc2d69b953878001f9e25b
flutter_local_notifications: 3805ca215b2fb7f397d78b66db91f6a747af52e4
flutter_secure_storage_macos: 59459653abe1adb92abbc8ea747d79f8d19866c9
flutter_webrtc: 2b4e4a2de70a1485836e40fd71a7a94c77d49bd9
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
gal: 61e868295d28fe67ffa297fae6dacebf56fd53e1
GoogleAppMeasurement: 6e49ffac7d3f2c3ded9cc663f912a13b67bbd0de
GoogleAppMeasurement: 76d4f8b36b03bd8381fa9a7fe2cc7f99c0a2e93a
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
in_app_review: a850789fad746e89bce03d4aeee8078b45a53fd0
livekit_client: 98d09566e3a936b3402be8091ec3845556d36800
macos_window_utils: 933f91f64805e2eb91a5bd057cf97cd097276663
media_kit_libs_macos_video: b3e2bbec2eef97c285f2b1baa7963c67c753fb82
@ -377,7 +383,7 @@ SPEC CHECKSUMS:
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
sqlite3: 0bb0e6389d824e40296f531b858a2a0b71c0d2fb
sqlite3_flutter_libs: 5ca46c1a04eddfbeeb5b16566164aa7ad1616e7b
url_launcher_macos: 5f437abeda8c85500ceb03f5c1938a8c5a705399
url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404
wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269
WebRTC-SDK: c3d69a87e7185fad3568f6f3cff7c9ac5890acf3

View File

@ -12,8 +12,6 @@
<true/>
<key>com.apple.security.device.audio-input</key>
<true/>
<key>com.apple.security.device.bluetooth</key>
<true/>
<key>com.apple.security.device.camera</key>
<true/>
<key>com.apple.security.files.user-selected.read-only</key>

View File

@ -47,10 +47,21 @@
<string>MainMenu</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
<key>CFBundleLocalizations</key>
<array>
<string>zh_CN</string>
<string>en</string>
</array>
<key>NSUserActivityTypes</key>
<array>
<string>INStartCallIntent</string>
<string>INSendMessageIntent</string>
</array>
<key>NSCameraUsageDescription</key>
<string>Allow you take photo/video for your message or post</string>
<key>NSMicrophoneUsageDescription</key>
<string>Allow you record audio for your message or post</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Allow you add photo to your message or post</string>
</dict>
</plist>

View File

@ -10,8 +10,6 @@
<true/>
<key>com.apple.security.device.audio-input</key>
<true/>
<key>com.apple.security.device.bluetooth</key>
<true/>
<key>com.apple.security.device.camera</key>
<true/>
<key>com.apple.security.files.user-selected.read-only</key>

View File

@ -13,10 +13,10 @@ packages:
dependency: transitive
description:
name: _flutterfire_internals
sha256: "5fdcea390499dd26c808a3c662df5f4208d6bbc0643072eee94f1476249e2818"
sha256: "5534e701a2c505fed1f0799e652dd6ae23bd4d2c4cf797220e5ced5764a7c1c2"
url: "https://pub.dev"
source: hosted
version: "1.3.43"
version: "1.3.44"
_macros:
dependency: transitive
description: dart
@ -146,10 +146,10 @@ packages:
dependency: "direct dev"
description:
name: build_runner
sha256: dd09dd4e2b078992f42aac7f1a622f01882a8492fef08486b27ddde929c19f04
sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d"
url: "https://pub.dev"
source: hosted
version: "2.4.12"
version: "2.4.13"
build_runner_core:
dependency: transitive
description:
@ -386,10 +386,10 @@ packages:
dependency: "direct main"
description:
name: drift
sha256: "5b561ec76fff260e1e0593a29ca0d058a140a4b4dfb11dcc0c3813820cd20200"
sha256: d6ff1ec6a0f3fa097dda6b776cf601f1f3d88b53b287288e09c1306f394fb1b3
url: "https://pub.dev"
source: hosted
version: "2.20.2"
version: "2.20.3"
drift_dev:
dependency: "direct dev"
description:
@ -466,18 +466,18 @@ packages:
dependency: transitive
description:
name: file_selector_linux
sha256: "045d372bf19b02aeb69cacf8b4009555fb5f6f0b7ad8016e5f46dd1387ddd492"
sha256: "712ce7fab537ba532c8febdb1a8f167b32441e74acd68c3ccb2e36dcb52c4ab2"
url: "https://pub.dev"
source: hosted
version: "0.9.2+1"
version: "0.9.3"
file_selector_macos:
dependency: transitive
description:
name: file_selector_macos
sha256: f42eacb83b318e183b1ae24eead1373ab1334084404c8c16e0354f9a3e55d385
sha256: "271ab9986df0c135d45c3cdb6bd0faa5db6f4976d3e4b437cf7d0f258d941bfc"
url: "https://pub.dev"
source: hosted
version: "0.9.4"
version: "0.9.4+2"
file_selector_platform_interface:
dependency: transitive
description:
@ -490,42 +490,42 @@ packages:
dependency: transitive
description:
name: file_selector_windows
sha256: "2ad726953f6e8affbc4df8dc78b77c3b4a060967a291e528ef72ae846c60fb69"
sha256: "8f5d2f6590d51ecd9179ba39c64f722edc15226cc93dcc8698466ad36a4a85a4"
url: "https://pub.dev"
source: hosted
version: "0.9.3+2"
version: "0.9.3+3"
firebase_analytics:
dependency: "direct main"
description:
name: firebase_analytics
sha256: "9c52c099e9cbb852c7f1d2302c7eb34a15758834eca1877f7a779e6082f9882d"
sha256: "2c4e7b548d41b46e8aa08bc3bd1163146be7e6d48f678f2e6dd3114994e42458"
url: "https://pub.dev"
source: hosted
version: "11.3.2"
version: "11.3.3"
firebase_analytics_platform_interface:
dependency: transitive
description:
name: firebase_analytics_platform_interface
sha256: "4ec57aee951832fdbf10ca722bbb83fe0001d6168d6c4cfea9ccee0df6afb1e0"
sha256: c259ae890c7d4c5d1675d35936be0b1fcd587fce9645948982cd87ad08df6222
url: "https://pub.dev"
source: hosted
version: "4.2.4"
version: "4.2.5"
firebase_analytics_web:
dependency: transitive
description:
name: firebase_analytics_web
sha256: "95c594fb1e8960992a607b135459e2f9ea3683dd8d01e6b845cace7c6665ec7e"
sha256: "5988d1fd022e55515c2a14811c9b5104c32acde115874a9a69ff7c77c4c05cd9"
url: "https://pub.dev"
source: hosted
version: "0.5.10+1"
version: "0.5.10+2"
firebase_core:
dependency: "direct main"
description:
name: firebase_core
sha256: c7de9354eb2cd8bfe8059e1112174c9a58beda7051807207306bc48283277cfb
sha256: "51dfe2fbf3a984787a2e7b8592f2f05c986bfedd6fdacea3f9e0a7beb334de96"
url: "https://pub.dev"
source: hosted
version: "3.5.0"
version: "3.6.0"
firebase_core_platform_interface:
dependency: transitive
description:
@ -546,66 +546,66 @@ packages:
dependency: "direct main"
description:
name: firebase_crashlytics
sha256: "7821f9d8373b91f2a5ca8214226891d5870e196a7376f66350f65204387e9c15"
sha256: "6899800fff1af819955aef740f18c4c8600f8b952a2a1ea97bc0872ebb257387"
url: "https://pub.dev"
source: hosted
version: "4.1.2"
version: "4.1.3"
firebase_crashlytics_platform_interface:
dependency: transitive
description:
name: firebase_crashlytics_platform_interface
sha256: "8ed539fd9e9b6c07905f9f44c5f6d4785ac841a5a8195bd35586c8b1d54ec26d"
sha256: "97c47b0a1779a3d4118416a3f0c6c564cc59ad89095e899893204d4b2ad08f4c"
url: "https://pub.dev"
source: hosted
version: "3.6.43"
version: "3.6.44"
firebase_messaging:
dependency: "direct main"
description:
name: firebase_messaging
sha256: "32ce60b747e755b48d7112d728d4f736ba82acd98ec825626558d444d385fa3a"
sha256: eb6e28a3a35deda61fe8634967c84215efc19133ba58d8e0fc6c9a2af2cba05e
url: "https://pub.dev"
source: hosted
version: "15.1.2"
version: "15.1.3"
firebase_messaging_platform_interface:
dependency: transitive
description:
name: firebase_messaging_platform_interface
sha256: "69671a0f1a40c7b7c46ad0283e6f34ca2a59a0362ca14a240a4ea01c46e8a521"
sha256: b316c4ee10d93d32c033644207afc282d9b2b4372f3cf9c6022f3558b3873d2d
url: "https://pub.dev"
source: hosted
version: "4.5.45"
version: "4.5.46"
firebase_messaging_web:
dependency: transitive
description:
name: firebase_messaging_web
sha256: "6890111a9d01d7b13d0f6fe74850812c334e903d2c80a2d9356a3abb8c3a9e9a"
sha256: d7f0147a1a9fe4313168e20154a01fd5cf332898de1527d3930ff77b8c7f5387
url: "https://pub.dev"
source: hosted
version: "3.9.1"
version: "3.9.2"
firebase_performance:
dependency: "direct main"
description:
name: firebase_performance
sha256: ed9a408b6d1f221fc0e2890dcf0733b604d1aea6cd3b897f97dd3f889f01ddfc
sha256: "0df8208afad64aa1d774bd267033312284bd73e68210caf6936dc1f8a8fa0878"
url: "https://pub.dev"
source: hosted
version: "0.10.0+7"
version: "0.10.0+8"
firebase_performance_platform_interface:
dependency: transitive
description:
name: firebase_performance_platform_interface
sha256: bfcfbfcefeaf3853a72602675b786e13a609d49ac70fc325d302b5794b8b0c06
sha256: "97cc3fcda0a835142ff2e93b19dd72904d666a576e196f12cdaf492921e6ea44"
url: "https://pub.dev"
source: hosted
version: "0.1.4+43"
version: "0.1.4+44"
firebase_performance_web:
dependency: transitive
description:
name: firebase_performance_web
sha256: "2ac9e44a1be7b20f1a7a3912d84bf2e1ec76398f2dadc07b6b7c3173d590e329"
sha256: "322a4ae99cb952cdfd788399f52421dd6ecd713723ac7e5e6bb7856bb28ef270"
url: "https://pub.dev"
source: hosted
version: "0.1.7+1"
version: "0.1.7+2"
fixnum:
dependency: transitive
description:
@ -751,18 +751,18 @@ packages:
dependency: "direct dev"
description:
name: flutter_launcher_icons
sha256: a38f2f1b3c373d42bf08bd17d60e20d3c73abce7727607b4d085ec7d5acaa294
sha256: "619817c4b65b322b5104b6bb6dfe6cda62d9729bd7ad4303ecc8b4e690a67a77"
url: "https://pub.dev"
source: hosted
version: "0.14.0"
version: "0.14.1"
flutter_lints:
dependency: "direct dev"
description:
name: flutter_lints
sha256: "3f41d009ba7172d5ff9be5f6e6e6abb4300e263aab8866d2a0842ed2a70f8f0c"
sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1"
url: "https://pub.dev"
source: hosted
version: "4.0.0"
version: "5.0.0"
flutter_local_notifications:
dependency: "direct main"
description:
@ -791,18 +791,10 @@ packages:
dependency: "direct main"
description:
name: flutter_markdown
sha256: a23c41ee57573e62fc2190a1f36a0480c4d90bde3a8a8d7126e5d5992fb53fb7
sha256: e17575ca576a34b46c58c91f9948891117a1bd97815d2e661813c7f90c647a78
url: "https://pub.dev"
source: hosted
version: "0.7.3+1"
flutter_markdown_selectionarea:
dependency: "direct main"
description:
name: flutter_markdown_selectionarea
sha256: d4bc27e70a5c40ebdab23a4b81f75d53696a214d4d1f13c12045b38a0ddc58a2
url: "https://pub.dev"
source: hosted
version: "0.6.17+1"
version: "0.7.3+2"
flutter_native_splash:
dependency: "direct dev"
description:
@ -815,10 +807,10 @@ packages:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
sha256: "9ee02950848f61c4129af3d6ec84a1cfc0e47931abc746b03e7a3bc3e8ff6eda"
sha256: "9b78450b89f059e96c9ebb355fa6b3df1d6b330436e0b885fb49594c41721398"
url: "https://pub.dev"
source: hosted
version: "2.0.22"
version: "2.0.23"
flutter_secure_storage:
dependency: "direct main"
description:
@ -871,10 +863,10 @@ packages:
dependency: transitive
description:
name: flutter_shaders
sha256: "02750b545c01ff4d8e9bbe8f27a7731aa3778402506c67daa1de7f5fc3f4befe"
sha256: "34794acadd8275d971e02df03afee3dee0f98dbfb8c4837082ad0034f612a3e2"
url: "https://pub.dev"
source: hosted
version: "0.1.2"
version: "0.1.3"
flutter_staggered_grid_view:
dependency: transitive
description:
@ -969,10 +961,10 @@ packages:
dependency: "direct main"
description:
name: go_router
sha256: "2ddb88e9ad56ae15ee144ed10e33886777eb5ca2509a914850a5faa7b52ff459"
sha256: "6f1b756f6e863259a99135ff3c95026c3cdca17d10ebef2bba2261a25ddc8bbc"
url: "https://pub.dev"
source: hosted
version: "14.2.7"
version: "14.3.0"
google_fonts:
dependency: "direct main"
description:
@ -1073,10 +1065,10 @@ packages:
dependency: transitive
description:
name: image_picker_android
sha256: c0a6763d50b354793d0192afd0a12560b823147d3ded7c6b77daf658fa05cc85
sha256: d3e5e00fdfeca8fd4ffb3227001264d449cc8950414c2ff70b0e06b9c628e643
url: "https://pub.dev"
source: hosted
version: "0.8.12+13"
version: "0.8.12+15"
image_picker_for_web:
dependency: transitive
description:
@ -1125,6 +1117,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.2.1+1"
in_app_review:
dependency: "direct main"
description:
name: in_app_review
sha256: "99869244d09adc76af16bf8fd731dd13cef58ecafd5917847589c49f378cbb30"
url: "https://pub.dev"
source: hosted
version: "2.0.9"
in_app_review_platform_interface:
dependency: transitive
description:
name: in_app_review_platform_interface
sha256: fed2c755f2125caa9ae10495a3c163aa7fab5af3585a9c62ef4a6920c5b45f10
url: "https://pub.dev"
source: hosted
version: "2.0.5"
infinite_scroll_pagination:
dependency: "direct main"
description:
@ -1201,10 +1209,10 @@ packages:
dependency: transitive
description:
name: lints
sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235"
sha256: "3315600f3fb3b135be672bf4a178c55f274bebe368325ae18462c89ac1e3b413"
url: "https://pub.dev"
source: hosted
version: "4.0.0"
version: "5.0.0"
livekit_client:
dependency: "direct main"
description:
@ -1441,10 +1449,10 @@ packages:
dependency: transitive
description:
name: path_provider_android
sha256: "6f01f8e37ec30b07bc424b4deabac37cacb1bc7e2e515ad74486039918a37eb7"
sha256: f7544c346a0742aee1450f9e5c0f5269d7c602b9c95fdbcd9fb8f5b1df13b1cc
url: "https://pub.dev"
source: hosted
version: "2.2.10"
version: "2.2.11"
path_provider_foundation:
dependency: transitive
description:
@ -1785,18 +1793,18 @@ packages:
dependency: transitive
description:
name: shared_preferences_android
sha256: "480ba4345773f56acda9abf5f50bd966f581dac5d514e5fc4a18c62976bbba7e"
sha256: "3b9febd815c9ca29c9e3520d50ec32f49157711e143b7a4ca039eb87e8ade5ab"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
version: "2.3.3"
shared_preferences_foundation:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: c4b35f6cb8f63c147312c054ce7c2254c8066745125264f0c88739c417fc9d9f
sha256: "07e050c7cd39bad516f8d64c455f04508d09df104be326d8c02551590a0d513d"
url: "https://pub.dev"
source: hosted
version: "2.5.2"
version: "2.5.3"
shared_preferences_linux:
dependency: transitive
description:
@ -1894,18 +1902,18 @@ packages:
dependency: transitive
description:
name: sqflite
sha256: a43e5a27235518c03ca238e7b4732cf35eabe863a369ceba6cbefa537a66f16d
sha256: ff5a2436ef8ebdfda748fbfe957f9981524cb5ff11e7bafa8c42771840e8a788
url: "https://pub.dev"
source: hosted
version: "2.3.3+1"
version: "2.3.3+2"
sqflite_common:
dependency: transitive
description:
name: sqflite_common
sha256: "4058172e418eb7e7f2058dcb7657d451a8fc264afa0dea4dbd0f304a57131611"
sha256: "2d8e607db72e9cb7748c9c6e739e2c9618320a5517de693d5a24609c4671b1a4"
url: "https://pub.dev"
source: hosted
version: "2.5.4+3"
version: "2.5.4+4"
sqlite3:
dependency: transitive
description:
@ -1966,10 +1974,18 @@ packages:
dependency: transitive
description:
name: synchronized
sha256: "51b08572b9f091f8c3eb4d9d4be253f196ff0075d5ec9b10a884026d5b55d7bc"
sha256: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225"
url: "https://pub.dev"
source: hosted
version: "3.3.0+2"
version: "3.3.0+3"
syntax_highlight:
dependency: "direct main"
description:
name: syntax_highlight
sha256: ee33b6aa82cc722bb9b40152a792181dee222353b486c0255fde666a3e3a4997
url: "https://pub.dev"
source: hosted
version: "0.4.0"
term_glyph:
dependency: transitive
description:
@ -2054,10 +2070,10 @@ packages:
dependency: transitive
description:
name: url_launcher_android
sha256: e35a698ac302dd68e41f73250bd9517fe3ab5fa4f18fe4647a0872db61bacbab
sha256: "8fc3bae0b68c02c47c5c86fa8bfa74471d42687b0eded01b78de87872db745e2"
url: "https://pub.dev"
source: hosted
version: "6.3.10"
version: "6.3.12"
url_launcher_ios:
dependency: transitive
description:
@ -2078,10 +2094,10 @@ packages:
dependency: transitive
description:
name: url_launcher_macos
sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de"
sha256: "769549c999acdb42b8bcfa7c43d72bf79a382ca7441ab18a808e101149daf672"
url: "https://pub.dev"
source: hosted
version: "3.2.0"
version: "3.2.1"
url_launcher_platform_interface:
dependency: transitive
description:
@ -2110,10 +2126,10 @@ packages:
dependency: "direct main"
description:
name: uuid
sha256: f33d6bb662f0e4f79dcd7ada2e6170f3b3a2530c28fc41f49a411ddedd576a77
sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff
url: "https://pub.dev"
source: hosted
version: "4.5.0"
version: "4.5.1"
vector_graphics:
dependency: transitive
description:
@ -2206,10 +2222,10 @@ packages:
dependency: transitive
description:
name: web
sha256: d43c1d6b787bf0afad444700ae7f4db8827f701bc61c255ac8d328c6f4d52062
sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb
url: "https://pub.dev"
source: hosted
version: "1.0.0"
version: "1.1.0"
web_socket:
dependency: transitive
description:
@ -2238,10 +2254,10 @@ packages:
dependency: transitive
description:
name: win32
sha256: "68d1e89a91ed61ad9c370f9f8b6effed9ae5e0ede22a270bdfa6daf79fc2290a"
sha256: "4d45dc9069dba4619dc0ebd93c7cec5e66d8482cb625a370ac806dcc8165f2ec"
url: "https://pub.dev"
source: hosted
version: "5.5.4"
version: "5.5.5"
win32_registry:
dependency: transitive
description:
@ -2254,10 +2270,10 @@ packages:
dependency: transitive
description:
name: xdg_directories
sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
url: "https://pub.dev"
source: hosted
version: "1.0.4"
version: "1.1.0"
xml:
dependency: transitive
description:

View File

@ -2,7 +2,7 @@ name: solian
description: "The Solar Network App"
publish_to: "none"
version: 1.2.3+2
version: 1.3.6+5
environment:
sdk: ">=3.3.4 <4.0.0"
@ -49,7 +49,6 @@ dependencies:
dismissible_page: ^1.0.2
share_plus: ^10.0.0
flutter_cache_manager: ^3.3.3
flutter_markdown_selectionarea: ^0.6.17+1
shared_preferences: ^2.2.3
provider: ^6.1.2
gal: ^2.3.0
@ -83,12 +82,14 @@ dependencies:
flutter_app_update: ^3.1.0
version: ^3.0.2
action_slider: ^0.7.0
in_app_review: ^2.0.9
syntax_highlight: ^0.4.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^4.0.0
flutter_lints: ^5.0.0
flutter_launcher_icons: ^0.14.0
build_runner: ^2.4.12
@ -102,6 +103,7 @@ flutter:
assets:
- assets/logo.png
- assets/locales/
- assets/highlighting/
fonts:
- family: NotoSansEmoji