Compare commits

..

No commits in common. "master" and "1.3.6+3" have entirely different histories.

110 changed files with 2939 additions and 6305 deletions

View File

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

View File

@ -1,5 +0,0 @@
{
"cSpell.words": [
"annvisery"
]
}

View File

@ -1,358 +0,0 @@
{
"name": "C++",
"version": "1.0.0",
"fileTypes": ["cpp", "hpp", "cc", "h"],
"scopeName": "source.cpp",
"foldingStartMarker": "\\{\\s*$",
"foldingStopMarker": "^\\s*\\}",
"patterns": [
{
"name": "meta.preprocessor.script.cpp",
"match": "^\\s*#\\s*(include|define|if|ifdef|ifndef|else|endif|pragma)\\b"
},
{
"name": "meta.declaration.cpp",
"begin": "^\\w*\\b(namespace|class|struct|enum|typedef|template)\\b",
"beginCaptures": {
"0": {
"name": "keyword.other.declaration.cpp"
}
},
"end": "(\\{|;)",
"endCaptures": {
"0": {
"name": "punctuation.terminator.cpp"
}
},
"patterns": [
{
"include": "#strings"
},
{
"include": "#comments"
},
{
"name": "keyword.other.cpp",
"match": "\\b(public|private|protected|virtual|override|final)\\b"
}
]
},
{
"include": "#comments"
},
{
"include": "#punctuation"
},
{
"include": "#annotations"
},
{
"include": "#keywords"
},
{
"include": "#constants-and-special-vars"
},
{
"include": "#operators"
},
{
"include": "#strings"
}
],
"repository": {
"comments": {
"patterns": [
{
"name": "comment.block.empty.cpp",
"match": "/\\*\\*/",
"captures": {
"0": {
"name": "punctuation.definition.comment.cpp"
}
}
},
{
"include": "#comments-doc-oldschool"
},
{
"include": "#comments-doc"
},
{
"include": "#comments-inline"
}
]
},
"comments-doc-oldschool": {
"patterns": [
{
"name": "comment.block.documentation.cpp",
"begin": "/\\*\\*",
"end": "\\*/",
"patterns": [
{
"include": "#comments-doc-oldschool"
},
{
"include": "#comments-block"
}
]
}
]
},
"comments-doc": {
"patterns": [
{
"name": "comment.block.documentation.cpp",
"begin": "///",
"while": "^\\s*///",
"patterns": [
{
"include": "#comments-inline"
}
]
}
]
},
"comments-inline": {
"patterns": [
{
"include": "#comments-block"
},
{
"match": "(//.*)$",
"captures": {
"1": {
"name": "comment.line.double-slash.cpp"
}
}
}
]
},
"comments-block": {
"patterns": [
{
"name": "comment.block.cpp",
"begin": "/\\*",
"end": "\\*/",
"patterns": [
{
"include": "#comments-block"
}
]
}
]
},
"annotations": {
"patterns": [
{
"name": "storage.type.annotation.cpp",
"match": "__attribute__\\(\\w+\\)"
}
]
},
"constants-and-special-vars": {
"patterns": [
{
"name": "constant.language.cpp",
"match": "\\b(true|false|nullptr)\\b"
},
{
"name": "variable.language.cpp",
"match": "\\b(this|super)\\b"
},
{
"name": "constant.numeric.cpp",
"match": "\\b((0(x|X)[0-9a-fA-F]+)|(([0-9]+\\.?[0-9]*)|(\\.[0-9]+))((e|E)(\\+|-)?[0-9]+)?)\\b"
},
{
"include": "#class-identifier"
},
{
"include": "#function-identifier"
}
]
},
"class-identifier": {
"patterns": [
{
"match": "\\b(bool|int|char|double|float|long|short|signed|unsigned|void)\\b",
"name": "storage.type.primitive.cpp"
},
{
"begin": "(\\b[A-Z]\\w*\\b)",
"end": "(?!<)",
"beginCaptures": {
"1": {
"name": "support.class.cpp"
}
},
"patterns": [
{
"include": "#type-args"
}
]
}
]
},
"function-identifier": {
"patterns": [
{
"match": "\\b([a-z_][a-zA-Z0-9_]*)\\s*\\(",
"captures": {
"1": {
"name": "entity.name.function.cpp"
}
}
}
]
},
"type-args": {
"begin": "(<)",
"end": "(>)",
"beginCaptures": {
"1": {
"name": "other.source.cpp"
}
},
"endCaptures": {
"1": {
"name": "other.source.cpp"
}
},
"patterns": [
{
"include": "#class-identifier"
},
{
"match": ","
},
{
"name": "keyword.declaration.cpp",
"match": "extends"
},
{
"include": "#comments"
}
]
},
"keywords": {
"patterns": [
{
"name": "keyword.control.cpp",
"match": "\\b(if|else|for|while|do|switch|case|break|continue|goto|return)\\b"
},
{
"name": "keyword.operator.cpp",
"match": "\\b(sizeof|typeid|decltype|new|delete)\\b"
},
{
"name": "keyword.control.try.cpp",
"match": "\\b(try|catch|throw)\\b"
},
{
"name": "keyword.control.cpp",
"match": "\\b(static|inline|virtual|override|const|volatile|explicit|friend|constexpr)\\b"
}
]
},
"operators": {
"patterns": [
{
"name": "keyword.operator.comparison.cpp",
"match": "(==|!=|<=?|>=?)"
},
{
"name": "keyword.operator.arithmetic.cpp",
"match": "(\\+|\\-|\\*|\\/|%)"
},
{
"name": "keyword.operator.assignment.cpp",
"match": "(=|\\+=|-=|\\*=|/=|%=)"
},
{
"name": "keyword.operator.logical.cpp",
"match": "(\\&\\&|\\|\\||!)"
},
{
"name": "keyword.operator.bitwise.cpp",
"match": "(<<|>>|\\&|\\||\\^|~)"
}
]
},
"string-interp": {
"patterns": [
{
"match": "\\$([a-zA-Z0-9_]+)",
"captures": {
"1": {
"name": "variable.parameter.cpp"
}
}
},
{
"name": "string.interpolated.expression.cpp",
"begin": "\\$\\{",
"end": "\\}",
"patterns": [
{
"include": "#constants-and-special-vars",
"name": "variable.parameter.cpp"
},
{
"include": "#strings"
},
{
"name": "variable.parameter.cpp",
"match": "[a-zA-Z0-9_]+"
}
]
},
{
"name": "constant.character.escape.cpp",
"match": "\\\\."
}
]
},
"strings": {
"patterns": [
{
"name": "string.quoted.double.cpp",
"begin": "\"",
"end": "\"",
"patterns": [
{
"name": "constant.character.escape.cpp",
"match": "\\\\."
}
]
},
{
"name": "string.quoted.single.cpp",
"begin": "'",
"end": "'",
"patterns": [
{
"name": "constant.character.escape.cpp",
"match": "\\\\."
}
]
}
]
},
"punctuation": {
"patterns": [
{
"name": "punctuation.comma.cpp",
"match": ","
},
{
"name": "punctuation.terminator.cpp",
"match": ";"
}
]
}
}
}

View File

@ -1,531 +0,0 @@
{
"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

@ -1,212 +0,0 @@
{
"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

@ -1,98 +0,0 @@
{
"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

@ -1,145 +0,0 @@
{
"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

@ -1,66 +0,0 @@
{
"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

@ -54,7 +54,6 @@
"about": "About", "about": "About",
"edit": "Edit", "edit": "Edit",
"delete": "Delete", "delete": "Delete",
"insert": "Insert",
"settings": "Settings", "settings": "Settings",
"settingsNotificationBgService": "Background notification service", "settingsNotificationBgService": "Background notification service",
"settingsNotificationBgServiceDesc": "A notification service is always installed on the device, so that some devices that do not support push notifications can receive notifications in the background. When this feature is enabled, push notifications will not be registered with the server, and you will always appear to be online in the eyes of others (except for invisible). You may need to turn off power and traffic optimization in the settings.", "settingsNotificationBgServiceDesc": "A notification service is always installed on the device, so that some devices that do not support push notifications can receive notifications in the background. When this feature is enabled, push notifications will not be registered with the server, and you will always appear to be online in the eyes of others (except for invisible). You may need to turn off power and traffic optimization in the settings.",
@ -141,7 +140,7 @@
"clear": "Clear", "clear": "Clear",
"pinPost": "Pin this post", "pinPost": "Pin this post",
"unpinPost": "Unpin this post", "unpinPost": "Unpin this post",
"postRestoreFromLocal": "Restored", "postRestoreFromLocal": "Restore from local",
"postAutoSaveAt": "Auto saved at @date", "postAutoSaveAt": "Auto saved at @date",
"postCategoriesAndTags": "Categories n' Tags", "postCategoriesAndTags": "Categories n' Tags",
"postPublishDate": "Publish Date", "postPublishDate": "Publish Date",
@ -367,9 +366,8 @@
"bsPreparingData": "Preparing User Data", "bsPreparingData": "Preparing User Data",
"bsRegisteringPushNotify": "Enabling Push Notifications", "bsRegisteringPushNotify": "Enabling Push Notifications",
"bsDismissibleErrorHint": "Click anywhere to ignore this error", "bsDismissibleErrorHint": "Click anywhere to ignore this error",
"bsContinuable": "Click anywhere to continue",
"postShareContent": "@content\n\n@username on the Solar Network\nCheck it out: @link", "postShareContent": "@content\n\n@username on the Solar Network\nCheck it out: @link",
"postShareSubject": "@title by @username on Solar Network", "postShareSubject": "@username posted a post on the Solar Network",
"themeColor": "Global Theme Color", "themeColor": "Global Theme Color",
"themeColorRed": "Modern Red", "themeColorRed": "Modern Red",
"themeColorBlue": "Classic Blue", "themeColorBlue": "Classic Blue",
@ -479,20 +477,5 @@
"agedTheme": "Old school style theme", "agedTheme": "Old school style theme",
"agedThemeDesc": "Downgrade the global theme to Material Design 2. Unexpected issues may occur. For experimental use only.", "agedThemeDesc": "Downgrade the global theme to Material Design 2. Unexpected issues may occur. For experimental use only.",
"appBackgroundImage": "Global background image", "appBackgroundImage": "Global background image",
"appBackgroundImageDesc": "The global background image will be displayed on all pages", "appBackgroundImageDesc": "The global background image will be displayed on all pages"
"authPreferences": "Auth preferences",
"authPreferencesDesc": "Set the security behavior of your account",
"authMaximumAuthSteps": "Maximum authentication steps",
"authMaximumAuthStepsDesc": "The maximum number of authentication steps when logging in, higher value is more secure, lower value is more convenient; default is 2",
"auditLog": "Audit log",
"shareImage": "Share as image",
"shareImageFooter": "Only on the Solar Network",
"fileSavedAt": "File saved at @path",
"showIp": "Show IP Address",
"shotOn": "Shot on @device",
"unread": "Unread",
"searchTook": "Took @time",
"searchResult": "@count Matches",
"happyBirthday": "Happy birthday @name!",
"happyBirthdayDesc": "Today is your @count birthday"
} }

View File

@ -14,7 +14,6 @@
"about": "关于", "about": "关于",
"edit": "编辑", "edit": "编辑",
"delete": "删除", "delete": "删除",
"insert": "插入",
"settings": "设置", "settings": "设置",
"settingsNotificationBgService": "常驻通知服务", "settingsNotificationBgService": "常驻通知服务",
"settingsNotificationBgServiceDesc": "在设备常驻一个通知服务,使得部分不支持推送通知的设备可以在后台收到通知;启用该功能的情况下不会向服务器注册推送通知,并且你会始终在他人眼中成为在线(隐身除外);可能需要在设置中关闭电量与流量优化。", "settingsNotificationBgServiceDesc": "在设备常驻一个通知服务,使得部分不支持推送通知的设备可以在后台收到通知;启用该功能的情况下不会向服务器注册推送通知,并且你会始终在他人眼中成为在线(隐身除外);可能需要在设置中关闭电量与流量优化。",
@ -363,9 +362,8 @@
"bsPreparingData": "正在准备用户资料", "bsPreparingData": "正在准备用户资料",
"bsRegisteringPushNotify": "正在启用推送通知", "bsRegisteringPushNotify": "正在启用推送通知",
"bsDismissibleErrorHint": "点击任意地方忽略此错误", "bsDismissibleErrorHint": "点击任意地方忽略此错误",
"bsContinuable": "点击任意处继续",
"postShareContent": "@content\n\n@username 在 Solar Network\n原帖地址@link", "postShareContent": "@content\n\n@username 在 Solar Network\n原帖地址@link",
"postShareSubject": "@username 在 Solar Network 发表的 @title", "postShareSubject": "@username 在 Solar Network 上发布了一篇帖子",
"themeColor": "全局主题色", "themeColor": "全局主题色",
"themeColorRed": "现代红", "themeColorRed": "现代红",
"themeColorBlue": "经典蓝", "themeColorBlue": "经典蓝",
@ -475,20 +473,5 @@
"agedTheme": "过时主题", "agedTheme": "过时主题",
"agedThemeDesc": "将全局主题降级为 Material Design 2可能发生意料之外的问题仅供实验使用", "agedThemeDesc": "将全局主题降级为 Material Design 2可能发生意料之外的问题仅供实验使用",
"appBackgroundImage": "全局背景图片", "appBackgroundImage": "全局背景图片",
"appBackgroundImageDesc": "全局背景图片将会在所有页面中展示", "appBackgroundImageDesc": "全局背景图片将会在所有页面中展示"
"authPreferences": "安全偏好设置",
"authPreferencesDesc": "调整账号的安全行为模式",
"authMaximumAuthSteps": "最大认证步数",
"authMaximumAuthStepsDesc": "登陆时最多的验证步数,值越高则越安全,反之则会相对方便;默认设置为 2",
"auditLog": "活动日志",
"shareImage": "分享图片",
"shareImageFooter": "上 Solar Network 看更多有趣帖子",
"fileSavedAt": "文件保存于 @path",
"showIp": "显示 IP 地址",
"shotOn": "由 @device 拍摄",
"unread": "未读",
"searchTook": "耗时 @time",
"searchResult": "匹配到 @count 条结果",
"happyBirthday": "生日快乐,@name!",
"happyBirthdayDesc": "今天是你的第 @count 个生日"
} }

View File

@ -38,47 +38,45 @@ PODS:
- file_picker (0.0.1): - file_picker (0.0.1):
- DKImagePickerController/PhotoGallery - DKImagePickerController/PhotoGallery
- Flutter - Flutter
- file_saver (0.0.1): - Firebase/Analytics (11.0.0):
- Flutter
- Firebase/Analytics (11.2.0):
- Firebase/Core - Firebase/Core
- Firebase/Core (11.2.0): - Firebase/Core (11.0.0):
- Firebase/CoreOnly - Firebase/CoreOnly
- FirebaseAnalytics (~> 11.2.0) - FirebaseAnalytics (~> 11.0.0)
- Firebase/CoreOnly (11.2.0): - Firebase/CoreOnly (11.0.0):
- FirebaseCore (= 11.2.0) - FirebaseCore (= 11.0.0)
- Firebase/Crashlytics (11.2.0): - Firebase/Crashlytics (11.0.0):
- Firebase/CoreOnly - Firebase/CoreOnly
- FirebaseCrashlytics (~> 11.2.0) - FirebaseCrashlytics (~> 11.0.0)
- Firebase/Messaging (11.2.0): - Firebase/Messaging (11.0.0):
- Firebase/CoreOnly - Firebase/CoreOnly
- FirebaseMessaging (~> 11.2.0) - FirebaseMessaging (~> 11.0.0)
- Firebase/Performance (11.2.0): - Firebase/Performance (11.0.0):
- Firebase/CoreOnly - Firebase/CoreOnly
- FirebasePerformance (~> 11.2.0) - FirebasePerformance (~> 11.0.0)
- firebase_analytics (11.3.3): - firebase_analytics (11.3.2):
- Firebase/Analytics (= 11.2.0) - Firebase/Analytics (= 11.0.0)
- firebase_core - firebase_core
- Flutter - Flutter
- firebase_core (3.6.0): - firebase_core (3.5.0):
- Firebase/CoreOnly (= 11.2.0) - Firebase/CoreOnly (= 11.0.0)
- Flutter - Flutter
- firebase_crashlytics (4.1.3): - firebase_crashlytics (4.1.2):
- Firebase/Crashlytics (= 11.2.0) - Firebase/Crashlytics (= 11.0.0)
- firebase_core - firebase_core
- Flutter - Flutter
- firebase_messaging (15.1.3): - firebase_messaging (15.1.2):
- Firebase/Messaging (= 11.2.0) - Firebase/Messaging (= 11.0.0)
- firebase_core - firebase_core
- Flutter - Flutter
- firebase_performance (0.10.0-8): - firebase_performance (0.10.0-7):
- Firebase/Performance (= 11.2.0) - Firebase/Performance (= 11.0.0)
- firebase_core - firebase_core
- Flutter - Flutter
- FirebaseABTesting (11.3.0): - FirebaseABTesting (11.2.0):
- FirebaseCore (~> 11.0) - FirebaseCore (~> 11.0)
- FirebaseAnalytics (11.2.0): - FirebaseAnalytics (11.0.0):
- FirebaseAnalytics/AdIdSupport (= 11.2.0) - FirebaseAnalytics/AdIdSupport (= 11.0.0)
- FirebaseCore (~> 11.0) - FirebaseCore (~> 11.0)
- FirebaseInstallations (~> 11.0) - FirebaseInstallations (~> 11.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.0) - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
@ -86,24 +84,24 @@ PODS:
- GoogleUtilities/Network (~> 8.0) - GoogleUtilities/Network (~> 8.0)
- "GoogleUtilities/NSData+zlib (~> 8.0)" - "GoogleUtilities/NSData+zlib (~> 8.0)"
- nanopb (~> 3.30910.0) - nanopb (~> 3.30910.0)
- FirebaseAnalytics/AdIdSupport (11.2.0): - FirebaseAnalytics/AdIdSupport (11.0.0):
- FirebaseCore (~> 11.0) - FirebaseCore (~> 11.0)
- FirebaseInstallations (~> 11.0) - FirebaseInstallations (~> 11.0)
- GoogleAppMeasurement (= 11.2.0) - GoogleAppMeasurement (= 11.0.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.0) - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
- GoogleUtilities/MethodSwizzler (~> 8.0) - GoogleUtilities/MethodSwizzler (~> 8.0)
- GoogleUtilities/Network (~> 8.0) - GoogleUtilities/Network (~> 8.0)
- "GoogleUtilities/NSData+zlib (~> 8.0)" - "GoogleUtilities/NSData+zlib (~> 8.0)"
- nanopb (~> 3.30910.0) - nanopb (~> 3.30910.0)
- FirebaseCore (11.2.0): - FirebaseCore (11.0.0):
- FirebaseCoreInternal (~> 11.0) - FirebaseCoreInternal (~> 11.0)
- GoogleUtilities/Environment (~> 8.0) - GoogleUtilities/Environment (~> 8.0)
- GoogleUtilities/Logger (~> 8.0) - GoogleUtilities/Logger (~> 8.0)
- FirebaseCoreExtension (11.3.0): - FirebaseCoreExtension (11.2.0):
- FirebaseCore (~> 11.0) - FirebaseCore (~> 11.0)
- FirebaseCoreInternal (11.3.0): - FirebaseCoreInternal (11.2.0):
- "GoogleUtilities/NSData+zlib (~> 8.0)" - "GoogleUtilities/NSData+zlib (~> 8.0)"
- FirebaseCrashlytics (11.2.0): - FirebaseCrashlytics (11.0.0):
- FirebaseCore (~> 11.0) - FirebaseCore (~> 11.0)
- FirebaseInstallations (~> 11.0) - FirebaseInstallations (~> 11.0)
- FirebaseRemoteConfigInterop (~> 11.0) - FirebaseRemoteConfigInterop (~> 11.0)
@ -112,12 +110,12 @@ PODS:
- GoogleUtilities/Environment (~> 8.0) - GoogleUtilities/Environment (~> 8.0)
- nanopb (~> 3.30910.0) - nanopb (~> 3.30910.0)
- PromisesObjC (~> 2.4) - PromisesObjC (~> 2.4)
- FirebaseInstallations (11.3.0): - FirebaseInstallations (11.2.0):
- FirebaseCore (~> 11.0) - FirebaseCore (~> 11.0)
- GoogleUtilities/Environment (~> 8.0) - GoogleUtilities/Environment (~> 8.0)
- GoogleUtilities/UserDefaults (~> 8.0) - GoogleUtilities/UserDefaults (~> 8.0)
- PromisesObjC (~> 2.4) - PromisesObjC (~> 2.4)
- FirebaseMessaging (11.2.0): - FirebaseMessaging (11.0.0):
- FirebaseCore (~> 11.0) - FirebaseCore (~> 11.0)
- FirebaseInstallations (~> 11.0) - FirebaseInstallations (~> 11.0)
- GoogleDataTransport (~> 10.0) - GoogleDataTransport (~> 10.0)
@ -126,7 +124,7 @@ PODS:
- GoogleUtilities/Reachability (~> 8.0) - GoogleUtilities/Reachability (~> 8.0)
- GoogleUtilities/UserDefaults (~> 8.0) - GoogleUtilities/UserDefaults (~> 8.0)
- nanopb (~> 3.30910.0) - nanopb (~> 3.30910.0)
- FirebasePerformance (11.2.0): - FirebasePerformance (11.0.0):
- FirebaseCore (~> 11.0) - FirebaseCore (~> 11.0)
- FirebaseInstallations (~> 11.0) - FirebaseInstallations (~> 11.0)
- FirebaseRemoteConfig (~> 11.0) - FirebaseRemoteConfig (~> 11.0)
@ -136,7 +134,7 @@ PODS:
- GoogleUtilities/MethodSwizzler (~> 8.0) - GoogleUtilities/MethodSwizzler (~> 8.0)
- GoogleUtilities/UserDefaults (~> 8.0) - GoogleUtilities/UserDefaults (~> 8.0)
- nanopb (~> 3.30910.0) - nanopb (~> 3.30910.0)
- FirebaseRemoteConfig (11.3.0): - FirebaseRemoteConfig (11.2.0):
- FirebaseABTesting (~> 11.0) - FirebaseABTesting (~> 11.0)
- FirebaseCore (~> 11.0) - FirebaseCore (~> 11.0)
- FirebaseInstallations (~> 11.0) - FirebaseInstallations (~> 11.0)
@ -144,8 +142,8 @@ PODS:
- FirebaseSharedSwift (~> 11.0) - FirebaseSharedSwift (~> 11.0)
- GoogleUtilities/Environment (~> 8.0) - GoogleUtilities/Environment (~> 8.0)
- "GoogleUtilities/NSData+zlib (~> 8.0)" - "GoogleUtilities/NSData+zlib (~> 8.0)"
- FirebaseRemoteConfigInterop (11.3.0) - FirebaseRemoteConfigInterop (11.2.0)
- FirebaseSessions (11.3.0): - FirebaseSessions (11.2.0):
- FirebaseCore (~> 11.0) - FirebaseCore (~> 11.0)
- FirebaseCoreExtension (~> 11.0) - FirebaseCoreExtension (~> 11.0)
- FirebaseInstallations (~> 11.0) - FirebaseInstallations (~> 11.0)
@ -154,7 +152,7 @@ PODS:
- GoogleUtilities/UserDefaults (~> 8.0) - GoogleUtilities/UserDefaults (~> 8.0)
- nanopb (~> 3.30910.0) - nanopb (~> 3.30910.0)
- PromisesSwift (~> 2.1) - PromisesSwift (~> 2.1)
- FirebaseSharedSwift (11.3.0) - FirebaseSharedSwift (11.2.0)
- Flutter (1.0.0) - Flutter (1.0.0)
- flutter_app_update (0.0.1): - flutter_app_update (0.0.1):
- Flutter - Flutter
@ -168,30 +166,27 @@ PODS:
- Flutter - Flutter
- flutter_secure_storage (6.0.0): - flutter_secure_storage (6.0.0):
- Flutter - Flutter
- flutter_udid (0.0.1):
- Flutter
- SAMKeychain
- flutter_webrtc (0.11.3): - flutter_webrtc (0.11.3):
- Flutter - Flutter
- WebRTC-SDK (= 125.6422.04) - WebRTC-SDK (= 125.6422.04)
- gal (1.0.0): - gal (1.0.0):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- GoogleAppMeasurement (11.2.0): - GoogleAppMeasurement (11.0.0):
- GoogleAppMeasurement/AdIdSupport (= 11.2.0) - GoogleAppMeasurement/AdIdSupport (= 11.0.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.0) - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
- GoogleUtilities/MethodSwizzler (~> 8.0) - GoogleUtilities/MethodSwizzler (~> 8.0)
- GoogleUtilities/Network (~> 8.0) - GoogleUtilities/Network (~> 8.0)
- "GoogleUtilities/NSData+zlib (~> 8.0)" - "GoogleUtilities/NSData+zlib (~> 8.0)"
- nanopb (~> 3.30910.0) - nanopb (~> 3.30910.0)
- GoogleAppMeasurement/AdIdSupport (11.2.0): - GoogleAppMeasurement/AdIdSupport (11.0.0):
- GoogleAppMeasurement/WithoutAdIdSupport (= 11.2.0) - GoogleAppMeasurement/WithoutAdIdSupport (= 11.0.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.0) - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
- GoogleUtilities/MethodSwizzler (~> 8.0) - GoogleUtilities/MethodSwizzler (~> 8.0)
- GoogleUtilities/Network (~> 8.0) - GoogleUtilities/Network (~> 8.0)
- "GoogleUtilities/NSData+zlib (~> 8.0)" - "GoogleUtilities/NSData+zlib (~> 8.0)"
- nanopb (~> 3.30910.0) - nanopb (~> 3.30910.0)
- GoogleAppMeasurement/WithoutAdIdSupport (11.2.0): - GoogleAppMeasurement/WithoutAdIdSupport (11.0.0):
- GoogleUtilities/AppDelegateSwizzler (~> 8.0) - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
- GoogleUtilities/MethodSwizzler (~> 8.0) - GoogleUtilities/MethodSwizzler (~> 8.0)
- GoogleUtilities/Network (~> 8.0) - GoogleUtilities/Network (~> 8.0)
@ -264,7 +259,6 @@ PODS:
- PromisesObjC (= 2.4.0) - PromisesObjC (= 2.4.0)
- protocol_handler_ios (0.0.1): - protocol_handler_ios (0.0.1):
- Flutter - Flutter
- SAMKeychain (1.5.3)
- screen_brightness_ios (0.1.0): - screen_brightness_ios (0.1.0):
- Flutter - Flutter
- SDWebImage (5.19.7): - SDWebImage (5.19.7):
@ -275,7 +269,7 @@ PODS:
- shared_preferences_foundation (0.0.1): - shared_preferences_foundation (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- sqflite_darwin (0.0.4): - sqflite (0.0.3):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- "sqlite3 (3.46.1+1)": - "sqlite3 (3.46.1+1)":
@ -310,7 +304,6 @@ DEPENDENCIES:
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/darwin`) - connectivity_plus (from `.symlinks/plugins/connectivity_plus/darwin`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- file_picker (from `.symlinks/plugins/file_picker/ios`) - file_picker (from `.symlinks/plugins/file_picker/ios`)
- file_saver (from `.symlinks/plugins/file_saver/ios`)
- firebase_analytics (from `.symlinks/plugins/firebase_analytics/ios`) - firebase_analytics (from `.symlinks/plugins/firebase_analytics/ios`)
- firebase_core (from `.symlinks/plugins/firebase_core/ios`) - firebase_core (from `.symlinks/plugins/firebase_core/ios`)
- firebase_crashlytics (from `.symlinks/plugins/firebase_crashlytics/ios`) - firebase_crashlytics (from `.symlinks/plugins/firebase_crashlytics/ios`)
@ -323,7 +316,6 @@ DEPENDENCIES:
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
- flutter_udid (from `.symlinks/plugins/flutter_udid/ios`)
- flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`) - flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`)
- gal (from `.symlinks/plugins/gal/darwin`) - gal (from `.symlinks/plugins/gal/darwin`)
- image_cropper (from `.symlinks/plugins/image_cropper/ios`) - image_cropper (from `.symlinks/plugins/image_cropper/ios`)
@ -342,7 +334,7 @@ DEPENDENCIES:
- screen_brightness_ios (from `.symlinks/plugins/screen_brightness_ios/ios`) - screen_brightness_ios (from `.symlinks/plugins/screen_brightness_ios/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`) - sqflite (from `.symlinks/plugins/sqflite/darwin`)
- sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/ios`) - sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- volume_controller (from `.symlinks/plugins/volume_controller/ios`) - volume_controller (from `.symlinks/plugins/volume_controller/ios`)
@ -372,7 +364,6 @@ SPEC REPOS:
- nanopb - nanopb
- PromisesObjC - PromisesObjC
- PromisesSwift - PromisesSwift
- SAMKeychain
- SDWebImage - SDWebImage
- sqlite3 - sqlite3
- SwiftyGif - SwiftyGif
@ -386,8 +377,6 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/device_info_plus/ios" :path: ".symlinks/plugins/device_info_plus/ios"
file_picker: file_picker:
:path: ".symlinks/plugins/file_picker/ios" :path: ".symlinks/plugins/file_picker/ios"
file_saver:
:path: ".symlinks/plugins/file_saver/ios"
firebase_analytics: firebase_analytics:
:path: ".symlinks/plugins/firebase_analytics/ios" :path: ".symlinks/plugins/firebase_analytics/ios"
firebase_core: firebase_core:
@ -412,8 +401,6 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/flutter_native_splash/ios" :path: ".symlinks/plugins/flutter_native_splash/ios"
flutter_secure_storage: flutter_secure_storage:
:path: ".symlinks/plugins/flutter_secure_storage/ios" :path: ".symlinks/plugins/flutter_secure_storage/ios"
flutter_udid:
:path: ".symlinks/plugins/flutter_udid/ios"
flutter_webrtc: flutter_webrtc:
:path: ".symlinks/plugins/flutter_webrtc/ios" :path: ".symlinks/plugins/flutter_webrtc/ios"
gal: gal:
@ -450,8 +437,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/share_plus/ios" :path: ".symlinks/plugins/share_plus/ios"
shared_preferences_foundation: shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin" :path: ".symlinks/plugins/shared_preferences_foundation/darwin"
sqflite_darwin: sqflite:
:path: ".symlinks/plugins/sqflite_darwin/darwin" :path: ".symlinks/plugins/sqflite/darwin"
sqlite3_flutter_libs: sqlite3_flutter_libs:
:path: ".symlinks/plugins/sqlite3_flutter_libs/ios" :path: ".symlinks/plugins/sqlite3_flutter_libs/ios"
url_launcher_ios: url_launcher_ios:
@ -462,31 +449,30 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/wakelock_plus/ios" :path: ".symlinks/plugins/wakelock_plus/ios"
SPEC CHECKSUMS: SPEC CHECKSUMS:
connectivity_plus: 4c41c08fc6d7c91f63bc7aec70ffe3730b04f563 connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db
device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342 device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655 file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808 Firebase: 9f574c08c2396885b5e7e100ed4293d956218af9
Firebase: 98e6bf5278170668a7983e12971a66b2cd57fc8c firebase_analytics: 4fd10182fd08bb8358f26ac8aca8dad7b6d0f592
firebase_analytics: fbc57838bdb94eef1e0ff504f127d974ff2981ad firebase_core: 2ec6b789859c7c24766344ec71fdf78639402d56
firebase_core: 2bedc3136ec7c7b8561c6123ed0239387b53f2af firebase_crashlytics: 60630a0f91ee432275fa1660fd8593079761448a
firebase_crashlytics: 37d104d457b51760b48504a93a12b3bf70995d77 firebase_messaging: a18e1e02b2e8e69097c8173e0c851be223b21c50
firebase_messaging: 15d114e1a41fc31e4fbabcd48d765a19eec94a38 firebase_performance: 12d45fdf120992fa879d990929bf73d4a5ced053
firebase_performance: 26ad47755d3e8d7b04b9bb36bdfbf1cec8d8dfcc FirebaseABTesting: 2104d957ce33888a3d6f3bde298cdee376dde8f1
FirebaseABTesting: c4559fcd2eba9f6bdaf0599e2c37ded01c343e4c FirebaseAnalytics: 27eb78b97880ea4a004839b9bac0b58880f5a92a
FirebaseAnalytics: c36efd5710c60c17558650fa58c2066eca7e9265 FirebaseCore: 3cf438f431f18c12cdf2aaf64434648b63f7e383
FirebaseCore: a282032ae9295c795714ded2ec9c522fc237f8da FirebaseCoreExtension: cda74ddfb001224bd8fd1d6e74698b4ed07803de
FirebaseCoreExtension: 30bb063476ef66cd46925243d64ad8b2c8ac3264 FirebaseCoreInternal: 0c569513412da9f3b31bd0b340013bbee8f295c5
FirebaseCoreInternal: ac26d09a70c730e497936430af4e60fb0c68ec4e FirebaseCrashlytics: 745d8f0221fe49c62865391d1bf56f5a12eeec0b
FirebaseCrashlytics: cfc69af5b53565dc6a5e563788809b5778ac4eac FirebaseInstallations: 771177d89d6c451dc6e50085ec82e2fc77ed0a4a
FirebaseInstallations: 58cf94dabf1e2bb2fa87725a9be5c2249171cda0 FirebaseMessaging: d2d1d9c62c46dd2db49a952f7deb5b16ad2c9742
FirebaseMessaging: c9ec7b90c399c7a6100297e9d16f8a27fc7f7152 FirebasePerformance: efdc02bacb1b4710588c9f867011605c081cdf79
FirebasePerformance: c39138c0700b8ef6040f0b80b5707320808e2862 FirebaseRemoteConfig: fca0b2d017fc1de52b28a4e5bcf2007c1a840457
FirebaseRemoteConfig: 5be2ca4f9870d475b39214210955fdaeecf7e5ca FirebaseRemoteConfigInterop: 477b26fdeb8fb5fbaf22fa9db5343b42289dc7db
FirebaseRemoteConfigInterop: c3a5c31b3c22079f41ba1dc645df889d9ce38cb9 FirebaseSessions: adcec8b72d0066a385e3affcd1bcb1ebb3908ce6
FirebaseSessions: 655ff17f3cc1a635cbdc2d69b953878001f9e25b FirebaseSharedSwift: 7a0d78d155ede78407f0fdc89fbc914014c7c540
FirebaseSharedSwift: d39c2ad64a11a8d936ce25a42b00df47078bb59c
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_app_update: 65f61da626cb111d1b24674abc4b01728d7723bc flutter_app_update: 65f61da626cb111d1b24674abc4b01728d7723bc
flutter_background_service_ios: e30e0d3ee69e4cee66272d0c78eacd48c2e94aac flutter_background_service_ios: e30e0d3ee69e4cee66272d0c78eacd48c2e94aac
@ -494,10 +480,9 @@ SPEC CHECKSUMS:
flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086 flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086
flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778 flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778
flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12 flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
flutter_udid: a2482c67a61b9c806ef59dd82ed8d007f1b7ac04
flutter_webrtc: 75b868e4f9e817c7a9a42ca4b6169063de4eec9f flutter_webrtc: 75b868e4f9e817c7a9a42ca4b6169063de4eec9f
gal: 61e868295d28fe67ffa297fae6dacebf56fd53e1 gal: 61e868295d28fe67ffa297fae6dacebf56fd53e1
GoogleAppMeasurement: 76d4f8b36b03bd8381fa9a7fe2cc7f99c0a2e93a GoogleAppMeasurement: 6e49ffac7d3f2c3ded9cc663f912a13b67bbd0de
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
image_cropper: 37d40f62177c101ff4c164906d259ea2c3aa70cf image_cropper: 37d40f62177c101ff4c164906d259ea2c3aa70cf
@ -508,7 +493,7 @@ SPEC CHECKSUMS:
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4 package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c
pasteboard: 982969ebaa7c78af3e6cc7761e8f5e77565d9ce0 pasteboard: 982969ebaa7c78af3e6cc7761e8f5e77565d9ce0
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
@ -516,12 +501,11 @@ SPEC CHECKSUMS:
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851 PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
protocol_handler_ios: a5db8abc38526ee326988b808be621e5fd568990 protocol_handler_ios: a5db8abc38526ee326988b808be621e5fd568990
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625 screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625
SDWebImage: 8a6b7b160b4d710e2a22b6900e25301075c34cb3 SDWebImage: 8a6b7b160b4d710e2a22b6900e25301075c34cb3
share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
sqflite_darwin: a553b1fd6fe66f53bbb0fe5b4f5bab93f08d7a13 sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
sqlite3: 0bb0e6389d824e40296f531b858a2a0b71c0d2fb sqlite3: 0bb0e6389d824e40296f531b858a2a0b71c0d2fb
sqlite3_flutter_libs: c00457ebd31e59fa6bb830380ddba24d44fbcd3b sqlite3_flutter_libs: c00457ebd31e59fa6bb830380ddba24d44fbcd3b
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4

View File

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

View File

@ -83,6 +83,7 @@
</array> </array>
<key>CFBundleLocalizations</key> <key>CFBundleLocalizations</key>
<array> <array>
<string>zh_CN</string>
<string>en</string> <string>en</string>
</array> </array>
<key>UIStatusBarHidden</key> <key>UIStatusBarHidden</key>

View File

@ -1,7 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:developer'; import 'dart:developer';
import 'package:confetti/confetti.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
@ -11,11 +10,9 @@ import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:solian/exceptions/request.dart'; import 'package:solian/exceptions/request.dart';
import 'package:solian/exts.dart'; import 'package:solian/exts.dart';
import 'package:solian/models/account.dart';
import 'package:solian/platform.dart'; import 'package:solian/platform.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
import 'package:solian/providers/content/realm.dart'; import 'package:solian/providers/content/realm.dart';
import 'package:solian/providers/notifications.dart';
import 'package:solian/providers/relation.dart'; import 'package:solian/providers/relation.dart';
import 'package:solian/providers/theme_switcher.dart'; import 'package:solian/providers/theme_switcher.dart';
import 'package:solian/providers/websocket.dart'; import 'package:solian/providers/websocket.dart';
@ -25,11 +22,6 @@ import 'package:solian/widgets/sized_container.dart';
import 'package:flutter_app_update/flutter_app_update.dart'; import 'package:flutter_app_update/flutter_app_update.dart';
import 'package:version/version.dart'; import 'package:version/version.dart';
enum BootstrapperSpecialState {
userBirthday,
appAnniversary,
}
class BootstrapperShell extends StatefulWidget { class BootstrapperShell extends StatefulWidget {
final Widget child; final Widget child;
@ -50,9 +42,6 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
int _periodCursor = 0; int _periodCursor = 0;
// Special state is some special event triggered after bootstrapping
BootstrapperSpecialState? _specialState;
final Completer _bootCompleter = Completer(); final Completer _bootCompleter = Completer();
void _requestRating() async { void _requestRating() async {
@ -209,26 +198,11 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
try { try {
await Future.wait([ await Future.wait([
if (auth.isAuthorized.isTrue)
Get.find<NotificationProvider>().fetchNotification(),
if (auth.isAuthorized.isTrue) if (auth.isAuthorized.isTrue)
Get.find<RelationshipProvider>().refreshRelativeList(), Get.find<RelationshipProvider>().refreshRelativeList(),
if (auth.isAuthorized.isTrue) if (auth.isAuthorized.isTrue)
Get.find<RealmProvider>().refreshAvailableRealms(), Get.find<RealmProvider>().refreshAvailableRealms(),
]); ]);
if (auth.isAuthorized.isTrue && auth.userProfile.value != null) {
final account = Account.fromJson(auth.userProfile.value!);
if (account.profile?.birthday != null) {
final birthDate = account.profile!.birthday!.toLocal();
final isBirthday = birthDate.day == DateTime.now().day;
if (isBirthday) {
setState(
() => _specialState = BootstrapperSpecialState.userBirthday,
);
}
}
}
} catch (e) { } catch (e) {
context.showErrorDialog(e); context.showErrorDialog(e);
} }
@ -240,7 +214,7 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
if (auth.isAuthorized.isTrue) { if (auth.isAuthorized.isTrue) {
try { try {
Get.find<NotificationProvider>().registerPushNotifications(); Get.find<WebSocketProvider>().registerPushNotifications();
} catch (err) { } catch (err) {
context.showSnackbar( context.showSnackbar(
'pushNotifyRegisterFailed'.trParams({'reason': err.toString()}), 'pushNotifyRegisterFailed'.trParams({'reason': err.toString()}),
@ -378,142 +352,8 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
} }
}, },
); );
} else if (_specialState != null) {
return GestureDetector(
child: RootContainer(
child: switch (_specialState) {
BootstrapperSpecialState.appAnniversary => const Placeholder(),
_ => _BirthdaySpecialScreen(),
},
),
onTap: () {
setState(() => _specialState = null);
},
);
} }
return widget.child; return widget.child;
} }
} }
class _BirthdaySpecialScreen extends StatefulWidget {
const _BirthdaySpecialScreen();
@override
State<_BirthdaySpecialScreen> createState() => _BirthdaySpecialScreenState();
}
class _BirthdaySpecialScreenState extends State<_BirthdaySpecialScreen> {
late final ConfettiController _confettiController =
ConfettiController(duration: const Duration(seconds: 10));
@override
void initState() {
_confettiController.play();
super.initState();
}
@override
void dispose() {
_confettiController.dispose();
super.dispose();
}
Color get _unFocusColor =>
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
String _toOrdinal(int num) {
if (num >= 11 && num <= 13) {
return '${num}th';
}
switch (num % 10) {
case 1:
return '${num}st';
case 2:
return '${num}nd';
case 3:
return '${num}rd';
default:
return '${num}th';
}
}
@override
Widget build(BuildContext context) {
final AuthProvider auth = Get.find();
final account = Account.fromJson(auth.userProfile.value!);
final birthDate = account.profile!.birthday!.toLocal();
final birthdayCount = DateTime.now().difference(birthDate).inDays ~/ 365;
return Stack(
children: <Widget>[
Align(
alignment: Alignment.center,
child: ConfettiWidget(
confettiController: _confettiController,
blastDirectionality: BlastDirectionality.explosive,
shouldLoop: true,
colors: const [
Colors.green,
Colors.blue,
Colors.pink,
Colors.orange,
Colors.purple
],
maxBlastForce: 30,
minBlastForce: 15,
emissionFrequency: 0.05,
numberOfParticles: 20,
gravity: 0.2,
),
),
Align(
child: CenteredContainer(
maxWidth: 320,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'🎂',
style: TextStyle(fontSize: 60),
),
const Gap(8),
Text(
'happyBirthday'.trParams({
'name': account.profile?.firstName != null
? [
account.profile?.firstName,
account.profile?.lastName
].join(' ')
: '@${account.name}',
}),
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.titleLarge,
),
Text(
'happyBirthdayDesc'.trParams({
'count': _toOrdinal(birthdayCount),
}),
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium,
),
const Gap(8),
Text(
'bsContinuable'.tr,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 13,
color: _unFocusColor,
),
).paddingOnly(bottom: 5),
],
),
),
),
],
);
}
}

View File

@ -43,17 +43,14 @@ class PostEditorController extends GetxController {
RxBool isRestoreFromLocal = false.obs; RxBool isRestoreFromLocal = false.obs;
Rx<DateTime?> lastSaveTime = Rx(null); Rx<DateTime?> lastSaveTime = Rx(null);
Future? _saveFuture; Timer? _saveTimer;
PostEditorController() { PostEditorController() {
SharedPreferences.getInstance().then((inst) { SharedPreferences.getInstance().then((inst) {
_prefs = inst; _prefs = inst;
}); _saveTimer = Timer.periodic(
contentController.addListener(() { const Duration(seconds: 3),
contentLength.value = contentController.text.length; (Timer t) {
_saveFuture ??= Future.delayed(
const Duration(seconds: 1),
() {
if (isNotEmpty) { if (isNotEmpty) {
localSave(); localSave();
lastSaveTime.value = DateTime.now(); lastSaveTime.value = DateTime.now();
@ -62,10 +59,12 @@ class PostEditorController extends GetxController {
localClear(); localClear();
lastSaveTime.value = null; lastSaveTime.value = null;
} }
_saveFuture = null;
}, },
); );
}); });
contentController.addListener(() {
contentLength.value = contentController.text.length;
});
} }
Future<void> editOverview(BuildContext context) { Future<void> editOverview(BuildContext context) {
@ -125,21 +124,6 @@ class PostEditorController extends GetxController {
onRemove: (String value) { onRemove: (String value) {
attachments.remove(value); attachments.remove(value);
}, },
onInsert: (String str) {
final text = contentController.text;
final selection = contentController.selection;
final newText = text.replaceRange(
selection.start,
selection.end,
str,
);
contentController.value = TextEditingValue(
text: newText,
selection: TextSelection.collapsed(
offset: selection.baseOffset + str.length,
),
);
},
), ),
); );
} }
@ -371,6 +355,8 @@ class PostEditorController extends GetxController {
@override @override
void dispose() { void dispose() {
_saveTimer?.cancel();
titleController.dispose(); titleController.dispose();
descriptionController.dispose(); descriptionController.dispose();
contentController.dispose(); contentController.dispose();

View File

@ -1,12 +1,9 @@
import 'dart:async';
import 'dart:math'; import 'dart:math';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:solian/models/attachment.dart';
import 'package:solian/models/pagination.dart'; import 'package:solian/models/pagination.dart';
import 'package:solian/models/post.dart'; import 'package:solian/models/post.dart';
import 'package:solian/providers/content/attachment.dart';
import 'package:solian/providers/content/posts.dart'; import 'package:solian/providers/content/posts.dart';
import 'package:solian/providers/last_read.dart'; import 'package:solian/providers/last_read.dart';
@ -34,18 +31,9 @@ class PostListController extends GetxController {
pagingController.addPageRequestListener(_onPagingControllerRequest); pagingController.addPageRequestListener(_onPagingControllerRequest);
} }
Completer<void>? _pagingLoadCompleter;
Future<void> _onPagingControllerRequest(int pageKey) async { Future<void> _onPagingControllerRequest(int pageKey) async {
try { try {
if (_pagingLoadCompleter != null) {
await _pagingLoadCompleter!.future;
return;
}
_pagingLoadCompleter = Completer();
final result = await loadMore(); final result = await loadMore();
_pagingLoadCompleter!.complete();
_pagingLoadCompleter = null;
if (result != null && hasMore.value) { if (result != null && hasMore.value) {
pagingController.appendPage(result, nextPageKey.value); pagingController.appendPage(result, nextPageKey.value);
@ -109,6 +97,9 @@ class PostListController extends GetxController {
hasMore.value = false; hasMore.value = false;
} }
final idx = <dynamic>{};
postList.retainWhere((x) => idx.add(x.id));
if (postList.isNotEmpty) { if (postList.isNotEmpty) {
var lastId = postList.map((x) => x.id).reduce(max); var lastId = postList.map((x) => x.id).reduce(max);
Get.find<LastReadProvider>().feedLastReadAt = lastId; Get.find<LastReadProvider>().feedLastReadAt = lastId;
@ -120,39 +111,35 @@ class PostListController extends GetxController {
Future<List<Post>?> _loadPosts(int pageKey) async { Future<List<Post>?> _loadPosts(int pageKey) async {
isBusy.value = true; isBusy.value = true;
final PostProvider posts = Get.find(); final PostProvider provider = Get.find();
Response resp; Response resp;
try { try {
if (author != null) { if (author != null) {
resp = await posts.listPost( resp = await provider.listPost(
pageKey, pageKey,
author: author, author: author,
take: 10,
); );
} else { } else {
switch (mode.value) { switch (mode.value) {
case 2: case 2:
resp = await posts.listRecommendations( resp = await provider.listRecommendations(
pageKey, pageKey,
channel: 'shuffle', channel: 'shuffle',
realm: realm, realm: realm,
take: 10,
); );
break; break;
case 1: case 1:
resp = await posts.listRecommendations( resp = await provider.listRecommendations(
pageKey, pageKey,
channel: 'friends', channel: 'friends',
realm: realm, realm: realm,
take: 10,
); );
break; break;
default: default:
resp = await posts.listRecommendations( resp = await provider.listRecommendations(
pageKey, pageKey,
realm: realm, realm: realm,
take: 10,
); );
break; break;
} }
@ -166,27 +153,6 @@ class PostListController extends GetxController {
final result = PaginationResult.fromJson(resp.body); final result = PaginationResult.fromJson(resp.body);
final out = result.data?.map((e) => Post.fromJson(e)).toList(); final out = result.data?.map((e) => Post.fromJson(e)).toList();
final AttachmentProvider attach = Get.find();
if (out != null) {
final attachmentIds = out
.mapMany((x) => x.body['attachments'] ?? [])
.cast<String>()
.toSet()
.toList();
final attachmentOut = await attach.listMetadata(attachmentIds);
for (var idx = 0; idx < out.length; idx++) {
final rids = List<String>.from(out[idx].body['attachments'] ?? []);
out[idx].preload = PostPreload(
attachments: attachmentOut
.where((x) => x != null && rids.contains(x.rid))
.cast<Attachment>()
.toList(),
);
}
}
postTotal.value = result.count; postTotal.value = result.count;
return out; return out;

View File

@ -18,7 +18,6 @@ import 'package:solian/providers/database/services/messages.dart';
import 'package:solian/providers/last_read.dart'; import 'package:solian/providers/last_read.dart';
import 'package:solian/providers/link_expander.dart'; import 'package:solian/providers/link_expander.dart';
import 'package:solian/providers/navigation.dart'; import 'package:solian/providers/navigation.dart';
import 'package:solian/providers/notifications.dart';
import 'package:solian/providers/stickers.dart'; import 'package:solian/providers/stickers.dart';
import 'package:solian/providers/subscription.dart'; import 'package:solian/providers/subscription.dart';
import 'package:solian/providers/theme_switcher.dart'; import 'package:solian/providers/theme_switcher.dart';
@ -139,12 +138,11 @@ class SolianApp extends StatelessWidget {
Get.put(NavigationStateProvider()); Get.put(NavigationStateProvider());
Get.lazyPut(() => AuthProvider()); Get.lazyPut(() => AuthProvider());
Get.lazyPut(() => WebSocketProvider());
Get.lazyPut(() => RelationshipProvider()); Get.lazyPut(() => RelationshipProvider());
Get.lazyPut(() => PostProvider()); Get.lazyPut(() => PostProvider());
Get.lazyPut(() => StickerProvider()); Get.lazyPut(() => StickerProvider());
Get.lazyPut(() => AttachmentProvider()); Get.lazyPut(() => AttachmentProvider());
Get.lazyPut(() => NotificationProvider()); Get.lazyPut(() => WebSocketProvider());
Get.lazyPut(() => StatusProvider()); Get.lazyPut(() => StatusProvider());
Get.lazyPut(() => ChannelProvider()); Get.lazyPut(() => ChannelProvider());
Get.lazyPut(() => RealmProvider()); Get.lazyPut(() => RealmProvider());
@ -156,6 +154,6 @@ class SolianApp extends StatelessWidget {
Get.lazyPut(() => LastReadProvider()); Get.lazyPut(() => LastReadProvider());
Get.lazyPut(() => SubscriptionProvider()); Get.lazyPut(() => SubscriptionProvider());
Get.find<NotificationProvider>().requestPermissions(); Get.find<WebSocketProvider>().requestPermissions();
} }
} }

View File

@ -1,38 +0,0 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:solian/models/account.dart';
part 'audit_log.g.dart';
@JsonSerializable()
class AuditEvent {
int id;
DateTime createdAt;
DateTime updatedAt;
DateTime? deletedAt;
String type;
String target;
String location;
String ipAddress;
String userAgent;
Account account;
int accountId;
AuditEvent({
required this.id,
required this.createdAt,
required this.updatedAt,
required this.deletedAt,
required this.type,
required this.target,
required this.location,
required this.ipAddress,
required this.userAgent,
required this.account,
required this.accountId,
});
static AuditEvent fromJson(Map<String, dynamic> json) =>
_$AuditEventFromJson(json);
Map<String, dynamic> toJson() => _$AuditEventToJson(this);
}

View File

@ -1,38 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'audit_log.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
AuditEvent _$AuditEventFromJson(Map<String, dynamic> json) => AuditEvent(
id: (json['id'] as num).toInt(),
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: json['deleted_at'] == null
? null
: DateTime.parse(json['deleted_at'] as String),
type: json['type'] as String,
target: json['target'] as String,
location: json['location'] as String,
ipAddress: json['ip_address'] as String,
userAgent: json['user_agent'] as String,
account: Account.fromJson(json['account'] as Map<String, dynamic>),
accountId: (json['account_id'] as num).toInt(),
);
Map<String, dynamic> _$AuditEventToJson(AuditEvent instance) =>
<String, dynamic>{
'id': instance.id,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
'type': instance.type,
'target': instance.target,
'location': instance.location,
'ip_address': instance.ipAddress,
'user_agent': instance.userAgent,
'account': instance.account.toJson(),
'account_id': instance.accountId,
};

View File

@ -1,29 +1,18 @@
import 'package:flutter/material.dart';
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
part 'notification.g.dart'; part 'notification.g.dart';
const Map<String, IconData> NotificationTopicIcons = {
'passport.security.alert': Icons.gpp_maybe,
'interactive.subscription': Icons.subscriptions,
'interactive.feedback': Icons.add_reaction,
'messaging.callStart': Icons.call_received,
};
@JsonSerializable() @JsonSerializable()
class Notification { class Notification {
int id; int id;
DateTime createdAt; DateTime createdAt;
DateTime updatedAt; DateTime updatedAt;
DateTime? deletedAt; DateTime? deletedAt;
DateTime? readAt;
String topic;
String title; String title;
String? subtitle; String? subtitle;
String body; String body;
String? avatar; String? avatar;
String? picture; String? picture;
Map<String, dynamic>? metadata;
int? senderId; int? senderId;
int accountId; int accountId;
@ -32,14 +21,11 @@ class Notification {
required this.createdAt, required this.createdAt,
required this.updatedAt, required this.updatedAt,
required this.deletedAt, required this.deletedAt,
required this.readAt,
required this.topic,
required this.title, required this.title,
required this.subtitle, required this.subtitle,
required this.body, required this.body,
required this.avatar, required this.avatar,
required this.picture, required this.picture,
required this.metadata,
required this.senderId, required this.senderId,
required this.accountId, required this.accountId,
}); });

View File

@ -13,16 +13,11 @@ Notification _$NotificationFromJson(Map<String, dynamic> json) => Notification(
deletedAt: json['deleted_at'] == null deletedAt: json['deleted_at'] == null
? null ? null
: DateTime.parse(json['deleted_at'] as String), : DateTime.parse(json['deleted_at'] as String),
readAt: json['read_at'] == null
? null
: DateTime.parse(json['read_at'] as String),
topic: json['topic'] as String,
title: json['title'] as String, title: json['title'] as String,
subtitle: json['subtitle'] as String?, subtitle: json['subtitle'] as String?,
body: json['body'] as String, body: json['body'] as String,
avatar: json['avatar'] as String?, avatar: json['avatar'] as String?,
picture: json['picture'] as String?, picture: json['picture'] as String?,
metadata: json['metadata'] as Map<String, dynamic>?,
senderId: (json['sender_id'] as num?)?.toInt(), senderId: (json['sender_id'] as num?)?.toInt(),
accountId: (json['account_id'] as num).toInt(), accountId: (json['account_id'] as num).toInt(),
); );
@ -33,14 +28,11 @@ Map<String, dynamic> _$NotificationToJson(Notification instance) =>
'created_at': instance.createdAt.toIso8601String(), 'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(), 'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(), 'deleted_at': instance.deletedAt?.toIso8601String(),
'read_at': instance.readAt?.toIso8601String(),
'topic': instance.topic,
'title': instance.title, 'title': instance.title,
'subtitle': instance.subtitle, 'subtitle': instance.subtitle,
'body': instance.body, 'body': instance.body,
'avatar': instance.avatar, 'avatar': instance.avatar,
'picture': instance.picture, 'picture': instance.picture,
'metadata': instance.metadata,
'sender_id': instance.senderId, 'sender_id': instance.senderId,
'account_id': instance.accountId, 'account_id': instance.accountId,
}; };

View File

@ -1,19 +1,10 @@
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'package:solian/models/account.dart'; import 'package:solian/models/account.dart';
import 'package:solian/models/attachment.dart';
import 'package:solian/models/post_categories.dart'; import 'package:solian/models/post_categories.dart';
import 'package:solian/models/realm.dart'; import 'package:solian/models/realm.dart';
part 'post.g.dart'; part 'post.g.dart';
class PostPreload {
List<Attachment> attachments;
PostPreload({
required this.attachments,
});
}
@JsonSerializable() @JsonSerializable()
class Post { class Post {
int id; int id;
@ -24,7 +15,6 @@ class Post {
String? alias; String? alias;
String? areaAlias; String? areaAlias;
dynamic body; dynamic body;
int visibility;
List<Tag>? tags; List<Tag>? tags;
List<Category>? categories; List<Category>? categories;
List<Post>? replies; List<Post>? replies;
@ -43,9 +33,6 @@ class Post {
Account author; Account author;
PostMetric? metric; PostMetric? metric;
@JsonKey(includeFromJson: false, includeToJson: false)
PostPreload? preload;
Post({ Post({
required this.id, required this.id,
required this.createdAt, required this.createdAt,
@ -56,7 +43,6 @@ class Post {
required this.areaAlias, required this.areaAlias,
required this.type, required this.type,
required this.body, required this.body,
required this.visibility,
required this.tags, required this.tags,
required this.categories, required this.categories,
required this.replies, required this.replies,

View File

@ -20,7 +20,6 @@ Post _$PostFromJson(Map<String, dynamic> json) => Post(
areaAlias: json['area_alias'] as String?, areaAlias: json['area_alias'] as String?,
type: json['type'] as String, type: json['type'] as String,
body: json['body'], body: json['body'],
visibility: (json['visibility'] as num).toInt(),
tags: (json['tags'] as List<dynamic>?) tags: (json['tags'] as List<dynamic>?)
?.map((e) => Tag.fromJson(e as Map<String, dynamic>)) ?.map((e) => Tag.fromJson(e as Map<String, dynamic>))
.toList(), .toList(),
@ -68,7 +67,6 @@ Map<String, dynamic> _$PostToJson(Post instance) => <String, dynamic>{
'alias': instance.alias, 'alias': instance.alias,
'area_alias': instance.areaAlias, 'area_alias': instance.areaAlias,
'body': instance.body, 'body': instance.body,
'visibility': instance.visibility,
'tags': instance.tags?.map((e) => e.toJson()).toList(), 'tags': instance.tags?.map((e) => e.toJson()).toList(),
'categories': instance.categories?.map((e) => e.toJson()).toList(), 'categories': instance.categories?.map((e) => e.toJson()).toList(),
'replies': instance.replies?.map((e) => e.toJson()).toList(), 'replies': instance.replies?.map((e) => e.toJson()).toList(),

View File

@ -11,7 +11,6 @@ import 'package:solian/exceptions/request.dart';
import 'package:solian/exceptions/unauthorized.dart'; import 'package:solian/exceptions/unauthorized.dart';
import 'package:solian/models/auth.dart'; import 'package:solian/models/auth.dart';
import 'package:solian/providers/database/database.dart'; import 'package:solian/providers/database/database.dart';
import 'package:solian/providers/notifications.dart';
import 'package:solian/providers/websocket.dart'; import 'package:solian/providers/websocket.dart';
import 'package:solian/services.dart'; import 'package:solian/services.dart';
@ -126,7 +125,7 @@ class AuthProvider extends GetConnect {
userAgent: await ServiceFinder.getUserAgent(), userAgent: await ServiceFinder.getUserAgent(),
sendUserAgent: true, sendUserAgent: true,
); );
client.httpClient.addRequestModifier(requestAuthenticator); client.httpClient.addAuthenticator(requestAuthenticator);
client.httpClient.baseUrl = ServiceFinder.buildUrl(service, null); client.httpClient.baseUrl = ServiceFinder.buildUrl(service, null);
return client; return client;
@ -175,7 +174,7 @@ class AuthProvider extends GetConnect {
); );
Get.find<WebSocketProvider>().connect(); Get.find<WebSocketProvider>().connect();
Get.find<NotificationProvider>().fetchNotification(); Get.find<WebSocketProvider>().notifyPrefetch();
return credentials!; return credentials!;
} }
@ -185,8 +184,8 @@ class AuthProvider extends GetConnect {
userProfile.value = null; userProfile.value = null;
Get.find<WebSocketProvider>().disconnect(); Get.find<WebSocketProvider>().disconnect();
Get.find<NotificationProvider>().notifications.clear(); Get.find<WebSocketProvider>().notifications.clear();
Get.find<NotificationProvider>().notificationUnread.value = 0; Get.find<WebSocketProvider>().notificationUnread.value = 0;
AppDatabase.removeDatabase(); AppDatabase.removeDatabase();
autoStopBackgroundNotificationService(); autoStopBackgroundNotificationService();

View File

@ -23,21 +23,6 @@ class AttachmentProvider extends GetConnect {
final Map<String, Attachment> _cachedResponses = {}; final Map<String, Attachment> _cachedResponses = {};
List<Attachment?> listMetadataFromCache(List<String> rid) {
if (rid.isEmpty) return List.empty();
List<Attachment?> result = List.filled(rid.length, null);
for (var idx = 0; idx < rid.length; idx++) {
if (_cachedResponses.containsKey(rid[idx])) {
result[idx] = _cachedResponses[rid[idx]];
} else {
result[idx] = null;
}
}
return result;
}
Future<List<Attachment?>> listMetadata( Future<List<Attachment?>> listMetadata(
List<String> rid, { List<String> rid, {
noCache = false, noCache = false,
@ -56,27 +41,25 @@ class AttachmentProvider extends GetConnect {
} }
} }
if (pendingQuery.isNotEmpty) { final resp = await get(
final resp = await get( '/attachments?take=${pendingQuery.length}&id=${pendingQuery.join(',')}',
'/attachments?take=${pendingQuery.length}&id=${pendingQuery.join(',')}', );
); if (resp.statusCode != 200) return result;
if (resp.statusCode != 200) return result;
final rawOut = PaginationResult.fromJson(resp.body); final rawOut = PaginationResult.fromJson(resp.body);
if (rawOut.data == null) return result; if (rawOut.data == null) return result;
final List<Attachment> out = final List<Attachment> out =
rawOut.data!.map((x) => Attachment.fromJson(x)).toList(); rawOut.data!.map((x) => Attachment.fromJson(x)).toList();
for (final item in out) { for (final item in out) {
if (item.destination != 0 && item.isAnalyzed) { if (item.destination != 0 && item.isAnalyzed) {
_cachedResponses[item.rid] = item; _cachedResponses[item.rid] = item;
}
} }
for (var i = 0; i < out.length; i++) { }
for (var j = 0; j < rid.length; j++) { for (var i = 0; i < out.length; i++) {
if (out[i].rid == rid[j]) { for (var j = 0; j < rid.length; j++) {
result[j] = out[i]; if (out[i].rid == rid[j]) {
} result[j] = out[i];
} }
} }
} }

View File

@ -3,11 +3,22 @@ import 'package:solian/exceptions/request.dart';
import 'package:solian/exceptions/unauthorized.dart'; import 'package:solian/exceptions/unauthorized.dart';
import 'package:solian/models/post.dart'; import 'package:solian/models/post.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
import 'package:solian/services.dart';
class PostProvider extends GetConnect {
@override
void onInit() {
httpClient.baseUrl = ServiceFinder.buildUrl('interactive', null);
}
class PostProvider extends GetxController {
Future<Response> seeWhatsNew(int pivot) async { Future<Response> seeWhatsNew(int pivot) async {
GetConnect client;
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
final client = await auth.configureClient('co'); if (auth.isAuthorized.value) {
client = await auth.configureClient('co');
} else {
client = await ServiceFinder.configureClient('co');
}
final resp = await client.get('/whats-new?pivot=$pivot'); final resp = await client.get('/whats-new?pivot=$pivot');
if (resp.statusCode != 200) { if (resp.statusCode != 200) {
throw RequestException(resp); throw RequestException(resp);
@ -17,14 +28,19 @@ class PostProvider extends GetxController {
} }
Future<Response> listRecommendations(int page, Future<Response> listRecommendations(int page,
{String? realm, String? channel, int take = 10}) async { {String? realm, String? channel}) async {
GetConnect client;
final AuthProvider auth = Get.find();
final queries = [ final queries = [
'take=$take', 'take=${10}',
'offset=$page', 'offset=$page',
if (realm != null) 'realm=$realm', if (realm != null) 'realm=$realm',
]; ];
final AuthProvider auth = Get.find(); if (auth.isAuthorized.value) {
final client = await auth.configureClient('interactive'); client = await auth.configureClient('co');
} else {
client = await ServiceFinder.configureClient('co');
}
final resp = await client.get( final resp = await client.get(
channel == null channel == null
? '/recommendations?${queries.join('&')}' ? '/recommendations?${queries.join('&')}'
@ -44,33 +60,9 @@ class PostProvider extends GetxController {
final queries = [ final queries = [
'take=${10}', 'take=${10}',
'offset=$page', 'offset=$page',
'truncate=false',
]; ];
final client = await auth.configureClient('interactive'); final client = await auth.configureClient('interactive');
final resp = await client.get( final resp = await client.get('/posts/drafts?${queries.join('&')}');
'/posts/drafts?${queries.join('&')}',
);
if (resp.statusCode != 200) {
throw RequestException(resp);
}
return resp;
}
Future<Response> searchPost(String probe, int page,
{String? realm, String? author, tag, category, int take = 10}) async {
final queries = [
'probe=$probe',
'take=$take',
'offset=$page',
if (tag != null) 'tag=$tag',
if (category != null) 'category=$category',
if (author != null) 'author=$author',
if (realm != null) 'realm=$realm',
];
final AuthProvider auth = Get.find();
final client = await auth.configureClient('co');
final resp = await client.get('/posts/search?${queries.join('&')}');
if (resp.statusCode != 200) { if (resp.statusCode != 200) {
throw RequestException(resp); throw RequestException(resp);
} }
@ -79,18 +71,16 @@ class PostProvider extends GetxController {
} }
Future<Response> listPost(int page, Future<Response> listPost(int page,
{String? realm, String? author, tag, category, int take = 10}) async { {String? realm, String? author, tag, category}) async {
final queries = [ final queries = [
'take=$take', 'take=${10}',
'offset=$page', 'offset=$page',
if (tag != null) 'tag=$tag', if (tag != null) 'tag=$tag',
if (category != null) 'category=$category', if (category != null) 'category=$category',
if (author != null) 'author=$author', if (author != null) 'author=$author',
if (realm != null) 'realm=$realm', if (realm != null) 'realm=$realm',
]; ];
final AuthProvider auth = Get.find(); final resp = await get('/posts?${queries.join('&')}');
final client = await auth.configureClient('co');
final resp = await client.get('/posts?${queries.join('&')}');
if (resp.statusCode != 200) { if (resp.statusCode != 200) {
throw RequestException(resp); throw RequestException(resp);
} }
@ -99,10 +89,7 @@ class PostProvider extends GetxController {
} }
Future<Response> listPostReplies(String alias, int page) async { Future<Response> listPostReplies(String alias, int page) async {
final AuthProvider auth = Get.find(); final resp = await get('/posts/$alias/replies?take=${10}&offset=$page');
final client = await auth.configureClient('co');
final resp =
await client.get('/posts/$alias/replies?take=${10}&offset=$page');
if (resp.statusCode != 200) { if (resp.statusCode != 200) {
throw RequestException(resp); throw RequestException(resp);
} }
@ -111,9 +98,7 @@ class PostProvider extends GetxController {
} }
Future<List<Post>> listPostFeaturedReply(String alias, {int take = 1}) async { Future<List<Post>> listPostFeaturedReply(String alias, {int take = 1}) async {
final AuthProvider auth = Get.find(); final resp = await get('/posts/$alias/replies/featured?take=$take');
final client = await auth.configureClient('co');
final resp = await client.get('/posts/$alias/replies/featured?take=$take');
if (resp.statusCode != 200) { if (resp.statusCode != 200) {
throw RequestException(resp); throw RequestException(resp);
} }
@ -122,9 +107,16 @@ class PostProvider extends GetxController {
} }
Future<Response> getPost(String alias) async { Future<Response> getPost(String alias) async {
final AuthProvider auth = Get.find(); final resp = await get('/posts/$alias');
final client = await auth.configureClient('co'); if (resp.statusCode != 200) {
final resp = await client.get('/posts/$alias'); throw RequestException(resp);
}
return resp;
}
Future<Response> getArticle(String alias) async {
final resp = await get('/articles/$alias');
if (resp.statusCode != 200) { if (resp.statusCode != 200) {
throw RequestException(resp); throw RequestException(resp);
} }

View File

@ -299,71 +299,53 @@ typedef $$LocalMessageEventTableTableUpdateCompanionBuilder
}); });
class $$LocalMessageEventTableTableFilterComposer class $$LocalMessageEventTableTableFilterComposer
extends Composer<_$AppDatabase, $LocalMessageEventTableTable> { extends FilterComposer<_$AppDatabase, $LocalMessageEventTableTable> {
$$LocalMessageEventTableTableFilterComposer({ $$LocalMessageEventTableTableFilterComposer(super.$state);
required super.$db, ColumnFilters<int> get id => $state.composableBuilder(
required super.$table, column: $state.table.id,
super.joinBuilder, builder: (column, joinBuilders) =>
super.$addJoinBuilderToRootComposer, ColumnFilters(column, joinBuilders: joinBuilders));
super.$removeJoinBuilderFromRootComposer,
});
ColumnFilters<int> get id => $composableBuilder(
column: $table.id, builder: (column) => ColumnFilters(column));
ColumnFilters<int> get channelId => $composableBuilder( ColumnFilters<int> get channelId => $state.composableBuilder(
column: $table.channelId, builder: (column) => ColumnFilters(column)); column: $state.table.channelId,
builder: (column, joinBuilders) =>
ColumnFilters(column, joinBuilders: joinBuilders));
ColumnWithTypeConverterFilters<Event?, Event, String> get data => ColumnWithTypeConverterFilters<Event?, Event, String> get data =>
$composableBuilder( $state.composableBuilder(
column: $table.data, column: $state.table.data,
builder: (column) => ColumnWithTypeConverterFilters(column)); builder: (column, joinBuilders) => ColumnWithTypeConverterFilters(
column,
joinBuilders: joinBuilders));
ColumnFilters<DateTime> get createdAt => $composableBuilder( ColumnFilters<DateTime> get createdAt => $state.composableBuilder(
column: $table.createdAt, builder: (column) => ColumnFilters(column)); column: $state.table.createdAt,
builder: (column, joinBuilders) =>
ColumnFilters(column, joinBuilders: joinBuilders));
} }
class $$LocalMessageEventTableTableOrderingComposer class $$LocalMessageEventTableTableOrderingComposer
extends Composer<_$AppDatabase, $LocalMessageEventTableTable> { extends OrderingComposer<_$AppDatabase, $LocalMessageEventTableTable> {
$$LocalMessageEventTableTableOrderingComposer({ $$LocalMessageEventTableTableOrderingComposer(super.$state);
required super.$db, ColumnOrderings<int> get id => $state.composableBuilder(
required super.$table, column: $state.table.id,
super.joinBuilder, builder: (column, joinBuilders) =>
super.$addJoinBuilderToRootComposer, ColumnOrderings(column, joinBuilders: joinBuilders));
super.$removeJoinBuilderFromRootComposer,
});
ColumnOrderings<int> get id => $composableBuilder(
column: $table.id, builder: (column) => ColumnOrderings(column));
ColumnOrderings<int> get channelId => $composableBuilder( ColumnOrderings<int> get channelId => $state.composableBuilder(
column: $table.channelId, builder: (column) => ColumnOrderings(column)); column: $state.table.channelId,
builder: (column, joinBuilders) =>
ColumnOrderings(column, joinBuilders: joinBuilders));
ColumnOrderings<String> get data => $composableBuilder( ColumnOrderings<String> get data => $state.composableBuilder(
column: $table.data, builder: (column) => ColumnOrderings(column)); column: $state.table.data,
builder: (column, joinBuilders) =>
ColumnOrderings(column, joinBuilders: joinBuilders));
ColumnOrderings<DateTime> get createdAt => $composableBuilder( ColumnOrderings<DateTime> get createdAt => $state.composableBuilder(
column: $table.createdAt, builder: (column) => ColumnOrderings(column)); column: $state.table.createdAt,
} builder: (column, joinBuilders) =>
ColumnOrderings(column, joinBuilders: joinBuilders));
class $$LocalMessageEventTableTableAnnotationComposer
extends Composer<_$AppDatabase, $LocalMessageEventTableTable> {
$$LocalMessageEventTableTableAnnotationComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
GeneratedColumn<int> get id =>
$composableBuilder(column: $table.id, builder: (column) => column);
GeneratedColumn<int> get channelId =>
$composableBuilder(column: $table.channelId, builder: (column) => column);
GeneratedColumnWithTypeConverter<Event?, String> get data =>
$composableBuilder(column: $table.data, builder: (column) => column);
GeneratedColumn<DateTime> get createdAt =>
$composableBuilder(column: $table.createdAt, builder: (column) => column);
} }
class $$LocalMessageEventTableTableTableManager extends RootTableManager< class $$LocalMessageEventTableTableTableManager extends RootTableManager<
@ -372,7 +354,6 @@ class $$LocalMessageEventTableTableTableManager extends RootTableManager<
LocalMessageEventTableData, LocalMessageEventTableData,
$$LocalMessageEventTableTableFilterComposer, $$LocalMessageEventTableTableFilterComposer,
$$LocalMessageEventTableTableOrderingComposer, $$LocalMessageEventTableTableOrderingComposer,
$$LocalMessageEventTableTableAnnotationComposer,
$$LocalMessageEventTableTableCreateCompanionBuilder, $$LocalMessageEventTableTableCreateCompanionBuilder,
$$LocalMessageEventTableTableUpdateCompanionBuilder, $$LocalMessageEventTableTableUpdateCompanionBuilder,
( (
@ -387,15 +368,10 @@ class $$LocalMessageEventTableTableTableManager extends RootTableManager<
: super(TableManagerState( : super(TableManagerState(
db: db, db: db,
table: table, table: table,
createFilteringComposer: () => filteringComposer: $$LocalMessageEventTableTableFilterComposer(
$$LocalMessageEventTableTableFilterComposer( ComposerState(db, table)),
$db: db, $table: table), orderingComposer: $$LocalMessageEventTableTableOrderingComposer(
createOrderingComposer: () => ComposerState(db, table)),
$$LocalMessageEventTableTableOrderingComposer(
$db: db, $table: table),
createComputedFieldComposer: () =>
$$LocalMessageEventTableTableAnnotationComposer(
$db: db, $table: table),
updateCompanionCallback: ({ updateCompanionCallback: ({
Value<int> id = const Value.absent(), Value<int> id = const Value.absent(),
Value<int> channelId = const Value.absent(), Value<int> channelId = const Value.absent(),
@ -434,7 +410,6 @@ typedef $$LocalMessageEventTableTableProcessedTableManager
LocalMessageEventTableData, LocalMessageEventTableData,
$$LocalMessageEventTableTableFilterComposer, $$LocalMessageEventTableTableFilterComposer,
$$LocalMessageEventTableTableOrderingComposer, $$LocalMessageEventTableTableOrderingComposer,
$$LocalMessageEventTableTableAnnotationComposer,
$$LocalMessageEventTableTableCreateCompanionBuilder, $$LocalMessageEventTableTableCreateCompanionBuilder,
$$LocalMessageEventTableTableUpdateCompanionBuilder, $$LocalMessageEventTableTableUpdateCompanionBuilder,
( (

View File

@ -4,19 +4,19 @@ import 'package:intl/intl.dart';
class ExperienceProvider extends GetxController { class ExperienceProvider extends GetxController {
static List<int> experienceToLevelRequirements = [ static List<int> experienceToLevelRequirements = [
0, // Level 0 0, // Level 0
1000, // Level 1 100, // Level 1
4000, // Level 2 400, // Level 2
9000, // Level 3 900, // Level 3
16000, // Level 4 1600, // Level 4
25000, // Level 5 2500, // Level 5
36000, // Level 6 3600, // Level 6
49000, // Level 7 4900, // Level 7
64000, // Level 8 6400, // Level 8
81000, // Level 9 8100, // Level 9
100000, // Level 10 10000, // Level 10
121000, // Level 11 12100, // Level 11
144000, // Level 12 14400, // Level 12
368000 // Level 13 36800 // Level 13
]; ];
static List<String> levelLabelMapping = static List<String> levelLabelMapping =
@ -35,7 +35,7 @@ class ExperienceProvider extends GetxController {
final idx = experienceToLevelRequirements.indexOf(exp); final idx = experienceToLevelRequirements.indexOf(exp);
if (idx + 1 >= experienceToLevelRequirements.length) return 1; if (idx + 1 >= experienceToLevelRequirements.length) return 1;
final nextExp = experienceToLevelRequirements[idx + 1]; final nextExp = experienceToLevelRequirements[idx + 1];
return (experience - exp).abs() / (exp - nextExp).abs(); return exp / nextExp;
} }
static String calcLevelUpProgressLevel(int experience) { static String calcLevelUpProgressLevel(int experience) {
@ -43,9 +43,9 @@ class ExperienceProvider extends GetxController {
.firstWhere((x) => x <= experience); .firstWhere((x) => x <= experience);
final idx = experienceToLevelRequirements.indexOf(exp); final idx = experienceToLevelRequirements.indexOf(exp);
if (idx + 1 >= experienceToLevelRequirements.length) return 'Infinity'; if (idx + 1 >= experienceToLevelRequirements.length) return 'Infinity';
final nextExp = exp - experienceToLevelRequirements[idx + 1]; final nextExp = experienceToLevelRequirements[idx + 1];
final formatter = final formatter =
NumberFormat.compactCurrency(symbol: '', decimalDigits: 1); NumberFormat.compactCurrency(symbol: '', decimalDigits: 1);
return '${formatter.format((exp - experience).abs())}/${formatter.format(nextExp.abs())}'; return '${formatter.format(exp)}/${formatter.format(nextExp)}';
} }
} }

View File

@ -1,175 +0,0 @@
import 'dart:developer';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter_udid/flutter_udid.dart';
import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:solian/exceptions/request.dart';
import 'package:solian/models/notification.dart';
import 'package:solian/models/pagination.dart';
import 'package:solian/platform.dart';
import 'package:solian/providers/auth.dart';
class NotificationProvider extends GetxController {
RxBool isBusy = false.obs;
RxInt notificationUnread = 0.obs;
RxList<Notification> notifications =
List<Notification>.empty(growable: true).obs;
Future<void> fetchNotification() async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) return;
final client = await auth.configureClient('auth');
final resp = await client.get('/notifications?skip=0&take=100');
if (resp.statusCode == 200) {
final result = PaginationResult.fromJson(resp.body);
final data = result.data?.map((x) => Notification.fromJson(x)).toList();
if (data != null) {
notifications.addAll(data);
notificationUnread.value = data.where((x) => x.readAt == null).length;
}
}
}
Future<void> markAllRead() async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) return;
isBusy.value = true;
final NotificationProvider nty = Get.find();
List<int> markList = List.empty(growable: true);
for (final element in nty.notifications) {
if (element.id <= 0) continue;
if (element.readAt != null) continue;
markList.add(element.id);
}
if (markList.isNotEmpty) {
final client = await auth.configureClient('auth');
await client.put('/notifications/read', {'messages': markList});
}
nty.notifications.value = nty.notifications.map((x) {
x.readAt = DateTime.now();
return x;
}).toList();
nty.notifications.refresh();
isBusy.value = false;
}
Future<void> markOneRead(Notification element, int index) async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) return;
final NotificationProvider nty = Get.find();
if (element.id <= 0) {
nty.notifications.removeAt(index);
return;
} else if (element.readAt != null) {
return;
}
isBusy.value = true;
final client = await auth.configureClient('auth');
await client.put('/notifications/read/${element.id}', {});
nty.notifications[0].readAt = DateTime.now();
nty.notifications.refresh();
isBusy.value = false;
}
void requestPermissions() {
try {
FirebaseMessaging.instance.requestPermission(
alert: true,
announcement: true,
carPlay: true,
badge: true,
sound: true);
} catch (_) {
// When firebase isn't initialized (background service)
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin();
flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>()
?.requestNotificationsPermission();
flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<
IOSFlutterLocalNotificationsPlugin>()
?.requestPermissions(
alert: true,
badge: true,
sound: true,
);
flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<
MacOSFlutterLocalNotificationsPlugin>()
?.requestPermissions(
alert: true,
badge: true,
sound: true,
);
}
}
Future<void> registerPushNotifications() async {
if (PlatformInfo.isWeb) return;
final prefs = await SharedPreferences.getInstance();
if (prefs.getBool('service_background_notification') == true) {
log('Background notification service has been enabled, skip register push notifications');
return;
}
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) return;
late final String? token;
late final String provider;
var deviceUuid = await _getDeviceUuid();
if (deviceUuid == null || deviceUuid.isEmpty) {
log("Unable to active push notifications, couldn't get device uuid");
return;
} else {
log('Device UUID is $deviceUuid');
}
if (PlatformInfo.isIOS || PlatformInfo.isMacOS) {
provider = 'apple';
token = await FirebaseMessaging.instance.getAPNSToken();
} else {
provider = 'firebase';
token = await FirebaseMessaging.instance.getToken();
}
log('Device Push Token is $token');
final client = await auth.configureClient('auth');
final resp = await client.post('/notifications/subscribe', {
'provider': provider,
'device_token': token,
'device_id': deviceUuid,
});
if (resp.statusCode != 200 && resp.statusCode != 400) {
throw RequestException(resp);
}
}
Future<String?> _getDeviceUuid() async {
if (PlatformInfo.isWeb) return null;
return await FlutterUdid.consistentUdid;
}
}

View File

@ -3,11 +3,17 @@ import 'dart:convert';
import 'dart:developer'; import 'dart:developer';
import 'dart:io'; import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:solian/exceptions/request.dart';
import 'package:solian/models/notification.dart'; import 'package:solian/models/notification.dart';
import 'package:solian/models/packet.dart'; import 'package:solian/models/packet.dart';
import 'package:solian/models/pagination.dart';
import 'package:solian/platform.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
import 'package:solian/providers/notifications.dart';
import 'package:solian/services.dart'; import 'package:solian/services.dart';
import 'package:web_socket_channel/web_socket_channel.dart'; import 'package:web_socket_channel/web_socket_channel.dart';
@ -15,10 +21,56 @@ class WebSocketProvider extends GetxController {
RxBool isConnected = false.obs; RxBool isConnected = false.obs;
RxBool isConnecting = false.obs; RxBool isConnecting = false.obs;
RxInt notificationUnread = 0.obs;
RxList<Notification> notifications =
List<Notification>.empty(growable: true).obs;
WebSocketChannel? websocket; WebSocketChannel? websocket;
StreamController<NetworkPackage> stream = StreamController.broadcast(); StreamController<NetworkPackage> stream = StreamController.broadcast();
@override
onInit() {
notifyPrefetch();
super.onInit();
}
void requestPermissions() {
try {
FirebaseMessaging.instance.requestPermission(
alert: true,
announcement: true,
carPlay: true,
badge: true,
sound: true);
} catch (_) {
// When firebase isn't initialized (background service)
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin();
flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>()
?.requestNotificationsPermission();
flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<
IOSFlutterLocalNotificationsPlugin>()
?.requestPermissions(
alert: true,
badge: true,
sound: true,
);
flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<
MacOSFlutterLocalNotificationsPlugin>()
?.requestPermissions(
alert: true,
badge: true,
sound: true,
);
}
}
Future<void> connect({noRetry = false}) async { Future<void> connect({noRetry = false}) async {
if (isConnected.value) { if (isConnected.value) {
return; return;
@ -67,9 +119,8 @@ class WebSocketProvider extends GetxController {
log('Websocket incoming message: ${packet.method} ${packet.message}'); log('Websocket incoming message: ${packet.method} ${packet.message}');
stream.sink.add(packet); stream.sink.add(packet);
if (packet.method == 'notifications.new') { if (packet.method == 'notifications.new') {
final NotificationProvider nty = Get.find(); notifications.add(Notification.fromJson(packet.payload!));
nty.notifications.add(Notification.fromJson(packet.payload!)); notificationUnread.value++;
nty.notificationUnread.value++;
} }
}, },
onDone: () { onDone: () {
@ -82,4 +133,95 @@ class WebSocketProvider extends GetxController {
}, },
); );
} }
Future<void> notifyPrefetch() async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) return;
final client = await auth.configureClient('auth');
final resp = await client.get('/notifications?skip=0&take=100');
if (resp.statusCode == 200) {
final result = PaginationResult.fromJson(resp.body);
final data = result.data?.map((x) => Notification.fromJson(x)).toList();
if (data != null) {
notifications.addAll(data);
notificationUnread.value = data.length;
}
}
}
Future<void> registerPushNotifications() async {
if (PlatformInfo.isWeb) return;
final prefs = await SharedPreferences.getInstance();
if (prefs.getBool('service_background_notification') == true) {
log('Background notification service has been enabled, skip register push notifications');
return;
}
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) return;
late final String? token;
late final String provider;
final deviceUuid = await _getDeviceUuid();
if (deviceUuid == null || deviceUuid.isEmpty) {
log("Unable to active push notifications, couldn't get device uuid");
} else {
log('Device UUID is $deviceUuid');
}
if (PlatformInfo.isIOS || PlatformInfo.isMacOS) {
provider = 'apple';
token = await FirebaseMessaging.instance.getAPNSToken();
} else {
provider = 'firebase';
token = await FirebaseMessaging.instance.getToken();
}
log('Device Push Token is $token');
final client = await auth.configureClient('auth');
final resp = await client.post('/notifications/subscribe', {
'provider': provider,
'device_token': token,
'device_id': deviceUuid,
});
if (resp.statusCode != 200 && resp.statusCode != 400) {
throw RequestException(resp);
}
}
Future<String?> _getDeviceUuid() async {
DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
if (PlatformInfo.isWeb) {
final webInfo = await deviceInfo.webBrowserInfo;
return webInfo.vendor! +
webInfo.userAgent! +
webInfo.hardwareConcurrency.toString();
}
if (PlatformInfo.isAndroid) {
final androidInfo = await deviceInfo.androidInfo;
return androidInfo.id;
}
if (PlatformInfo.isIOS) {
final iosInfo = await deviceInfo.iosInfo;
return iosInfo.identifierForVendor!;
}
if (PlatformInfo.isLinux) {
final linuxInfo = await deviceInfo.linuxInfo;
return linuxInfo.machineId!;
}
if (PlatformInfo.isWindows) {
final windowsInfo = await deviceInfo.windowsInfo;
return windowsInfo.deviceId;
}
if (PlatformInfo.isMacOS) {
final macosInfo = await deviceInfo.macOsInfo;
return macosInfo.systemGUID;
}
return null;
}
} }

View File

@ -2,14 +2,11 @@ import 'package:animations/animations.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:solian/bootstrapper.dart'; import 'package:solian/bootstrapper.dart';
import 'package:solian/models/post.dart';
import 'package:solian/models/realm.dart'; import 'package:solian/models/realm.dart';
import 'package:solian/screens/about.dart'; import 'package:solian/screens/about.dart';
import 'package:solian/screens/account.dart'; import 'package:solian/screens/account.dart';
import 'package:solian/screens/account/audit_log.dart';
import 'package:solian/screens/account/friend.dart'; import 'package:solian/screens/account/friend.dart';
import 'package:solian/screens/account/preferences/notifications.dart'; import 'package:solian/screens/account/preferences/notifications.dart';
import 'package:solian/screens/account/preferences/security.dart';
import 'package:solian/screens/account/profile_edit.dart'; import 'package:solian/screens/account/profile_edit.dart';
import 'package:solian/screens/account/profile_page.dart'; import 'package:solian/screens/account/profile_page.dart';
import 'package:solian/screens/auth/signin.dart'; import 'package:solian/screens/auth/signin.dart';
@ -19,9 +16,9 @@ import 'package:solian/screens/channel/channel_detail.dart';
import 'package:solian/screens/channel/channel_organize.dart'; import 'package:solian/screens/channel/channel_organize.dart';
import 'package:solian/screens/chat.dart'; import 'package:solian/screens/chat.dart';
import 'package:solian/screens/dashboard.dart'; import 'package:solian/screens/dashboard.dart';
import 'package:solian/screens/posts/post_search.dart'; import 'package:solian/screens/feed/search.dart';
import 'package:solian/screens/posts/post_detail.dart'; import 'package:solian/screens/posts/post_detail.dart';
import 'package:solian/screens/posts/draft_box.dart'; import 'package:solian/screens/feed/draft_box.dart';
import 'package:solian/screens/realms.dart'; import 'package:solian/screens/realms.dart';
import 'package:solian/screens/realms/realm_detail.dart'; import 'package:solian/screens/realms/realm_detail.dart';
import 'package:solian/screens/realms/realm_organize.dart'; import 'package:solian/screens/realms/realm_organize.dart';
@ -97,7 +94,7 @@ abstract class AppRouter {
name: 'postSearch', name: 'postSearch',
builder: (context, state) => TitleShell( builder: (context, state) => TitleShell(
state: state, state: state,
child: PostSearchScreen( child: FeedSearchScreen(
tag: state.uri.queryParameters['tag'], tag: state.uri.queryParameters['tag'],
category: state.uri.queryParameters['category'], category: state.uri.queryParameters['category'],
), ),
@ -110,7 +107,6 @@ abstract class AppRouter {
state: state, state: state,
child: PostDetailScreen( child: PostDetailScreen(
id: state.pathParameters['id']!, id: state.pathParameters['id']!,
post: state.extra as Post?,
), ),
), ),
), ),
@ -182,7 +178,6 @@ abstract class AppRouter {
final arguments = state.extra as ChannelDetailArguments; final arguments = state.extra as ChannelDetailArguments;
return TitleShell( return TitleShell(
state: state, state: state,
isResponsive: true,
child: ChannelDetailScreen( child: ChannelDetailScreen(
channel: arguments.channel, channel: arguments.channel,
profile: arguments.profile, profile: arguments.profile,
@ -268,22 +263,6 @@ abstract class AppRouter {
child: const NotificationPreferencesScreen(), child: const NotificationPreferencesScreen(),
), ),
), ),
GoRoute(
path: '/account/preferences/auth',
name: 'authPreferences',
builder: (context, state) => TitleShell(
state: state,
child: const AuthPreferencesScreen(),
),
),
GoRoute(
path: '/account/audit',
name: 'auditLog',
builder: (context, state) => TitleShell(
state: state,
child: const AuditLogScreen(),
),
),
GoRoute( GoRoute(
path: '/account/view/:name', path: '/account/view/:name',
name: 'accountProfilePage', name: 'accountProfilePage',

View File

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

View File

@ -7,6 +7,7 @@ import 'package:solian/providers/account_status.dart';
import 'package:solian/providers/relation.dart'; import 'package:solian/providers/relation.dart';
import 'package:solian/router.dart'; import 'package:solian/router.dart';
import 'package:solian/widgets/account/account_heading.dart'; import 'package:solian/widgets/account/account_heading.dart';
import 'package:solian/widgets/root_container.dart';
import 'package:solian/widgets/sized_container.dart'; import 'package:solian/widgets/sized_container.dart';
import 'package:badges/badges.dart' as badges; import 'package:badges/badges.dart' as badges;
@ -49,128 +50,112 @@ class _AccountScreenState extends State<AccountScreen> {
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
return SafeArea( return RootContainer(
child: Obx(() { child: SafeArea(
if (auth.isAuthorized.isFalse) { child: Obx(() {
return Center( if (auth.isAuthorized.isFalse) {
child: Column( return Center(
mainAxisSize: MainAxisSize.min, child: Column(
children: [ mainAxisSize: MainAxisSize.min,
_ActionCard( children: [
icon: Icon( _ActionCard(
Icons.login, icon: Icon(
color: Theme.of(context).colorScheme.onPrimary, Icons.login,
), color: Theme.of(context).colorScheme.onPrimary,
title: 'signin'.tr,
caption: 'signinCaption'.tr,
onTap: () {
AppRouter.instance.pushNamed('signin').then((val) async {
if (val == true) {
await auth.refreshUserProfile();
}
});
},
),
_ActionCard(
icon: Icon(
Icons.add,
color: Theme.of(context).colorScheme.onPrimary,
),
title: 'signup'.tr,
caption: 'signupCaption'.tr,
onTap: () {
AppRouter.instance.pushNamed('signup').then((_) {
setState(() {});
});
},
),
const Gap(4),
TextButton(
style: const ButtonStyle(
visualDensity: VisualDensity(
horizontal: -4,
vertical: -2,
), ),
title: 'signin'.tr,
caption: 'signinCaption'.tr,
onTap: () {
AppRouter.instance.pushNamed('signin').then((val) async {
if (val == true) {
await auth.refreshUserProfile();
}
});
},
), ),
onPressed: () { _ActionCard(
icon: Icon(
Icons.add,
color: Theme.of(context).colorScheme.onPrimary,
),
title: 'signup'.tr,
caption: 'signupCaption'.tr,
onTap: () {
AppRouter.instance.pushNamed('signup').then((_) {
setState(() {});
});
},
),
const Gap(4),
TextButton(
style: const ButtonStyle(
visualDensity: VisualDensity(
horizontal: -4,
vertical: -2,
),
),
onPressed: () {
AppRouter.instance.pushNamed('settings');
},
child: Text('settings'.tr),
),
],
),
);
}
return CenteredContainer(
child: ListView(
children: [
if (auth.userProfile.value != null)
const AccountHeading().paddingOnly(bottom: 8, top: 8),
...(actionItems.map(
(x) => ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 34),
leading: x.$1,
title: Text(x.$2),
onTap: () {
AppRouter.instance
.pushNamed(x.$3)
.then((_) => setState(() {}));
},
),
)),
const Divider(thickness: 0.3, height: 1)
.paddingSymmetric(vertical: 4),
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 34),
leading: const Icon(Icons.settings),
title: Text('settings'.tr),
onTap: () {
AppRouter.instance.pushNamed('settings'); AppRouter.instance.pushNamed('settings');
}, },
child: Text('settings'.tr), ),
if (auth.isAuthorized.value)
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 34),
leading: const Icon(Icons.edit_notifications),
title: Text('notificationPreferences'.tr),
onTap: () {
AppRouter.instance.pushNamed('notificationPreferences');
},
),
const Divider(thickness: 0.3, height: 1)
.paddingSymmetric(vertical: 4),
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 34),
leading: const Icon(Icons.logout),
title: Text('signout'.tr),
onTap: () {
auth.signout();
setState(() {});
},
), ),
], ],
), ),
); );
} }),
),
return CenteredContainer(
child: ListView(
children: [
if (auth.userProfile.value != null)
const AccountHeading().paddingOnly(bottom: 8, top: 16),
...(actionItems.map(
(x) => ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 34),
leading: x.$1,
title: Text(x.$2),
onTap: () {
AppRouter.instance
.pushNamed(x.$3)
.then((_) => setState(() {}));
},
),
)),
const Divider(thickness: 0.3, height: 1)
.paddingSymmetric(vertical: 4),
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 34),
leading: const Icon(Icons.settings),
title: Text('settings'.tr),
onTap: () {
AppRouter.instance.pushNamed('settings');
},
),
if (auth.isAuthorized.value)
ListTile(
leading: const Icon(Icons.event_repeat),
contentPadding: const EdgeInsets.symmetric(horizontal: 34),
title: Text('auditLog'.tr),
onTap: () {
AppRouter.instance.pushNamed('auditLog');
},
),
if (auth.isAuthorized.value)
ListTile(
leading: const Icon(Icons.lock),
contentPadding: const EdgeInsets.symmetric(horizontal: 34),
title: Text('authPreferences'.tr),
onTap: () {
AppRouter.instance.pushNamed('authPreferences');
},
),
if (auth.isAuthorized.value)
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 34),
leading: const Icon(Icons.edit_notifications),
title: Text('notificationPreferences'.tr),
onTap: () {
AppRouter.instance.pushNamed('notificationPreferences');
},
),
const Divider(thickness: 0.3, height: 1)
.paddingSymmetric(vertical: 4),
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 34),
leading: const Icon(Icons.logout),
title: Text('signout'.tr),
onTap: () {
auth.signout();
setState(() {});
},
),
],
),
);
}),
); );
} }
} }

View File

@ -1,154 +0,0 @@
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:get/get.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:marquee/marquee.dart';
import 'package:solian/exceptions/request.dart';
import 'package:solian/exts.dart';
import 'package:solian/models/audit_log.dart';
import 'package:solian/models/pagination.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/widgets/relative_date.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
import 'package:timeline_tile/timeline_tile.dart';
class AuditLogScreen extends StatefulWidget {
const AuditLogScreen({super.key});
@override
State<AuditLogScreen> createState() => _AuditLogScreenState();
}
class _AuditLogScreenState extends State<AuditLogScreen> {
bool _isBusy = true;
final List<AuditEvent> _events = List.empty(growable: true);
Future<void> _getEvents() async {
if (!_isBusy) setState(() => _isBusy = true);
final AuthProvider auth = Get.find();
final client = await auth.configureClient('id');
final resp =
await client.get('/users/me/events?take=15&offset=${_events.length}');
if (resp.statusCode != 200) {
context.showErrorDialog(RequestException(resp));
}
final result = PaginationResult.fromJson(resp.body);
setState(() {
_events.addAll(
result.data?.map((x) => AuditEvent.fromJson(x)).toList() ??
List.empty(),
);
_isBusy = false;
});
}
bool _showIp = false;
String _censorIpAddress(String ip) {
List<String> parts = ip.split('.');
if (parts.length == 4) {
String censoredPart1 = '*' * parts[1].length;
String censoredPart2 = '*' * parts[2].length;
String censoredPart3 = '*' * parts[3].length;
return '${parts[0]}.$censoredPart1.$censoredPart2.$censoredPart3';
} else {
return '***.***.***.***';
}
}
@override
void initState() {
super.initState();
_getEvents();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
CheckboxListTile(
value: _showIp,
title: Text('showIp'.tr),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
secondary: const Icon(Icons.alternate_email),
tileColor:
Theme.of(context).colorScheme.surfaceContainer.withOpacity(0.5),
onChanged: (val) {
setState(() => _showIp = val ?? false);
},
),
Expanded(
child: RefreshIndicator(
onRefresh: () {
_events.clear();
return _getEvents();
},
child: InfiniteList(
padding: const EdgeInsets.symmetric(vertical: 12),
itemCount: _events.length,
isLoading: _isBusy,
onFetchData: () {
_getEvents();
},
itemBuilder: (context, idx) {
final element = _events[idx];
return TimelineTile(
isFirst: idx == 0,
isLast: _events.length - 1 == idx,
alignment: TimelineAlign.start,
indicatorStyle: IndicatorStyle(width: 15),
endChild: Container(
child: Card(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
element.type,
style: GoogleFonts.robotoMono(fontSize: 15),
),
Text(
_showIp
? element.ipAddress
: _censorIpAddress(element.ipAddress),
style: GoogleFonts.sourceCodePro(
fontWeight: FontWeight.bold,
),
),
SizedBox(
height: 20,
width: double.maxFinite,
child: Marquee(
text: element.userAgent,
velocity: 25,
startAfter: Duration(milliseconds: 500),
pauseAfterRound: Duration(milliseconds: 3000),
),
),
Row(
children: [
RelativeDate(element.createdAt),
const Gap(6),
Text('·'),
const Gap(6),
RelativeDate(element.createdAt, isFull: true),
],
),
],
).paddingSymmetric(horizontal: 12, vertical: 8),
).paddingOnly(left: 16),
),
).paddingSymmetric(horizontal: 18);
},
),
),
),
],
);
}
}

View File

@ -1,14 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gap/gap.dart'; import 'package:flutter_animate/flutter_animate.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:solian/models/notification.dart'; import 'package:solian/providers/websocket.dart';
import 'package:solian/models/post.dart'; import 'package:solian/providers/auth.dart';
import 'package:solian/providers/notifications.dart'; import 'package:solian/models/notification.dart' as notify;
import 'package:solian/router.dart';
import 'package:solian/widgets/loading_indicator.dart';
import 'package:solian/widgets/markdown_text_content.dart';
import 'package:solian/widgets/posts/post_item.dart';
import 'package:solian/widgets/relative_date.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
class NotificationScreen extends StatefulWidget { class NotificationScreen extends StatefulWidget {
@ -19,9 +14,57 @@ class NotificationScreen extends StatefulWidget {
} }
class _NotificationScreenState extends State<NotificationScreen> { class _NotificationScreenState extends State<NotificationScreen> {
bool _isBusy = false;
Future<void> _markAllRead() async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) return;
setState(() => _isBusy = true);
final WebSocketProvider provider = Get.find();
List<int> markList = List.empty(growable: true);
for (final element in provider.notifications) {
if (element.id <= 0) continue;
markList.add(element.id);
}
if (markList.isNotEmpty) {
final client = await auth.configureClient('auth');
await client.put('/notifications/read', {'messages': markList});
}
provider.notifications.clear();
setState(() => _isBusy = false);
}
Future<void> _markOneRead(notify.Notification element, int index) async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) return;
final WebSocketProvider provider = Get.find();
if (element.id <= 0) {
provider.notifications.removeAt(index);
return;
}
setState(() => _isBusy = true);
final client = await auth.configureClient('auth');
await client.put('/notifications/read/${element.id}', {});
provider.notifications.removeAt(index);
setState(() => _isBusy = false);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final NotificationProvider nty = Get.find(); final WebSocketProvider ws = Get.find();
return SizedBox( return SizedBox(
height: MediaQuery.of(context).size.height * 0.85, height: MediaQuery.of(context).size.height * 0.85,
@ -34,174 +77,71 @@ class _NotificationScreenState extends State<NotificationScreen> {
).paddingOnly(left: 24, right: 24, top: 32, bottom: 16), ).paddingOnly(left: 24, right: 24, top: 32, bottom: 16),
Expanded( Expanded(
child: Obx(() { child: Obx(() {
return RefreshIndicator( return CustomScrollView(
onRefresh: () => nty.fetchNotification(), slivers: [
child: CustomScrollView( if (_isBusy)
slivers: [
SliverToBoxAdapter( SliverToBoxAdapter(
child: LoadingIndicator( child: const LinearProgressIndicator().animate().scaleX(),
isActive: nty.isBusy.value,
),
), ),
if (nty.notifications if (ws.notifications.isEmpty)
.where((x) => x.readAt == null) SliverToBoxAdapter(
.isEmpty) child: Container(
SliverToBoxAdapter( padding: const EdgeInsets.symmetric(horizontal: 10),
child: Container( color:
padding: const EdgeInsets.symmetric(horizontal: 10), Theme.of(context).colorScheme.surfaceContainerHigh,
color: Theme.of(context) child: ListTile(
.colorScheme leading: const Icon(Icons.check),
.surfaceContainerHigh, title: Text('notifyEmpty'.tr),
child: ListTile( subtitle: Text('notifyEmptyCaption'.tr),
leading: const Icon(Icons.check),
title: Text('notifyEmpty'.tr),
subtitle: Text('notifyEmptyCaption'.tr),
),
), ),
), ),
if (nty.notifications ),
.where((x) => x.readAt == null) if (ws.notifications.isNotEmpty)
.isNotEmpty) SliverToBoxAdapter(
SliverToBoxAdapter( child: Container(
child: Container( padding: const EdgeInsets.symmetric(horizontal: 10),
padding: const EdgeInsets.symmetric(horizontal: 10), color: Theme.of(context).colorScheme.secondaryContainer,
color: child: ListTile(
Theme.of(context).colorScheme.secondaryContainer, leading: const Icon(Icons.checklist),
child: ListTile( title: Text('notifyAllRead'.tr),
leading: const Icon(Icons.checklist), onTap: _isBusy ? null : () => _markAllRead(),
title: Text('notifyAllRead'.tr),
onTap: nty.isBusy.value
? null
: () => nty.markAllRead(),
),
), ),
), ),
SliverList.separated(
itemCount: nty.notifications.length,
itemBuilder: (BuildContext context, int index) {
var element = nty.notifications[index];
return ClipRect(
child: Dismissible(
direction: element.readAt == null
? DismissDirection.horizontal
: DismissDirection.none,
key: Key(const Uuid().v4()),
background: Container(
color: Colors.lightBlue,
padding:
const EdgeInsets.symmetric(horizontal: 20),
alignment: Alignment.centerLeft,
child:
const Icon(Icons.check, color: Colors.white),
),
secondaryBackground: Container(
color: Colors.lightBlue,
padding:
const EdgeInsets.symmetric(horizontal: 20),
alignment: Alignment.centerRight,
child:
const Icon(Icons.check, color: Colors.white),
),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 28,
vertical: 16,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(NotificationTopicIcons[element.topic]),
const Gap(16),
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
if (element.readAt == null)
Badge(
label: Row(
children: [
Icon(
Icons.new_releases_outlined,
color: Theme.of(context)
.colorScheme
.onSurface,
size: 12,
),
const Gap(4),
Text('unread'.tr),
],
),
).paddingOnly(bottom: 4),
Text(
element.title,
style: Theme.of(context)
.textTheme
.titleMedium,
),
if (element.subtitle != null)
Text(
element.subtitle!,
style: Theme.of(context)
.textTheme
.titleSmall,
),
if (element.subtitle != null)
const Gap(4),
MarkdownTextContent(
content: element.body,
isAutoWarp: true,
isSelectable: true,
parentId:
'notification-${element.id}',
),
if ([
'interactive.feedback',
'interactive.subscription'
].contains(element.topic) &&
element.metadata?['related_post'] !=
null)
_PostRelatedNotificationWidget(
metadata: element.metadata!,
),
const Gap(8),
Opacity(
opacity: 0.75,
child: Row(
children: [
RelativeDate(
element.createdAt,
style: TextStyle(fontSize: 12),
),
const Gap(4),
Text(
'·',
style: TextStyle(fontSize: 12),
),
const Gap(4),
RelativeDate(
element.createdAt,
style: TextStyle(fontSize: 12),
isFull: true,
),
],
),
),
],
),
),
],
),
),
onDismissed: (_) => nty.markOneRead(element, index),
),
);
},
separatorBuilder: (_, __) =>
const Divider(thickness: 0.3, height: 0.3),
), ),
], SliverList.separated(
), itemCount: ws.notifications.length,
itemBuilder: (BuildContext context, int index) {
var element = ws.notifications[index];
return Dismissible(
key: Key(const Uuid().v4()),
background: Container(
color: Colors.lightBlue,
padding: const EdgeInsets.symmetric(horizontal: 20),
alignment: Alignment.centerLeft,
child: const Icon(Icons.check, color: Colors.white),
),
child: ListTile(
contentPadding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 8,
),
title: Text(element.title),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (element.subtitle != null)
Text(element.subtitle!),
Text(element.body),
],
),
),
onDismissed: (_) => _markOneRead(element, index),
);
},
separatorBuilder: (_, __) =>
const Divider(thickness: 0.3, height: 0.3),
),
],
); );
}), }),
), ),
@ -216,7 +156,7 @@ class NotificationButton extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final NotificationProvider nty = Get.find(); final WebSocketProvider provider = Get.find();
final button = IconButton( final button = IconButton(
icon: const Icon(Icons.notifications), icon: const Icon(Icons.notifications),
@ -226,16 +166,16 @@ class NotificationButton extends StatelessWidget {
isScrollControlled: true, isScrollControlled: true,
context: context, context: context,
builder: (context) => const NotificationScreen(), builder: (context) => const NotificationScreen(),
).then((_) => nty.notificationUnread.value = 0); ).then((_) => provider.notificationUnread.value = 0);
}, },
); );
return Obx(() { return Obx(() {
if (nty.notificationUnread.value > 0) { if (provider.notificationUnread.value > 0) {
return Badge( return Badge(
isLabelVisible: true, isLabelVisible: true,
offset: const Offset(-8, 2), offset: const Offset(-8, 2),
label: Text(nty.notificationUnread.value.toString()), label: Text(provider.notificationUnread.value.toString()),
child: button, child: button,
); );
} else { } else {
@ -244,31 +184,3 @@ class NotificationButton extends StatelessWidget {
}); });
} }
} }
class _PostRelatedNotificationWidget extends StatelessWidget {
final Map<String, dynamic> metadata;
const _PostRelatedNotificationWidget({super.key, required this.metadata});
@override
Widget build(BuildContext context) {
return GestureDetector(
child: Card(
margin: const EdgeInsets.symmetric(vertical: 4),
child: PostItem(
item: Post.fromJson(metadata['related_post']),
isCompact: true,
).paddingAll(8),
),
onTap: () {
final data = Post.fromJson(metadata['related_post']);
Navigator.pop(context);
AppRouter.instance.pushNamed(
'postDetail',
pathParameters: {'id': data.id.toString()},
extra: data,
);
},
);
}
}

View File

@ -1,11 +1,12 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:get/get_connect/http/src/exceptions/exceptions.dart'; import 'package:get/get_connect/http/src/exceptions/exceptions.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import 'package:solian/exceptions/request.dart'; import 'package:solian/exceptions/request.dart';
import 'package:solian/exts.dart'; import 'package:solian/exts.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
import 'package:solian/widgets/loading_indicator.dart'; import 'package:solian/widgets/root_container.dart';
class NotificationPreferencesScreen extends StatefulWidget { class NotificationPreferencesScreen extends StatefulWidget {
const NotificationPreferencesScreen({super.key}); const NotificationPreferencesScreen({super.key});
@ -59,10 +60,10 @@ class _NotificationPreferencesScreenState
}); });
if (resp.statusCode != 200) { if (resp.statusCode != 200) {
context.showErrorDialog(RequestException(resp)); context.showErrorDialog(RequestException(resp));
} else {
context.showSnackbar('preferencesApplied'.tr);
} }
context.showSnackbar('preferencesApplied'.tr);
setState(() => _isBusy = false); setState(() => _isBusy = false);
} }
@ -74,42 +75,44 @@ class _NotificationPreferencesScreenState
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return RootContainer(
children: [ child: Column(
LoadingIndicator(isActive: _isBusy), children: [
ListTile( if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
tileColor: Theme.of(context).colorScheme.surfaceContainer, ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 24), tileColor: Theme.of(context).colorScheme.surfaceContainer,
leading: const Icon(Icons.save), contentPadding: const EdgeInsets.symmetric(horizontal: 24),
title: Text('save'.tr), leading: const Icon(Icons.save),
enabled: !_isBusy, title: Text('save'.tr),
onTap: () { enabled: !_isBusy,
_savePreferences(); onTap: () {
}, _savePreferences();
),
Expanded(
child: ListView.builder(
itemCount: _topicMap.length,
itemBuilder: (context, index) {
final element = _topicMap.entries.elementAt(index);
return CheckboxListTile(
title: Text(element.value),
subtitle: Text(
element.key,
style: GoogleFonts.robotoMono(fontSize: 12),
),
value: _config[element.key] ?? true,
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
onChanged: (value) {
setState(() {
_config[element.key] = value ?? false;
});
},
);
}, },
), ),
), Expanded(
], child: ListView.builder(
itemCount: _topicMap.length,
itemBuilder: (context, index) {
final element = _topicMap.entries.elementAt(index);
return CheckboxListTile(
title: Text(element.value),
subtitle: Text(
element.key,
style: GoogleFonts.robotoMono(fontSize: 12),
),
value: _config[element.key] ?? true,
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
onChanged: (value) {
setState(() {
_config[element.key] = value ?? false;
});
},
);
},
),
),
],
),
); );
} }
} }

View File

@ -1,118 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:get/get_connect/http/src/exceptions/exceptions.dart';
import 'package:solian/exceptions/request.dart';
import 'package:solian/exts.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/widgets/loading_indicator.dart';
class AuthPreferencesScreen extends StatefulWidget {
const AuthPreferencesScreen({super.key});
@override
State<AuthPreferencesScreen> createState() => _AuthPreferencesScreenState();
}
class _AuthPreferencesScreenState extends State<AuthPreferencesScreen> {
bool _isBusy = true;
Map<String, dynamic> _config = {
'maximum_auth_steps': 2,
};
Future<void> _getPreferences() async {
setState(() => _isBusy = true);
final auth = Get.find<AuthProvider>();
if (!auth.isAuthorized.value) throw UnauthorizedException();
final client = await auth.configureClient('id');
final resp = await client.get('/preferences/auth');
if (resp.statusCode != 200 && resp.statusCode != 404) {
context.showErrorDialog(RequestException(resp));
}
if (resp.statusCode == 200) {
_config = resp.body;
}
setState(() => _isBusy = false);
}
Future<void> _savePreferences() async {
setState(() => _isBusy = true);
final auth = Get.find<AuthProvider>();
if (!auth.isAuthorized.value) throw UnauthorizedException();
final client = await auth.configureClient('id');
final resp = await client.put('/preferences/auth', _config);
if (resp.statusCode != 200) {
context.showErrorDialog(RequestException(resp));
} else {
context.showSnackbar('preferencesApplied'.tr);
}
setState(() => _isBusy = false);
}
@override
void initState() {
super.initState();
_getPreferences();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
LoadingIndicator(isActive: _isBusy),
ListTile(
tileColor: Theme.of(context).colorScheme.surfaceContainer,
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Icons.save),
title: Text('save'.tr),
enabled: !_isBusy,
onTap: () {
_savePreferences();
},
),
Expanded(
child: ListView(
children: [
ListTile(
title: Text('authMaximumAuthSteps'.tr),
subtitle: Text('authMaximumAuthStepsDesc'.tr),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
trailing: SizedBox(
width: 60,
child: _isBusy
? null
: TextFormField(
decoration: InputDecoration(
border: const OutlineInputBorder(),
isDense: true,
),
initialValue:
_config['maximum_auth_steps']?.toString() ?? '2',
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly
],
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
onChanged: (value) {
_config['maximum_auth_steps'] =
int.tryParse(value) ?? 2;
},
),
),
),
],
),
),
],
);
}
}

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:image_cropper/image_cropper.dart'; import 'package:image_cropper/image_cropper.dart';
@ -11,7 +12,7 @@ import 'package:solian/providers/auth.dart';
import 'package:solian/providers/content/attachment.dart'; import 'package:solian/providers/content/attachment.dart';
import 'package:solian/services.dart'; import 'package:solian/services.dart';
import 'package:solian/widgets/account/account_avatar.dart'; import 'package:solian/widgets/account/account_avatar.dart';
import 'package:solian/widgets/loading_indicator.dart'; import 'package:solian/widgets/root_container.dart';
class PersonalizeScreen extends StatefulWidget { class PersonalizeScreen extends StatefulWidget {
const PersonalizeScreen({super.key}); const PersonalizeScreen({super.key});
@ -186,161 +187,163 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
const double padding = 32; const double padding = 32;
return ListView( return RootContainer(
children: [ child: ListView(
LoadingIndicator(isActive: _isBusy), children: [
const Gap(24), if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
Stack( const Gap(24),
children: [ Stack(
AttachedCircleAvatar(content: _avatar, radius: 40), children: [
Positioned( AccountAvatar(content: _avatar, radius: 40),
bottom: 0, Positioned(
left: 40, bottom: 0,
child: FloatingActionButton.small( left: 40,
heroTag: const Key('avatar-editor'), child: FloatingActionButton.small(
onPressed: () => _editImage('avatar'), heroTag: const Key('avatar-editor'),
child: const Icon( onPressed: () => _editImage('avatar'),
Icons.camera, child: const Icon(
Icons.camera,
),
), ),
), ),
), ],
], ).paddingSymmetric(horizontal: padding),
).paddingSymmetric(horizontal: padding), const Gap(16),
const Gap(16), Stack(
Stack( children: [
children: [ ClipRRect(
ClipRRect( borderRadius: const BorderRadius.all(Radius.circular(8)),
borderRadius: const BorderRadius.all(Radius.circular(8)), child: AspectRatio(
child: AspectRatio( aspectRatio: 16 / 9,
aspectRatio: 16 / 9, child: Container(
child: Container( color: Theme.of(context).colorScheme.surfaceContainerHigh,
color: Theme.of(context).colorScheme.surfaceContainerHigh, child: _banner != null
child: _banner != null ? Image.network(
? Image.network( ServiceFinder.buildUrl(
ServiceFinder.buildUrl( 'files', '/attachments/$_banner'),
'files', '/attachments/$_banner'), fit: BoxFit.cover,
fit: BoxFit.cover, loadingBuilder: (BuildContext context, Widget child,
loadingBuilder: (BuildContext context, Widget child, ImageChunkEvent? loadingProgress) {
ImageChunkEvent? loadingProgress) { if (loadingProgress == null) return child;
if (loadingProgress == null) return child; return Center(
return Center( child: CircularProgressIndicator(
child: CircularProgressIndicator( value: loadingProgress.expectedTotalBytes !=
value: loadingProgress.expectedTotalBytes != null
null ? loadingProgress.cumulativeBytesLoaded /
? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes!
loadingProgress.expectedTotalBytes! : null,
: null, ),
), );
); },
}, )
) : Container(),
: Container(), ),
), ),
), ),
), Positioned(
Positioned( bottom: 16,
bottom: 16, right: 16,
right: 16, child: FloatingActionButton(
child: FloatingActionButton( heroTag: const Key('banner-editor'),
heroTag: const Key('banner-editor'), onPressed: () => _editImage('banner'),
onPressed: () => _editImage('banner'), child: const Icon(
child: const Icon( Icons.camera_alt,
Icons.camera_alt, ),
), ),
), ),
), ],
], ).paddingSymmetric(horizontal: padding),
).paddingSymmetric(horizontal: padding), const Gap(24),
const Gap(24), Row(
Row( children: [
children: [ Flexible(
Flexible( flex: 1,
flex: 1, child: TextField(
child: TextField( readOnly: true,
readOnly: true, controller: _usernameController,
controller: _usernameController, decoration: InputDecoration(
decoration: InputDecoration( border: const OutlineInputBorder(),
border: const OutlineInputBorder(), labelText: 'username'.tr,
labelText: 'username'.tr, prefixText: '@',
prefixText: '@', ),
), ),
), ),
), const Gap(16),
const Gap(16), Flexible(
Flexible( flex: 1,
flex: 1, child: TextField(
child: TextField( controller: _nicknameController,
controller: _nicknameController, decoration: InputDecoration(
decoration: InputDecoration( border: const OutlineInputBorder(),
border: const OutlineInputBorder(), labelText: 'nickname'.tr,
labelText: 'nickname'.tr, ),
), ),
), ),
), ],
], ).paddingSymmetric(horizontal: padding),
).paddingSymmetric(horizontal: padding), const Gap(16),
const Gap(16), Row(
Row( children: [
children: [ Flexible(
Flexible( flex: 1,
flex: 1, child: TextField(
child: TextField( controller: _firstNameController,
controller: _firstNameController, decoration: InputDecoration(
decoration: InputDecoration( border: const OutlineInputBorder(),
border: const OutlineInputBorder(), labelText: 'firstName'.tr,
labelText: 'firstName'.tr, ),
), ),
), ),
), const Gap(16),
const Gap(16), Flexible(
Flexible( flex: 1,
flex: 1, child: TextField(
child: TextField( controller: _lastNameController,
controller: _lastNameController, decoration: InputDecoration(
decoration: InputDecoration( border: const OutlineInputBorder(),
border: const OutlineInputBorder(), labelText: 'lastName'.tr,
labelText: 'lastName'.tr, ),
), ),
), ),
],
).paddingSymmetric(horizontal: padding),
const Gap(16),
TextField(
controller: _descriptionController,
keyboardType: TextInputType.multiline,
maxLines: null,
minLines: 3,
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: 'description'.tr,
), ),
], ).paddingSymmetric(horizontal: padding),
).paddingSymmetric(horizontal: padding), const Gap(16),
const Gap(16), TextField(
TextField( controller: _birthdayController,
controller: _descriptionController, readOnly: true,
keyboardType: TextInputType.multiline, decoration: InputDecoration(
maxLines: null, border: const OutlineInputBorder(),
minLines: 3, labelText: 'birthday'.tr,
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: 'description'.tr,
),
).paddingSymmetric(horizontal: padding),
const Gap(16),
TextField(
controller: _birthdayController,
readOnly: true,
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: 'birthday'.tr,
),
onTap: () => _selectBirthday(),
).paddingSymmetric(horizontal: padding),
const Gap(16),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: _isBusy ? null : () => _syncWidget(),
child: Text('reset'.tr),
), ),
ElevatedButton( onTap: () => _selectBirthday(),
onPressed: _isBusy ? null : () => _editUserInfo(), ).paddingSymmetric(horizontal: padding),
child: Text('apply'.tr), const Gap(16),
), Row(
], mainAxisAlignment: MainAxisAlignment.end,
).paddingSymmetric(horizontal: padding), children: [
], TextButton(
onPressed: _isBusy ? null : () => _syncWidget(),
child: Text('reset'.tr),
),
ElevatedButton(
onPressed: _isBusy ? null : () => _editUserInfo(),
child: Text('apply'.tr),
),
],
).paddingSymmetric(horizontal: padding),
],
),
); );
} }

View File

@ -26,6 +26,7 @@ import 'package:solian/widgets/app_bar_leading.dart';
import 'package:solian/widgets/attachments/attachment_list.dart'; import 'package:solian/widgets/attachments/attachment_list.dart';
import 'package:solian/widgets/daily_sign/history_chart.dart'; import 'package:solian/widgets/daily_sign/history_chart.dart';
import 'package:solian/widgets/posts/post_list.dart'; import 'package:solian/widgets/posts/post_list.dart';
import 'package:solian/widgets/posts/post_warped_list.dart';
import 'package:solian/widgets/reports/abuse_report.dart'; import 'package:solian/widgets/reports/abuse_report.dart';
import 'package:solian/widgets/root_container.dart'; import 'package:solian/widgets/root_container.dart';
import 'package:solian/widgets/sized_container.dart'; import 'package:solian/widgets/sized_container.dart';
@ -230,11 +231,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (_isBusy || _userinfo == null) { if (_isBusy || _userinfo == null) {
return RootContainer( return const Center(child: CircularProgressIndicator());
child: const Center(
child: CircularProgressIndicator(),
),
);
} }
return RootContainer( return RootContainer(
@ -253,15 +250,10 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
height: 56, height: 56,
child: Row( child: Row(
children: [ children: [
AppBarLeadingButton.adaptive( AppBarLeadingButton.adaptive(context) ?? const Gap(8),
context,
forceBack: true,
) ??
const Gap(8),
const Gap(8), const Gap(8),
if (_userinfo != null) if (_userinfo != null)
AttachedCircleAvatar( AccountAvatar(content: _userinfo!.avatar, radius: 16),
content: _userinfo!.avatar, radius: 16),
const Gap(12), const Gap(12),
Expanded( Expanded(
child: Column( child: Column(
@ -348,7 +340,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
detail: _userinfo, detail: _userinfo,
profile: _userinfo!.profile, profile: _userinfo!.profile,
extraWidgets: [ extraWidgets: [
if (_dailySignRecords.length > 1) if (_dailySignRecords.isNotEmpty)
Card( Card(
child: SizedBox( child: SizedBox(
height: 180, height: 180,
@ -588,6 +580,8 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
color: color:
Theme.of(context).colorScheme.surfaceContainerLow, Theme.of(context).colorScheme.surfaceContainerLow,
child: PostListEntryWidget( child: PostListEntryWidget(
backgroundColor:
Theme.of(context).colorScheme.surfaceContainerLow,
item: element, item: element,
isClickable: true, isClickable: true,
isNestedClickable: true, isNestedClickable: true,
@ -607,7 +601,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
child: Center(child: CircularProgressIndicator()), child: Center(child: CircularProgressIndicator()),
), ),
if (_userinfo != null) if (_userinfo != null)
ControlledPostListWidget( PostWarpedListWidget(
isPinned: false, isPinned: false,
controller: _postController.pagingController, controller: _postController.pagingController,
onUpdate: () => _postController.reloadAllOver(), onUpdate: () => _postController.reloadAllOver(),

View File

@ -8,9 +8,10 @@ import 'package:solian/exts.dart';
import 'package:solian/models/auth.dart'; import 'package:solian/models/auth.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
import 'package:solian/providers/content/realm.dart'; import 'package:solian/providers/content/realm.dart';
import 'package:solian/providers/notifications.dart';
import 'package:solian/providers/relation.dart'; import 'package:solian/providers/relation.dart';
import 'package:solian/providers/websocket.dart';
import 'package:solian/services.dart'; import 'package:solian/services.dart';
import 'package:solian/widgets/root_container.dart';
import 'package:solian/widgets/sized_container.dart'; import 'package:solian/widgets/sized_container.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
@ -178,7 +179,7 @@ class _SignInScreenState extends State<SignInScreen> {
Get.find<RealmProvider>().refreshAvailableRealms(); Get.find<RealmProvider>().refreshAvailableRealms();
Get.find<RelationshipProvider>().refreshRelativeList(); Get.find<RelationshipProvider>().refreshRelativeList();
Get.find<NotificationProvider>().registerPushNotifications(); Get.find<WebSocketProvider>().registerPushNotifications();
autoConfigureBackgroundNotificationService(); autoConfigureBackgroundNotificationService();
autoStartBackgroundNotificationService(); autoStartBackgroundNotificationService();
@ -216,10 +217,9 @@ class _SignInScreenState extends State<SignInScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return CenteredContainer( return RootContainer(
maxWidth: 360, child: CenteredContainer(
child: Theme( maxWidth: 360,
data: Theme.of(context).copyWith(canvasColor: Colors.transparent),
child: PageTransitionSwitcher( child: PageTransitionSwitcher(
transitionBuilder: ( transitionBuilder: (
Widget child, Widget child,

View File

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

View File

@ -198,7 +198,7 @@ class _CallScreenState extends State<CallScreen> with TickerProviderStateMixin {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final ChatCallProvider ctrl = Get.find(); final ChatCallProvider ctrl = Get.find();
return ResponsiveRootContainer( return RootContainer(
child: Scaffold( child: Scaffold(
appBar: widget.hideAppBar appBar: widget.hideAppBar
? null ? null

View File

@ -218,7 +218,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen>
); );
} }
return ResponsiveRootContainer( return RootContainer(
child: Scaffold( child: Scaffold(
appBar: AppBar( appBar: AppBar(
leading: AppBarLeadingButton.adaptive(context), leading: AppBarLeadingButton.adaptive(context),

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:solian/exts.dart'; import 'package:solian/exts.dart';
import 'package:solian/models/channel.dart'; import 'package:solian/models/channel.dart';
@ -8,7 +9,6 @@ import 'package:solian/providers/content/channel.dart';
import 'package:solian/router.dart'; import 'package:solian/router.dart';
import 'package:solian/theme.dart'; import 'package:solian/theme.dart';
import 'package:solian/widgets/app_bar_title.dart'; import 'package:solian/widgets/app_bar_title.dart';
import 'package:solian/widgets/loading_indicator.dart';
import 'package:solian/widgets/root_container.dart'; import 'package:solian/widgets/root_container.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
@ -115,7 +115,7 @@ class _ChannelOrganizeScreenState extends State<ChannelOrganizeScreen> {
), ),
]; ];
return ResponsiveRootContainer( return RootContainer(
child: Scaffold( child: Scaffold(
appBar: AppBar( appBar: AppBar(
title: AppBarTitle('channelOrganizing'.tr), title: AppBarTitle('channelOrganizing'.tr),
@ -132,7 +132,7 @@ class _ChannelOrganizeScreenState extends State<ChannelOrganizeScreen> {
top: false, top: false,
child: Column( child: Column(
children: [ children: [
LoadingIndicator(isActive: _isBusy), if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
if (widget.edit != null) if (widget.edit != null)
MaterialBanner( MaterialBanner(
leading: const Icon(Icons.edit), leading: const Icon(Icons.edit),

View File

@ -1,5 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_resizable_container/flutter_resizable_container.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
@ -20,7 +19,6 @@ import 'package:solian/widgets/app_bar_title.dart';
import 'package:solian/widgets/channel/channel_list.dart'; import 'package:solian/widgets/channel/channel_list.dart';
import 'package:solian/widgets/chat/call/chat_call_indicator.dart'; import 'package:solian/widgets/chat/call/chat_call_indicator.dart';
import 'package:solian/widgets/current_state_action.dart'; import 'package:solian/widgets/current_state_action.dart';
import 'package:solian/widgets/loading_indicator.dart';
import 'package:solian/widgets/root_container.dart'; import 'package:solian/widgets/root_container.dart';
import 'package:solian/widgets/sidebar/empty_placeholder.dart'; import 'package:solian/widgets/sidebar/empty_placeholder.dart';
@ -29,7 +27,7 @@ class ChatScreen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return const ResponsiveRootContainer( return const RootContainer(
child: ChatList(), child: ChatList(),
); );
} }
@ -43,23 +41,14 @@ class ChatListShell extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return RootContainer( return RootContainer(
child: ResizableContainer( child: Row(
direction: Axis.horizontal,
divider: ResizableDivider(
thickness: 0.3,
color: Theme.of(context).dividerColor.withOpacity(0.3),
),
children: [ children: [
const ResizableChild( const SizedBox(
minSize: 280, width: 360,
maxSize: 520,
size: ResizableSize.pixels(360),
child: ChatList(), child: ChatList(),
), ),
ResizableChild( const VerticalDivider(thickness: 0.3, width: 0.3),
minSize: 280, Expanded(child: child ?? const EmptyPagePlaceholder()),
child: child ?? const EmptyPagePlaceholder(),
),
], ],
), ),
); );
@ -80,8 +69,6 @@ class _ChatListState extends State<ChatList> {
late final ChannelProvider _channels = Get.find(); late final ChannelProvider _channels = Get.find();
bool _isBusy = true;
List<Channel> _sortChannels(List<Channel> channels) { List<Channel> _sortChannels(List<Channel> channels) {
channels.sort( channels.sort(
(a, b) => (a, b) =>
@ -130,25 +117,18 @@ class _ChatListState extends State<ChatList> {
final ctrl = ChatEventController(); final ctrl = ChatEventController();
await ctrl.initialize(); await ctrl.initialize();
final messages = await ctrl.src.getLastInAllChannels(); final messages = await ctrl.src.getLastInAllChannels();
if (mounted) { setState(() {
setState(() { _lastMessages = messages
_lastMessages = messages .map((k, v) => MapEntry(k, v.firstOrNull))
.map((k, v) => MapEntry(k, v.firstOrNull)) .cast<int, LocalMessageEventTableData>();
.cast<int, LocalMessageEventTableData>(); });
});
}
} }
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_loadLastMessages().then((_) { _loadLastMessages().then((_) {
if (!mounted) return; _loadAllChannels();
_loadAllChannels().then((_) {
if (mounted) {
setState(() => _isBusy = false);
}
});
}); });
} }
@ -160,10 +140,19 @@ class _ChatListState extends State<ChatList> {
return Obx( return Obx(
() => DefaultTabController( () => DefaultTabController(
length: 2 + realms.availableRealms.length, length: 2 + realms.availableRealms.length,
child: ResponsiveRootContainer( child: RootContainer(
child: Scaffold( child: Scaffold(
appBar: AppBar( 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), title: AppBarTitle('chat'.tr),
centerTitle: true, centerTitle: true,
toolbarHeight: AppTheme.toolbarHeight(context), toolbarHeight: AppTheme.toolbarHeight(context),
@ -263,7 +252,7 @@ class _ChatListState extends State<ChatList> {
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
AttachedCircleAvatar( AccountAvatar(
content: x.avatar, content: x.avatar,
radius: 14, radius: 14,
fallbackWidget: const Icon( fallbackWidget: const Icon(
@ -291,7 +280,6 @@ class _ChatListState extends State<ChatList> {
return Column( return Column(
children: [ children: [
const ChatCallCurrentIndicator(), const ChatCallCurrentIndicator(),
LoadingIndicator(isActive: _isBusy),
Expanded( Expanded(
child: TabBarView( child: TabBarView(
children: [ children: [

View File

@ -20,7 +20,7 @@ import 'package:solian/providers/content/posts.dart';
import 'package:solian/providers/daily_sign.dart'; import 'package:solian/providers/daily_sign.dart';
import 'package:solian/providers/database/services/messages.dart'; import 'package:solian/providers/database/services/messages.dart';
import 'package:solian/providers/last_read.dart'; import 'package:solian/providers/last_read.dart';
import 'package:solian/providers/notifications.dart'; import 'package:solian/providers/websocket.dart';
import 'package:solian/router.dart'; import 'package:solian/router.dart';
import 'package:solian/screens/account/notification.dart'; import 'package:solian/screens/account/notification.dart';
import 'package:solian/theme.dart'; import 'package:solian/theme.dart';
@ -38,7 +38,7 @@ class DashboardScreen extends StatefulWidget {
class _DashboardScreenState extends State<DashboardScreen> { class _DashboardScreenState extends State<DashboardScreen> {
late final AuthProvider _auth = Get.find(); late final AuthProvider _auth = Get.find();
late final LastReadProvider _lastRead = Get.find(); late final LastReadProvider _lastRead = Get.find();
late final NotificationProvider _nty = Get.find(); late final WebSocketProvider _ws = Get.find();
late final PostProvider _posts = Get.find(); late final PostProvider _posts = Get.find();
late final DailySignProvider _dailySign = Get.find(); late final DailySignProvider _dailySign = Get.find();
@ -46,7 +46,7 @@ class _DashboardScreenState extends State<DashboardScreen> {
Theme.of(context).colorScheme.onSurface.withOpacity(0.75); Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
List<Notification> get _pendingNotifications => List<Notification> get _pendingNotifications =>
List<Notification>.from(_nty.notifications.where((x) => x.readAt == null)) List<Notification>.from(_ws.notifications)
..sort((a, b) => b.createdAt.compareTo(a.createdAt)); ..sort((a, b) => b.createdAt.compareTo(a.createdAt));
List<Post>? _currentPosts; List<Post>? _currentPosts;
@ -75,12 +75,10 @@ class _DashboardScreenState extends State<DashboardScreen> {
final src = Get.find<MessagesFetchingProvider>(); final src = Get.find<MessagesFetchingProvider>();
final out = await src.getWhatsNewEvents(_lastRead.messagesLastReadAt!); final out = await src.getWhatsNewEvents(_lastRead.messagesLastReadAt!);
if (out == null) return; if (out == null) return;
if (mounted) { setState(() {
setState(() { _currentMessages = out.$1;
_currentMessages = out.$1; _currentMessagesCount = out.$2;
_currentMessagesCount = out.$2; });
});
}
} }
bool _signingDaily = true; bool _signingDaily = true;
@ -91,17 +89,13 @@ class _DashboardScreenState extends State<DashboardScreen> {
try { try {
_signRecord = await _dailySign.getToday(); _signRecord = await _dailySign.getToday();
_dailySign.listLastRecord(14).then((value) { _dailySign.listLastRecord(14).then((value) {
if (mounted) { setState(() => _signRecordHistory = value);
setState(() => _signRecordHistory = value);
}
}); });
} catch (e) { } catch (e) {
context.showErrorDialog(e); context.showErrorDialog(e);
} }
if (mounted) { setState(() => _signingDaily = false);
setState(() => _signingDaily = false);
}
} }
Future<void> _signDaily() async { Future<void> _signDaily() async {
@ -153,7 +147,7 @@ class _DashboardScreenState extends State<DashboardScreen> {
), ),
Text(DateFormat('yyyy/MM/dd').format(DateTime.now().toUtc())), Text(DateFormat('yyyy/MM/dd').format(DateTime.now().toUtc())),
], ],
).paddingOnly(top: 16, left: 18, right: 18, bottom: 12), ).paddingOnly(top: 8, left: 18, right: 18, bottom: 12),
Card( Card(
child: Column( child: Column(
children: [ children: [
@ -254,7 +248,7 @@ class _DashboardScreenState extends State<DashboardScreen> {
), ),
Text( Text(
'notificationUnreadCount'.trParams({ 'notificationUnreadCount'.trParams({
'count': _pendingNotifications.length.toString(), 'count': _ws.notifications.length.toString(),
}), }),
), ),
], ],
@ -267,12 +261,12 @@ class _DashboardScreenState extends State<DashboardScreen> {
isScrollControlled: true, isScrollControlled: true,
context: context, context: context,
builder: (context) => const NotificationScreen(), builder: (context) => const NotificationScreen(),
).then((_) => _nty.notificationUnread.value = 0); ).then((_) => _ws.notificationUnread.value = 0);
}, },
), ),
], ],
).paddingOnly(left: 18, right: 18, bottom: 8), ).paddingOnly(left: 18, right: 18, bottom: 8),
if (_pendingNotifications.isNotEmpty) if (_ws.notifications.isNotEmpty)
SizedBox( SizedBox(
height: 76, height: 76,
child: ListView.separated( child: ListView.separated(
@ -389,10 +383,9 @@ class _DashboardScreenState extends State<DashboardScreen> {
onUpdate: (_) { onUpdate: (_) {
_pullPosts(); _pullPosts();
}, },
padding: EdgeInsets.symmetric( backgroundColor: Theme.of(context)
vertical: 8, .colorScheme
horizontal: 4, .surfaceContainerLow,
),
), ),
), ),
), ),
@ -526,7 +519,7 @@ class _DashboardScreenState extends State<DashboardScreen> {
style: TextStyle(color: _unFocusColor, fontSize: 12), style: TextStyle(color: _unFocusColor, fontSize: 12),
) )
], ],
).paddingOnly(left: 8, right: 8, top: 8, bottom: 50), ).paddingAll(8),
], ],
), ),
); );

View File

@ -1,5 +1,4 @@
import 'dart:async'; import 'dart:async';
import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
@ -14,9 +13,8 @@ import 'package:solian/widgets/account/signin_required_overlay.dart';
import 'package:solian/widgets/current_state_action.dart'; import 'package:solian/widgets/current_state_action.dart';
import 'package:solian/widgets/app_bar_leading.dart'; import 'package:solian/widgets/app_bar_leading.dart';
import 'package:solian/widgets/navigation/realm_switcher.dart'; import 'package:solian/widgets/navigation/realm_switcher.dart';
import 'package:solian/widgets/posts/post_creation.dart';
import 'package:solian/widgets/posts/post_list.dart';
import 'package:solian/widgets/posts/post_shuffle_swiper.dart'; import 'package:solian/widgets/posts/post_shuffle_swiper.dart';
import 'package:solian/widgets/posts/post_warped_list.dart';
import 'package:solian/widgets/root_container.dart'; import 'package:solian/widgets/root_container.dart';
class ExploreScreen extends StatefulWidget { class ExploreScreen extends StatefulWidget {
@ -82,98 +80,62 @@ class _ExploreScreenState extends State<ExploreScreen>
body: NestedScrollView( body: NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return [ return [
SliverLayoutBuilder( SliverAppBar(
builder: (context, constraints) { flexibleSpace: SizedBox(
final scrollOffset = constraints.scrollOffset; height: 48,
final colorChangeOffset = 120; child: const Row(
children: [
final scrollProgress = RealmSwitcher(),
(scrollOffset / colorChangeOffset).clamp(0.0, 1.0); ],
final blurSigma = lerpDouble(0, 10, scrollProgress) ?? 0; ).paddingSymmetric(horizontal: 8),
).paddingOnly(top: MediaQuery.of(context).padding.top),
return SliverAppBar( floating: true,
flexibleSpace: ClipRRect( toolbarHeight: AppTheme.toolbarHeight(context),
child: BackdropFilter( leading: AppBarLeadingButton.adaptive(context),
filter: ImageFilter.blur( actions: [
sigmaX: blurSigma, const BackgroundStateWidget(),
sigmaY: blurSigma, const NotificationButton(),
), SizedBox(
child: ListView( width: AppTheme.isLargeScreen(context) ? 8 : 16,
padding: EdgeInsets.zero, ),
physics: const NeverScrollableScrollPhysics(), ],
children: [ bottom: TabBar(
SizedBox( controller: _tabController,
height: 48, dividerHeight: 0.3,
child: const Row( tabAlignment: TabAlignment.fill,
children: [ tabs: [
RealmSwitcher(), Tab(
], child: Row(
).paddingSymmetric(horizontal: 8), mainAxisSize: MainAxisSize.min,
).paddingSymmetric(vertical: 4), children: [
TabBar( const Icon(Icons.feed, size: 20),
controller: _tabController, const Gap(8),
dividerHeight: scrollProgress > 0 ? 0 : 0.3, Text('postListNews'.tr),
tabAlignment: TabAlignment.fill, ],
tabs: [
Tab(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.feed, size: 20),
const Gap(8),
Text('postListNews'.tr),
],
),
),
Tab(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.people, size: 20),
const Gap(8),
Text('postListFriends'.tr),
],
),
),
Tab(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.shuffle_on_outlined,
size: 20,
),
const Gap(8),
Text('postListShuffle'.tr),
],
),
),
],
),
],
).paddingOnly(top: MediaQuery.of(context).padding.top),
), ),
), ),
expandedHeight: 104, Tab(
snap: true, child: Row(
floating: true, mainAxisSize: MainAxisSize.min,
toolbarHeight: AppTheme.toolbarHeight(context), children: [
leading: AppBarLeadingButton.adaptive(context), const Icon(Icons.people, size: 20),
actions: [ const Gap(8),
const BackgroundStateWidget(), Text('postListFriends'.tr),
IconButton( ],
icon: const Icon(Icons.search),
onPressed: () {
AppRouter.instance.pushNamed('postSearch');
},
), ),
const NotificationButton(), ),
SizedBox( Tab(
width: AppTheme.isLargeScreen(context) ? 8 : 16, child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.shuffle_on_outlined, size: 20),
const Gap(8),
Text('postListShuffle'.tr),
],
), ),
], ),
); ],
}, ),
) )
]; ];
}, },
@ -194,13 +156,7 @@ class _ExploreScreenState extends State<ExploreScreen>
RefreshIndicator( RefreshIndicator(
onRefresh: () => _postController.reloadAllOver(), onRefresh: () => _postController.reloadAllOver(),
child: CustomScrollView(slivers: [ child: CustomScrollView(slivers: [
ControlledPostListWidget( PostWarpedListWidget(
padding: AppTheme.isLargeScreen(context)
? EdgeInsets.symmetric(
horizontal: 4,
vertical: 8,
)
: EdgeInsets.zero,
controller: _postController.pagingController, controller: _postController.pagingController,
onUpdate: () => _postController.reloadAllOver(), onUpdate: () => _postController.reloadAllOver(),
), ),
@ -211,10 +167,7 @@ class _ExploreScreenState extends State<ExploreScreen>
return RefreshIndicator( return RefreshIndicator(
onRefresh: () => _postController.reloadAllOver(), onRefresh: () => _postController.reloadAllOver(),
child: CustomScrollView(slivers: [ child: CustomScrollView(slivers: [
ControlledPostListWidget( PostWarpedListWidget(
padding: AppTheme.isLargeScreen(context)
? EdgeInsets.symmetric(horizontal: 16)
: EdgeInsets.zero,
controller: _postController.pagingController, controller: _postController.pagingController,
onUpdate: () => _postController.reloadAllOver(), onUpdate: () => _postController.reloadAllOver(),
), ),
@ -249,3 +202,106 @@ class _ExploreScreenState extends State<ExploreScreen>
super.dispose(); super.dispose();
} }
} }
class PostCreatePopup extends StatelessWidget {
final bool hideDraftBox;
const PostCreatePopup({
super.key,
this.hideDraftBox = false,
});
@override
Widget build(BuildContext context) {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) {
return const SizedBox.shrink();
}
final List<dynamic> actionList = [
(
icon: const Icon(Icons.post_add),
label: 'postEditorModeStory'.tr,
onTap: () {
Navigator.pop(
context,
AppRouter.instance.pushNamed(
'postEditor',
queryParameters: {
'mode': 0.toString(),
},
),
);
},
),
(
icon: const Icon(Icons.description),
label: 'postEditorModeArticle'.tr,
onTap: () {
Navigator.pop(
context,
AppRouter.instance.pushNamed(
'postEditor',
queryParameters: {
'mode': 1.toString(),
},
),
);
},
),
(
icon: const Icon(Icons.drafts),
label: 'draftBoxOpen'.tr,
onTap: () {
Navigator.pop(
context,
AppRouter.instance.pushNamed('draftBox'),
);
},
),
];
return SizedBox(
height: MediaQuery.of(context).size.height * 0.38,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'postNew'.tr,
style: Theme.of(context).textTheme.headlineSmall,
).paddingOnly(left: 24, right: 24, top: 32, bottom: 16),
Expanded(
child: GridView.count(
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: 3,
children: actionList
.map((x) => Card(
color: Theme.of(context).colorScheme.surfaceContainer,
child: InkWell(
borderRadius:
const BorderRadius.all(Radius.circular(8)),
onTap: x.onTap,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
x.icon,
const Gap(8),
Expanded(
child: Text(
x.label,
overflow: TextOverflow.fade,
),
),
],
).paddingAll(18),
),
))
.toList(),
).paddingSymmetric(horizontal: 20),
),
],
),
);
}
}

View File

@ -0,0 +1,114 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:solian/models/pagination.dart';
import 'package:solian/models/post.dart';
import 'package:solian/providers/content/posts.dart';
import 'package:solian/theme.dart';
import 'package:solian/widgets/app_bar_leading.dart';
import 'package:solian/widgets/app_bar_title.dart';
import 'package:solian/widgets/posts/post_action.dart';
import 'package:solian/widgets/posts/post_owned_list.dart';
import 'package:solian/widgets/root_container.dart';
class DraftBoxScreen extends StatefulWidget {
const DraftBoxScreen({super.key});
@override
State<DraftBoxScreen> createState() => _DraftBoxScreenState();
}
class _DraftBoxScreenState extends State<DraftBoxScreen> {
final PagingController<int, Post> _pagingController =
PagingController(firstPageKey: 0);
_getPosts(int pageKey) async {
final PostProvider provider = Get.find();
Response resp;
try {
resp = await provider.listDraft(pageKey);
} catch (e) {
_pagingController.error = e;
return;
}
final PaginationResult result = PaginationResult.fromJson(resp.body);
if (result.count == 0) {
_pagingController.appendLastPage([]);
return;
}
final parsed = result.data?.map((e) => Post.fromJson(e)).toList();
if (parsed != null && parsed.length >= 10) {
_pagingController.appendPage(parsed, pageKey + parsed.length);
} else if (parsed != null) {
_pagingController.appendLastPage(parsed);
}
}
@override
void initState() {
super.initState();
_pagingController.addPageRequestListener(_getPosts);
}
@override
Widget build(BuildContext context) {
return RootContainer(
child: Scaffold(
appBar: AppBar(
leading: AppBarLeadingButton.adaptive(context),
title: AppBarTitle('draftBox'.tr),
centerTitle: false,
toolbarHeight: AppTheme.toolbarHeight(context),
actions: [
SizedBox(
width: AppTheme.isLargeScreen(context) ? 8 : 16,
),
],
),
body: RefreshIndicator(
onRefresh: () => Future.sync(() => _pagingController.refresh()),
child: PagedListView<int, Post>(
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate(
itemBuilder: (context, item, index) {
return PostOwnedListEntry(
item: item,
isFullContent: true,
backgroundColor:
Theme.of(context).colorScheme.surfaceContainerLow,
onTap: () async {
showModalBottomSheet(
useRootNavigator: true,
context: context,
builder: (context) => PostAction(
item: item,
noReact: true,
),
).then((value) {
if (value is Future) {
value.then((_) {
_pagingController.refresh();
});
} else if (value != null) {
_pagingController.refresh();
}
});
},
).paddingOnly(left: 12, right: 12, bottom: 4);
},
),
),
),
),
);
}
@override
void dispose() {
_pagingController.dispose();
super.dispose();
}
}

View File

@ -0,0 +1,99 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:solian/models/pagination.dart';
import 'package:solian/providers/content/posts.dart';
import 'package:solian/widgets/posts/post_warped_list.dart';
import '../../models/post.dart';
class FeedSearchScreen extends StatefulWidget {
final String? tag;
final String? category;
const FeedSearchScreen({super.key, this.tag, this.category});
@override
State<FeedSearchScreen> createState() => _FeedSearchScreenState();
}
class _FeedSearchScreenState extends State<FeedSearchScreen> {
final PagingController<int, Post> _pagingController =
PagingController(firstPageKey: 0);
getPosts(int pageKey) async {
final PostProvider provider = Get.find();
Response resp;
try {
resp = await provider.listPost(
pageKey,
tag: widget.tag,
category: widget.category,
);
} catch (e) {
_pagingController.error = e;
return;
}
final PaginationResult result = PaginationResult.fromJson(resp.body);
final parsed = result.data?.map((e) => Post.fromJson(e)).toList();
if (parsed != null && parsed.length >= 10) {
_pagingController.appendPage(parsed, pageKey + parsed.length);
} else if (parsed != null) {
_pagingController.appendLastPage(parsed);
}
}
@override
void initState() {
super.initState();
_pagingController.addPageRequestListener(getPosts);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Material(
color: Theme.of(context).colorScheme.surface,
child: Column(
children: [
if (widget.tag != null)
ListTile(
leading: const Icon(Icons.label),
tileColor: Theme.of(context).colorScheme.surfaceContainer,
title: Text('postSearchWithTag'.trParams({'key': widget.tag!})),
),
if (widget.category != null)
ListTile(
leading: const Icon(Icons.category),
tileColor: Theme.of(context).colorScheme.surfaceContainer,
title: Text('postSearchWithCategory'
.trParams({'key': widget.category!})),
),
Expanded(
child: RefreshIndicator(
onRefresh: () => Future.sync(() => _pagingController.refresh()),
child: CustomScrollView(
slivers: [
PostWarpedListWidget(
controller: _pagingController,
onUpdate: () => _pagingController.refresh(),
),
],
),
),
),
],
),
),
);
}
@override
void dispose() {
_pagingController.dispose();
super.dispose();
}
}

View File

@ -1,128 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:solian/models/pagination.dart';
import 'package:solian/models/post.dart';
import 'package:solian/providers/content/posts.dart';
import 'package:solian/theme.dart';
import 'package:solian/widgets/app_bar_leading.dart';
import 'package:solian/widgets/app_bar_title.dart';
import 'package:solian/widgets/loading_indicator.dart';
import 'package:solian/widgets/posts/post_action.dart';
import 'package:solian/widgets/posts/post_item.dart';
import 'package:solian/widgets/root_container.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
class DraftBoxScreen extends StatefulWidget {
const DraftBoxScreen({super.key});
@override
State<DraftBoxScreen> createState() => _DraftBoxScreenState();
}
class _DraftBoxScreenState extends State<DraftBoxScreen> {
bool _isBusy = true;
int? _totalPosts;
final List<Post> _posts = List.empty(growable: true);
_getPosts() async {
setState(() => _isBusy = true);
final PostProvider posts = Get.find();
final resp = await posts.listDraft(_posts.length);
final PaginationResult result = PaginationResult.fromJson(resp.body);
final parsed = result.data?.map((e) => Post.fromJson(e)).toList();
_totalPosts = result.count;
_posts.addAll(parsed ?? List.empty());
setState(() => _isBusy = false);
}
Future<void> _openActions(Post item) async {
showModalBottomSheet(
useRootNavigator: true,
context: context,
builder: (context) => PostAction(
item: item,
noReact: true,
),
).then((value) {
if (value is Future) {
value.then((_) {
_posts.clear();
_getPosts();
});
} else if (value != null) {
_posts.clear();
_getPosts();
}
});
}
@override
void initState() {
super.initState();
_getPosts();
}
@override
Widget build(BuildContext context) {
return RootContainer(
child: Scaffold(
appBar: AppBar(
leading: AppBarLeadingButton.adaptive(context),
title: AppBarTitle('draftBox'.tr),
centerTitle: false,
toolbarHeight: AppTheme.toolbarHeight(context),
actions: [
SizedBox(
width: AppTheme.isLargeScreen(context) ? 8 : 16,
),
],
),
body: Column(
children: [
LoadingIndicator(isActive: _isBusy),
Expanded(
child: RefreshIndicator(
onRefresh: () {
_posts.clear();
return _getPosts();
},
child: InfiniteList(
itemCount: _posts.length,
hasReachedMax: _totalPosts == _posts.length,
isLoading: _isBusy,
onFetchData: () => _getPosts(),
itemBuilder: (context, index) {
final item = _posts[index];
return Card(
child: GestureDetector(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
PostItem(
key: Key('p${item.id}'),
item: item,
isShowEmbed: false,
isClickable: false,
isShowReply: false,
isReactable: false,
onTapMore: () => _openActions(item),
).paddingSymmetric(vertical: 8),
],
),
onTap: () => _openActions(item),
),
).paddingOnly(left: 12, right: 12, bottom: 4);
},
),
),
),
],
),
),
);
}
}

View File

@ -4,11 +4,9 @@ import 'package:solian/exts.dart';
import 'package:solian/models/post.dart'; import 'package:solian/models/post.dart';
import 'package:solian/providers/content/posts.dart'; import 'package:solian/providers/content/posts.dart';
import 'package:solian/providers/last_read.dart'; import 'package:solian/providers/last_read.dart';
import 'package:solian/theme.dart';
import 'package:solian/widgets/loading_indicator.dart';
import 'package:solian/widgets/posts/post_action.dart';
import 'package:solian/widgets/posts/post_item.dart'; import 'package:solian/widgets/posts/post_item.dart';
import 'package:solian/widgets/posts/post_replies.dart'; import 'package:solian/widgets/posts/post_replies.dart';
import 'package:solian/widgets/root_container.dart';
class PostDetailScreen extends StatefulWidget { class PostDetailScreen extends StatefulWidget {
final String id; final String id;
@ -25,109 +23,75 @@ class PostDetailScreen extends StatefulWidget {
} }
class _PostDetailScreenState extends State<PostDetailScreen> { class _PostDetailScreenState extends State<PostDetailScreen> {
bool _isBusy = true; Post? item;
Post? _item; Future<Post?> getDetail() async {
if (widget.post != null) {
item = widget.post;
Get.find<LastReadProvider>().feedLastReadAt = item?.id;
return widget.post;
}
Future<void> _getDetail() async { final PostProvider provider = Get.find();
final PostProvider posts = Get.find();
try { try {
final resp = await posts.getPost(widget.id); final resp = await provider.getPost(widget.id);
_item = Post.fromJson(resp.body); item = Post.fromJson(resp.body);
} catch (e) { } catch (e) {
context.showErrorDialog(e).then((_) => Navigator.pop(context)); context.showErrorDialog(e).then((_) => Navigator.pop(context));
} }
Get.find<LastReadProvider>().feedLastReadAt = _item?.id; Get.find<LastReadProvider>().feedLastReadAt = item?.id;
if (mounted) setState(() => _isBusy = false); return item;
}
@override
void initState() {
super.initState();
if (widget.post != null) {
_item = widget.post;
}
_getDetail();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (_isBusy && _item == null) { return RootContainer(
return const Center( child: FutureBuilder(
child: CircularProgressIndicator(), future: getDetail(),
); builder: (context, snapshot) {
} if (!snapshot.hasData || snapshot.data == null) {
return const Center(
child: CircularProgressIndicator(),
);
}
return CustomScrollView( return CustomScrollView(
slivers: [ slivers: [
SliverToBoxAdapter( SliverToBoxAdapter(
child: LoadingIndicator(isActive: _isBusy), child: PostItem(
), item: item!,
SliverToBoxAdapter( isClickable: false,
child: PostItem( isOverrideEmbedClickable: true,
key: ValueKey(_item), isFullDate: true,
item: _item!, isFullContent: true,
isClickable: false, isShowReply: false,
isOverrideEmbedClickable: true, isContentSelectable: true,
isFullDate: true,
isShowReply: false,
isContentSelectable: true,
padding: AppTheme.isLargeScreen(context)
? EdgeInsets.symmetric(
horizontal: 4,
vertical: 8,
)
: EdgeInsets.zero,
onTapMore: () {
showModalBottomSheet(
useRootNavigator: true,
context: context,
builder: (context) => PostAction(
item: _item!,
noReact: true,
), ),
).then((value) { ),
if (value is Future) { SliverToBoxAdapter(
value.then((_) { child: const Divider(thickness: 0.3, height: 1)
_getDetail(); .paddingOnly(top: 4),
}); ),
} else if (value != null) { SliverToBoxAdapter(
_getDetail(); child: Align(
} alignment: Alignment.centerLeft,
}); child: Text(
}, 'postReplies'.tr,
), style: Theme.of(context).textTheme.headlineSmall,
), ).paddingOnly(left: 24, right: 24, top: 16),
SliverToBoxAdapter( ),
child: const Divider(thickness: 0.3, height: 1).paddingOnly( ),
top: 8, PostReplyList(item: item!),
), SliverToBoxAdapter(
), child: SizedBox(height: MediaQuery.of(context).padding.bottom),
SliverToBoxAdapter( ),
child: Align( ],
alignment: Alignment.centerLeft, );
child: Text( },
'postReplies'.tr, ),
style: Theme.of(context).textTheme.headlineSmall,
).paddingOnly(left: 24, right: 24, top: 16),
),
),
PostReplyList(
item: _item!,
padding: AppTheme.isLargeScreen(context)
? EdgeInsets.symmetric(
horizontal: 4,
vertical: 8,
)
: EdgeInsets.zero,
),
SliverToBoxAdapter(
child: SizedBox(height: MediaQuery.of(context).padding.bottom),
),
],
); );
} }
} }

View File

@ -16,7 +16,6 @@ import 'package:solian/router.dart';
import 'package:solian/theme.dart'; import 'package:solian/theme.dart';
import 'package:solian/widgets/app_bar_leading.dart'; import 'package:solian/widgets/app_bar_leading.dart';
import 'package:solian/widgets/app_bar_title.dart'; import 'package:solian/widgets/app_bar_title.dart';
import 'package:solian/widgets/loading_indicator.dart';
import 'package:solian/widgets/markdown_text_content.dart'; import 'package:solian/widgets/markdown_text_content.dart';
import 'package:solian/widgets/posts/post_item.dart'; import 'package:solian/widgets/posts/post_item.dart';
import 'package:badges/badges.dart' as badges; import 'package:badges/badges.dart' as badges;
@ -183,10 +182,7 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
ListTile( ListTile(
tileColor: Theme.of(context) tileColor: Theme.of(context).colorScheme.surfaceContainerLow,
.colorScheme
.surfaceContainerLow
.withOpacity(0.5),
title: Column( title: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -275,71 +271,118 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
), ),
], ],
), ),
LoadingIndicator(isActive: _isBusy), if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
Expanded( Expanded(
child: DefaultTabController( child: Row(
length: 2, crossAxisAlignment: CrossAxisAlignment.start,
child: AppTheme.isLargeScreen(context) children: [
? Row( Expanded(
crossAxisAlignment: CrossAxisAlignment.start, child: Column(
children: [ children: [
Expanded( Expanded(
child: _PostEditorTextField( child: ListView(
focusNode: _contentFocusNode, children: [
controller: _editorController, Container(
onUpdate: () => setState(() {}), padding: const EdgeInsets.symmetric(
), horizontal: 16,
), vertical: 8,
const VerticalDivider(width: 0.3, thickness: 0.3) ),
.paddingSymmetric(horizontal: 16), child: TextField(
Expanded( maxLines: null,
child: SingleChildScrollView( autofocus: true,
padding: autocorrect: true,
const EdgeInsets.only(top: 12, bottom: 64), keyboardType: TextInputType.multiline,
child: MarkdownTextContent( controller:
isAutoWarp: _editorController.mode.value == 0, _editorController.contentController,
content: focusNode: _contentFocusNode,
_editorController.contentController.text, decoration: InputDecoration.collapsed(
parentId: 'post-editor-preview', hintText: 'postContentPlaceholder'.tr,
).paddingOnly(right: 16), ),
), onTapOutside: (_) => FocusManager
), .instance.primaryFocus
], ?.unfocus(),
) ),
: Column( ),
children: [ const Gap(120)
TabBar(
tabs: [
const Tab(icon: Icon(Icons.edit)),
const Tab(icon: Icon(Icons.preview)),
], ],
), ),
Expanded( ),
child: TabBarView( Obx(() {
final textStyle = TextStyle(
fontSize: 12,
color: Theme.of(context)
.colorScheme
.onSurface
.withOpacity(0.75),
);
final showFactors = [
_editorController.isRestoreFromLocal.value,
_editorController.lastSaveTime.value != null,
];
final doShow = showFactors.any((x) => x);
return Container(
padding: const EdgeInsets.symmetric(
vertical: 4,
horizontal: 16,
),
child: Row(
children: [ children: [
_PostEditorTextField( if (showFactors[0])
focusNode: _contentFocusNode, Text('postRestoreFromLocal'.tr,
controller: _editorController, style: textStyle)
onUpdate: () => setState(() {}), .paddingOnly(right: 4),
), if (showFactors[0])
SingleChildScrollView( InkWell(
padding: const EdgeInsets.only( child: Text('clear'.tr, style: textStyle),
top: 12, onTap: () {
bottom: 64, _editorController.localClear();
_editorController.currentClear();
setState(() {});
},
),
if (showFactors.where((x) => x).length > 1)
Text(
'·',
style: textStyle,
).paddingSymmetric(horizontal: 8),
if (showFactors[1])
Text(
'postAutoSaveAt'.trParams({
'date': DateFormat('HH:mm:ss').format(
_editorController.lastSaveTime.value ??
DateTime.now(),
)
}),
style: textStyle,
), ),
child: MarkdownTextContent(
isAutoWarp:
_editorController.mode.value == 0,
content: _editorController
.contentController.text,
parentId: 'post-editor-preview',
).paddingOnly(left: 16, right: 16),
)
], ],
), ),
), )
], .animate(
key: const Key('post-editor-hint-animation'),
target: doShow ? 1 : 0,
)
.fade(curve: Curves.easeInOut, duration: 300.ms);
}),
],
),
),
if (AppTheme.isLargeScreen(context))
const VerticalDivider(width: 0.3, thickness: 0.3)
.paddingSymmetric(
horizontal: 16,
),
if (AppTheme.isLargeScreen(context))
Expanded(
child: SingleChildScrollView(
child: MarkdownTextContent(
isAutoWarp: _editorController.mode.value == 0,
content: _editorController.contentController.text,
parentId: 'post-editor-preview',
).paddingOnly(top: 12, right: 16),
), ),
),
],
), ),
), ),
Material( Material(
@ -348,26 +391,6 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const Divider(thickness: 0.3, height: 0.3), const Divider(thickness: 0.3, height: 0.3),
SizedBox(
height: 40,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: MarkdownToolbar(
width: 38,
height: 38,
iconSize: 20,
spacing: 8,
hideImage: true,
useIncludedTextField: false,
backgroundColor: Theme.of(context).colorScheme.surface,
iconColor: Theme.of(context).colorScheme.onSurface,
controller: _editorController.contentController,
focusNode: _contentFocusNode,
borderRadius:
const BorderRadius.all(Radius.circular(20)),
).paddingSymmetric(horizontal: 12),
),
).paddingOnly(top: 12),
SizedBox( SizedBox(
height: 56, height: 56,
child: ListView( child: ListView(
@ -497,7 +520,7 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
top: -4, top: -4,
end: -6, end: -6,
), ),
child: const Icon(Icons.wallpaper), child: const Icon(Icons.preview),
); );
}), }),
color: Theme.of(context).colorScheme.primary, color: Theme.of(context).colorScheme.primary,
@ -524,6 +547,18 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
_editorController.editPublishDate(context); _editorController.editPublishDate(context);
}, },
), ),
MarkdownToolbar(
hideImage: true,
useIncludedTextField: false,
backgroundColor:
Theme.of(context).colorScheme.surface,
iconColor: Theme.of(context).colorScheme.onSurface,
controller: _editorController.contentController,
focusNode: _contentFocusNode,
borderRadius:
const BorderRadius.all(Radius.circular(20)),
width: 40,
).paddingSymmetric(horizontal: 2),
], ],
).paddingSymmetric(horizontal: 6, vertical: 8), ).paddingSymmetric(horizontal: 6, vertical: 8),
), ),
@ -543,101 +578,3 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
super.dispose(); super.dispose();
} }
} }
class _PostEditorTextField extends StatelessWidget {
final FocusNode focusNode;
final PostEditorController controller;
final Function onUpdate;
const _PostEditorTextField({
required this.focusNode,
required this.controller,
required this.onUpdate,
});
@override
Widget build(BuildContext context) {
return Column(
children: [
Expanded(
child: ListView(
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
child: TextField(
maxLines: null,
autofocus: true,
autocorrect: true,
keyboardType: TextInputType.multiline,
controller: controller.contentController,
focusNode: focusNode,
decoration: InputDecoration.collapsed(
hintText: 'postContentPlaceholder'.tr,
),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
),
const Gap(120)
],
),
),
Obx(() {
final textStyle = TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.75),
);
final showFactors = [
controller.isRestoreFromLocal.value,
controller.lastSaveTime.value != null,
];
final doShow = showFactors.any((x) => x);
return Container(
padding: const EdgeInsets.symmetric(
vertical: 4,
horizontal: 16,
),
child: Row(
children: [
if (showFactors[0])
Text('postRestoreFromLocal'.tr, style: textStyle)
.paddingOnly(right: 4),
if (showFactors[0])
InkWell(
child: Text('clear'.tr, style: textStyle),
onTap: () {
controller.localClear();
controller.currentClear();
onUpdate();
},
),
if (showFactors.where((x) => x).length > 1)
Text(
'·',
style: textStyle,
).paddingSymmetric(horizontal: 8),
if (showFactors[1])
Text(
'postAutoSaveAt'.trParams({
'date': DateFormat('HH:mm:ss').format(
controller.lastSaveTime.value ?? DateTime.now(),
)
}),
style: textStyle,
),
],
),
)
.animate(
key: const Key('post-editor-hint-animation'),
target: doShow ? 1 : 0,
)
.fade(curve: Curves.easeInOut, duration: 300.ms);
}),
],
);
}
}

View File

@ -1,206 +0,0 @@
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:get/get.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:solian/models/pagination.dart';
import 'package:solian/providers/content/posts.dart';
import 'package:solian/widgets/loading_indicator.dart';
import 'package:solian/widgets/posts/post_list.dart';
import '../../models/post.dart';
class PostSearchScreen extends StatefulWidget {
final String? tag;
final String? category;
const PostSearchScreen({super.key, this.tag, this.category});
@override
State<PostSearchScreen> createState() => _PostSearchScreenState();
}
class _PostSearchScreenState extends State<PostSearchScreen> {
int? _totalCount;
Duration? _lastTook;
final TextEditingController _probeController = TextEditingController();
final PagingController<int, Post> _pagingController =
PagingController(firstPageKey: 0);
late bool _isBusy = widget.tag != null || widget.category != null;
_searchPosts(int pageKey) async {
if (widget.tag == null &&
widget.category == null &&
_probeController.text.isEmpty) {
_pagingController.appendLastPage([]);
return;
}
if (!_isBusy) {
setState(() => _isBusy = true);
}
if (pageKey == 0) {
_pagingController.itemList?.clear();
_pagingController.nextPageKey = 0;
}
final PostProvider posts = Get.find();
Stopwatch stopwatch = new Stopwatch()..start();
Response resp;
try {
if (_probeController.text.isEmpty) {
resp = await posts.listPost(
pageKey,
tag: widget.tag,
category: widget.category,
);
} else {
resp = await posts.searchPost(
_probeController.text,
pageKey,
tag: widget.tag,
category: widget.category,
);
}
} catch (e) {
_pagingController.error = e;
return;
}
final PaginationResult result = PaginationResult.fromJson(resp.body);
final parsed = result.data?.map((e) => Post.fromJson(e)).toList();
if (parsed != null && parsed.length >= 10) {
_pagingController.appendPage(parsed, pageKey + parsed.length);
} else if (parsed != null) {
_pagingController.appendLastPage(parsed);
}
stopwatch.stop();
_totalCount = result.count;
_lastTook = stopwatch.elapsed;
setState(() => _isBusy = false);
}
@override
void initState() {
super.initState();
_pagingController.addPageRequestListener(_searchPosts);
}
@override
void dispose() {
_probeController.dispose();
_pagingController.dispose();
super.dispose();
}
Color get _unFocusColor =>
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
if (widget.tag != null)
ListTile(
leading: const Icon(Icons.label),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
tileColor: Theme.of(context)
.colorScheme
.surfaceContainer
.withOpacity(0.5),
title: Text('postSearchWithTag'.trParams({'key': widget.tag!})),
),
if (widget.category != null)
ListTile(
leading: const Icon(Icons.category),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
tileColor: Theme.of(context)
.colorScheme
.surfaceContainer
.withOpacity(0.5),
title: Text('postSearchWithCategory'.trParams({
'key': widget.category!,
})),
),
Container(
color: Theme.of(context)
.colorScheme
.secondaryContainer
.withOpacity(0.5),
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
child: TextField(
controller: _probeController,
decoration: InputDecoration(
isCollapsed: true,
border: InputBorder.none,
hintText: 'search'.tr,
),
onSubmitted: (_) {
_searchPosts(0);
},
),
),
LoadingIndicator(isActive: _isBusy),
if (_totalCount != null || _lastTook != null)
Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 4),
child: Row(
children: [
Icon(
Icons.summarize_outlined,
size: 16,
color: _unFocusColor,
),
const Gap(4),
if (_totalCount != null)
Text(
'searchResult'.trParams({
'count': _totalCount!.toString(),
}),
style: TextStyle(
fontSize: 13,
color: _unFocusColor,
),
),
const Gap(4),
if (_lastTook != null)
Text(
'searchTook'.trParams({
'time':
'${(_lastTook!.inMilliseconds / 1000).toStringAsFixed(3)}s',
}),
style: TextStyle(
fontSize: 13,
color: _unFocusColor,
),
),
],
),
),
Expanded(
child: RefreshIndicator(
onRefresh: () => Future.sync(() => _pagingController.refresh()),
child: CustomScrollView(
slivers: [
ControlledPostListWidget(
controller: _pagingController,
onUpdate: () => _pagingController.refresh(),
),
SliverGap(MediaQuery.of(context).padding.bottom),
],
),
),
),
],
),
);
}
}

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:solian/models/realm.dart'; import 'package:solian/models/realm.dart';
@ -14,7 +15,6 @@ import 'package:solian/widgets/app_bar_leading.dart';
import 'package:solian/widgets/app_bar_title.dart'; import 'package:solian/widgets/app_bar_title.dart';
import 'package:solian/widgets/auto_cache_image.dart'; import 'package:solian/widgets/auto_cache_image.dart';
import 'package:solian/widgets/current_state_action.dart'; import 'package:solian/widgets/current_state_action.dart';
import 'package:solian/widgets/loading_indicator.dart';
import 'package:solian/widgets/root_container.dart'; import 'package:solian/widgets/root_container.dart';
import 'package:solian/widgets/sized_container.dart'; import 'package:solian/widgets/sized_container.dart';
@ -93,13 +93,12 @@ class _RealmListScreenState extends State<RealmListScreen> {
return Column( return Column(
children: [ children: [
LoadingIndicator(isActive: _isBusy), if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
Expanded( Expanded(
child: CenteredContainer( child: CenteredContainer(
child: RefreshIndicator( child: RefreshIndicator(
onRefresh: () => _getRealms(), onRefresh: () => _getRealms(),
child: ListView.builder( child: ListView.builder(
padding: const EdgeInsets.symmetric(vertical: 16),
itemCount: _realms.length, itemCount: _realms.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final element = _realms[index]; final element = _realms[index];
@ -156,7 +155,7 @@ class _RealmListScreenState extends State<RealmListScreen> {
size: 18, size: 18,
), ),
) )
: AttachedCircleAvatar( : AccountAvatar(
content: element.avatar!, content: element.avatar!,
bgColor: Theme.of(context).colorScheme.primary, bgColor: Theme.of(context).colorScheme.primary,
), ),

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:image_cropper/image_cropper.dart'; import 'package:image_cropper/image_cropper.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
@ -12,7 +13,6 @@ import 'package:solian/router.dart';
import 'package:solian/theme.dart'; import 'package:solian/theme.dart';
import 'package:solian/widgets/app_bar_leading.dart'; import 'package:solian/widgets/app_bar_leading.dart';
import 'package:solian/widgets/app_bar_title.dart'; import 'package:solian/widgets/app_bar_title.dart';
import 'package:solian/widgets/loading_indicator.dart';
import 'package:solian/widgets/root_container.dart'; import 'package:solian/widgets/root_container.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
@ -208,7 +208,7 @@ class _RealmOrganizeScreenState extends State<RealmOrganizeScreen> {
top: false, top: false,
child: Column( child: Column(
children: [ children: [
LoadingIndicator(isActive: _isBusy), if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
if (widget.edit != null) if (widget.edit != null)
MaterialBanner( MaterialBanner(
leading: const Icon(Icons.edit), leading: const Icon(Icons.edit),

View File

@ -19,6 +19,7 @@ import 'package:solian/providers/database/database.dart';
import 'package:solian/providers/theme_switcher.dart'; import 'package:solian/providers/theme_switcher.dart';
import 'package:solian/router.dart'; import 'package:solian/router.dart';
import 'package:solian/widgets/reports/abuse_report.dart'; import 'package:solian/widgets/reports/abuse_report.dart';
import 'package:solian/widgets/root_container.dart';
class SettingScreen extends StatefulWidget { class SettingScreen extends StatefulWidget {
const SettingScreen({super.key}); const SettingScreen({super.key});
@ -82,258 +83,259 @@ class _SettingScreenState extends State<SettingScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ListView( return RootContainer(
children: [ child: ListView(
_buildCaptionHeader('theme'.tr), children: [
ListTile( _buildCaptionHeader('theme'.tr),
leading: const Icon(Icons.palette), ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 22), leading: const Icon(Icons.palette),
title: Text('globalTheme'.tr), contentPadding: const EdgeInsets.symmetric(horizontal: 22),
trailing: DropdownButtonHideUnderline( title: Text('globalTheme'.tr),
child: DropdownButton2<SolianThemeData>( trailing: DropdownButtonHideUnderline(
isExpanded: true, child: DropdownButton2<SolianThemeData>(
hint: Text( isExpanded: true,
'theme'.tr, hint: Text(
style: TextStyle( 'theme'.tr,
fontSize: 14, style: TextStyle(
color: Theme.of(context).hintColor, fontSize: 14,
color: Theme.of(context).hintColor,
),
), ),
), items: _presentTheme
items: _presentTheme .map((SolianThemeData item) =>
.map((SolianThemeData item) => DropdownMenuItem<SolianThemeData>(
DropdownMenuItem<SolianThemeData>( value: item,
value: item, child: Row(
child: Row( crossAxisAlignment: CrossAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center, children: [
children: [ Icon(Icons.circle, color: item.seedColor),
Icon(Icons.circle, color: item.seedColor), const Gap(8),
const Gap(8), Text(
Expanded(
child: Text(
item.id.tr, item.id.tr,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle( style: const TextStyle(
fontSize: 14, fontSize: 14,
), ),
), ),
), ],
], ),
), ))
)) .toList(),
.toList(), value: (_prefs?.containsKey('global_theme') ?? false)
value: (_prefs?.containsKey('global_theme') ?? false) ? SolianThemeData.fromJson(
? SolianThemeData.fromJson( jsonDecode(_prefs!.getString('global_theme')!),
jsonDecode(_prefs!.getString('global_theme')!), )
) : null,
: null, onChanged: (SolianThemeData? value) {
onChanged: (SolianThemeData? value) { context.read<ThemeSwitcher>().setThemeData(value);
context.read<ThemeSwitcher>().setThemeData(value); setState(() {});
setState(() {}); },
}, buttonStyleData: const ButtonStyleData(
buttonStyleData: const ButtonStyleData( padding: EdgeInsets.symmetric(horizontal: 8),
padding: EdgeInsets.symmetric(horizontal: 8), height: 40,
height: 40, width: 140,
width: 140, ),
), menuItemStyleData: const MenuItemStyleData(
menuItemStyleData: const MenuItemStyleData( height: 40,
height: 40, ),
), ),
), ),
), ),
), CheckboxListTile(
CheckboxListTile( secondary: const Icon(Icons.military_tech),
secondary: const Icon(Icons.military_tech), contentPadding: const EdgeInsets.symmetric(horizontal: 22),
contentPadding: const EdgeInsets.symmetric(horizontal: 22), title: Text('agedTheme'.tr),
title: Text('agedTheme'.tr), subtitle: Text('agedThemeDesc'.tr),
subtitle: Text('agedThemeDesc'.tr), value: _prefs?.getBool('aged_theme') ?? false,
value: _prefs?.getBool('aged_theme') ?? false, onChanged: (value) {
onChanged: (value) { if (value != null) {
if (value != null) { context.read<ThemeSwitcher>().setAgedTheme(value);
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(() {}); setState(() {});
}, },
), ),
_buildCaptionHeader('notification'.tr), if (!PlatformInfo.isWeb)
Tooltip( ListTile(
message: 'settingsNotificationBgServiceDesc'.tr, leading: const Icon(Icons.wallpaper),
child: CheckboxListTile( contentPadding: const EdgeInsets.only(left: 22, right: 31),
contentPadding: const EdgeInsets.symmetric(horizontal: 22), title: Text('appBackgroundImage'.tr),
secondary: const Icon(Icons.system_security_update_warning), subtitle: Text('appBackgroundImageDesc'.tr),
enabled: PlatformInfo.isAndroid, trailing: File('$_docBasepath/app_background_image').existsSync()
title: Text('settingsNotificationBgService'.tr), ? const Icon(Icons.check_box)
subtitle: Column( : const Icon(Icons.check_box_outline_blank),
crossAxisAlignment: CrossAxisAlignment.start, onTap: () async {
children: [ if (File('$_docBasepath/app_background_image').existsSync()) {
Text('holdToSeeDetail'.tr), File('$_docBasepath/app_background_image').deleteSync();
Text( } else {
'needRestartToApply'.tr, final image = await ImagePicker().pickImage(
style: const TextStyle(fontWeight: FontWeight.bold), source: ImageSource.gallery,
) );
], if (image == null) return;
await File(image.path)
.copy('$_docBasepath/app_background_image');
}
setState(() {});
},
), ),
value: _prefs?.getBool('service_background_notification') ?? false, _buildCaptionHeader('notification'.tr),
Tooltip(
message: 'settingsNotificationBgServiceDesc'.tr,
child: CheckboxListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
secondary: const Icon(Icons.system_security_update_warning),
enabled: PlatformInfo.isAndroid,
title: Text('settingsNotificationBgService'.tr),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('holdToSeeDetail'.tr),
Text(
'needRestartToApply'.tr,
style: const TextStyle(fontWeight: FontWeight.bold),
)
],
),
value:
_prefs?.getBool('service_background_notification') ?? false,
onChanged: (value) {
_prefs
?.setBool('service_background_notification', value ?? false)
.then((_) {
setState(() {});
});
},
),
),
_buildCaptionHeader('update'.tr),
CheckboxListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
secondary: const Icon(Icons.sync_alt),
title: Text('updateCheckStrictly'.tr),
subtitle: Text('updateCheckStrictlyDesc'.tr),
value: _prefs?.getBool('check_update_strictly') ?? false,
onChanged: (value) { onChanged: (value) {
_prefs _prefs
?.setBool('service_background_notification', value ?? false) ?.setBool('check_update_strictly', value ?? false)
.then((_) { .then((_) {
setState(() {}); setState(() {});
}); });
}, },
), ),
), Obx(() {
_buildCaptionHeader('update'.tr), final AuthProvider auth = Get.find<AuthProvider>();
CheckboxListTile( if (!auth.isAuthorized.value) return const SizedBox.shrink();
contentPadding: const EdgeInsets.symmetric(horizontal: 22), return Column(
secondary: const Icon(Icons.sync_alt), children: [
title: Text('updateCheckStrictly'.tr), _buildCaptionHeader('account'.tr),
subtitle: Text('updateCheckStrictlyDesc'.tr), ListTile(
value: _prefs?.getBool('check_update_strictly') ?? false, leading: const Icon(Icons.flag),
onChanged: (value) { trailing: const Icon(Icons.chevron_right),
_prefs?.setBool('check_update_strictly', value ?? false).then((_) { contentPadding: const EdgeInsets.symmetric(horizontal: 22),
setState(() {}); title: Text('reportAbuse'.tr),
}); subtitle: Text('reportAbuseDesc'.tr),
}, onTap: () {
), showDialog(
Obx(() { context: context,
final AuthProvider auth = Get.find<AuthProvider>(); builder: (context) => const AbuseReportDialog(),
if (!auth.isAuthorized.value) return const SizedBox.shrink(); );
return Column( },
children: [ ),
_buildCaptionHeader('account'.tr), ListTile(
ListTile( leading: const Icon(Icons.person_remove),
leading: const Icon(Icons.flag), trailing: const Icon(Icons.chevron_right),
trailing: const Icon(Icons.chevron_right), contentPadding: const EdgeInsets.symmetric(horizontal: 22),
contentPadding: const EdgeInsets.symmetric(horizontal: 22), title: Text('accountDeletion'.tr),
title: Text('reportAbuse'.tr), subtitle: Text('accountDeletionDesc'.tr),
subtitle: Text('reportAbuseDesc'.tr), onTap: () {
onTap: () { context
showDialog( .showSlideToConfirmDialog(
context: context, 'accountDeletionConfirm'.tr,
builder: (context) => const AbuseReportDialog(), 'accountDeletionConfirmDesc'.trParams({
); 'account': '@${auth.userProfile.value!['name']}',
}, }),
), )
ListTile( .then((value) async {
leading: const Icon(Icons.person_remove), if (value != true) return;
trailing: const Icon(Icons.chevron_right), final client = await auth.configureClient('id');
contentPadding: const EdgeInsets.symmetric(horizontal: 22), final resp = await client.post('/users/me/deletion', {});
title: Text('accountDeletion'.tr), if (resp.statusCode != 200) {
subtitle: Text('accountDeletionDesc'.tr), context.showErrorDialog(RequestException(resp));
onTap: () { } else {
context context.showSnackbar('accountDeletionRequested'.tr);
.showSlideToConfirmDialog( }
'accountDeletionConfirm'.tr, });
'accountDeletionConfirmDesc'.trParams({ },
'account': '@${auth.userProfile.value!['name']}', ),
}), ],
) );
.then((value) async { }),
if (value != true) return; _buildCaptionHeader('performance'.tr),
final client = await auth.configureClient('id'); CheckboxListTile(
final resp = await client.post('/users/me/deletion', {}); contentPadding: const EdgeInsets.symmetric(horizontal: 22),
if (resp.statusCode != 200) { secondary: const Icon(Icons.message),
context.showErrorDialog(RequestException(resp)); title: Text('animatedMessageList'.tr),
} else { subtitle: Text('animatedMessageListDesc'.tr),
context.showSnackbar('accountDeletionRequested'.tr); value: _prefs?.getBool('non_animated_message_list') ?? false,
} onChanged: (value) {
}); _prefs
}, ?.setBool('non_animated_message_list', value ?? false)
), .then((_) {
], setState(() {});
); });
}),
_buildCaptionHeader('performance'.tr),
CheckboxListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
secondary: const Icon(Icons.message),
title: Text('animatedMessageList'.tr),
subtitle: Text('animatedMessageListDesc'.tr),
value: _prefs?.getBool('non_animated_message_list') ?? false,
onChanged: (value) {
_prefs
?.setBool('non_animated_message_list', value ?? false)
.then((_) {
setState(() {});
});
},
),
_buildCaptionHeader('more'.tr),
ListTile(
leading: const Icon(Icons.delete_sweep),
trailing: const Icon(Icons.chevron_right),
subtitle: FutureBuilder(
future: AppDatabase.getDatabaseSize(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Text('localDatabaseSize'.trParams(
{'size': 'unknown'.tr},
));
}
return Text('localDatabaseSize'.trParams(
{'size': snapshot.data!.formatBytes()},
));
}, },
), ),
contentPadding: const EdgeInsets.symmetric(horizontal: 22), _buildCaptionHeader('more'.tr),
title: Text('localDatabaseWipe'.tr),
onTap: () {
AppDatabase.removeDatabase().then((_) {
setState(() {});
});
},
),
if (PlatformInfo.canRateTheApp)
ListTile( ListTile(
leading: const Icon(Icons.star), leading: const Icon(Icons.delete_sweep),
trailing: const Icon(Icons.chevron_right),
subtitle: FutureBuilder(
future: AppDatabase.getDatabaseSize(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Text('localDatabaseSize'.trParams(
{'size': 'unknown'.tr},
));
}
return Text('localDatabaseSize'.trParams(
{'size': snapshot.data!.formatBytes()},
));
},
),
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
title: Text('localDatabaseWipe'.tr),
onTap: () {
AppDatabase.removeDatabase().then((_) {
setState(() {});
});
},
),
if (PlatformInfo.canRateTheApp)
ListTile(
leading: const Icon(Icons.star),
trailing: const Icon(Icons.chevron_right),
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
title: Text('rateTheApp'.tr),
subtitle: Text('rateTheAppDesc'.tr),
onTap: () {
final inAppReview = InAppReview.instance;
inAppReview.openStoreListing(
appStoreId: '6499032345',
);
},
),
ListTile(
leading: const Icon(Icons.info_outline),
trailing: const Icon(Icons.chevron_right), trailing: const Icon(Icons.chevron_right),
contentPadding: const EdgeInsets.symmetric(horizontal: 22), contentPadding: const EdgeInsets.symmetric(horizontal: 22),
title: Text('rateTheApp'.tr), title: Text('about'.tr),
subtitle: Text('rateTheAppDesc'.tr),
onTap: () { onTap: () {
final inAppReview = InAppReview.instance; AppRouter.instance.pushNamed('about');
inAppReview.openStoreListing(
appStoreId: '6499032345',
);
}, },
), ),
ListTile( ],
leading: const Icon(Icons.info_outline), ),
trailing: const Icon(Icons.chevron_right),
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
title: Text('about'.tr),
onTap: () {
AppRouter.instance.pushNamed('about');
},
),
],
); );
} }
} }

View File

@ -43,34 +43,21 @@ class RootShell extends StatelessWidget {
final showRailNavigation = AppTheme.isLargeScreen(context); final showRailNavigation = AppTheme.isLargeScreen(context);
final destNames = [ final destNames = AppNavigation.destinations.map((x) => x.page).toList();
'postDetail',
...AppNavigation.destinations.map((x) => x.page),
];
final showBottomNavigation = final showBottomNavigation =
destNames.contains(routeName) && !showRailNavigation; destNames.contains(routeName) && !showRailNavigation;
return Scaffold( return Scaffold(
key: rootScaffoldKey, key: rootScaffoldKey,
backgroundColor: Theme.of(context).colorScheme.surface,
bottomNavigationBar: showBottomNavigation bottomNavigationBar: showBottomNavigation
? AppNavigationBottom( ? AppNavigationBottom(
initialIndex: AppNavigation.destinations initialIndex: destNames.indexOf(routeName ?? 'page'),
.map((x) => x.page)
.toList()
.indexOf(routeName ?? 'page'),
) )
: null, : null,
body: AppTheme.isLargeScreen(context) body: AppTheme.isLargeScreen(context)
? Row( ? Row(
children: [ children: [
if (showRailNavigation) if (showRailNavigation) const AppNavigationRail(),
AppNavigationRail(
initialIndex: AppNavigation.destinations
.map((x) => x.page)
.toList()
.indexOf(routeName ?? 'page'),
),
if (showRailNavigation) if (showRailNavigation)
const VerticalDivider( const VerticalDivider(
width: 0.3, width: 0.3,

View File

@ -10,7 +10,6 @@ import 'package:solian/widgets/root_container.dart';
class TitleShell extends StatelessWidget { class TitleShell extends StatelessWidget {
final bool showAppBar; final bool showAppBar;
final bool isCenteredTitle; final bool isCenteredTitle;
final bool isResponsive;
final String? title; final String? title;
final GoRouterState? state; final GoRouterState? state;
final Widget child; final Widget child;
@ -22,37 +21,32 @@ class TitleShell extends StatelessWidget {
this.state, this.state,
this.showAppBar = true, this.showAppBar = true,
this.isCenteredTitle = false, this.isCenteredTitle = false,
this.isResponsive = false,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(state != null || title != null); assert(state != null || title != null);
final widget = Scaffold( return RootContainer(
appBar: showAppBar child: Scaffold(
? AppBar( appBar: showAppBar
leading: AppBarLeadingButton.adaptive(context), ? AppBar(
title: AppBarTitle( leading: AppBarLeadingButton.adaptive(context),
title ?? (state!.topRoute?.name?.tr ?? 'page'.tr), title: AppBarTitle(
), title ?? (state!.topRoute?.name?.tr ?? 'page'.tr),
centerTitle: isCenteredTitle,
toolbarHeight: AppTheme.toolbarHeight(context),
actions: [
const BackgroundStateWidget(),
SizedBox(
width: AppTheme.isLargeScreen(context) ? 8 : 16,
), ),
], centerTitle: isCenteredTitle,
) toolbarHeight: AppTheme.toolbarHeight(context),
: null, actions: [
body: child, const BackgroundStateWidget(),
SizedBox(
width: AppTheme.isLargeScreen(context) ? 8 : 16,
),
],
)
: null,
body: child,
),
); );
if (isResponsive) {
return ResponsiveRootContainer(child: widget);
} else {
return RootContainer(child: widget);
}
} }
} }

View File

@ -39,13 +39,10 @@ abstract class AppTheme {
brightness: brightness, brightness: brightness,
seedColor: seedColor ?? const Color.fromRGBO(154, 98, 91, 1), seedColor: seedColor ?? const Color.fromRGBO(154, 98, 91, 1),
), ),
scaffoldBackgroundColor: Colors.transparent,
snackBarTheme: const SnackBarThemeData( snackBarTheme: const SnackBarThemeData(
behavior: SnackBarBehavior.floating, behavior: SnackBarBehavior.floating,
), ),
scaffoldBackgroundColor: Colors.transparent,
appBarTheme: const AppBarTheme(
backgroundColor: Colors.transparent,
),
fontFamily: 'Comfortaa', fontFamily: 'Comfortaa',
fontFamilyFallback: [ fontFamilyFallback: [
'NotoSansSC', 'NotoSansSC',
@ -77,7 +74,6 @@ abstract class AppTheme {
behavior: SnackBarBehavior.floating, behavior: SnackBarBehavior.floating,
), ),
scaffoldBackgroundColor: Colors.transparent, scaffoldBackgroundColor: Colors.transparent,
appBarTheme: const AppBarTheme(backgroundColor: Colors.transparent),
fontFamily: data.fontFamily ?? 'Comfortaa', fontFamily: data.fontFamily ?? 'Comfortaa',
fontFamilyFallback: data.fontFamilyFallback ?? fontFamilyFallback: data.fontFamilyFallback ??
[ [

View File

@ -1,16 +1,15 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:solian/services.dart'; import 'package:solian/services.dart';
import 'package:solian/widgets/account/account_profile_popup.dart';
import 'package:solian/widgets/auto_cache_image.dart'; import 'package:solian/widgets/auto_cache_image.dart';
class AttachedCircleAvatar extends StatelessWidget { class AccountAvatar extends StatelessWidget {
final dynamic content; final dynamic content;
final Color? bgColor; final Color? bgColor;
final Color? feColor; final Color? feColor;
final double? radius; final double? radius;
final Widget? fallbackWidget; final Widget? fallbackWidget;
const AttachedCircleAvatar({ const AccountAvatar({
super.key, super.key,
required this.content, required this.content,
this.bgColor, this.bgColor,
@ -40,7 +39,7 @@ class AttachedCircleAvatar extends StatelessWidget {
child: isEmpty child: isEmpty
? (fallbackWidget ?? ? (fallbackWidget ??
Icon( Icon(
Icons.image, Icons.account_circle,
size: radius != null ? radius! * 1.2 : 24, size: radius != null ? radius! * 1.2 : 24,
color: feColor, color: feColor,
)) ))
@ -49,54 +48,6 @@ class AttachedCircleAvatar extends StatelessWidget {
} }
} }
class AccountAvatar extends StatelessWidget {
final dynamic content;
final String username;
final Color? bgColor;
final Color? feColor;
final double? radius;
final Widget? fallbackWidget;
const AccountAvatar({
super.key,
required this.content,
required this.username,
this.bgColor,
this.feColor,
this.radius,
this.fallbackWidget,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
child: AttachedCircleAvatar(
content: content,
bgColor: bgColor,
feColor: feColor,
radius: radius,
fallbackWidget: (fallbackWidget ??
Icon(
Icons.account_circle,
size: radius != null ? radius! * 1.2 : 24,
color: feColor,
)),
),
onTap: () {
showModalBottomSheet(
useRootNavigator: true,
isScrollControlled: true,
backgroundColor: Theme.of(context).colorScheme.surface,
context: context,
builder: (context) => AccountProfilePopup(
name: username,
),
);
},
);
}
}
class AccountProfileImage extends StatelessWidget { class AccountProfileImage extends StatelessWidget {
final dynamic content; final dynamic content;
final BoxFit fit; final BoxFit fit;

View File

@ -84,7 +84,7 @@ class AccountHeadingWidget extends StatelessWidget {
Positioned( Positioned(
bottom: -30, bottom: -30,
left: 32, left: 32,
child: AttachedCircleAvatar(content: avatar, radius: 40), child: AccountAvatar(content: avatar, radius: 40),
), ),
], ],
), ),

View File

@ -89,7 +89,8 @@ class _AccountProfilePopupState extends State<AccountProfilePopup> {
return SizedBox( return SizedBox(
height: MediaQuery.of(context).size.height * 0.75, height: MediaQuery.of(context).size.height * 0.75,
child: ListView( child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
AccountHeadingWidget( AccountHeadingWidget(
avatar: _userinfo!.avatar, avatar: _userinfo!.avatar,
@ -117,7 +118,7 @@ class _AccountProfilePopupState extends State<AccountProfilePopup> {
const VisualDensity(horizontal: -4, vertical: -2), const VisualDensity(horizontal: -4, vertical: -2),
trailing: const Icon(Icons.chevron_right), trailing: const Icon(Icons.chevron_right),
onTap: () { onTap: () {
AppRouter.instance.pushNamed( AppRouter.instance.goNamed(
'accountProfilePage', 'accountProfilePage',
pathParameters: {'name': _userinfo!.name}, pathParameters: {'name': _userinfo!.name},
); );

View File

@ -138,7 +138,7 @@ class _AccountSelectorState extends State<AccountSelector> {
return ListTile( return ListTile(
title: Text(element.nick), title: Text(element.nick),
subtitle: Text(element.name), subtitle: Text(element.name),
leading: AttachedCircleAvatar(content: element.avatar), leading: AccountAvatar(content: element.avatar),
trailing: widget.trailingBuilder != null trailing: widget.trailingBuilder != null
? widget.trailingBuilder!(element) ? widget.trailingBuilder!(element)
: _checkSelected(element) : _checkSelected(element)

View File

@ -23,7 +23,7 @@ class SilverRelativeList extends StatelessWidget {
title: Text(element.related.nick), title: Text(element.related.nick),
subtitle: Text(element.related.name), subtitle: Text(element.related.name),
leading: GestureDetector( leading: GestureDetector(
child: AttachedCircleAvatar(content: element.related.avatar), child: AccountAvatar(content: element.related.avatar),
onTap: () { onTap: () {
showModalBottomSheet( showModalBottomSheet(
useRootNavigator: true, useRootNavigator: true,

View File

@ -56,7 +56,7 @@ class _RelativeSelectorState extends State<RelativeSelector> {
return ListTile( return ListTile(
title: Text(element.nick), title: Text(element.nick),
subtitle: Text(element.name), subtitle: Text(element.name),
leading: AttachedCircleAvatar(content: element.avatar), leading: AccountAvatar(content: element.avatar),
trailing: widget.trailingBuilder != null trailing: widget.trailingBuilder != null
? widget.trailingBuilder!(element) ? widget.trailingBuilder!(element)
: null, : null,

View File

@ -1,22 +1,28 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:solian/shells/root_shell.dart';
class AppBarLeadingButton extends StatelessWidget { class AppBarLeadingButton extends StatelessWidget {
final bool forceBack; const AppBarLeadingButton({super.key});
const AppBarLeadingButton({super.key, this.forceBack = false}); static Widget? adaptive(BuildContext context) {
final hasContent =
static Widget? adaptive(BuildContext context, {bool forceBack = false}) { Navigator.canPop(context) || rootScaffoldKey.currentState!.hasDrawer;
final hasContent = Navigator.canPop(context) || forceBack; return hasContent ? const AppBarLeadingButton() : null;
return hasContent ? AppBarLeadingButton(forceBack: forceBack) : null;
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (Navigator.canPop(context) || forceBack) { if (Navigator.canPop(context)) {
return BackButton( return BackButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
); );
} }
return const SizedBox.shrink(); if (rootScaffoldKey.currentState!.hasDrawer) {
return DrawerButton(
onPressed: () => rootScaffoldKey.currentState!.openDrawer(),
);
} else {
return const SizedBox.shrink();
}
} }
} }

View File

@ -21,7 +21,6 @@ import 'package:solian/providers/content/attachment.dart';
import 'package:solian/widgets/attachments/attachment_attr_editor.dart'; import 'package:solian/widgets/attachments/attachment_attr_editor.dart';
import 'package:solian/widgets/attachments/attachment_editor_thumbnail.dart'; import 'package:solian/widgets/attachments/attachment_editor_thumbnail.dart';
import 'package:solian/widgets/attachments/attachment_fullscreen.dart'; import 'package:solian/widgets/attachments/attachment_fullscreen.dart';
import 'package:solian/widgets/loading_indicator.dart';
class AttachmentEditorPopup extends StatefulWidget { class AttachmentEditorPopup extends StatefulWidget {
final String pool; final String pool;
@ -33,14 +32,12 @@ class AttachmentEditorPopup extends StatefulWidget {
final List<String>? initialAttachments; final List<String>? initialAttachments;
final void Function(String) onAdd; final void Function(String) onAdd;
final void Function(String) onRemove; final void Function(String) onRemove;
final void Function(String)? onInsert;
const AttachmentEditorPopup({ const AttachmentEditorPopup({
super.key, super.key,
required this.pool, required this.pool,
required this.onAdd, required this.onAdd,
required this.onRemove, required this.onRemove,
this.onInsert,
this.singleMode = false, this.singleMode = false,
this.imageOnly = false, this.imageOnly = false,
this.autoUpload = false, this.autoUpload = false,
@ -231,10 +228,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
.listMetadata(widget.initialAttachments ?? List.empty()) .listMetadata(widget.initialAttachments ?? List.empty())
.then((result) { .then((result) {
setState(() { setState(() {
_attachments = List.from( _attachments = List.from(result, growable: true);
result.where((x) => x != null),
growable: true,
);
_isBusy = false; _isBusy = false;
_isFirstTimeBusy = false; _isFirstTimeBusy = false;
}); });
@ -559,22 +553,6 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
setState(() => _attachments.removeAt(idx)); setState(() => _attachments.removeAt(idx));
}, },
), ),
if (widget.onInsert != null)
PopupMenuItem(
child: ListTile(
title: Text('insert'.tr),
leading: const Icon(Icons.insert_link),
contentPadding: const EdgeInsets.symmetric(
horizontal: 8,
),
),
onTap: () {
widget.onInsert!(
'![](solink://attachments/${element.rid})',
);
Navigator.pop(context);
},
),
], ],
), ),
], ],
@ -682,7 +660,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
), ),
], ],
).paddingOnly(left: 24, right: 24, top: 32, bottom: 16), ).paddingOnly(left: 24, right: 24, top: 32, bottom: 16),
LoadingIndicator(isActive: _isBusy), if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
Expanded( Expanded(
child: CustomScrollView( child: CustomScrollView(
slivers: [ slivers: [

View File

@ -8,7 +8,6 @@ import 'package:flutter_animate/flutter_animate.dart';
import 'package:gal/gal.dart'; import 'package:gal/gal.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:solian/exts.dart'; import 'package:solian/exts.dart';
import 'package:solian/models/attachment.dart'; import 'package:solian/models/attachment.dart';
import 'package:solian/platform.dart'; import 'package:solian/platform.dart';
@ -104,10 +103,9 @@ class _AttachmentFullScreenState extends State<AttachmentFullScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final metaTextStyle = GoogleFonts.roboto( final metaTextStyle = TextStyle(
fontSize: 12, fontSize: 12,
color: _unFocusColor, color: _unFocusColor,
height: 1,
); );
return DismissiblePage( return DismissiblePage(
@ -177,7 +175,7 @@ class _AttachmentFullScreenState extends State<AttachmentFullScreen> {
Row( Row(
children: [ children: [
IgnorePointer( IgnorePointer(
child: AttachedCircleAvatar( child: AccountAvatar(
content: widget.item.account!.avatar, content: widget.item.account!.avatar,
radius: 19, radius: 19,
), ),
@ -241,58 +239,25 @@ class _AttachmentFullScreenState extends State<AttachmentFullScreen> {
child: Wrap( child: Wrap(
spacing: 6, spacing: 6,
children: [ children: [
if (widget.item.metadata?['exif'] == null) Text(
'#${widget.item.rid}',
style: metaTextStyle,
),
if (widget.item.metadata?['width'] != null &&
widget.item.metadata?['height'] != null)
Text( Text(
'#${widget.item.rid}', '${widget.item.metadata?['width']}x${widget.item.metadata?['height']}',
style: metaTextStyle, style: metaTextStyle,
), ),
if (widget.item.metadata?['exif']?['Model'] != null) if (widget.item.metadata?['ratio'] != null)
Text( Text(
'shotOn'.trParams({ '${_getRatio().toPrecision(2)}',
'device': widget.item.metadata?['exif']
?['Model']
}),
style: metaTextStyle,
).paddingOnly(right: 2),
if (widget.item.metadata?['exif']?['ShutterSpeed'] !=
null)
Text(
widget.item.metadata?['exif']?['ShutterSpeed'],
style: metaTextStyle,
).paddingOnly(right: 2),
if (widget.item.metadata?['exif']?['ISO'] != null)
Text(
'ISO${widget.item.metadata?['exif']?['ISO']}',
style: metaTextStyle,
).paddingOnly(right: 2),
if (widget.item.metadata?['exif']?['Aperture'] !=
null)
Text(
'f/${widget.item.metadata?['exif']?['Aperture']}',
style: metaTextStyle,
).paddingOnly(right: 2),
if (widget.item.metadata?['exif']?['Megapixels'] !=
null &&
widget.item.metadata?['exif']?['Model'] != null)
Text(
'${widget.item.metadata?['exif']?['Megapixels']}MP',
style: metaTextStyle,
)
else
Text(
widget.item.size.formatBytes(),
style: metaTextStyle, style: metaTextStyle,
), ),
Text( Text(
'${widget.item.metadata?['width']}x${widget.item.metadata?['height']}', widget.item.size.formatBytes(),
style: metaTextStyle, style: metaTextStyle,
), ),
if (widget.item.metadata?['ratio'] != null)
Text(
(widget.item.metadata?['ratio'] as num)
.toStringAsFixed(2),
style: metaTextStyle,
),
Text( Text(
widget.item.mimetype, widget.item.mimetype,
style: metaTextStyle, style: metaTextStyle,

View File

@ -155,18 +155,11 @@ class _AttachmentItemImage extends StatelessWidget {
), ),
if (showBadge && badge != null) if (showBadge && badge != null)
Positioned( Positioned(
right: 8, right: 12,
bottom: 4, bottom: 8,
child: Material( child: Material(
color: Colors.transparent, color: Colors.transparent,
child: Chip( child: Chip(label: Text(badge!)),
label: Text(badge!),
labelStyle: GoogleFonts.robotoMono(),
visualDensity: const VisualDensity(
horizontal: -4,
vertical: -2,
),
),
), ),
), ),
if (showHideButton && item.isMature) if (showHideButton && item.isMature)

View File

@ -1,6 +1,7 @@
import 'dart:math' as math; import 'dart:math' as math;
import 'dart:ui'; import 'dart:ui';
import 'package:carousel_slider/carousel_slider.dart';
import 'package:dismissible_page/dismissible_page.dart'; import 'package:dismissible_page/dismissible_page.dart';
import 'package:flutter/material.dart' hide CarouselController; import 'package:flutter/material.dart' hide CarouselController;
import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_animate/flutter_animate.dart';
@ -14,29 +15,27 @@ import 'package:solian/widgets/sized_container.dart';
class AttachmentList extends StatefulWidget { class AttachmentList extends StatefulWidget {
final String parentId; final String parentId;
final List<String>? attachmentIds; final List<String> attachmentsId;
final List<Attachment>? attachments;
final bool isGrid; final bool isGrid;
final bool isColumn; final bool isColumn;
final bool isFullWidth; final bool isForceGrid;
final bool autoload; final bool autoload;
final double flatMaxHeight;
final double columnMaxWidth; final double columnMaxWidth;
final EdgeInsets? padding;
final double? width; final double? width;
final double? viewport; final double? viewport;
const AttachmentList({ const AttachmentList({
super.key, super.key,
required this.parentId, required this.parentId,
this.attachmentIds, required this.attachmentsId,
this.attachments,
this.isGrid = false, this.isGrid = false,
this.isColumn = false, this.isColumn = false,
this.isFullWidth = false, this.isForceGrid = false,
this.autoload = false, this.autoload = false,
this.flatMaxHeight = 720,
this.columnMaxWidth = 480, this.columnMaxWidth = 480,
this.padding,
this.width, this.width,
this.viewport, this.viewport,
}); });
@ -49,24 +48,23 @@ class _AttachmentListState extends State<AttachmentList> {
bool _isLoading = true; bool _isLoading = true;
bool _showMature = false; bool _showMature = false;
// ignore: unused_field
double _aspectRatio = 1; double _aspectRatio = 1;
List<Attachment?> _attachments = List.empty(); List<Attachment?> _attachmentsMeta = List.empty();
void _getMetadataList() { void _getMetadataList() {
final AttachmentProvider attach = Get.find(); final AttachmentProvider attach = Get.find();
if (widget.attachmentIds?.isEmpty ?? false) { if (widget.attachmentsId.isEmpty) {
return; return;
} else { } else {
_attachments = List.filled(widget.attachmentIds!.length, null); _attachmentsMeta = List.filled(widget.attachmentsId.length, null);
} }
attach.listMetadata(widget.attachmentIds!).then((result) { attach.listMetadata(widget.attachmentsId).then((result) {
if (mounted) { if (mounted) {
setState(() { setState(() {
_attachments = result; _attachmentsMeta = result;
_isLoading = false; _isLoading = false;
}); });
} }
@ -78,7 +76,7 @@ class _AttachmentListState extends State<AttachmentList> {
bool isConsistent = true; bool isConsistent = true;
double? consistentValue; double? consistentValue;
int portrait = 0, square = 0, landscape = 0; int portrait = 0, square = 0, landscape = 0;
for (var entry in _attachments) { for (var entry in _attachmentsMeta) {
if (entry == null) continue; if (entry == null) continue;
if (entry.metadata?['ratio'] != null) { if (entry.metadata?['ratio'] != null) {
if (entry.metadata?['ratio'] is int) { if (entry.metadata?['ratio'] is int) {
@ -119,9 +117,10 @@ class _AttachmentListState extends State<AttachmentList> {
item: element, item: element,
parentId: widget.parentId, parentId: widget.parentId,
width: width ?? widget.width, width: width ?? widget.width,
badgeContent: '${idx + 1}/${_attachments.length}', badgeContent: '${idx + 1}/${_attachmentsMeta.length}',
showBadge: _attachments.length > 1 && !widget.isGrid && !widget.isColumn, showBadge:
showBorder: _attachments.length > 1, _attachmentsMeta.length > 1 && !widget.isGrid && !widget.isColumn,
showBorder: widget.attachmentsId.length > 1,
showMature: _showMature, showMature: _showMature,
autoload: widget.autoload, autoload: widget.autoload,
onReveal: (value) { onReveal: (value) {
@ -133,26 +132,7 @@ class _AttachmentListState extends State<AttachmentList> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
assert(widget.attachmentIds != null || widget.attachments != null); _getMetadataList();
if (widget.attachments == null) {
final AttachmentProvider attach = Get.find();
final cachedResult = attach.listMetadataFromCache(widget.attachmentIds!);
if (cachedResult.every((x) => x != null)) {
setState(() {
_attachments = cachedResult;
_isLoading = false;
});
_calculateAspectRatio();
} else {
_getMetadataList();
}
} else {
setState(() {
_attachments = widget.attachments!;
_isLoading = false;
});
_calculateAspectRatio();
}
} }
Color get _unFocusColor => Color get _unFocusColor =>
@ -160,7 +140,7 @@ class _AttachmentListState extends State<AttachmentList> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (widget.attachmentIds?.isEmpty ?? widget.attachments!.isEmpty) { if (widget.attachmentsId.isEmpty) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
@ -173,7 +153,9 @@ class _AttachmentListState extends State<AttachmentList> {
color: _unFocusColor, color: _unFocusColor,
).paddingOnly(right: 5), ).paddingOnly(right: 5),
Text( Text(
'attachmentHint'.trParams({'count': _attachments.toString()}), 'attachmentHint'.trParams(
{'count': widget.attachmentsId.length.toString()},
),
style: TextStyle(color: _unFocusColor, fontSize: 12), style: TextStyle(color: _unFocusColor, fontSize: 12),
) )
], ],
@ -183,89 +165,17 @@ class _AttachmentListState extends State<AttachmentList> {
.fadeIn(duration: 1250.ms); .fadeIn(duration: 1250.ms);
} }
const radius = BorderRadius.all(Radius.circular(8));
if (widget.isFullWidth && _attachments.length == 1) {
final element = _attachments.first;
final isImage = element!.mimetype.split('/').firstOrNull == 'image';
double ratio =
element.metadata?['ratio']?.toDouble() ?? (isImage ? 1 : 16 / 9);
return Container(
width: MediaQuery.of(context).size.width,
constraints: BoxConstraints(
maxHeight: 640,
),
child: AspectRatio(
aspectRatio: ratio,
child: Container(
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.surfaceContainer
.withOpacity(0.5),
border: Border.symmetric(
horizontal: BorderSide(
color: Theme.of(context).dividerColor,
width: 1,
),
),
),
child: _buildEntry(element, 0),
),
),
);
}
final isNotPureImage = _attachments.any(
(x) => x?.mimetype.split('/').firstOrNull != 'image',
);
if (widget.isGrid && !isNotPureImage) {
return GridView.builder(
padding: EdgeInsets.zero,
primary: false,
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: math.min(3, _attachments.length),
mainAxisSpacing: 8.0,
crossAxisSpacing: 8.0,
),
itemCount: _attachments.length,
itemBuilder: (context, idx) {
final element = _attachments[idx];
return Container(
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.surfaceContainer
.withOpacity(0.5),
border: Border.all(
color: Theme.of(context).dividerColor,
width: 1,
),
borderRadius: radius,
),
child: ClipRRect(
borderRadius: radius,
child: _buildEntry(element, idx),
),
);
},
);
}
if (widget.isColumn) { if (widget.isColumn) {
var idx = 0; var idx = 0;
const radius = BorderRadius.all(Radius.circular(8));
return Wrap( return Wrap(
spacing: 8, spacing: 8,
runSpacing: 8, runSpacing: 8,
children: _attachments.map((x) { children: widget.attachmentsId.map((x) {
final element = _attachments[idx]; final element = _attachmentsMeta[idx];
idx++; idx++;
if (element == null) return const SizedBox.shrink(); if (element == null) return const SizedBox.shrink();
final isImage = element.mimetype.split('/').firstOrNull == 'image'; double ratio = element.metadata?['ratio']?.toDouble() ?? 16 / 9;
double ratio =
element.metadata?['ratio']?.toDouble() ?? (isImage ? 1 : 16 / 9);
return Container( return Container(
constraints: BoxConstraints( constraints: BoxConstraints(
maxWidth: widget.columnMaxWidth, maxWidth: widget.columnMaxWidth,
@ -275,10 +185,6 @@ class _AttachmentListState extends State<AttachmentList> {
aspectRatio: ratio, aspectRatio: ratio,
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.surfaceContainer
.withOpacity(0.5),
border: Border.all( border: Border.all(
color: Theme.of(context).dividerColor, color: Theme.of(context).dividerColor,
width: 1, width: 1,
@ -296,52 +202,69 @@ class _AttachmentListState extends State<AttachmentList> {
); );
} }
return Container( final isNotPureImage = _attachmentsMeta.any(
constraints: BoxConstraints( (x) => x?.mimetype.split('/').firstOrNull != 'image',
maxHeight: 320, );
), if (widget.isGrid && (widget.isForceGrid || !isNotPureImage)) {
child: ListView.separated( const radius = BorderRadius.all(Radius.circular(8));
padding: widget.padding, return GridView.builder(
scrollDirection: Axis.horizontal, padding: EdgeInsets.zero,
primary: false,
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true, shrinkWrap: true,
itemCount: _attachments.length, gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: math.min(3, widget.attachmentsId.length),
mainAxisSpacing: 8.0,
crossAxisSpacing: 8.0,
),
itemCount: widget.attachmentsId.length,
itemBuilder: (context, idx) { itemBuilder: (context, idx) {
final element = _attachments[idx]; final element = _attachmentsMeta[idx];
if (element == null) const SizedBox.shrink();
final isImage = element!.mimetype.split('/').firstOrNull == 'image';
double ratio =
element.metadata?['ratio']?.toDouble() ?? (isImage ? 1 : 16 / 9);
return Container( return Container(
constraints: BoxConstraints( decoration: BoxDecoration(
maxWidth: math.min( color: Theme.of(context).colorScheme.surfaceContainerHigh,
widget.columnMaxWidth, border: Border.all(
MediaQuery.of(context).size.width - color: Theme.of(context).dividerColor,
(widget.padding?.horizontal ?? 0), width: 1,
), ),
borderRadius: radius,
), ),
child: AspectRatio( child: ClipRRect(
aspectRatio: ratio, borderRadius: radius,
child: Container( child: _buildEntry(element, idx),
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.surfaceContainer
.withOpacity(0.5),
border: Border.all(
color: Theme.of(context).dividerColor,
width: 1,
),
borderRadius: radius,
),
child: ClipRRect(
borderRadius: radius,
child: _buildEntry(element, idx),
),
),
), ),
); );
}, },
separatorBuilder: (context, _) => const Gap(8), ).paddingSymmetric(horizontal: 24);
}
return Container(
width: MediaQuery.of(context).size.width,
constraints: BoxConstraints(
maxHeight: widget.flatMaxHeight,
),
decoration: BoxDecoration(
color: Colors.transparent,
border: Border.symmetric(
horizontal: BorderSide(
width: 0.3,
color: Theme.of(context).dividerColor,
),
),
),
child: CarouselSlider.builder(
options: CarouselOptions(
animateToClosest: true,
aspectRatio: _aspectRatio,
viewportFraction:
widget.viewport ?? (widget.attachmentsId.length > 1 ? 0.95 : 1),
enableInfiniteScroll: false,
),
itemCount: _attachmentsMeta.length,
itemBuilder: (context, idx, _) {
final element = _attachmentsMeta[idx];
return _buildEntry(element, idx);
},
), ),
); );
} }
@ -420,13 +343,11 @@ class AttachmentListEntry extends StatelessWidget {
}, },
), ),
if (item!.isMature && !showMature) if (item!.isMature && !showMature)
ClipRect( BackdropFilter(
child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 100, sigmaY: 100),
filter: ImageFilter.blur(sigmaX: 100, sigmaY: 100), child: Container(
child: Container( decoration: BoxDecoration(
decoration: BoxDecoration( color: Colors.black.withOpacity(0.5),
color: Colors.black.withOpacity(0.5),
),
), ),
), ),
), ),

View File

@ -38,13 +38,11 @@ class _ChannelListWidgetState extends State<ChannelListWidget> {
Future<void> _loadLastMessages() async { Future<void> _loadLastMessages() async {
final messages = await _eventController.src.getLastInAllChannels(); final messages = await _eventController.src.getLastInAllChannels();
if (mounted) { setState(() {
setState(() { _lastMessages = messages
_lastMessages = messages .map((k, v) => MapEntry(k, v.firstOrNull))
.map((k, v) => MapEntry(k, v.firstOrNull)) .cast<int, LocalMessageEventTableData>();
.cast<int, LocalMessageEventTableData>(); });
});
}
} }
@override @override
@ -207,7 +205,7 @@ class _ChannelListWidgetState extends State<ChannelListWidget> {
item.members!.where((e) => e.account.id != widget.selfId).firstOrNull; item.members!.where((e) => e.account.id != widget.selfId).firstOrNull;
if (item.type == 1 && otherside != null) { if (item.type == 1 && otherside != null) {
final avatar = AttachedCircleAvatar( final avatar = AccountAvatar(
content: otherside.account.avatar, content: otherside.account.avatar,
radius: 20, radius: 20,
bgColor: Theme.of(context).colorScheme.primary, bgColor: Theme.of(context).colorScheme.primary,
@ -243,7 +241,7 @@ class _ChannelListWidgetState extends State<ChannelListWidget> {
padding: const EdgeInsets.all(2), padding: const EdgeInsets.all(2),
elevation: 8, elevation: 8,
), ),
badgeContent: AttachedCircleAvatar( badgeContent: AccountAvatar(
content: item.realm?.avatar, content: item.realm?.avatar,
radius: 10, radius: 10,
fallbackWidget: const Icon( fallbackWidget: const Icon(

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:solian/exts.dart'; import 'package:solian/exts.dart';
import 'package:solian/models/channel.dart'; import 'package:solian/models/channel.dart';
@ -7,7 +8,6 @@ import 'package:solian/services.dart';
import 'package:solian/widgets/account/account_avatar.dart'; import 'package:solian/widgets/account/account_avatar.dart';
import 'package:solian/widgets/account/account_profile_popup.dart'; import 'package:solian/widgets/account/account_profile_popup.dart';
import 'package:solian/widgets/account/relative_select.dart'; import 'package:solian/widgets/account/relative_select.dart';
import 'package:solian/widgets/loading_indicator.dart';
class ChannelMemberListPopup extends StatefulWidget { class ChannelMemberListPopup extends StatefulWidget {
final Channel channel; final Channel channel;
@ -131,7 +131,7 @@ class _ChannelMemberListPopupState extends State<ChannelMemberListPopup> {
'channelMembers'.tr, 'channelMembers'.tr,
style: Theme.of(context).textTheme.headlineSmall, style: Theme.of(context).textTheme.headlineSmall,
).paddingOnly(left: 24, right: 24, top: 32, bottom: 16), ).paddingOnly(left: 24, right: 24, top: 32, bottom: 16),
LoadingIndicator(isActive: _isBusy), if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
ListTile( ListTile(
tileColor: Theme.of(context).colorScheme.surfaceContainerHigh, tileColor: Theme.of(context).colorScheme.surfaceContainerHigh,
contentPadding: const EdgeInsets.symmetric(horizontal: 20), contentPadding: const EdgeInsets.symmetric(horizontal: 20),
@ -152,8 +152,7 @@ class _ChannelMemberListPopupState extends State<ChannelMemberListPopup> {
title: Text(element.account.nick), title: Text(element.account.nick),
subtitle: Text(element.account.name), subtitle: Text(element.account.name),
leading: GestureDetector( leading: GestureDetector(
child: child: AccountAvatar(content: element.account.avatar),
AttachedCircleAvatar(content: element.account.avatar),
onTap: () { onTap: () {
showModalBottomSheet( showModalBottomSheet(
useRootNavigator: true, useRootNavigator: true,

View File

@ -74,7 +74,7 @@ class _NoContentWidgetState extends State<NoContentWidget>
), ),
) )
], ],
child: AttachedCircleAvatar( child: AccountAvatar(
content: widget.userinfo!.avatar, content: widget.userinfo!.avatar,
bgColor: Colors.transparent, bgColor: Colors.transparent,
radius: radius, radius: radius,

View File

@ -78,7 +78,7 @@ class ChatEvent extends StatelessWidget {
child: AttachmentList( child: AttachmentList(
key: Key('m${item.uuid}attachments'), key: Key('m${item.uuid}attachments'),
parentId: item.uuid, parentId: item.uuid,
attachmentIds: attachments, attachmentsId: attachments,
isColumn: true, isColumn: true,
), ),
); );
@ -220,7 +220,7 @@ class ChatEvent extends StatelessWidget {
children: [ children: [
Row( Row(
children: [ children: [
AttachedCircleAvatar( AccountAvatar(
content: item.sender.account.avatar, content: item.sender.account.avatar,
radius: 9, radius: 9,
), ),
@ -250,8 +250,7 @@ class ChatEvent extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
GestureDetector( GestureDetector(
child: child: AccountAvatar(content: item.sender.account.avatar),
AttachedCircleAvatar(content: item.sender.account.avatar),
onTap: () { onTap: () {
showModalBottomSheet( showModalBottomSheet(
useRootNavigator: true, useRootNavigator: true,

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:solian/models/channel.dart'; import 'package:solian/models/channel.dart';
@ -6,7 +7,6 @@ import 'package:solian/models/event.dart';
import 'package:solian/models/realm.dart'; import 'package:solian/models/realm.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
import 'package:solian/widgets/chat/chat_event_deletion.dart'; import 'package:solian/widgets/chat/chat_event_deletion.dart';
import 'package:solian/widgets/loading_indicator.dart';
class ChatEventAction extends StatefulWidget { class ChatEventAction extends StatefulWidget {
final Channel channel; final Channel channel;
@ -73,7 +73,7 @@ class _ChatEventActionState extends State<ChatEventAction> {
), ),
], ],
).paddingOnly(left: 24, right: 24, top: 32, bottom: 16), ).paddingOnly(left: 24, right: 24, top: 32, bottom: 16),
LoadingIndicator(isActive: _isBusy), if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
Expanded( Expanded(
child: ListView( child: ListView(
children: [ children: [

View File

@ -1,4 +1,3 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_animate/flutter_animate.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
@ -35,28 +34,9 @@ class ChatEventList extends StatelessWidget {
return a.createdAt.difference(b.createdAt).inMinutes <= 3; return a.createdAt.difference(b.createdAt).inMinutes <= 3;
} }
void _openActions(BuildContext context, Event item) {
showModalBottomSheet(
useRootNavigator: true,
context: context,
builder: (context) => ChatEventAction(
channel: channel,
realm: channel.realm,
item: item,
onEdit: () {
onEdit(item);
},
onReply: () {
onReply(item);
},
),
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return CustomScrollView( return CustomScrollView(
cacheExtent: 100,
reverse: true, reverse: true,
slivers: [ slivers: [
Obx(() { Obx(() {
@ -84,45 +64,50 @@ class ChatEventList extends StatelessWidget {
final item = chatController.currentEvents[index].data; final item = chatController.currentEvents[index].data;
return TapRegion( return GestureDetector(
child: GestureDetector( behavior: HitTestBehavior.opaque,
behavior: HitTestBehavior.opaque, child: Builder(builder: (context) {
child: Builder(builder: (context) { final widget = ChatEvent(
final widget = ChatEvent( key: Key('m${item!.uuid}'),
key: Key('m${item!.uuid}'), item: item,
item: item, isMerged: isMerged,
isMerged: isMerged, chatController: chatController,
chatController: chatController, ).paddingOnly(
).paddingOnly( top: !isMerged ? 8 : 0,
top: !isMerged ? 8 : 0, bottom: !hasMerged ? 8 : 0,
bottom: !hasMerged ? 8 : 0, );
);
if (noAnimated) { if (noAnimated) {
return widget; return widget;
} else { } else {
return widget return widget
.animate( .animate(
key: Key('animated-m${item.uuid}'), key: Key('animated-m${item.uuid}'),
) )
.slideY( .slideY(
curve: Curves.fastLinearToSlowEaseIn, curve: Curves.fastLinearToSlowEaseIn,
duration: 250.ms, duration: 250.ms,
begin: 0.5, begin: 0.5,
end: 0, end: 0,
); );
}
}),
onLongPress: () {
_openActions(context, item!);
},
),
onTapInside: (event) {
if (event.buttons == kSecondaryMouseButton) {
_openActions(context, item!);
} else if (event.buttons == kMiddleMouseButton) {
onReply(item!);
} }
}),
onLongPress: () {
showModalBottomSheet(
useRootNavigator: true,
context: context,
builder: (context) => ChatEventAction(
channel: channel,
realm: channel.realm,
item: item!,
onEdit: () {
onEdit(item);
},
onReply: () {
onReply(item);
},
),
);
}, },
); );
}, },

View File

@ -443,7 +443,7 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
.map( .map(
(x) => ChatMessageSuggestion( (x) => ChatMessageSuggestion(
type: 'users', type: 'users',
leading: AttachedCircleAvatar(content: x.avatar), leading: AccountAvatar(content: x.avatar),
display: x.nick, display: x.nick,
content: '@${x.name}', content: '@${x.name}',
), ),

View File

@ -2,21 +2,15 @@ import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:flutter_svg/svg.dart'; import 'package:flutter_svg/svg.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:solian/models/link.dart';
import 'package:solian/providers/link_expander.dart'; import 'package:solian/providers/link_expander.dart';
import 'package:solian/widgets/auto_cache_image.dart'; import 'package:solian/widgets/auto_cache_image.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
class LinkExpansion extends StatefulWidget { class LinkExpansion extends StatelessWidget {
final String content; final String content;
const LinkExpansion({super.key, required this.content}); const LinkExpansion({super.key, required this.content});
@override
State<LinkExpansion> createState() => _LinkExpansionState();
}
class _LinkExpansionState extends State<LinkExpansion> {
Widget _buildImage(String url, {double? width, double? height}) { Widget _buildImage(String url, {double? width, double? height}) {
if (url.endsWith('svg')) { if (url.endsWith('svg')) {
return SvgPicture.network(url, width: width, height: height); return SvgPicture.network(url, width: width, height: height);
@ -28,74 +22,61 @@ class _LinkExpansionState extends State<LinkExpansion> {
); );
} }
List<LinkMeta>? _meta; @override
Widget build(BuildContext context) {
Future<void> _doExpand() async {
final linkRegex = RegExp( final linkRegex = RegExp(
r'(?<!\()(?:(?:https?):\/\/|www\.)(?:[-_a-z0-9]+\.)*(?:[-a-z0-9]+\.[-a-z0-9]+)[^\s<]*[^\s<?!.,:*_~]', r'(?<!\()(?:(?:https?):\/\/|www\.)(?:[-_a-z0-9]+\.)*(?:[-a-z0-9]+\.[-a-z0-9]+)[^\s<]*[^\s<?!.,:*_~]',
); );
final matches = linkRegex.allMatches(widget.content); final matches = linkRegex.allMatches(content);
if (matches.isEmpty) return; if (matches.isEmpty) {
return const SizedBox.shrink();
}
final LinkExpandProvider expandController = Get.find(); final LinkExpandProvider expandController = Get.find();
if (matches.isEmpty) return;
List<LinkMeta> out = List.empty(growable: true);
for (final x in matches) {
final result = await expandController.expandLink(x.group(0)!);
if (result != null) out.add(result);
}
setState(() => _meta = out);
}
@override
void initState() {
super.initState();
_doExpand();
}
@override
Widget build(BuildContext context) {
if (_meta?.isEmpty ?? true) return const SizedBox.shrink();
return Wrap( return Wrap(
children: _meta!.map((x) { children: matches.map((x) {
return Container( return Container(
constraints: BoxConstraints( constraints: BoxConstraints(
maxWidth: _meta!.length == 1 ? 480 : 340, maxWidth: matches.length == 1 ? 480 : 340,
), ),
child: Builder( child: FutureBuilder(
builder: (context) { future: expandController.expandLink(x.group(0)!),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const SizedBox.shrink();
}
final isRichDescription = [ final isRichDescription = [
'solsynth.dev', 'solsynth.dev',
].contains(Uri.parse(x.url).host); ].contains(Uri.parse(snapshot.data!.url).host);
return GestureDetector( return GestureDetector(
child: Card( child: Card(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if ([(x.icon?.isNotEmpty ?? false), x.siteName != null] if ([
.any((x) => x)) (snapshot.data!.icon?.isNotEmpty ?? false),
snapshot.data!.siteName != null
].any((x) => x))
Row( Row(
children: [ children: [
if (x.icon?.isNotEmpty ?? false) if (snapshot.data!.icon?.isNotEmpty ?? false)
ClipRRect( ClipRRect(
borderRadius: const BorderRadius.all( borderRadius: const BorderRadius.all(
Radius.circular(8), Radius.circular(8),
), ),
child: _buildImage( child: _buildImage(
x.icon!, snapshot.data!.icon!,
width: 32, width: 32,
height: 32, height: 32,
), ),
).paddingOnly(right: 8), ).paddingOnly(right: 8),
if (x.siteName != null) if (snapshot.data!.siteName != null)
Expanded( Expanded(
child: Text( child: Text(
x.siteName!, snapshot.data!.siteName!,
style: Theme.of(context).textTheme.labelLarge, style: Theme.of(context).textTheme.labelLarge,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
@ -103,27 +84,32 @@ class _LinkExpansionState extends State<LinkExpansion> {
), ),
], ],
).paddingOnly( ).paddingOnly(
bottom: (x.icon?.isNotEmpty ?? false) ? 8 : 4, bottom: (snapshot.data!.icon?.isNotEmpty ?? false)
? 8
: 4,
), ),
if (x.image != null && if (snapshot.data!.image != null &&
(x.image?.startsWith('http') ?? false)) (snapshot.data!.image?.startsWith('http') ?? false))
ClipRRect( ClipRRect(
borderRadius: const BorderRadius.all( borderRadius: const BorderRadius.all(
Radius.circular(8), Radius.circular(8),
), ),
child: _buildImage(x.image!), child: _buildImage(
snapshot.data!.image!,
),
).paddingOnly(bottom: 8), ).paddingOnly(bottom: 8),
Text( Text(
x.title ?? 'No Title', snapshot.data!.title ?? 'No Title',
maxLines: 1, maxLines: 1,
overflow: TextOverflow.fade, overflow: TextOverflow.fade,
style: Theme.of(context).textTheme.bodyLarge, style: Theme.of(context).textTheme.bodyLarge,
), ),
if (x.description != null && isRichDescription) if (snapshot.data!.description != null &&
MarkdownBody(data: x.description!) isRichDescription)
else if (x.description != null) MarkdownBody(data: snapshot.data!.description!)
else if (snapshot.data!.description != null)
Text( Text(
x.description!, snapshot.data!.description!,
maxLines: 3, maxLines: 3,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
@ -131,7 +117,7 @@ class _LinkExpansionState extends State<LinkExpansion> {
).paddingAll(12), ).paddingAll(12),
), ),
onTap: () { onTap: () {
launchUrlString(x.url); launchUrlString(x.group(0)!);
}, },
); );
}, },

View File

@ -1,89 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:gap/gap.dart';
class LoadingIndicator extends StatefulWidget {
final bool isActive;
final Color? backgroundColor;
const LoadingIndicator({
super.key,
this.isActive = true,
this.backgroundColor,
});
@override
State<LoadingIndicator> createState() => _LoadingIndicatorState();
}
class _LoadingIndicatorState extends State<LoadingIndicator>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 300),
);
_animation = CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
);
if (widget.isActive) {
_controller.forward();
} else {
_controller.reverse();
}
}
@override
void didUpdateWidget(covariant LoadingIndicator oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.isActive != oldWidget.isActive) {
if (widget.isActive) {
_controller.forward();
} else {
_controller.reverse();
}
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return SizeTransition(
sizeFactor: _animation,
axisAlignment: -1, // Align animation from the top
child: Container(
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 24),
color: widget.backgroundColor ??
Theme.of(context).colorScheme.surfaceContainerLow.withOpacity(0.5),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const SizedBox(
height: 16,
width: 16,
child: CircularProgressIndicator(strokeWidth: 2.5),
),
const Gap(8),
Text('loading'.tr),
],
),
),
);
}
}

View File

@ -1,25 +1,19 @@
import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_markdown_selectionarea/flutter_markdown.dart';
import 'package:gap/gap.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:markdown/markdown.dart' as markdown; import 'package:markdown/markdown.dart' as markdown;
import 'package:path/path.dart'; import 'package:markdown/markdown.dart';
import 'package:solian/models/attachment.dart';
import 'package:solian/providers/stickers.dart'; import 'package:solian/providers/stickers.dart';
import 'package:solian/widgets/attachments/attachment_item.dart';
import 'package:solian/widgets/attachments/attachment_list.dart'; import 'package:solian/widgets/attachments/attachment_list.dart';
import 'package:solian/widgets/auto_cache_image.dart'; import 'package:solian/widgets/auto_cache_image.dart';
import 'package:syntax_highlight/syntax_highlight.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
import 'account/account_profile_popup.dart'; import 'account/account_profile_popup.dart';
class MarkdownTextContent extends StatefulWidget { class MarkdownTextContent extends StatelessWidget {
final String content; final String content;
final String parentId; final String parentId;
final List<Attachment>? attachments;
final bool isSelectable; final bool isSelectable;
final bool isLargeText; final bool isLargeText;
final bool isAutoWarp; final bool isAutoWarp;
@ -28,228 +22,190 @@ class MarkdownTextContent extends StatefulWidget {
super.key, super.key,
required this.content, required this.content,
required this.parentId, required this.parentId,
this.attachments,
this.isSelectable = false, this.isSelectable = false,
this.isLargeText = false, this.isLargeText = false,
this.isAutoWarp = false, this.isAutoWarp = false,
}); });
@override Widget _buildContent(BuildContext context) {
State<MarkdownTextContent> createState() => _MarkdownTextContentState();
}
class _MarkdownTextContentState extends State<MarkdownTextContent> {
final List<int> _stickerSizes = [];
@override
initState() {
super.initState();
final stickerRegex = RegExp(r':([-\w]+):'); final stickerRegex = RegExp(r':([-\w]+):');
// Split the content into paragraphs // Split the content into paragraphs
final paragraphs = widget.content.split(RegExp(r'\n\s*\n')); final paragraphs = content.split(RegExp(r'\n\s*\n'));
// Iterate over each paragraph to process stickers individually // Iterate over each paragraph to process stickers individually
List<Widget> contentWidgets = [];
for (var idx = 0; idx < paragraphs.length; idx++) { for (var idx = 0; idx < paragraphs.length; idx++) {
// Getting paragraph // Getting paragraph
var paragraph = paragraphs[idx]; var paragraph = paragraphs[idx];
// Auto adding new-lines
if (isAutoWarp) {
paragraph = paragraph.replaceAll('\n', '\\\n');
}
// Matching stickers // Matching stickers
final stickerMatch = stickerRegex.allMatches(paragraph); final stickerMatch = stickerRegex.allMatches(paragraph);
if (stickerMatch.length > 3) { final isOnlySticker =
_stickerSizes.addAll(List.filled(stickerMatch.length, 16)); paragraph.replaceAll(stickerRegex, '').trim().isEmpty;
} else if (stickerMatch.length > 1) {
_stickerSizes.addAll(List.filled(stickerMatch.length, 32));
} else {
_stickerSizes.addAll(List.filled(stickerMatch.length, 128));
}
}
}
Widget _buildContent(BuildContext context) { contentWidgets.add(
var stickerIdx = 0; Markdown(
shrinkWrap: true,
return Markdown( physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true, data: paragraph,
physics: const NeverScrollableScrollPhysics(), padding: EdgeInsets.zero,
data: widget.content, styleSheet: MarkdownStyleSheet.fromTheme(
padding: EdgeInsets.zero, Theme.of(context),
styleSheet: MarkdownStyleSheet.fromTheme( ).copyWith(
Theme.of(context), textScaleFactor: isLargeText ? 1.1 : 1,
).copyWith( blockquote: TextStyle(
textScaler: TextScaler.linear(widget.isLargeText ? 1.1 : 1), color: Theme.of(context).colorScheme.onSurfaceVariant,
blockquote: TextStyle( ),
color: Theme.of(context).colorScheme.onSurfaceVariant, blockquoteDecoration: BoxDecoration(
), color: Theme.of(context).colorScheme.surfaceContainerHigh,
blockquoteDecoration: BoxDecoration( borderRadius: const BorderRadius.all(Radius.circular(4)),
color: Theme.of(context).colorScheme.surfaceContainerHigh, ),
borderRadius: const BorderRadius.all(Radius.circular(4)), horizontalRuleDecoration: BoxDecoration(
), border: Border(
horizontalRuleDecoration: BoxDecoration( top: BorderSide(
border: Border( width: 1.0,
top: BorderSide( color: Theme.of(context).dividerColor,
width: 1.0, ),
color: Theme.of(context).dividerColor,
), ),
), ),
), ),
codeblockDecoration: BoxDecoration( extensionSet: markdown.ExtensionSet(
border: Border.all( markdown.ExtensionSet.gitHubFlavored.blockSyntaxes,
color: Theme.of(context).dividerColor, <markdown.InlineSyntax>[
width: 0.3, _UserNameCardInlineSyntax(),
), _CustomEmoteInlineSyntax(),
borderRadius: const BorderRadius.all(Radius.circular(4)), markdown.EmojiSyntax(),
color: Theme.of(context).colorScheme.surface.withOpacity(0.5), markdown.AutolinkSyntax(),
)), markdown.AutolinkExtensionSyntax(),
builders: { ...markdown.ExtensionSet.gitHubFlavored.inlineSyntaxes
'code': _MarkdownTextCodeElement(), ],
}, ),
softLineBreak: true, onTapLink: (text, href, title) async {
extensionSet: markdown.ExtensionSet( if (href == null) return;
<markdown.BlockSyntax>[ if (href.startsWith('solink://')) {
markdown.CodeBlockSyntax(), final segments = href.replaceFirst('solink://', '').split('/');
...markdown.ExtensionSet.commonMark.blockSyntaxes, switch (segments[0]) {
...markdown.ExtensionSet.gitHubFlavored.blockSyntaxes, case 'users':
], showModalBottomSheet(
<markdown.InlineSyntax>[ useRootNavigator: true,
if (widget.isAutoWarp) markdown.LineBreakSyntax(), isScrollControlled: true,
_UserNameCardInlineSyntax(), backgroundColor: Theme.of(context).colorScheme.surface,
_CustomEmoteInlineSyntax(), context: context,
markdown.AutolinkSyntax(), builder: (context) => AccountProfilePopup(
markdown.AutolinkExtensionSyntax(), name: segments[1],
markdown.CodeSyntax(), ),
...markdown.ExtensionSet.commonMark.inlineSyntaxes, );
...markdown.ExtensionSet.gitHubFlavored.inlineSyntaxes
],
),
onTapLink: (text, href, title) async {
if (href == null) return;
if (href.startsWith('solink://')) {
final segments = href.replaceFirst('solink://', '').split('/');
switch (segments[0]) {
case 'users':
showModalBottomSheet(
useRootNavigator: true,
isScrollControlled: true,
backgroundColor: Theme.of(context).colorScheme.surface,
context: context,
builder: (context) => AccountProfilePopup(
name: segments[1],
),
);
}
return;
}
await launchUrlString(
href,
mode: LaunchMode.externalApplication,
);
},
imageBuilder: (uri, title, alt) {
var url = uri.toString();
double? width, height;
BoxFit? fit;
if (url.startsWith('solink://')) {
final segments = url.replaceFirst('solink://', '').split('/');
switch (segments[0]) {
case 'stickers':
double radius = 4;
final StickerProvider sticker = Get.find();
// Adjust sticker size based on the sticker count in this paragraph
width =
_stickerSizes.elementAtOrNull(stickerIdx)?.toDouble() ?? 16;
height =
_stickerSizes.elementAtOrNull(stickerIdx)?.toDouble() ?? 16;
if (width > 16) {
radius = 8;
} }
stickerIdx++; return;
fit = BoxFit.contain; }
return ClipRRect(
borderRadius: BorderRadius.all(Radius.circular(radius)), await launchUrlString(
child: Container( href,
width: width, mode: LaunchMode.externalApplication,
height: height, );
color: Theme.of(context).colorScheme.surfaceContainer, },
child: FutureBuilder( imageBuilder: (uri, title, alt) {
future: sticker.getStickerByAlias(segments[1]), var url = uri.toString();
builder: (context, snapshot) { double? width, height;
if (!snapshot.hasData) { BoxFit? fit;
return const Center(child: CircularProgressIndicator()); if (url.startsWith('solink://')) {
} final segments = url.replaceFirst('solink://', '').split('/');
return AutoCacheImage( switch (segments[0]) {
snapshot.data!.imageUrl, case 'stickers':
width: width, double radius = 8;
height: height, final StickerProvider sticker = Get.find();
fit: fit,
noErrorWidget: true, // Adjust sticker size based on the sticker count in this paragraph
); if (stickerMatch.length <= 1 && isOnlySticker) {
}, width = 128;
), height = 128;
), } else if (stickerMatch.length <= 3 && isOnlySticker) {
).paddingSymmetric(vertical: 4); width = 32;
case 'attachments': height = 32;
final match = widget.attachments } else {
?.where((x) => x.rid == segments[1]) radius = 4;
.firstOrNull; width = 16;
const radius = BorderRadius.all(Radius.circular(8)); height = 16;
if (match != null) { }
final isImage = fit = BoxFit.contain;
match.mimetype.split('/').firstOrNull == 'image'; return ClipRRect(
double ratio = match.metadata?['ratio']?.toDouble() ?? borderRadius: BorderRadius.all(Radius.circular(radius)),
(isImage ? 1 : 16 / 9); child: Container(
return LimitedBox( width: width,
maxWidth: 480, height: height,
maxHeight: 640, color: Theme.of(context).colorScheme.surfaceContainer,
child: AspectRatio( child: FutureBuilder(
aspectRatio: ratio, future: sticker.getStickerByAlias(segments[1]),
child: ClipRRect( builder: (context, snapshot) {
borderRadius: radius, if (!snapshot.hasData) {
child: AttachmentItem( return const Center(
parentId: widget.parentId, child: CircularProgressIndicator());
item: match, }
return AutoCacheImage(
snapshot.data!.imageUrl,
width: width,
height: height,
fit: fit,
noErrorWidget: true,
);
},
), ),
), ),
), ).paddingSymmetric(vertical: 4);
).paddingSymmetric(vertical: 4); case 'attachments':
} else { const radius = BorderRadius.all(Radius.circular(8));
return LimitedBox( return LimitedBox(
maxHeight: MediaQuery.of(context).size.width, maxHeight: MediaQuery.of(context).size.width,
child: ClipRRect( child: ClipRRect(
borderRadius: radius, borderRadius: radius,
child: AttachmentSelfContainedEntry( child: AttachmentSelfContainedEntry(
isDense: true, isDense: true,
parentId: widget.parentId, parentId: parentId,
rid: segments[1], rid: segments[1],
),
), ),
), ).paddingSymmetric(vertical: 4);
).paddingSymmetric(vertical: 4);
} }
} }
} return AutoCacheImage(
return AutoCacheImage( url,
url, width: width,
width: width, height: height,
height: height, fit: fit,
fit: fit, );
); },
}, ),
);
if (idx < paragraphs.length - 1) {
contentWidgets.add(const Gap(4));
}
}
// Return the list of widgets for the paragraphs
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: contentWidgets,
); );
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (widget.isSelectable) { if (isSelectable) {
return SelectionArea(child: _buildContent(context)); return SelectionArea(child: _buildContent(context));
} }
return _buildContent(context); return _buildContent(context);
} }
} }
class _UserNameCardInlineSyntax extends markdown.InlineSyntax { class _UserNameCardInlineSyntax extends InlineSyntax {
_UserNameCardInlineSyntax() : super(r'@[a-zA-Z0-9_]+'); _UserNameCardInlineSyntax() : super(r'@[a-zA-Z0-9_]+');
@override @override
@ -265,7 +221,7 @@ class _UserNameCardInlineSyntax extends markdown.InlineSyntax {
} }
} }
class _CustomEmoteInlineSyntax extends markdown.InlineSyntax { class _CustomEmoteInlineSyntax extends InlineSyntax {
_CustomEmoteInlineSyntax() : super(r':([-\w]+):'); _CustomEmoteInlineSyntax() : super(r':([-\w]+):');
@override @override
@ -285,48 +241,3 @@ class _CustomEmoteInlineSyntax extends markdown.InlineSyntax {
return true; return true;
} }
} }
class _MarkdownTextCodeElement extends MarkdownElementBuilder {
@override
Widget? visitElementAfter(
markdown.Element element,
TextStyle? preferredStyle,
) {
var language = '';
if (element.attributes['class'] != null) {
String lg = element.attributes['class'] as String;
language = lg.substring(9).trim();
}
return SizedBox(
child: FutureBuilder(
future: (() async {
final docPath = '../../../';
final highlightingPath =
join(docPath, 'assets/highlighting', language);
await Highlighter.initialize([highlightingPath]);
return Highlighter(
language: highlightingPath,
theme: PlatformDispatcher.instance.platformBrightness ==
Brightness.light
? await HighlighterTheme.loadLightTheme()
: await HighlighterTheme.loadDarkTheme(),
);
})(),
builder: (context, snapshot) {
if (snapshot.hasData) {
final highlighter = snapshot.data!;
return Text.rich(
highlighter.highlight(element.textContent.trim()),
style: GoogleFonts.robotoMono(),
);
}
return Text(
element.textContent.trim(),
style: GoogleFonts.robotoMono(),
);
},
),
).paddingAll(8);
}
}

View File

@ -69,7 +69,7 @@ class _AppAccountWidgetState extends State<AppAccountWidget> {
bottom: 0, bottom: 0,
end: -2, end: -2,
), ),
child: AttachedCircleAvatar( child: AccountAvatar(
radius: 14, radius: 14,
content: auth.userProfile.value!['avatar'], content: auth.userProfile.value!['avatar'],
), ),

View File

@ -27,43 +27,39 @@ class _AppNavigationRailState extends State<AppNavigationRail> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Material( return NavigationRail(
color: Theme.of(context).colorScheme.surface, selectedIndex: _currentIndex,
child: NavigationRail( labelType: NavigationRailLabelType.selected,
selectedIndex: _currentIndex, groupAlignment: -1,
labelType: NavigationRailLabelType.selected, destinations: AppNavigation.destinations
groupAlignment: -1, .sublist(0, AppNavigation.destinations.length - 1)
destinations: AppNavigation.destinations .map(
.sublist(0, AppNavigation.destinations.length - 1) (x) => NavigationRailDestination(
.map( icon: x.icon,
(x) => NavigationRailDestination( label: Text(x.label),
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);
},
), ),
)
.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),
), ),
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

@ -36,7 +36,7 @@ class RealmSwitcher extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
if (item != null) if (item != null)
AttachedCircleAvatar( AccountAvatar(
content: item.avatar, content: item.avatar,
radius: 14, radius: 14,
fallbackWidget: const Icon( fallbackWidget: const Icon(

View File

@ -1,21 +1,17 @@
import 'dart:math'; import 'dart:math';
import 'package:file_saver/file_saver.dart';
import 'package:firebase_analytics/firebase_analytics.dart'; import 'package:firebase_analytics/firebase_analytics.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:screenshot/screenshot.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
import 'package:solian/exts.dart'; import 'package:solian/exts.dart';
import 'package:solian/models/post.dart'; import 'package:solian/models/post.dart';
import 'package:solian/platform.dart'; import 'package:solian/platform.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
import 'package:solian/providers/content/posts.dart';
import 'package:solian/router.dart'; import 'package:solian/router.dart';
import 'package:solian/screens/posts/post_editor.dart'; import 'package:solian/screens/posts/post_editor.dart';
import 'package:solian/widgets/loading_indicator.dart';
import 'package:solian/widgets/posts/post_share.dart';
import 'package:solian/widgets/reports/abuse_report.dart'; import 'package:solian/widgets/reports/abuse_report.dart';
class PostAction extends StatefulWidget { class PostAction extends StatefulWidget {
@ -29,14 +25,20 @@ class PostAction extends StatefulWidget {
} }
class _PostActionState extends State<PostAction> { class _PostActionState extends State<PostAction> {
bool _isBusy = false; bool _isBusy = true;
bool _canModifyContent = false; bool _canModifyContent = false;
void _checkAbleToModifyContent() async { void _checkAbleToModifyContent() async {
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) return; if (auth.isAuthorized.isFalse) return;
_canModifyContent = auth.userProfile.value!['id'] == widget.item.author.id; setState(() => _isBusy = true);
setState(() {
_canModifyContent =
auth.userProfile.value!['id'] == widget.item.author.id;
_isBusy = false;
});
} }
Future<void> _doShare({bool noUri = false}) async { Future<void> _doShare({bool noUri = false}) async {
@ -67,8 +69,7 @@ class _PostActionState extends State<PostAction> {
'link': 'https://solsynth.dev/posts/$id', 'link': 'https://solsynth.dev/posts/$id',
}), }),
subject: 'postShareSubject'.trParams({ subject: 'postShareSubject'.trParams({
'username': '@${widget.item.author.name}', 'username': widget.item.author.nick,
'title': widget.item.body['title'] ?? '#${widget.item.id}',
}), }),
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
); );
@ -83,78 +84,6 @@ class _PostActionState extends State<PostAction> {
} }
} }
Future<void> _shareImage() async {
final List<String> attachments = widget.item.body['attachments'] is List
? List.from(widget.item.body['attachments']?.whereType<String>())
: List.empty();
final hasMultipleAttachment = attachments.length > 1;
setState(() => _isBusy = true);
final double width = hasMultipleAttachment ? 640 : 480;
final screenshot = ScreenshotController();
final image = await screenshot.captureFromLongWidget(
MediaQuery(
data: MediaQuery.of(context).copyWith(
size: Size(width, double.infinity),
),
child: PostShareImage(item: widget.item),
),
context: context,
pixelRatio: 2,
constraints: BoxConstraints(
minWidth: 480,
maxWidth: width,
minHeight: 640,
maxHeight: double.infinity,
),
);
final filename = 'share_post#${widget.item.id}';
if (PlatformInfo.isAndroid || PlatformInfo.isIOS) {
final box = context.findRenderObject() as RenderBox?;
final file = XFile.fromData(
image,
mimeType: 'image/png',
name: filename,
);
await Share.shareXFiles(
[file],
subject: 'postShareSubject'.trParams({
'username': '@${widget.item.author.name}',
'title': widget.item.body['title'] ?? '#${widget.item.id}',
}),
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
);
} else {
final filepath = await FileSaver.instance.saveFile(
name: filename,
ext: 'png',
mimeType: MimeType.png,
bytes: image,
);
context.showSnackbar('fileSavedAt'.trParams({'path': filepath}));
}
setState(() => _isBusy = false);
}
Future<Post> _getFullPost() async {
final PostProvider posts = Get.find();
try {
final resp = await posts.getPost(widget.item.id.toString());
return Post.fromJson(resp.body);
} catch (e) {
context.showErrorDialog(e).then((_) => Navigator.pop(context));
}
return widget.item;
}
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -198,13 +127,7 @@ class _PostActionState extends State<PostAction> {
), ),
], ],
).paddingOnly(left: 24, right: 24, top: 32, bottom: 16), ).paddingOnly(left: 24, right: 24, top: 32, bottom: 16),
LoadingIndicator( if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
isActive: _isBusy,
backgroundColor: Theme.of(context)
.colorScheme
.surfaceContainerHigh
.withOpacity(0.5),
),
Expanded( Expanded(
child: ListView( child: ListView(
children: [ children: [
@ -212,30 +135,16 @@ class _PostActionState extends State<PostAction> {
contentPadding: const EdgeInsets.symmetric(horizontal: 24), contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Icons.share), leading: const Icon(Icons.share),
title: Text('share'.tr), title: Text('share'.tr),
trailing: Row( trailing: PlatformInfo.isIOS || PlatformInfo.isAndroid
mainAxisSize: MainAxisSize.min, ? IconButton(
children: [
if (PlatformInfo.isIOS || PlatformInfo.isAndroid)
IconButton(
icon: const Icon(Icons.link_off), icon: const Icon(Icons.link_off),
tooltip: 'shareNoUri'.tr, tooltip: 'shareNoUri'.tr,
onPressed: () async { onPressed: () async {
await _doShare(noUri: true); await _doShare(noUri: true);
Navigator.pop(context); Navigator.pop(context);
}, },
), )
IconButton( : null,
icon: const Icon(Icons.image),
tooltip: 'shareImage'.tr,
onPressed: _isBusy
? null
: () async {
await _shareImage();
Navigator.pop(context);
},
),
],
),
onTap: () async { onTap: () async {
await _doShare(); await _doShare();
Navigator.pop(context); Navigator.pop(context);
@ -312,23 +221,15 @@ class _PostActionState extends State<PostAction> {
contentPadding: const EdgeInsets.symmetric(horizontal: 24), contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Icons.edit), leading: const Icon(Icons.edit),
title: Text('edit'.tr), title: Text('edit'.tr),
onTap: _isBusy onTap: () async {
? null Navigator.pop(
: () async { context,
setState(() => _isBusy = true); AppRouter.instance.pushNamed(
var item = widget.item; 'postEditor',
if (item.body?['content_truncated'] == true) { extra: PostPublishArguments(edit: widget.item),
item = await _getFullPost(); ),
} );
Navigator.pop( },
context,
AppRouter.instance.pushNamed(
'postEditor',
extra: PostPublishArguments(edit: item),
),
);
if (mounted) setState(() => _isBusy = false);
},
), ),
if (_canModifyContent) if (_canModifyContent)
ListTile( ListTile(

View File

@ -1,108 +0,0 @@
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:get/get.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/router.dart';
class PostCreatePopup extends StatelessWidget {
final bool hideDraftBox;
const PostCreatePopup({
super.key,
this.hideDraftBox = false,
});
@override
Widget build(BuildContext context) {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) {
return const SizedBox.shrink();
}
final List<dynamic> actionList = [
(
icon: const Icon(Icons.post_add),
label: 'postEditorModeStory'.tr,
onTap: () {
Navigator.pop(
context,
AppRouter.instance.pushNamed(
'postEditor',
queryParameters: {
'mode': 0.toString(),
},
),
);
},
),
(
icon: const Icon(Icons.description),
label: 'postEditorModeArticle'.tr,
onTap: () {
Navigator.pop(
context,
AppRouter.instance.pushNamed(
'postEditor',
queryParameters: {
'mode': 1.toString(),
},
),
);
},
),
(
icon: const Icon(Icons.drafts),
label: 'draftBoxOpen'.tr,
onTap: () {
Navigator.pop(
context,
AppRouter.instance.pushNamed('draftBox'),
);
},
),
];
return SizedBox(
height: MediaQuery.of(context).size.height * 0.38,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'postNew'.tr,
style: Theme.of(context).textTheme.headlineSmall,
).paddingOnly(left: 24, right: 24, top: 32, bottom: 16),
Expanded(
child: GridView.count(
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: 3,
children: actionList
.map((x) => Card(
color: Theme.of(context).colorScheme.surfaceContainer,
child: InkWell(
borderRadius:
const BorderRadius.all(Radius.circular(8)),
onTap: x.onTap,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
x.icon,
const Gap(8),
Expanded(
child: Text(
x.label,
overflow: TextOverflow.fade,
),
),
],
).paddingAll(18),
),
))
.toList(),
).paddingSymmetric(horizontal: 20),
),
],
),
);
}
}

View File

@ -1,5 +1,6 @@
import 'package:animations/animations.dart'; import 'package:animations/animations.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_animate/flutter_animate.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
@ -7,11 +8,11 @@ import 'package:get/get.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:solian/models/post.dart'; import 'package:solian/models/post.dart';
import 'package:solian/providers/content/posts.dart'; import 'package:solian/providers/content/posts.dart';
import 'package:solian/router.dart';
import 'package:solian/screens/posts/post_detail.dart'; import 'package:solian/screens/posts/post_detail.dart';
import 'package:solian/shells/title_shell.dart'; import 'package:solian/shells/title_shell.dart';
import 'package:solian/theme.dart'; import 'package:solian/theme.dart';
import 'package:solian/widgets/account/account_avatar.dart'; import 'package:solian/widgets/account/account_avatar.dart';
import 'package:solian/widgets/account/account_profile_popup.dart';
import 'package:solian/widgets/attachments/attachment_list.dart'; import 'package:solian/widgets/attachments/attachment_list.dart';
import 'package:solian/widgets/link_expansion.dart'; import 'package:solian/widgets/link_expansion.dart';
import 'package:solian/widgets/markdown_text_content.dart'; import 'package:solian/widgets/markdown_text_content.dart';
@ -30,15 +31,11 @@ class PostItem extends StatefulWidget {
final bool isShowEmbed; final bool isShowEmbed;
final bool isOverrideEmbedClickable; final bool isOverrideEmbedClickable;
final bool isFullDate; final bool isFullDate;
final bool isFullContent;
final bool isContentSelectable; final bool isContentSelectable;
final bool isNonScrollAttachment;
final bool showFeaturedReply; final bool showFeaturedReply;
final String? attachmentParent; final String? attachmentParent;
final Color? backgroundColor;
final EdgeInsets? padding;
final Function? onComment;
final Function? onTapMore;
const PostItem({ const PostItem({
super.key, super.key,
@ -50,13 +47,11 @@ class PostItem extends StatefulWidget {
this.isShowEmbed = true, this.isShowEmbed = true,
this.isOverrideEmbedClickable = false, this.isOverrideEmbedClickable = false,
this.isFullDate = false, this.isFullDate = false,
this.isFullContent = false,
this.isContentSelectable = false, this.isContentSelectable = false,
this.isNonScrollAttachment = false,
this.showFeaturedReply = false, this.showFeaturedReply = false,
this.attachmentParent, this.attachmentParent,
this.padding, this.backgroundColor,
this.onComment,
this.onTapMore,
}); });
@override @override
@ -69,20 +64,14 @@ class _PostItemState extends State<PostItem> {
Color get _unFocusColor => Color get _unFocusColor =>
Theme.of(context).colorScheme.onSurface.withOpacity(0.75); Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
static final visibilityIcons = [
Icons.public,
Icons.group,
Icons.visibility,
Icons.visibility_off,
Icons.lock,
];
@override @override
void initState() { void initState() {
item = widget.item; item = widget.item;
super.initState(); super.initState();
} }
double _contentHeight = 0;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final List<String> attachments = item.body['attachments'] is List final List<String> attachments = item.body['attachments'] is List
@ -100,26 +89,37 @@ class _PostItemState extends State<PostItem> {
).paddingOnly(bottom: 8), ).paddingOnly(bottom: 8),
_PostHeaderWidget( _PostHeaderWidget(
isCompact: widget.isCompact, isCompact: widget.isCompact,
isFullDate: widget.isFullDate,
onTapMore: widget.onTapMore,
item: item, item: item,
).paddingSymmetric(horizontal: 12), ).paddingSymmetric(horizontal: 12),
_PostHeaderDividerWidget(item: item).paddingSymmetric(horizontal: 12), _PostHeaderDividerWidget(item: item).paddingSymmetric(horizontal: 12),
SizedContainer( Stack(
maxWidth: 640, children: [
child: MarkdownTextContent( SizedContainer(
parentId: 'p${item.id}', maxWidth: 640,
content: item.body['content'], maxHeight: widget.isFullContent ? double.infinity : 80,
attachments: item.preload?.attachments, child: _MeasureSize(
isAutoWarp: item.type == 'story', onChange: (size) {
isSelectable: widget.isContentSelectable, setState(() => _contentHeight = size.height);
), },
).paddingOnly( child: SingleChildScrollView(
left: 12, physics: const NeverScrollableScrollPhysics(),
right: 12, child: MarkdownTextContent(
bottom: hasAttachment ? 4 : 0, parentId: 'p${item.id}',
content: item.body['content'],
isAutoWarp: item.type == 'story',
isSelectable: widget.isContentSelectable,
),
).paddingOnly(
left: 16,
right: 12,
top: 2,
bottom: hasAttachment ? 4 : 0,
),
),
),
],
), ),
if (widget.item.body?['content_truncated'] == true) if (_contentHeight >= 80 && !widget.isFullContent)
Opacity( Opacity(
opacity: 0.8, opacity: 0.8,
child: InkWell(child: Text('readMore'.tr)), child: InkWell(child: Text('readMore'.tr)),
@ -130,96 +130,145 @@ class _PostItemState extends State<PostItem> {
LinkExpansion(content: item.body['content']).paddingOnly( LinkExpansion(content: item.body['content']).paddingOnly(
left: 8, left: 8,
right: 8, right: 8,
top: 4,
), ),
_PostFooterWidget(item: item).paddingOnly(left: 16),
if (attachments.isNotEmpty) if (attachments.isNotEmpty)
_PostAttachmentWidget( Row(
item: item, children: [
padding: widget.padding, Icon(
isCompact: true, Icons.file_copy,
isNonScrollAttachment: widget.isNonScrollAttachment, size: 15,
).paddingOnly(top: 4), color: _unFocusColor,
).paddingOnly(right: 5),
Text(
'attachmentHint'.trParams(
{'count': attachments.length.toString()},
),
style: TextStyle(color: _unFocusColor),
)
],
).paddingOnly(left: 16, top: 4),
], ],
); );
} }
return GestureDetector( return OpenContainer(
child: Column( tappable: widget.isClickable,
closedBuilder: (_, openContainer) => Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_PostThumbnail( _PostThumbnail(
rid: item.body['thumbnail'], rid: item.body['thumbnail'],
parentId: widget.item.id.toString(), parentId: widget.item.id.toString(),
).paddingOnly(bottom: 4), ).paddingOnly(bottom: 4),
Column( Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_PostHeaderWidget( GestureDetector(
isCompact: widget.isCompact, child: AccountAvatar(content: item.author.avatar),
isFullDate: widget.isFullDate, onTap: () {
onTapMore: widget.onTapMore, showModalBottomSheet(
item: item, useRootNavigator: true,
isScrollControlled: true,
backgroundColor: Theme.of(context).colorScheme.surface,
context: context,
builder: (context) => AccountProfilePopup(
name: item.author.name,
),
);
},
), ),
_PostHeaderDividerWidget(item: item), Expanded(
SizedContainer( child: Column(
maxWidth: 640, crossAxisAlignment: CrossAxisAlignment.start,
child: MarkdownTextContent( children: [
parentId: 'p${item.id}-embed', _PostHeaderWidget(
content: item.body['content'], isCompact: widget.isCompact,
attachments: item.preload?.attachments, item: item,
isAutoWarp: item.type == 'story', ),
isSelectable: widget.isContentSelectable, _PostHeaderDividerWidget(item: item),
Stack(
children: [
SizedContainer(
maxWidth: 640,
maxHeight:
widget.isFullContent ? double.infinity : 320,
child: _MeasureSize(
onChange: (size) {
setState(() => _contentHeight = size.height);
},
child: SingleChildScrollView(
physics: const NeverScrollableScrollPhysics(),
child: MarkdownTextContent(
parentId: 'p${item.id}-embed',
content: item.body['content'],
isAutoWarp: item.type == 'story',
isSelectable: widget.isContentSelectable,
isLargeText: item.type == 'article' &&
widget.isFullContent,
).paddingOnly(left: 12, right: 8),
),
),
),
],
),
if (_contentHeight >= 320 && !widget.isFullContent)
Opacity(
opacity: 0.8,
child: InkWell(child: Text('readMore'.tr)),
).paddingOnly(
left: 12,
top: 4,
),
if (widget.item.replyTo != null && widget.isShowEmbed)
Container(
constraints: const BoxConstraints(maxWidth: 480),
padding: const EdgeInsets.only(top: 4),
child: _PostEmbedWidget(
isClickable: widget.isClickable,
isOverrideEmbedClickable:
widget.isOverrideEmbedClickable,
item: widget.item.replyTo!,
username: widget.item.replyTo!.author.name,
hintText: 'postRepliedNotify',
icon: FontAwesomeIcons.reply,
id: widget.item.replyTo!.id.toString(),
),
),
if (widget.item.repostTo != null && widget.isShowEmbed)
Container(
constraints: const BoxConstraints(maxWidth: 480),
padding: const EdgeInsets.only(top: 4),
child: _PostEmbedWidget(
isClickable: widget.isClickable,
isOverrideEmbedClickable:
widget.isOverrideEmbedClickable,
item: widget.item.repostTo!,
username: widget.item.repostTo!.author.name,
hintText: 'postRepostedNotify',
icon: FontAwesomeIcons.retweet,
id: widget.item.repostTo!.id.toString(),
),
),
_PostFooterWidget(item: item).paddingOnly(left: 12),
LinkExpansion(content: item.body['content'])
.paddingOnly(top: 4),
],
), ),
), ),
if (widget.item.body?['content_truncated'] == true)
Opacity(
opacity: 0.8,
child: InkWell(child: Text('readMore'.tr)),
).paddingOnly(top: 4),
if (widget.item.replyTo != null && widget.isShowEmbed)
Container(
constraints: const BoxConstraints(maxWidth: 480),
padding: const EdgeInsets.only(top: 8),
child: _PostEmbedWidget(
isClickable: widget.isClickable,
isOverrideEmbedClickable: widget.isOverrideEmbedClickable,
item: widget.item.replyTo!,
username: widget.item.replyTo!.author.name,
hintText: 'postRepliedNotify',
icon: FontAwesomeIcons.reply,
id: widget.item.replyTo!.id.toString(),
),
),
if (widget.item.repostTo != null && widget.isShowEmbed)
Container(
constraints: const BoxConstraints(maxWidth: 480),
padding: const EdgeInsets.only(top: 8),
child: _PostEmbedWidget(
isClickable: widget.isClickable,
isOverrideEmbedClickable: widget.isOverrideEmbedClickable,
item: widget.item.repostTo!,
username: widget.item.repostTo!.author.name,
hintText: 'postRepostedNotify',
icon: FontAwesomeIcons.retweet,
id: widget.item.repostTo!.id.toString(),
),
),
_PostFooterWidget(item: item),
LinkExpansion(content: item.body['content']),
], ],
).paddingSymmetric( ).paddingOnly(
horizontal: (widget.padding?.horizontal ?? 0) + 16, top: 10,
bottom:
(attachments.length == 1 && !AppTheme.isLargeScreen(context))
? 10
: 0,
right: 16,
left: 16,
), ),
if (hasAttachment) const Gap(8), _PostAttachmentWidget(item: item),
_PostAttachmentWidget( if (widget.showFeaturedReply) _PostFeaturedReplyWidget(item: item),
item: item,
padding: widget.padding,
isCompact: item.type == 'article',
isNonScrollAttachment: widget.isNonScrollAttachment,
),
if (widget.showFeaturedReply)
_PostFeaturedReplyWidget(item: item).paddingSymmetric(
horizontal: (widget.padding?.horizontal ?? 0) + 12,
),
if (widget.isShowReply || widget.isReactable) if (widget.isShowReply || widget.isReactable)
PostQuickAction( PostQuickAction(
isShowReply: widget.isShowReply, isShowReply: widget.isShowReply,
@ -231,30 +280,32 @@ class _PostItemState extends State<PostItem> {
(item.metric!.reactionList[symbol] ?? 0) + changes; (item.metric!.reactionList[symbol] ?? 0) + changes;
}); });
}, },
onComment: () {
if (widget.onComment != null) {
widget.onComment!();
}
},
).paddingOnly( ).paddingOnly(
top: 8, top: (attachments.length == 1 && !AppTheme.isLargeScreen(context))
left: (widget.padding?.left ?? 0) + 14, ? 10
right: (widget.padding?.right ?? 0) + 14, : 6,
left:
(attachments.length == 1 && !AppTheme.isLargeScreen(context))
? 24
: 60,
right: 16,
bottom: 10,
) )
else
const Gap(10),
], ],
).paddingOnly(
top: widget.padding?.top ?? 0,
bottom: widget.padding?.bottom ?? 0,
), ),
onTap: () { openBuilder: (_, __) => TitleShell(
if (widget.isClickable) { title: 'postDetail'.tr,
AppRouter.instance.pushNamed( child: PostDetailScreen(
'postDetail', id: item.id.toString(),
pathParameters: {'id': item.id.toString()}, post: item,
extra: item, ),
); ),
} closedElevation: 0,
}, openElevation: 0,
closedColor: Colors.transparent,
openColor: Theme.of(context).colorScheme.surface,
); );
} }
} }
@ -266,6 +317,7 @@ class _PostFeaturedReplyWidget extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isLargeScreen = AppTheme.isLargeScreen(context);
final unFocusColor = final unFocusColor =
Theme.of(context).colorScheme.onSurface.withOpacity(0.75); Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
@ -273,17 +325,19 @@ class _PostFeaturedReplyWidget extends StatelessWidget {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
final List<String> attachments = item.body['attachments'] is List
? List.from(item.body['attachments']?.whereType<String>())
: List.empty();
return FutureBuilder( return FutureBuilder(
future: Get.find<PostProvider>().listPostFeaturedReply( future:
item.id.toString(), Get.find<PostProvider>().listPostFeaturedReply(item.id.toString()),
),
builder: (context, snapshot) { builder: (context, snapshot) {
if (!snapshot.hasData || snapshot.data!.isEmpty) { if (!snapshot.hasData || snapshot.data!.isEmpty) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
return Container( return Container(
padding: EdgeInsets.only(top: 8),
constraints: const BoxConstraints(maxWidth: 480), constraints: const BoxConstraints(maxWidth: 480),
child: Card( child: Card(
margin: EdgeInsets.zero, margin: EdgeInsets.zero,
@ -297,7 +351,7 @@ class _PostFeaturedReplyWidget extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
AttachedCircleAvatar( AccountAvatar(
content: reply.author.avatar, content: reply.author.avatar,
radius: 10, radius: 10,
), ),
@ -369,9 +423,16 @@ class _PostFeaturedReplyWidget extends StatelessWidget {
.toList(), .toList(),
), ),
), ),
).animate().fadeIn( )
.animate()
.fadeIn(
duration: 300.ms, duration: 300.ms,
curve: Curves.easeIn, curve: Curves.easeIn,
)
.paddingOnly(
top: (attachments.length == 1 && !isLargeScreen) ? 10 : 6,
left: (attachments.length == 1 && !isLargeScreen) ? 24 : 60,
right: 16,
); );
}, },
); );
@ -380,16 +441,8 @@ class _PostFeaturedReplyWidget extends StatelessWidget {
class _PostAttachmentWidget extends StatelessWidget { class _PostAttachmentWidget extends StatelessWidget {
final Post item; final Post item;
final EdgeInsets? padding;
final bool isNonScrollAttachment;
final bool isCompact;
const _PostAttachmentWidget({ const _PostAttachmentWidget({required this.item});
required this.item,
required this.padding,
required this.isNonScrollAttachment,
this.isCompact = false,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -399,72 +452,25 @@ class _PostAttachmentWidget extends StatelessWidget {
? List.from(item.body['attachments']?.whereType<String>()) ? List.from(item.body['attachments']?.whereType<String>())
: List.empty(); : List.empty();
final unFocusColor = if (attachments.length > 3) {
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
if (attachments.isEmpty) return const SizedBox.shrink();
if (isCompact) {
return Row(
children: [
Icon(
Icons.file_copy,
size: 13,
color: unFocusColor,
).paddingOnly(right: 5),
Text(
'attachmentHint'.trParams(
{'count': attachments.length.toString()},
),
style: TextStyle(color: unFocusColor, fontSize: 13),
)
],
).paddingOnly(
left: (padding?.left ?? 0) + 17,
right: (padding?.right ?? 0) + 17,
);
}
if (attachments.length == 1 && !isLargeScreen) {
return AttachmentList( return AttachmentList(
parentId: item.id.toString(), parentId: item.id.toString(),
attachmentIds: item.preload == null ? attachments : null, attachmentsId: attachments,
attachments: item.preload?.attachments,
autoload: false,
isFullWidth: true,
);
} else if (attachments.length > 1 &&
attachments.length % 3 == 0 &&
!isLargeScreen) {
return AttachmentList(
parentId: item.id.toString(),
attachmentIds: item.preload == null ? attachments : null,
attachments: item.preload?.attachments,
autoload: false, autoload: false,
isGrid: true, isGrid: true,
).paddingOnly( ).paddingOnly(left: 36, top: 4, bottom: 4);
left: (padding?.left ?? 0) + 14, } else if (attachments.length > 1 || isLargeScreen) {
right: (padding?.right ?? 0) + 14,
);
} else if (attachments.length == 1 || isNonScrollAttachment) {
return AttachmentList( return AttachmentList(
parentId: item.id.toString(), parentId: item.id.toString(),
attachmentIds: item.preload == null ? attachments : null, attachmentsId: attachments,
attachments: item.preload?.attachments,
autoload: false, autoload: false,
isColumn: true, isColumn: true,
).paddingOnly( ).paddingOnly(left: 60, right: 24, top: 4, bottom: 4);
left: (padding?.left ?? 0) + 14,
right: (padding?.right ?? 0) + 14,
);
} else { } else {
return AttachmentList( return AttachmentList(
flatMaxHeight: MediaQuery.of(context).size.width,
parentId: item.id.toString(), parentId: item.id.toString(),
attachmentIds: item.preload == null ? attachments : null, attachmentsId: attachments,
attachments: item.preload?.attachments,
padding: EdgeInsets.symmetric(
horizontal: (padding?.horizontal ?? 0) + 14,
),
autoload: false, autoload: false,
); );
} }
@ -506,17 +512,16 @@ class _PostEmbedWidget extends StatelessWidget {
size: 16, size: 16,
color: unFocusColor, color: unFocusColor,
), ),
const Gap(6),
Expanded( Expanded(
child: Text( child: Text(
hintText.trParams( hintText.trParams(
{'username': '@$username'}, {'username': '@$username'},
), ),
style: TextStyle(color: unFocusColor), style: TextStyle(color: unFocusColor),
), ).paddingOnly(left: 6),
), ),
], ],
).paddingOnly(left: 2), ).paddingOnly(left: 12),
Card( Card(
elevation: 1, elevation: 1,
child: PostItem( child: PostItem(
@ -552,7 +557,9 @@ class _PostHeaderDividerWidget extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (item.body['description'] != null || item.body['title'] != null) { if (item.body['description'] != null || item.body['title'] != null) {
return const SizedBox(height: 8); return const Divider(thickness: 0.3, height: 1).paddingSymmetric(
vertical: 8,
);
} }
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
@ -608,104 +615,64 @@ class _PostFooterWidget extends StatelessWidget {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: widgets, children: widgets,
).paddingSymmetric(vertical: 4); ).paddingOnly(top: 4);
} }
} }
} }
class _PostHeaderWidget extends StatelessWidget { class _PostHeaderWidget extends StatelessWidget {
final bool isCompact; final bool isCompact;
final bool isFullDate;
final Post item; final Post item;
final Function? onTapMore;
const _PostHeaderWidget({ const _PostHeaderWidget({
required this.isCompact, required this.isCompact,
required this.isFullDate,
required this.item, required this.item,
required this.onTapMore,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( if (isCompact)
crossAxisAlignment: CrossAxisAlignment.start, AccountAvatar(
children: [ content: item.author.avatar,
AccountAvatar( radius: 10,
content: item.author.avatar, ).paddingOnly(left: 2, top: 1),
username: item.author.name, Expanded(
radius: isCompact ? 10 : null, child: Column(
), crossAxisAlignment: CrossAxisAlignment.start,
Gap(isCompact ? 6 : 8), children: [
Expanded( Row(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( Text(
crossAxisAlignment: CrossAxisAlignment.center, item.author.nick,
children: [ style: const TextStyle(fontWeight: FontWeight.bold),
Text(
item.author.nick,
style: const TextStyle(fontWeight: FontWeight.bold),
),
if (isCompact) const Gap(4),
if (isCompact)
RelativeDate(
item.publishedAt?.toLocal() ?? DateTime.now(),
isFull: isFullDate,
).paddingOnly(top: 1),
],
), ),
if (!isCompact) RelativeDate(item.publishedAt?.toLocal() ?? DateTime.now())
Row( .paddingOnly(left: 4),
crossAxisAlignment: CrossAxisAlignment.center,
children: [
RelativeDate(
item.publishedAt?.toLocal() ?? DateTime.now(),
isFull: isFullDate,
),
const Gap(4),
Icon(
_PostItemState.visibilityIcons[item.visibility],
size: 16,
color: Theme.of(context)
.colorScheme
.onSurface
.withOpacity(0.75),
),
],
),
], ],
), ),
), if (item.body['title'] != null)
if (onTapMore != null) Text(
IconButton( item.body['title'],
color: Theme.of(context).colorScheme.primary, style: Theme.of(context)
icon: const Icon(Icons.more_vert), .textTheme
padding: const EdgeInsets.symmetric(horizontal: 4), .bodyMedium!
visualDensity: const VisualDensity( .copyWith(fontSize: 15),
horizontal: -4,
vertical: -2,
), ),
onPressed: () => onTapMore!(), if (item.body['description'] != null)
), Text(
], item.body['description'],
style: Theme.of(context).textTheme.bodySmall,
),
],
).paddingOnly(left: isCompact ? 6 : 12),
), ),
const Gap(8), if (item.type == 'article')
if (item.body['title'] != null) Badge(
Text( label: Text('article'.tr),
item.body['title'], ).paddingOnly(top: 3),
style: Theme.of(context).textTheme.titleMedium,
),
if (item.body['description'] != null)
Text(
item.body['description'],
style: Theme.of(context).textTheme.titleSmall,
),
], ],
); );
} }
@ -736,3 +703,45 @@ class _PostThumbnail extends StatelessWidget {
); );
} }
} }
typedef _OnWidgetSizeChange = void Function(Size size);
class _MeasureSizeRenderObject extends RenderProxyBox {
Size? oldSize;
_OnWidgetSizeChange onChange;
_MeasureSizeRenderObject(this.onChange);
@override
void performLayout() {
super.performLayout();
Size newSize = child!.size;
if (oldSize == newSize) return;
oldSize = newSize;
WidgetsBinding.instance.addPostFrameCallback((_) {
onChange(newSize);
});
}
}
class _MeasureSize extends SingleChildRenderObjectWidget {
final _OnWidgetSizeChange onChange;
const _MeasureSize({
required this.onChange,
required Widget super.child,
});
@override
RenderObject createRenderObject(BuildContext context) {
return _MeasureSizeRenderObject(onChange);
}
@override
void updateRenderObject(
BuildContext context, covariant _MeasureSizeRenderObject renderObject) {
renderObject.onChange = onChange;
}
}

View File

@ -1,11 +1,8 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:solian/models/post.dart'; import 'package:solian/models/post.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
import 'package:solian/router.dart';
import 'package:solian/screens/posts/post_editor.dart';
import 'package:solian/widgets/posts/post_action.dart'; import 'package:solian/widgets/posts/post_action.dart';
import 'package:solian/widgets/posts/post_item.dart'; import 'package:solian/widgets/posts/post_item.dart';
@ -15,7 +12,6 @@ class PostListWidget extends StatelessWidget {
final bool isNestedClickable; final bool isNestedClickable;
final PagingController<int, Post> controller; final PagingController<int, Post> controller;
final Color? backgroundColor; final Color? backgroundColor;
final EdgeInsets? padding;
const PostListWidget({ const PostListWidget({
super.key, super.key,
@ -24,7 +20,6 @@ class PostListWidget extends StatelessWidget {
this.isClickable = true, this.isClickable = true,
this.isNestedClickable = true, this.isNestedClickable = true,
this.backgroundColor, this.backgroundColor,
this.padding,
}); });
@override @override
@ -34,18 +29,16 @@ class PostListWidget extends StatelessWidget {
pagingController: controller, pagingController: controller,
builderDelegate: PagedChildBuilderDelegate<Post>( builderDelegate: PagedChildBuilderDelegate<Post>(
itemBuilder: (context, item, index) { itemBuilder: (context, item, index) {
return Padding( return PostListEntryWidget(
padding: padding ?? EdgeInsets.zero, isShowEmbed: isShowEmbed,
child: PostListEntryWidget( isNestedClickable: isNestedClickable,
isShowEmbed: isShowEmbed, isClickable: isClickable,
isNestedClickable: isNestedClickable, showFeaturedReply: true,
isClickable: isClickable, item: item,
showFeaturedReply: true, backgroundColor: backgroundColor,
item: item, onUpdate: () {
onUpdate: () { controller.refresh();
controller.refresh(); },
},
),
); );
}, },
), ),
@ -55,126 +48,56 @@ class PostListWidget extends StatelessWidget {
} }
class PostListEntryWidget extends StatelessWidget { class PostListEntryWidget extends StatelessWidget {
final int renderOrder;
final bool isShowEmbed; final bool isShowEmbed;
final bool isNestedClickable; final bool isNestedClickable;
final bool isClickable; final bool isClickable;
final bool showFeaturedReply; final bool showFeaturedReply;
final Post item; final Post item;
final EdgeInsets? padding;
final Function onUpdate; final Function onUpdate;
final Color? backgroundColor;
const PostListEntryWidget({ const PostListEntryWidget({
super.key, super.key,
this.renderOrder = 0,
required this.isShowEmbed, required this.isShowEmbed,
required this.isNestedClickable, required this.isNestedClickable,
required this.isClickable, required this.isClickable,
required this.showFeaturedReply, required this.showFeaturedReply,
required this.item, required this.item,
this.padding,
required this.onUpdate, required this.onUpdate,
this.backgroundColor,
}); });
void _openActions(BuildContext context) {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) return;
showModalBottomSheet(
useRootNavigator: true,
context: context,
builder: (context) => PostAction(item: item),
).then((value) {
if (value is Future) {
value.then((_) {
onUpdate();
});
} else if (value != null) {
onUpdate();
}
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return TapRegion( return GestureDetector(
child: GestureDetector( child: PostItem(
onLongPress: () => _openActions(context), key: Key('p${item.id}'),
child: PostItem( item: item,
key: Key('p${item.id}'), isShowEmbed: isShowEmbed,
item: item, isClickable: isNestedClickable,
isShowEmbed: isShowEmbed, showFeaturedReply: showFeaturedReply,
isClickable: isNestedClickable, backgroundColor: backgroundColor,
showFeaturedReply: showFeaturedReply, ).paddingSymmetric(vertical: 8),
padding: padding, onLongPress: () {
onTapMore: () => _openActions(context), final AuthProvider auth = Get.find();
onComment: () { if (auth.isAuthorized.isFalse) return;
AppRouter.instance
.pushNamed( showModalBottomSheet(
'postEditor', useRootNavigator: true,
extra: PostPublishArguments(reply: item), context: context,
) builder: (context) => PostAction(item: item),
.then((value) { ).then((value) {
if (value is Future) { if (value is Future) {
value.then((_) { value.then((_) {
onUpdate(); onUpdate();
});
} else if (value != null) {
onUpdate();
}
}); });
}, } else if (value != null) {
).paddingSymmetric(vertical: 8), onUpdate();
), }
onTapInside: (event) { });
if (event.buttons == kSecondaryMouseButton) {
_openActions(context);
}
}, },
); );
} }
} }
class ControlledPostListWidget extends StatelessWidget {
final bool isShowEmbed;
final bool isClickable;
final bool isNestedClickable;
final bool isPinned;
final PagingController<int, Post> controller;
final EdgeInsets? padding;
final Function? onUpdate;
const ControlledPostListWidget({
super.key,
required this.controller,
this.isShowEmbed = true,
this.isClickable = true,
this.isNestedClickable = true,
this.isPinned = true,
this.padding,
this.onUpdate,
});
@override
Widget build(BuildContext context) {
return PagedSliverList<int, Post>.separated(
addRepaintBoundaries: true,
pagingController: controller,
builderDelegate: PagedChildBuilderDelegate<Post>(
itemBuilder: (context, item, index) {
if (item.pinnedAt != null && !isPinned) {
return const SizedBox.shrink();
}
return PostListEntryWidget(
isShowEmbed: isShowEmbed,
isNestedClickable: isNestedClickable,
isClickable: isClickable,
showFeaturedReply: true,
padding: padding,
item: item,
onUpdate: onUpdate ?? () {},
);
},
),
separatorBuilder: (_, __) => const Divider(thickness: 0.3, height: 0.3),
);
}
}

View File

@ -0,0 +1,43 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:solian/models/post.dart';
import 'package:solian/widgets/posts/post_item.dart';
class PostOwnedListEntry extends StatelessWidget {
final Post item;
final Function onTap;
final bool isFullContent;
final Color? backgroundColor;
const PostOwnedListEntry({
super.key,
required this.item,
required this.onTap,
this.isFullContent = false,
this.backgroundColor,
});
@override
Widget build(BuildContext context) {
return Card(
child: GestureDetector(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
PostItem(
key: Key('p${item.id}'),
item: item,
isShowEmbed: false,
isClickable: false,
isShowReply: false,
isReactable: false,
isFullContent: isFullContent,
backgroundColor: backgroundColor,
).paddingSymmetric(vertical: 8),
],
),
onTap: () => onTap(),
),
);
}
}

View File

@ -11,7 +11,6 @@ class PostQuickAction extends StatefulWidget {
final Post item; final Post item;
final bool isReactable; final bool isReactable;
final bool isShowReply; final bool isShowReply;
final Function onComment;
final void Function(String symbol, int num) onReact; final void Function(String symbol, int num) onReact;
const PostQuickAction({ const PostQuickAction({
@ -19,7 +18,6 @@ class PostQuickAction extends StatefulWidget {
required this.item, required this.item,
this.isShowReply = true, this.isShowReply = true,
this.isReactable = true, this.isReactable = true,
required this.onComment,
required this.onReact, required this.onReact,
}); });
@ -108,11 +106,7 @@ class _PostQuickActionState extends State<PostQuickAction> {
builder: (context) { builder: (context) {
return PostReplyListPopup(item: widget.item); return PostReplyListPopup(item: widget.item);
}, },
).then((signal) { );
if (signal == true) {
widget.onComment();
}
});
}, },
), ),
), ),

View File

@ -8,13 +8,11 @@ import 'package:solian/widgets/posts/post_list.dart';
class PostReplyList extends StatefulWidget { class PostReplyList extends StatefulWidget {
final Post item; final Post item;
final EdgeInsets? padding;
final Color? backgroundColor; final Color? backgroundColor;
const PostReplyList({ const PostReplyList({
super.key, super.key,
required this.item, required this.item,
this.padding,
this.backgroundColor, this.backgroundColor,
}); });
@ -55,7 +53,6 @@ class _PostReplyListState extends State<PostReplyList> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return PostListWidget( return PostListWidget(
padding: widget.padding,
isShowEmbed: false, isShowEmbed: false,
controller: _pagingController, controller: _pagingController,
backgroundColor: widget.backgroundColor, backgroundColor: widget.backgroundColor,
@ -73,31 +70,16 @@ class PostReplyListPopup extends StatelessWidget {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( Text(
children: [ 'postReplies'.tr,
Expanded( style: Theme.of(context).textTheme.headlineSmall,
child: Text( ).paddingOnly(left: 24, right: 24, top: 32, bottom: 16),
'postReplies'.tr,
style: Theme.of(context).textTheme.headlineSmall,
),
),
IconButton(
icon: const Icon(Icons.add_comment),
visualDensity: const VisualDensity(horizontal: -4),
onPressed: () {
Navigator.pop(context, true);
},
),
],
).paddingOnly(left: 24, right: 24, top: 24, bottom: 8),
Expanded( Expanded(
child: CustomScrollView( child: CustomScrollView(
slivers: [ slivers: [
PostReplyList( PostReplyList(
item: item, item: item,
padding: EdgeInsets.symmetric(horizontal: 10), backgroundColor: Theme.of(context).colorScheme.surfaceContainerLow,
backgroundColor:
Theme.of(context).colorScheme.surfaceContainerLow,
), ),
], ],
), ),

View File

@ -1,103 +0,0 @@
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:get/get.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:solian/models/post.dart';
import 'package:solian/widgets/posts/post_item.dart';
import 'package:solian/widgets/root_container.dart';
class PostShareImage extends StatelessWidget {
final Post item;
const PostShareImage({super.key, required this.item});
@override
Widget build(BuildContext context) {
final textColor = Theme.of(context).colorScheme.onSurface.withOpacity(0.3);
return RootContainer(
child: Wrap(
alignment: WrapAlignment.spaceBetween,
runAlignment: WrapAlignment.center,
children: [
const SizedBox(height: 40),
Material(
color: Colors.transparent,
child: Card(
margin: EdgeInsets.zero,
child: PostItem(
item: item,
isShowEmbed: true,
isClickable: false,
showFeaturedReply: false,
isReactable: false,
isShowReply: false,
isNonScrollAttachment: true,
padding: const EdgeInsets.symmetric(
horizontal: 4,
vertical: 16,
),
onComment: () {},
),
),
).paddingOnly(bottom: 24),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: Image.asset(
'assets/logo.png',
width: 48,
height: 48,
),
),
const Gap(16),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
'shareImageFooter'.tr,
style: TextStyle(
fontSize: 13,
color: textColor,
),
),
Text(
'Solsynth LLC © ${DateTime.now().year}',
style: TextStyle(
fontSize: 11,
color: textColor,
),
),
],
),
],
),
ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: Material(
color: Theme.of(context).colorScheme.surface,
child: QrImageView(
data: 'https://solsynth.dev/posts/${item.id}',
version: QrVersions.auto,
padding: const EdgeInsets.all(4),
size: 48,
dataModuleStyle: QrDataModuleStyle(
color: Theme.of(context).colorScheme.onSurface,
),
eyeStyle: QrEyeStyle(
color: Theme.of(context).colorScheme.onSurface,
),
),
),
),
],
),
],
).paddingSymmetric(horizontal: 36, vertical: 24),
);
}
}

View File

@ -25,6 +25,7 @@ class PostSingleDisplay extends StatelessWidget {
isNestedClickable: true, isNestedClickable: true,
showFeaturedReply: true, showFeaturedReply: true,
onUpdate: onUpdate, onUpdate: onUpdate,
backgroundColor: Theme.of(context).colorScheme.surfaceContainerLow,
), ),
), ),
), ),

View File

@ -0,0 +1,48 @@
import 'package:flutter/material.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:solian/models/post.dart';
import 'package:solian/widgets/posts/post_list.dart';
class PostWarpedListWidget extends StatelessWidget {
final bool isShowEmbed;
final bool isClickable;
final bool isNestedClickable;
final bool isPinned;
final PagingController<int, Post> controller;
final Function? onUpdate;
const PostWarpedListWidget({
super.key,
required this.controller,
this.isShowEmbed = true,
this.isClickable = true,
this.isNestedClickable = true,
this.isPinned = true,
this.onUpdate,
});
@override
Widget build(BuildContext context) {
return PagedSliverList<int, Post>.separated(
addRepaintBoundaries: true,
pagingController: controller,
builderDelegate: PagedChildBuilderDelegate<Post>(
itemBuilder: (context, item, index) {
if (item.pinnedAt != null && !isPinned) {
return const SizedBox.shrink();
}
return PostListEntryWidget(
renderOrder: index,
isShowEmbed: isShowEmbed,
isNestedClickable: isNestedClickable,
isClickable: isClickable,
showFeaturedReply: true,
item: item,
onUpdate: onUpdate ?? () {},
);
},
),
separatorBuilder: (_, __) => const Divider(thickness: 0.3, height: 0.3),
);
}
}

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:solian/exts.dart'; import 'package:solian/exts.dart';
import 'package:solian/models/realm.dart'; import 'package:solian/models/realm.dart';
@ -7,7 +8,6 @@ import 'package:solian/services.dart';
import 'package:solian/widgets/account/account_avatar.dart'; import 'package:solian/widgets/account/account_avatar.dart';
import 'package:solian/widgets/account/account_profile_popup.dart'; import 'package:solian/widgets/account/account_profile_popup.dart';
import 'package:solian/widgets/account/relative_select.dart'; import 'package:solian/widgets/account/relative_select.dart';
import 'package:solian/widgets/loading_indicator.dart';
class RealmMemberListPopup extends StatefulWidget { class RealmMemberListPopup extends StatefulWidget {
final Realm realm; final Realm realm;
@ -128,7 +128,7 @@ class _RealmMemberListPopupState extends State<RealmMemberListPopup> {
'realmMembers'.tr, 'realmMembers'.tr,
style: Theme.of(context).textTheme.headlineSmall, style: Theme.of(context).textTheme.headlineSmall,
).paddingOnly(left: 24, right: 24, top: 32, bottom: 16), ).paddingOnly(left: 24, right: 24, top: 32, bottom: 16),
LoadingIndicator(isActive: _isBusy), if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
ListTile( ListTile(
tileColor: Theme.of(context).colorScheme.surfaceContainerHigh, tileColor: Theme.of(context).colorScheme.surfaceContainerHigh,
contentPadding: const EdgeInsets.symmetric(horizontal: 20), contentPadding: const EdgeInsets.symmetric(horizontal: 20),
@ -149,8 +149,7 @@ class _RealmMemberListPopupState extends State<RealmMemberListPopup> {
title: Text(element.account.nick), title: Text(element.account.nick),
subtitle: Text(element.account.name), subtitle: Text(element.account.name),
leading: GestureDetector( leading: GestureDetector(
child: child: AccountAvatar(content: element.account.avatar),
AttachedCircleAvatar(content: element.account.avatar),
onTap: () { onTap: () {
showModalBottomSheet( showModalBottomSheet(
useRootNavigator: true, useRootNavigator: true,

View File

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

View File

@ -3,7 +3,6 @@ import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:solian/platform.dart'; import 'package:solian/platform.dart';
import 'package:solian/theme.dart';
class RootContainer extends StatelessWidget { class RootContainer extends StatelessWidget {
final Widget? child; final Widget? child;
@ -47,18 +46,3 @@ class RootContainer extends StatelessWidget {
); );
} }
} }
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

@ -6,7 +6,7 @@ class EmptyPagePlaceholder extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ResponsiveRootContainer( return RootContainer(
child: Center( child: Center(
child: ClipRRect( child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(12)), borderRadius: const BorderRadius.all(Radius.circular(12)),

View File

@ -7,11 +7,9 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <desktop_drop/desktop_drop_plugin.h> #include <desktop_drop/desktop_drop_plugin.h>
#include <file_saver/file_saver_plugin.h>
#include <file_selector_linux/file_selector_plugin.h> #include <file_selector_linux/file_selector_plugin.h>
#include <flutter_acrylic/flutter_acrylic_plugin.h> #include <flutter_acrylic/flutter_acrylic_plugin.h>
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h> #include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
#include <flutter_udid/flutter_udid_plugin.h>
#include <flutter_webrtc/flutter_web_r_t_c_plugin.h> #include <flutter_webrtc/flutter_web_r_t_c_plugin.h>
#include <media_kit_libs_linux/media_kit_libs_linux_plugin.h> #include <media_kit_libs_linux/media_kit_libs_linux_plugin.h>
#include <media_kit_video/media_kit_video_plugin.h> #include <media_kit_video/media_kit_video_plugin.h>
@ -23,9 +21,6 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) desktop_drop_registrar = g_autoptr(FlPluginRegistrar) desktop_drop_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopDropPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopDropPlugin");
desktop_drop_plugin_register_with_registrar(desktop_drop_registrar); desktop_drop_plugin_register_with_registrar(desktop_drop_registrar);
g_autoptr(FlPluginRegistrar) file_saver_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSaverPlugin");
file_saver_plugin_register_with_registrar(file_saver_registrar);
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
file_selector_plugin_register_with_registrar(file_selector_linux_registrar); file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
@ -35,9 +30,6 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
g_autoptr(FlPluginRegistrar) flutter_udid_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterUdidPlugin");
flutter_udid_plugin_register_with_registrar(flutter_udid_registrar);
g_autoptr(FlPluginRegistrar) flutter_webrtc_registrar = g_autoptr(FlPluginRegistrar) flutter_webrtc_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterWebRTCPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterWebRTCPlugin");
flutter_web_r_t_c_plugin_register_with_registrar(flutter_webrtc_registrar); flutter_web_r_t_c_plugin_register_with_registrar(flutter_webrtc_registrar);

View File

@ -4,11 +4,9 @@
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
desktop_drop desktop_drop
file_saver
file_selector_linux file_selector_linux
flutter_acrylic flutter_acrylic
flutter_secure_storage_linux flutter_secure_storage_linux
flutter_udid
flutter_webrtc flutter_webrtc
media_kit_libs_linux media_kit_libs_linux
media_kit_video media_kit_video

View File

@ -8,7 +8,6 @@ import Foundation
import connectivity_plus import connectivity_plus
import desktop_drop import desktop_drop
import device_info_plus import device_info_plus
import file_saver
import file_selector_macos import file_selector_macos
import firebase_analytics import firebase_analytics
import firebase_core import firebase_core
@ -16,7 +15,6 @@ import firebase_crashlytics
import firebase_messaging import firebase_messaging
import flutter_local_notifications import flutter_local_notifications
import flutter_secure_storage_macos import flutter_secure_storage_macos
import flutter_udid
import flutter_webrtc import flutter_webrtc
import gal import gal
import in_app_review import in_app_review
@ -31,7 +29,7 @@ import protocol_handler_macos
import screen_brightness_macos import screen_brightness_macos
import share_plus import share_plus
import shared_preferences_foundation import shared_preferences_foundation
import sqflite_darwin import sqflite
import sqlite3_flutter_libs import sqlite3_flutter_libs
import url_launcher_macos import url_launcher_macos
import wakelock_plus import wakelock_plus
@ -40,7 +38,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin")) ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
DesktopDropPlugin.register(with: registry.registrar(forPlugin: "DesktopDropPlugin")) DesktopDropPlugin.register(with: registry.registrar(forPlugin: "DesktopDropPlugin"))
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
FileSaverPlugin.register(with: registry.registrar(forPlugin: "FileSaverPlugin"))
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
FLTFirebaseAnalyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAnalyticsPlugin")) FLTFirebaseAnalyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAnalyticsPlugin"))
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
@ -48,7 +45,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin")) FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin"))
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
FlutterUdidPlugin.register(with: registry.registrar(forPlugin: "FlutterUdidPlugin"))
FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin")) FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin"))
GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin")) GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin"))
InAppReviewPlugin.register(with: registry.registrar(forPlugin: "InAppReviewPlugin")) InAppReviewPlugin.register(with: registry.registrar(forPlugin: "InAppReviewPlugin"))

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