Basic large screen support

This commit is contained in:
LittleSheep 2024-05-02 00:49:38 +08:00
parent fceb3edbc6
commit b39c8c770e
41 changed files with 716 additions and 607 deletions

View File

@ -28,32 +28,32 @@ class Account {
}); });
factory Account.fromJson(Map<String, dynamic> json) => Account( factory Account.fromJson(Map<String, dynamic> json) => Account(
id: json["id"], id: json['id'],
createdAt: DateTime.parse(json["created_at"]), createdAt: DateTime.parse(json['created_at']),
updatedAt: DateTime.parse(json["updated_at"]), updatedAt: DateTime.parse(json['updated_at']),
deletedAt: json["deleted_at"], deletedAt: json['deleted_at'],
name: json["name"], name: json['name'],
nick: json["nick"], nick: json['nick'],
avatar: json["avatar"], avatar: json['avatar'],
banner: json["banner"], banner: json['banner'],
description: json["description"], description: json['description'],
emailAddress: json["email_address"], emailAddress: json['email_address'],
powerLevel: json["power_level"], powerLevel: json['power_level'],
externalId: json["external_id"], externalId: json['external_id'],
); );
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
"id": id, 'id': id,
"created_at": createdAt.toIso8601String(), 'created_at': createdAt.toIso8601String(),
"updated_at": updatedAt.toIso8601String(), 'updated_at': updatedAt.toIso8601String(),
"deleted_at": deletedAt, 'deleted_at': deletedAt,
"name": name, 'name': name,
"nick": nick, 'nick': nick,
"avatar": avatar, 'avatar': avatar,
"banner": banner, 'banner': banner,
"description": description, 'description': description,
"email_address": emailAddress, 'email_address': emailAddress,
"power_level": powerLevel, 'power_level': powerLevel,
"external_id": externalId, 'external_id': externalId,
}; };
} }

View File

@ -28,32 +28,32 @@ class Author {
}); });
factory Author.fromJson(Map<String, dynamic> json) => Author( factory Author.fromJson(Map<String, dynamic> json) => Author(
id: json["id"], id: json['id'],
createdAt: DateTime.parse(json["created_at"]), createdAt: DateTime.parse(json['created_at']),
updatedAt: DateTime.parse(json["updated_at"]), updatedAt: DateTime.parse(json['updated_at']),
deletedAt: json["deleted_at"], deletedAt: json['deleted_at'],
name: json["name"], name: json['name'],
nick: json["nick"], nick: json['nick'],
avatar: json["avatar"], avatar: json['avatar'],
banner: json["banner"], banner: json['banner'],
description: json["description"], description: json['description'],
emailAddress: json["email_address"], emailAddress: json['email_address'],
powerLevel: json["power_level"], powerLevel: json['power_level'],
externalId: json["external_id"], externalId: json['external_id'],
); );
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
"id": id, 'id': id,
"created_at": createdAt.toIso8601String(), 'created_at': createdAt.toIso8601String(),
"updated_at": updatedAt.toIso8601String(), 'updated_at': updatedAt.toIso8601String(),
"deleted_at": deletedAt, 'deleted_at': deletedAt,
"name": name, 'name': name,
"nick": nick, 'nick': nick,
"avatar": avatar, 'avatar': avatar,
"banner": banner, 'banner': banner,
"description": description, 'description': description,
"email_address": emailAddress, 'email_address': emailAddress,
"power_level": powerLevel, 'power_level': powerLevel,
"external_id": externalId, 'external_id': externalId,
}; };
} }

View File

@ -25,28 +25,28 @@ class Call {
}); });
factory Call.fromJson(Map<String, dynamic> json) => Call( factory Call.fromJson(Map<String, dynamic> json) => Call(
id: json["id"], id: json['id'],
createdAt: DateTime.parse(json["created_at"]), createdAt: DateTime.parse(json['created_at']),
updatedAt: DateTime.parse(json["updated_at"]), updatedAt: DateTime.parse(json['updated_at']),
deletedAt: json["deleted_at"], deletedAt: json['deleted_at'],
endedAt: endedAt:
json["ended_at"] != null ? DateTime.parse(json["ended_at"]) : null, json['ended_at'] != null ? DateTime.parse(json['ended_at']) : null,
externalId: json["external_id"], externalId: json['external_id'],
founderId: json["founder_id"], founderId: json['founder_id'],
channelId: json["channel_id"], channelId: json['channel_id'],
channel: Channel.fromJson(json["channel"]), channel: Channel.fromJson(json['channel']),
); );
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
"id": id, 'id': id,
"created_at": createdAt.toIso8601String(), 'created_at': createdAt.toIso8601String(),
"updated_at": updatedAt.toIso8601String(), 'updated_at': updatedAt.toIso8601String(),
"deleted_at": deletedAt, 'deleted_at': deletedAt,
"ended_at": endedAt?.toIso8601String(), 'ended_at': endedAt?.toIso8601String(),
"external_id": externalId, 'external_id': externalId,
"founder_id": founderId, 'founder_id': founderId,
"channel_id": channelId, 'channel_id': channelId,
"channel": channel.toJson(), 'channel': channel.toJson(),
}; };
} }

View File

@ -32,35 +32,35 @@ class Channel {
}); });
factory Channel.fromJson(Map<String, dynamic> json) => Channel( factory Channel.fromJson(Map<String, dynamic> json) => Channel(
id: json["id"], id: json['id'],
createdAt: DateTime.parse(json["created_at"]), createdAt: DateTime.parse(json['created_at']),
updatedAt: DateTime.parse(json["updated_at"]), updatedAt: DateTime.parse(json['updated_at']),
deletedAt: json["deleted_at"], deletedAt: json['deleted_at'],
alias: json["alias"], alias: json['alias'],
name: json["name"], name: json['name'],
description: json["description"], description: json['description'],
members: json["members"], members: json['members'],
calls: json["calls"], calls: json['calls'],
type: json["type"], type: json['type'],
account: Account.fromJson(json["account"]), account: Account.fromJson(json['account']),
accountId: json["account_id"], accountId: json['account_id'],
realmId: json["realm_id"], realmId: json['realm_id'],
); );
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
"id": id, 'id': id,
"created_at": createdAt.toIso8601String(), 'created_at': createdAt.toIso8601String(),
"updated_at": updatedAt.toIso8601String(), 'updated_at': updatedAt.toIso8601String(),
"deleted_at": deletedAt, 'deleted_at': deletedAt,
"alias": alias, 'alias': alias,
"name": name, 'name': name,
"description": description, 'description': description,
"members": members, 'members': members,
"calls": calls, 'calls': calls,
"type": type, 'type': type,
"account": account, 'account': account,
"account_id": accountId, 'account_id': accountId,
"realm_id": realmId, 'realm_id': realmId,
}; };
} }
@ -86,24 +86,24 @@ class ChannelMember {
}); });
factory ChannelMember.fromJson(Map<String, dynamic> json) => ChannelMember( factory ChannelMember.fromJson(Map<String, dynamic> json) => ChannelMember(
id: json["id"], id: json['id'],
createdAt: DateTime.parse(json["created_at"]), createdAt: DateTime.parse(json['created_at']),
updatedAt: DateTime.parse(json["updated_at"]), updatedAt: DateTime.parse(json['updated_at']),
deletedAt: json["deleted_at"], deletedAt: json['deleted_at'],
channelId: json["channel_id"], channelId: json['channel_id'],
accountId: json["account_id"], accountId: json['account_id'],
account: Account.fromJson(json["account"]), account: Account.fromJson(json['account']),
notify: json["notify"], notify: json['notify'],
); );
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
"id": id, 'id': id,
"created_at": createdAt.toIso8601String(), 'created_at': createdAt.toIso8601String(),
"updated_at": updatedAt.toIso8601String(), 'updated_at': updatedAt.toIso8601String(),
"deleted_at": deletedAt, 'deleted_at': deletedAt,
"channel_id": channelId, 'channel_id': channelId,
"account_id": accountId, 'account_id': accountId,
"account": account.toJson(), 'account': account.toJson(),
"notify": notify, 'notify': notify,
}; };
} }

View File

@ -26,29 +26,29 @@ class Friendship {
}); });
factory Friendship.fromJson(Map<String, dynamic> json) => Friendship( factory Friendship.fromJson(Map<String, dynamic> json) => Friendship(
id: json["id"], id: json['id'],
createdAt: DateTime.parse(json["created_at"]), createdAt: DateTime.parse(json['created_at']),
updatedAt: DateTime.parse(json["updated_at"]), updatedAt: DateTime.parse(json['updated_at']),
deletedAt: json["deleted_at"], deletedAt: json['deleted_at'],
accountId: json["account_id"], accountId: json['account_id'],
relatedId: json["related_id"], relatedId: json['related_id'],
blockedBy: json["blocked_by"], blockedBy: json['blocked_by'],
account: Account.fromJson(json["account"]), account: Account.fromJson(json['account']),
related: Account.fromJson(json["related"]), related: Account.fromJson(json['related']),
status: json["status"], status: json['status'],
); );
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
"id": id, 'id': id,
"created_at": createdAt.toIso8601String(), 'created_at': createdAt.toIso8601String(),
"updated_at": updatedAt.toIso8601String(), 'updated_at': updatedAt.toIso8601String(),
"deleted_at": deletedAt, 'deleted_at': deletedAt,
"account_id": accountId, 'account_id': accountId,
"related_id": relatedId, 'related_id': relatedId,
"blocked_by": blockedBy, 'blocked_by': blockedBy,
"account": account.toJson(), 'account': account.toJson(),
"related": related.toJson(), 'related': related.toJson(),
"status": status, 'status': status,
}; };
Account getOtherside(int selfId) { Account getOtherside(int selfId) {

View File

@ -36,42 +36,42 @@ class Message {
}); });
factory Message.fromJson(Map<String, dynamic> json) => Message( factory Message.fromJson(Map<String, dynamic> json) => Message(
id: json["id"], id: json['id'],
createdAt: DateTime.parse(json["created_at"]), createdAt: DateTime.parse(json['created_at']),
updatedAt: DateTime.parse(json["updated_at"]), updatedAt: DateTime.parse(json['updated_at']),
deletedAt: json["deleted_at"], deletedAt: json['deleted_at'],
content: json["content"], content: json['content'],
metadata: json["metadata"], metadata: json['metadata'],
type: json["type"], type: json['type'],
attachments: List<Attachment>.from( attachments: List<Attachment>.from(
json["attachments"]?.map((x) => Attachment.fromJson(x)) ?? json['attachments']?.map((x) => Attachment.fromJson(x)) ??
List.empty()), List.empty()),
channel: Channel.fromJson(json["channel"]), channel: Channel.fromJson(json['channel']),
sender: Sender.fromJson(json["sender"]), sender: Sender.fromJson(json['sender']),
replyId: json["reply_id"], replyId: json['reply_id'],
replyTo: json["reply_to"] != null replyTo: json['reply_to'] != null
? Message.fromJson(json["reply_to"]) ? Message.fromJson(json['reply_to'])
: null, : null,
channelId: json["channel_id"], channelId: json['channel_id'],
senderId: json["sender_id"], senderId: json['sender_id'],
); );
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
"id": id, 'id': id,
"created_at": createdAt.toIso8601String(), 'created_at': createdAt.toIso8601String(),
"updated_at": updatedAt.toIso8601String(), 'updated_at': updatedAt.toIso8601String(),
"deleted_at": deletedAt, 'deleted_at': deletedAt,
"content": content, 'content': content,
"metadata": metadata, 'metadata': metadata,
"type": type, 'type': type,
"attachments": List<dynamic>.from( 'attachments': List<dynamic>.from(
attachments?.map((x) => x.toJson()) ?? List.empty()), attachments?.map((x) => x.toJson()) ?? List.empty()),
"channel": channel?.toJson(), 'channel': channel?.toJson(),
"sender": sender.toJson(), 'sender': sender.toJson(),
"reply_id": replyId, 'reply_id': replyId,
"reply_to": replyTo?.toJson(), 'reply_to': replyTo?.toJson(),
"channel_id": channelId, 'channel_id': channelId,
"sender_id": senderId, 'sender_id': senderId,
}; };
} }
@ -97,24 +97,24 @@ class Sender {
}); });
factory Sender.fromJson(Map<String, dynamic> json) => Sender( factory Sender.fromJson(Map<String, dynamic> json) => Sender(
id: json["id"], id: json['id'],
createdAt: DateTime.parse(json["created_at"]), createdAt: DateTime.parse(json['created_at']),
updatedAt: DateTime.parse(json["updated_at"]), updatedAt: DateTime.parse(json['updated_at']),
deletedAt: json["deleted_at"], deletedAt: json['deleted_at'],
account: Account.fromJson(json["account"]), account: Account.fromJson(json['account']),
channelId: json["channel_id"], channelId: json['channel_id'],
accountId: json["account_id"], accountId: json['account_id'],
notify: json["notify"], notify: json['notify'],
); );
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
"id": id, 'id': id,
"created_at": createdAt.toIso8601String(), 'created_at': createdAt.toIso8601String(),
"updated_at": updatedAt.toIso8601String(), 'updated_at': updatedAt.toIso8601String(),
"deleted_at": deletedAt, 'deleted_at': deletedAt,
"account": account.toJson(), 'account': account.toJson(),
"channel_id": channelId, 'channel_id': channelId,
"account_id": accountId, 'account_id': accountId,
"notify": notify, 'notify': notify,
}; };
} }

View File

@ -28,37 +28,37 @@ class Notification {
}); });
factory Notification.fromJson(Map<String, dynamic> json) => Notification( factory Notification.fromJson(Map<String, dynamic> json) => Notification(
id: json["id"], id: json['id'],
createdAt: DateTime.parse(json["created_at"]), createdAt: DateTime.parse(json['created_at']),
updatedAt: DateTime.parse(json["updated_at"]), updatedAt: DateTime.parse(json['updated_at']),
deletedAt: json["deleted_at"], deletedAt: json['deleted_at'],
subject: json["subject"], subject: json['subject'],
content: json["content"], content: json['content'],
links: json["links"] != null links: json['links'] != null
? List<Link>.from(json["links"].map((x) => Link.fromJson(x))) ? List<Link>.from(json['links'].map((x) => Link.fromJson(x)))
: List.empty(), : List.empty(),
isImportant: json["is_important"], isImportant: json['is_important'],
isRealtime: json["is_realtime"], isRealtime: json['is_realtime'],
readAt: json["read_at"], readAt: json['read_at'],
senderId: json["sender_id"], senderId: json['sender_id'],
recipientId: json["recipient_id"], recipientId: json['recipient_id'],
); );
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
"id": id, 'id': id,
"created_at": createdAt.toIso8601String(), 'created_at': createdAt.toIso8601String(),
"updated_at": updatedAt.toIso8601String(), 'updated_at': updatedAt.toIso8601String(),
"deleted_at": deletedAt, 'deleted_at': deletedAt,
"subject": subject, 'subject': subject,
"content": content, 'content': content,
"links": links != null 'links': links != null
? List<dynamic>.from(links!.map((x) => x.toJson())) ? List<dynamic>.from(links!.map((x) => x.toJson()))
: List.empty(), : List.empty(),
"is_important": isImportant, 'is_important': isImportant,
"is_realtime": isRealtime, 'is_realtime': isRealtime,
"read_at": readAt, 'read_at': readAt,
"sender_id": senderId, 'sender_id': senderId,
"recipient_id": recipientId, 'recipient_id': recipientId,
}; };
} }
@ -72,12 +72,12 @@ class Link {
}); });
factory Link.fromJson(Map<String, dynamic> json) => Link( factory Link.fromJson(Map<String, dynamic> json) => Link(
label: json["label"], label: json['label'],
url: json["url"], url: json['url'],
); );
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
"label": label, 'label': label,
"url": url, 'url': url,
}; };
} }

View File

@ -10,14 +10,14 @@ class NetworkPackage {
}); });
factory NetworkPackage.fromJson(Map<String, dynamic> json) => NetworkPackage( factory NetworkPackage.fromJson(Map<String, dynamic> json) => NetworkPackage(
method: json["w"], method: json['w'],
message: json["m"], message: json['m'],
payload: json["p"], payload: json['p'],
); );
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
"w": method, 'w': method,
"m": message, 'm': message,
"p": payload, 'p': payload,
}; };
} }

View File

@ -8,10 +8,10 @@ class PaginationResult {
}); });
factory PaginationResult.fromJson(Map<String, dynamic> json) => factory PaginationResult.fromJson(Map<String, dynamic> json) =>
PaginationResult(count: json["count"], data: json["data"]); PaginationResult(count: json['count'], data: json['data']);
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
"count": count, 'count': count,
"data": data, 'data': data,
}; };
} }

View File

@ -18,6 +18,8 @@ class Post {
List<Attachment>? attachments; List<Attachment>? attachments;
Map<String, dynamic>? reactionList; Map<String, dynamic>? reactionList;
String get dataset => '${modelType}s';
Post({ Post({
required this.id, required this.id,
required this.createdAt, required this.createdAt,
@ -38,46 +40,46 @@ class Post {
}); });
factory Post.fromJson(Map<String, dynamic> json) => Post( factory Post.fromJson(Map<String, dynamic> json) => Post(
id: json["id"], id: json['id'],
createdAt: DateTime.parse(json["created_at"]), createdAt: DateTime.parse(json['created_at']),
updatedAt: DateTime.parse(json["updated_at"]), updatedAt: DateTime.parse(json['updated_at']),
deletedAt: json["deleted_at"], deletedAt: json['deleted_at'],
alias: json["alias"], alias: json['alias'],
title: json["title"], title: json['title'],
description: json["description"], description: json['description'],
content: json["content"], content: json['content'],
modelType: json["model_type"], modelType: json['model_type'],
commentCount: json["comment_count"], commentCount: json['comment_count'],
reactionCount: json["reaction_count"], reactionCount: json['reaction_count'],
authorId: json["author_id"], authorId: json['author_id'],
realmId: json["realm_id"], realmId: json['realm_id'],
author: Author.fromJson(json["author"]), author: Author.fromJson(json['author']),
attachments: json["attachments"] != null attachments: json['attachments'] != null
? List<Attachment>.from( ? List<Attachment>.from(
json["attachments"].map((x) => Attachment.fromJson(x))) json['attachments'].map((x) => Attachment.fromJson(x)))
: List.empty(), : List.empty(),
reactionList: json["reaction_list"], reactionList: json['reaction_list'],
); );
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
"id": id, 'id': id,
"created_at": createdAt.toIso8601String(), 'created_at': createdAt.toIso8601String(),
"updated_at": updatedAt.toIso8601String(), 'updated_at': updatedAt.toIso8601String(),
"deleted_at": deletedAt, 'deleted_at': deletedAt,
"alias": alias, 'alias': alias,
"title": title, 'title': title,
"description": description, 'description': description,
"content": content, 'content': content,
"model_type": modelType, 'model_type': modelType,
"comment_count": commentCount, 'comment_count': commentCount,
"reaction_count": reactionCount, 'reaction_count': reactionCount,
"author_id": authorId, 'author_id': authorId,
"realm_id": realmId, 'realm_id': realmId,
"author": author.toJson(), 'author': author.toJson(),
"attachments": attachments == null 'attachments': attachments == null
? List.empty() ? List.empty()
: List<dynamic>.from(attachments!.map((x) => x.toJson())), : List<dynamic>.from(attachments!.map((x) => x.toJson())),
"reaction_list": reactionList, 'reaction_list': reactionList,
}; };
} }
@ -117,38 +119,38 @@ class Attachment {
}); });
factory Attachment.fromJson(Map<String, dynamic> json) => Attachment( factory Attachment.fromJson(Map<String, dynamic> json) => Attachment(
id: json["id"], id: json['id'],
createdAt: DateTime.parse(json["created_at"]), createdAt: DateTime.parse(json['created_at']),
updatedAt: DateTime.parse(json["updated_at"]), updatedAt: DateTime.parse(json['updated_at']),
deletedAt: json["deleted_at"], deletedAt: json['deleted_at'],
fileId: json["file_id"], fileId: json['file_id'],
filesize: json["filesize"], filesize: json['filesize'],
filename: json["filename"], filename: json['filename'],
mimetype: json["mimetype"], mimetype: json['mimetype'],
type: json["type"], type: json['type'],
externalUrl: json["external_url"], externalUrl: json['external_url'],
author: Author.fromJson(json["author"]), author: Author.fromJson(json['author']),
articleId: json["article_id"], articleId: json['article_id'],
momentId: json["moment_id"], momentId: json['moment_id'],
commentId: json["comment_id"], commentId: json['comment_id'],
authorId: json["author_id"], authorId: json['author_id'],
); );
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
"id": id, 'id': id,
"created_at": createdAt.toIso8601String(), 'created_at': createdAt.toIso8601String(),
"updated_at": updatedAt.toIso8601String(), 'updated_at': updatedAt.toIso8601String(),
"deleted_at": deletedAt, 'deleted_at': deletedAt,
"file_id": fileId, 'file_id': fileId,
"filesize": filesize, 'filesize': filesize,
"filename": filename, 'filename': filename,
"mimetype": mimetype, 'mimetype': mimetype,
"type": type, 'type': type,
"external_url": externalUrl, 'external_url': externalUrl,
"author": author.toJson(), 'author': author.toJson(),
"article_id": articleId, 'article_id': articleId,
"moment_id": momentId, 'moment_id': momentId,
"comment_id": commentId, 'comment_id': commentId,
"author_id": authorId, 'author_id': authorId,
}; };
} }

View File

@ -14,12 +14,12 @@ class AuthProvider extends ChangeNotifier {
final userinfoEndpoint = getRequestUri('passport', '/api/users/me'); final userinfoEndpoint = getRequestUri('passport', '/api/users/me');
final redirectUrl = Uri.parse('solian://auth'); final redirectUrl = Uri.parse('solian://auth');
static const clientId = "solian"; static const clientId = 'solian';
static const clientSecret = "_F4%q2Eea3"; static const clientSecret = '_F4%q2Eea3';
static const storage = FlutterSecureStorage(); static const storage = FlutterSecureStorage();
static const storageKey = "identity"; static const storageKey = 'identity';
static const profileKey = "profiles"; static const profileKey = 'profiles';
/// Before use this variable to make request /// Before use this variable to make request
/// **MAKE SURE YOU HAVE CALL THE isAuthorized() METHOD** /// **MAKE SURE YOU HAVE CALL THE isAuthorized() METHOD**
@ -57,7 +57,7 @@ class AuthProvider extends ChangeNotifier {
password, password,
identifier: clientId, identifier: clientId,
secret: clientSecret, secret: clientSecret,
scopes: ["openid"], scopes: ['openid'],
basicAuth: false, basicAuth: false,
); );
} }
@ -115,6 +115,6 @@ class AuthProvider extends ChangeNotifier {
Future<dynamic> getProfiles() async { Future<dynamic> getProfiles() async {
const storage = FlutterSecureStorage(); const storage = FlutterSecureStorage();
return jsonDecode(await storage.read(key: profileKey) ?? "{}"); return jsonDecode(await storage.read(key: profileKey) ?? '{}');
} }
} }

View File

@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:http/http.dart';
import 'package:livekit_client/livekit_client.dart'; import 'package:livekit_client/livekit_client.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -17,9 +18,11 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class ChatProvider extends ChangeNotifier { class ChatProvider extends ChangeNotifier {
bool isOpened = false; bool isOpened = false;
bool isShown = false; bool isCallShown = false;
ChatCallInstance? call; Call? ongoingCall;
Channel? focusChannel;
ChatCallInstance? currentCall;
Future<WebSocketChannel?> connect(AuthProvider auth) async { Future<WebSocketChannel?> connect(AuthProvider auth) async {
if (auth.client == null) await auth.loadClient(); if (auth.client == null) await auth.loadClient();
@ -43,17 +46,51 @@ class ChatProvider extends ChangeNotifier {
return channel; return channel;
} }
bool handleCall(Call call, Channel channel, Future<Channel> fetchChannel(String alias) async {
{Function? onUpdate, Function? onDispose}) { final Client client = Client();
if (this.call != null) return false;
this.call = ChatCallInstance( var uri = getRequestUri('messaging', '/api/channels/$alias');
var res = await client.get(uri);
if (res.statusCode == 200) {
final result = jsonDecode(utf8.decode(res.bodyBytes));
focusChannel = Channel.fromJson(result);
notifyListeners();
return focusChannel!;
} else {
var message = utf8.decode(res.bodyBytes);
throw Exception(message);
}
}
Future<Call?> fetchOngoingCall(String alias) async {
final Client client = Client();
var uri = getRequestUri('messaging', '/api/channels/$alias/calls/ongoing');
var res = await client.get(uri);
if (res.statusCode == 200) {
final result = jsonDecode(utf8.decode(res.bodyBytes));
ongoingCall = Call.fromJson(result);
notifyListeners();
return ongoingCall;
} else if (res.statusCode != 404) {
var message = utf8.decode(res.bodyBytes);
throw Exception(message);
} else {
return null;
}
}
bool handleCallJoin(Call call, Channel channel,
{Function? onUpdate, Function? onDispose}) {
if (currentCall != null) return false;
currentCall = ChatCallInstance(
onUpdate: () { onUpdate: () {
notifyListeners(); notifyListeners();
if (onUpdate != null) onUpdate(); if (onUpdate != null) onUpdate();
}, },
onDispose: () { onDispose: () {
this.call = null; currentCall = null;
notifyListeners(); notifyListeners();
if (onDispose != null) onDispose(); if (onDispose != null) onDispose();
}, },
@ -64,8 +101,13 @@ class ChatProvider extends ChangeNotifier {
return true; return true;
} }
void setShown(bool state) { void setOngoingCall(Call? item) {
isShown = state; ongoingCall = item;
notifyListeners();
}
void setCallShown(bool state) {
isCallShown = state;
notifyListeners(); notifyListeners();
} }
} }
@ -118,8 +160,9 @@ class ChatCallInstance {
} }
Future<void> checkPermissions() async { Future<void> checkPermissions() async {
if (lkPlatformIs(PlatformType.macOS) || lkPlatformIs(PlatformType.linux)) if (lkPlatformIs(PlatformType.macOS) || lkPlatformIs(PlatformType.linux)) {
return; return;
}
await Permission.camera.request(); await Permission.camera.request();
await Permission.microphone.request(); await Permission.microphone.request();
@ -133,7 +176,7 @@ class ChatCallInstance {
final auth = context.read<AuthProvider>(); final auth = context.read<AuthProvider>();
if (!await auth.isAuthorized()) { if (!await auth.isAuthorized()) {
onDispose(); onDispose();
throw Exception("unauthorized"); throw Exception('unauthorized');
} }
var uri = getRequestUri( var uri = getRequestUri(

View File

@ -29,7 +29,7 @@ class NotifyProvider extends ChangeNotifier {
const androidSettings = AndroidInitializationSettings('app_icon'); const androidSettings = AndroidInitializationSettings('app_icon');
const darwinSettings = DarwinInitializationSettings( const darwinSettings = DarwinInitializationSettings(
notificationCategories: [ notificationCategories: [
DarwinNotificationCategory("general"), DarwinNotificationCategory('general'),
], ],
); );
const linuxSettings = const linuxSettings =
@ -46,8 +46,9 @@ class NotifyProvider extends ChangeNotifier {
} }
Future<void> requestPermissions() async { Future<void> requestPermissions() async {
if (lkPlatformIs(PlatformType.macOS) || lkPlatformIs(PlatformType.linux)) if (lkPlatformIs(PlatformType.macOS) || lkPlatformIs(PlatformType.linux)) {
return; return;
}
await Permission.notification.request(); await Permission.notification.request();
} }

View File

@ -97,7 +97,7 @@ class NameCard extends StatelessWidget {
Future<Widget> renderAvatar(BuildContext context) async { Future<Widget> renderAvatar(BuildContext context) async {
final auth = context.read<AuthProvider>(); final auth = context.read<AuthProvider>();
final profiles = await auth.getProfiles(); final profiles = await auth.getProfiles();
return AccountAvatar(source: profiles["picture"], direct: true); return AccountAvatar(source: profiles['picture'], direct: true);
} }
Future<Column> renderLabel(BuildContext context) async { Future<Column> renderLabel(BuildContext context) async {
@ -107,13 +107,13 @@ class NameCard extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
profiles["nick"], profiles['nick'],
style: const TextStyle( style: const TextStyle(
fontSize: 20, fontSize: 20,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
Text(profiles["email"]) Text(profiles['email'])
], ],
); );
} }

View File

@ -157,8 +157,9 @@ class _FriendScreenState extends State<FriendScreen> {
DismissDirection getDismissDirection(Friendship relation) { DismissDirection getDismissDirection(Friendship relation) {
if (relation.status == 2) return DismissDirection.endToStart; if (relation.status == 2) return DismissDirection.endToStart;
if (relation.status == 1) return DismissDirection.startToEnd; if (relation.status == 1) return DismissDirection.startToEnd;
if (relation.status == 0 && relation.relatedId != _selfId) if (relation.status == 0 && relation.relatedId != _selfId) {
return DismissDirection.startToEnd; return DismissDirection.startToEnd;
}
return DismissDirection.horizontal; return DismissDirection.horizontal;
} }

View File

@ -1,41 +0,0 @@
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class AuthorizationScreen extends StatelessWidget {
final Uri authorizationUrl;
const AuthorizationScreen(this.authorizationUrl, {super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(AppLocalizations.of(context)!.signIn),
),
body: Stack(children: [
WebViewWidget(
controller: WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setBackgroundColor(Colors.white)
..setNavigationDelegate(NavigationDelegate(
onNavigationRequest: (NavigationRequest request) {
if (request.url.startsWith('solian')) {
Navigator.of(context).pop(request.url);
WebViewCookieManager().clearCookies();
return NavigationDecision.prevent;
} else if (request.url.contains("sign-up")) {
launchUrl(Uri.parse(request.url));
return NavigationDecision.prevent;
}
return NavigationDecision.navigate;
},
))
..loadRequest(authorizationUrl)
..clearCache(),
),
]),
);
}
}

View File

@ -24,14 +24,14 @@ class _ChatCallState extends State<ChatCall> {
late ChatProvider _chat; late ChatProvider _chat;
ChatCallInstance get _call => _chat.call!; ChatCallInstance get _call => _chat.currentCall!;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
_chat.setShown(true); _chat.setCallShown(true);
}); });
} }
@ -40,13 +40,13 @@ class _ChatCallState extends State<ChatCall> {
_chat = context.watch<ChatProvider>(); _chat = context.watch<ChatProvider>();
if (!_isHandled) { if (!_isHandled) {
_isHandled = true; _isHandled = true;
if (_chat.handleCall(widget.call, widget.call.channel)) { if (_chat.handleCallJoin(widget.call, widget.call.channel)) {
_chat.call?.init(); _chat.currentCall?.init();
} }
} }
Widget content; Widget content;
if (_chat.call == null) { if (_chat.currentCall == null) {
content = const Center( content = const Center(
child: CircularProgressIndicator(), child: CircularProgressIndicator(),
); );
@ -136,7 +136,7 @@ class _ChatCallState extends State<ChatCall> {
@override @override
void deactivate() { void deactivate() {
WidgetsBinding.instance.addPostFrameCallback((_) => _chat.setShown(false)); WidgetsBinding.instance.addPostFrameCallback((_) => _chat.setCallShown(false));
super.deactivate(); super.deactivate();
} }
} }

View File

@ -42,7 +42,7 @@ class _ChannelEditorScreenState extends State<ChannelEditorScreen> {
? getRequestUri('messaging', '/api/channels') ? getRequestUri('messaging', '/api/channels')
: getRequestUri('messaging', '/api/channels/${widget.editing!.id}'); : getRequestUri('messaging', '/api/channels/${widget.editing!.id}');
final req = Request(widget.editing == null ? "POST" : "PUT", uri); final req = Request(widget.editing == null ? 'POST' : 'PUT', uri);
req.headers['Content-Type'] = 'application/json'; req.headers['Content-Type'] = 'application/json';
req.body = jsonEncode(<String, dynamic>{ req.body = jsonEncode(<String, dynamic>{
'alias': _aliasController.value.text.toLowerCase(), 'alias': _aliasController.value.text.toLowerCase(),

View File

@ -4,69 +4,52 @@ import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_animate/flutter_animate.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:solian/models/call.dart';
import 'package:solian/models/channel.dart';
import 'package:solian/models/message.dart'; import 'package:solian/models/message.dart';
import 'package:solian/models/pagination.dart'; import 'package:solian/models/pagination.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
import 'package:solian/providers/chat.dart';
import 'package:solian/router.dart'; import 'package:solian/router.dart';
import 'package:solian/utils/service_url.dart'; import 'package:solian/utils/service_url.dart';
import 'package:solian/widgets/chat/channel_action.dart';
import 'package:solian/widgets/chat/maintainer.dart'; import 'package:solian/widgets/chat/maintainer.dart';
import 'package:solian/widgets/chat/message.dart'; import 'package:solian/widgets/chat/message.dart';
import 'package:solian/widgets/chat/message_action.dart'; import 'package:solian/widgets/chat/message_action.dart';
import 'package:solian/widgets/chat/message_editor.dart'; import 'package:solian/widgets/chat/message_editor.dart';
import 'package:solian/widgets/exts.dart';
import 'package:solian/widgets/indent_wrapper.dart'; import 'package:solian/widgets/indent_wrapper.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:http/http.dart' as http;
class ChatScreen extends StatefulWidget { class ChatScreen extends StatelessWidget {
final String alias; final String alias;
const ChatScreen({super.key, required this.alias}); const ChatScreen({super.key, required this.alias});
@override @override
State<ChatScreen> createState() => _ChatScreenState(); Widget build(BuildContext context) {
return IndentWrapper(
title: AppLocalizations.of(context)!.post,
noSafeArea: true,
hideDrawer: true,
child: ChatScreenWidget(
alias: alias,
),
);
}
} }
class _ChatScreenState extends State<ChatScreen> { class ChatScreenWidget extends StatefulWidget {
Call? _ongoingCall; final String alias;
Channel? _channelMeta;
const ChatScreenWidget({super.key, required this.alias});
@override
State<ChatScreenWidget> createState() => _ChatScreenWidgetState();
}
class _ChatScreenWidgetState extends State<ChatScreenWidget> {
bool _isReady = false;
final PagingController<int, Message> _pagingController = PagingController(firstPageKey: 0); final PagingController<int, Message> _pagingController = PagingController(firstPageKey: 0);
final http.Client _client = http.Client(); late final ChatProvider _chat;
Future<Channel> fetchMetadata() async {
var uri = getRequestUri('messaging', '/api/channels/${widget.alias}');
var res = await _client.get(uri);
if (res.statusCode == 200) {
final result = jsonDecode(utf8.decode(res.bodyBytes));
setState(() => _channelMeta = Channel.fromJson(result));
return _channelMeta!;
} else {
var message = utf8.decode(res.bodyBytes);
context.showErrorDialog(message);
throw Exception(message);
}
}
Future<Call?> fetchCall() async {
var uri = getRequestUri('messaging', '/api/channels/${widget.alias}/calls/ongoing');
var res = await _client.get(uri);
if (res.statusCode == 200) {
final result = jsonDecode(utf8.decode(res.bodyBytes));
setState(() => _ongoingCall = Call.fromJson(result));
return _ongoingCall;
} else if (res.statusCode != 404) {
var message = utf8.decode(res.bodyBytes);
context.showErrorDialog(message);
throw Exception(message);
} else {
return null;
}
}
Future<void> fetchMessages(int pageKey, BuildContext context) async { Future<void> fetchMessages(int pageKey, BuildContext context) async {
final auth = context.read<AuthProvider>(); final auth = context.read<AuthProvider>();
@ -142,11 +125,6 @@ class _ChatScreenState extends State<ChatScreen> {
@override @override
void initState() { void initState() {
Future.delayed(Duration.zero, () {
fetchMetadata();
fetchCall();
});
_pagingController.addPageRequestListener((pageKey) => fetchMessages(pageKey, context)); _pagingController.addPageRequestListener((pageKey) => fetchMessages(pageKey, context));
super.initState(); super.initState();
@ -180,6 +158,11 @@ class _ChatScreenState extends State<ChatScreen> {
); );
} }
if (!_isReady) {
_isReady = true;
_chat = context.watch<ChatProvider>();
}
final callBanner = MaterialBanner( final callBanner = MaterialBanner(
padding: const EdgeInsets.only(top: 4, bottom: 4, left: 20), padding: const EdgeInsets.only(top: 4, bottom: 4, left: 20),
leading: const Icon(Icons.call_received), leading: const Icon(Icons.call_received),
@ -192,7 +175,7 @@ class _ChatScreenState extends State<ChatScreen> {
onPressed: () { onPressed: () {
router.pushNamed( router.pushNamed(
'chat.channel.call', 'chat.channel.call',
extra: _ongoingCall, extra: _chat.ongoingCall,
pathParameters: {'channel': widget.alias}, pathParameters: {'channel': widget.alias},
); );
}, },
@ -200,24 +183,8 @@ class _ChatScreenState extends State<ChatScreen> {
], ],
); );
return IndentWrapper( return FutureBuilder(
hideDrawer: true, future: _chat.fetchChannel(widget.alias),
title: _channelMeta?.name ?? 'Loading...',
appBarActions: _channelMeta != null
? [
ChannelCallAction(
call: _ongoingCall,
channel: _channelMeta!,
onUpdate: () => fetchMetadata(),
),
ChannelManageAction(
channel: _channelMeta!,
onUpdate: () => fetchMetadata(),
),
]
: [],
child: FutureBuilder(
future: fetchMetadata(),
builder: (context, snapshot) { builder: (context, snapshot) {
if (!snapshot.hasData || snapshot.data == null) { if (!snapshot.hasData || snapshot.data == null) {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
@ -252,17 +219,16 @@ class _ChatScreenState extends State<ChatScreen> {
), ),
], ],
), ),
_ongoingCall != null ? callBanner.animate().slideY() : Container(), _chat.ongoingCall != null ? callBanner.animate().slideY() : Container(),
], ],
), ),
onInsertMessage: (message) => addMessage(message), onInsertMessage: (message) => addMessage(message),
onUpdateMessage: (message) => updateMessage(message), onUpdateMessage: (message) => updateMessage(message),
onDeleteMessage: (message) => deleteMessage(message), onDeleteMessage: (message) => deleteMessage(message),
onCallStarted: (call) => setState(() => _ongoingCall = call), onCallStarted: (call) => _chat.setOngoingCall(call),
onCallEnded: () => setState(() => _ongoingCall = null), onCallEnded: () => _chat.setOngoingCall(null),
); );
}, },
),
); );
} }
} }

View File

@ -5,8 +5,10 @@ import 'package:provider/provider.dart';
import 'package:solian/models/channel.dart'; import 'package:solian/models/channel.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
import 'package:solian/router.dart'; import 'package:solian/router.dart';
import 'package:solian/screens/chat/chat.dart';
import 'package:solian/utils/service_url.dart'; import 'package:solian/utils/service_url.dart';
import 'package:solian/widgets/chat/chat_new.dart'; import 'package:solian/widgets/chat/chat_new.dart';
import 'package:solian/widgets/empty.dart';
import 'package:solian/widgets/exts.dart'; import 'package:solian/widgets/exts.dart';
import 'package:solian/widgets/indent_wrapper.dart'; import 'package:solian/widgets/indent_wrapper.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
@ -21,6 +23,68 @@ class ChatIndexScreen extends StatefulWidget {
} }
class _ChatIndexScreenState extends State<ChatIndexScreen> { class _ChatIndexScreenState extends State<ChatIndexScreen> {
Channel? _selectedChannel;
@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
final isLargeScreen = screenWidth >= 600;
return IndentWrapper(
title: AppLocalizations.of(context)!.chat,
appBarActions: const [NotificationButton()],
fixedAppBarColor: isLargeScreen,
child: isLargeScreen
? Row(
children: [
Flexible(
flex: 2,
child: ChatIndexScreenWidget(
onSelect: (item) {
setState(() => _selectedChannel = item);
},
),
),
const VerticalDivider(thickness: 0.3, width: 0.3),
Flexible(
flex: 4,
child: _selectedChannel == null
? const SelectionEmptyWidget()
: ChatScreenWidget(
key: Key('c${_selectedChannel!.id}'),
alias: _selectedChannel!.alias,
),
),
],
)
: ChatIndexScreenWidget(
onSelect: (item) async {
final result = await router.pushNamed(
'chat.channel',
pathParameters: {
'channel': item.alias,
},
);
switch (result) {
case 'refresh':
// fetchChannels();
}
},
),
);
}
}
class ChatIndexScreenWidget extends StatefulWidget {
final Function(Channel item) onSelect;
const ChatIndexScreenWidget({super.key, required this.onSelect});
@override
State<ChatIndexScreenWidget> createState() => _ChatIndexScreenWidgetState();
}
class _ChatIndexScreenWidgetState extends State<ChatIndexScreenWidget> {
List<Channel> _channels = List.empty(); List<Channel> _channels = List.empty();
Future<void> fetchChannels() async { Future<void> fetchChannels() async {
@ -61,9 +125,7 @@ class _ChatIndexScreenState extends State<ChatIndexScreen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final auth = context.read<AuthProvider>(); final auth = context.read<AuthProvider>();
return IndentWrapper( return Scaffold(
title: AppLocalizations.of(context)!.chat,
appBarActions: const [NotificationButton()],
floatingActionButton: FutureBuilder( floatingActionButton: FutureBuilder(
future: auth.isAuthorized(), future: auth.isAuthorized(),
builder: (context, snapshot) { builder: (context, snapshot) {
@ -78,7 +140,7 @@ class _ChatIndexScreenState extends State<ChatIndexScreen> {
} }
}, },
), ),
child: FutureBuilder( body: FutureBuilder(
future: auth.isAuthorized(), future: auth.isAuthorized(),
builder: (context, snapshot) { builder: (context, snapshot) {
if (!snapshot.hasData || !snapshot.data!) { if (!snapshot.hasData || !snapshot.data!) {
@ -98,23 +160,13 @@ class _ChatIndexScreenState extends State<ChatIndexScreen> {
), ),
title: Text(element.name), title: Text(element.name),
subtitle: Text(element.description), subtitle: Text(element.description),
onTap: () async { onTap: () => widget.onSelect(element),
final result = await router.pushNamed(
'chat.channel',
pathParameters: {
'channel': element.alias,
},
);
switch (result) {
case 'refresh':
fetchChannels();
}
},
); );
}, },
), ),
); );
}), },
),
); );
} }
} }

View File

@ -6,10 +6,12 @@ import 'package:solian/models/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/router.dart';
import 'package:solian/screens/posts/screen.dart';
import 'package:solian/utils/service_url.dart'; import 'package:solian/utils/service_url.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:solian/widgets/empty.dart';
import 'package:solian/widgets/indent_wrapper.dart'; import 'package:solian/widgets/indent_wrapper.dart';
import 'package:solian/widgets/notification_notifier.dart'; import 'package:solian/widgets/notification_notifier.dart';
import 'package:solian/widgets/posts/item.dart'; import 'package:solian/widgets/posts/item.dart';
@ -22,8 +24,68 @@ class ExploreScreen extends StatefulWidget {
} }
class _ExploreScreenState extends State<ExploreScreen> { class _ExploreScreenState extends State<ExploreScreen> {
final PagingController<int, Post> _pagingController = Post? _selectedPost;
PagingController(firstPageKey: 0);
@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
final isLargeScreen = screenWidth >= 600;
return IndentWrapper(
noSafeArea: true,
fixedAppBarColor: isLargeScreen,
appBarActions: const [NotificationButton()],
title: AppLocalizations.of(context)!.explore,
child: isLargeScreen
? Row(
children: [
Flexible(
flex: 2,
child: ExploreScreenWidget(
onSelect: (item) {
setState(() => _selectedPost = item);
},
),
),
const VerticalDivider(thickness: 0.3, width: 0.3),
Flexible(
flex: 4,
child: _selectedPost == null
? const SelectionEmptyWidget()
: PostScreenWidget(
key: Key('p${_selectedPost!.id}'),
dataset: _selectedPost!.dataset,
alias: _selectedPost!.alias,
),
),
],
)
: ExploreScreenWidget(
onSelect: (item) {
router.pushNamed(
'posts.screen',
pathParameters: {
'alias': item.alias,
'dataset': item.dataset,
},
);
},
),
);
}
}
class ExploreScreenWidget extends StatefulWidget {
final Function(Post item) onSelect;
const ExploreScreenWidget({super.key, required this.onSelect});
@override
State<ExploreScreenWidget> createState() => _ExploreScreenWidgetState();
}
class _ExploreScreenWidgetState extends State<ExploreScreenWidget> {
final PagingController<int, Post> _pagingController = PagingController(firstPageKey: 0);
final http.Client _client = http.Client(); final http.Client _client = http.Client();
@ -31,15 +93,12 @@ class _ExploreScreenState extends State<ExploreScreen> {
final offset = pageKey; final offset = pageKey;
const take = 5; const take = 5;
var uri = var uri = getRequestUri('interactive', '/api/feed?take=$take&offset=$offset');
getRequestUri('interactive', '/api/feed?take=$take&offset=$offset');
var res = await _client.get(uri); var res = await _client.get(uri);
if (res.statusCode == 200) { if (res.statusCode == 200) {
final result = final result = PaginationResult.fromJson(jsonDecode(utf8.decode(res.bodyBytes)));
PaginationResult.fromJson(jsonDecode(utf8.decode(res.bodyBytes))); final items = result.data?.map((x) => Post.fromJson(x)).toList() ?? List.empty();
final items =
result.data?.map((x) => Post.fromJson(x)).toList() ?? List.empty();
final isLastPage = (result.count - pageKey) < take; final isLastPage = (result.count - pageKey) < take;
if (isLastPage || result.data == null) { if (isLastPage || result.data == null) {
_pagingController.appendLastPage(items); _pagingController.appendLastPage(items);
@ -63,8 +122,7 @@ class _ExploreScreenState extends State<ExploreScreen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final auth = context.read<AuthProvider>(); final auth = context.read<AuthProvider>();
return IndentWrapper( return Scaffold(
noSafeArea: true,
floatingActionButton: FutureBuilder( floatingActionButton: FutureBuilder(
future: auth.isAuthorized(), future: auth.isAuthorized(),
builder: (context, snapshot) { builder: (context, snapshot) {
@ -72,7 +130,7 @@ class _ExploreScreenState extends State<ExploreScreen> {
return FloatingActionButton( return FloatingActionButton(
child: const Icon(Icons.edit), child: const Icon(Icons.edit),
onPressed: () async { onPressed: () async {
final did = await router.pushNamed("posts.moments.editor"); final did = await router.pushNamed('posts.moments.editor');
if (did == true) _pagingController.refresh(); if (did == true) _pagingController.refresh();
}, },
); );
@ -81,9 +139,7 @@ class _ExploreScreenState extends State<ExploreScreen> {
} }
}, },
), ),
appBarActions: const [NotificationButton()], body: RefreshIndicator(
title: AppLocalizations.of(context)!.explore,
child: RefreshIndicator(
onRefresh: () => Future.sync( onRefresh: () => Future.sync(
() => _pagingController.refresh(), () => _pagingController.refresh(),
), ),
@ -93,15 +149,7 @@ class _ExploreScreenState extends State<ExploreScreen> {
itemBuilder: (context, item, index) => PostItem( itemBuilder: (context, item, index) => PostItem(
item: item, item: item,
onUpdate: () => _pagingController.refresh(), onUpdate: () => _pagingController.refresh(),
onTap: () { onTap: () => widget.onSelect(item),
router.pushNamed(
'posts.screen',
pathParameters: {
'alias': item.alias,
'dataset': '${item.modelType}s',
},
);
},
), ),
), ),
), ),

View File

@ -97,7 +97,7 @@ class NotificationItem extends StatelessWidget {
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
left: 16, right: 16, top: 34, bottom: 12), left: 16, right: 16, top: 34, bottom: 12),
child: Text( child: Text(
"Links", 'Links',
style: Theme.of(context).textTheme.headlineSmall, style: Theme.of(context).textTheme.headlineSmall,
), ),
), ),
@ -151,7 +151,7 @@ class NotificationItem extends StatelessWidget {
text: item.subject, text: item.subject,
style: const TextStyle(fontWeight: FontWeight.bold), style: const TextStyle(fontWeight: FontWeight.bold),
), ),
const TextSpan(text: " is marked as read") const TextSpan(text: ' is marked as read')
], ],
), ),
), ),

View File

@ -65,7 +65,7 @@ class _CommentEditorScreenState extends State<CommentEditorScreen> {
? getRequestUri('interactive', '/api/p/$relatedDataset/$alias/comments') ? getRequestUri('interactive', '/api/p/$relatedDataset/$alias/comments')
: getRequestUri('interactive', '/api/p/comments/${widget.editing!.id}'); : getRequestUri('interactive', '/api/p/comments/${widget.editing!.id}');
final req = Request(widget.editing == null ? "POST" : "PUT", uri); final req = Request(widget.editing == null ? 'POST' : 'PUT', uri);
req.headers['Content-Type'] = 'application/json'; req.headers['Content-Type'] = 'application/json';
req.body = jsonEncode(<String, dynamic>{ req.body = jsonEncode(<String, dynamic>{
'alias': _alias, 'alias': _alias,
@ -140,12 +140,12 @@ class _CommentEditorScreenState extends State<CommentEditorScreen> {
if (snapshot.hasData) { if (snapshot.hasData) {
var userinfo = snapshot.data; var userinfo = snapshot.data;
return ListTile( return ListTile(
title: Text(userinfo["nick"]), title: Text(userinfo['nick']),
subtitle: Text( subtitle: Text(
AppLocalizations.of(context)!.postIdentityNotify, AppLocalizations.of(context)!.postIdentityNotify,
), ),
leading: AccountAvatar( leading: AccountAvatar(
source: userinfo["picture"], source: userinfo['picture'],
direct: true, direct: true,
), ),
); );

View File

@ -55,7 +55,7 @@ class _MomentEditorScreenState extends State<MomentEditorScreen> {
? getRequestUri('interactive', '/api/p/moments') ? getRequestUri('interactive', '/api/p/moments')
: getRequestUri('interactive', '/api/p/moments/${widget.editing!.id}'); : getRequestUri('interactive', '/api/p/moments/${widget.editing!.id}');
final req = Request(widget.editing == null ? "POST" : "PUT", uri); final req = Request(widget.editing == null ? 'POST' : 'PUT', uri);
req.headers['Content-Type'] = 'application/json'; req.headers['Content-Type'] = 'application/json';
req.body = jsonEncode(<String, dynamic>{ req.body = jsonEncode(<String, dynamic>{
'alias': _alias, 'alias': _alias,
@ -130,12 +130,12 @@ class _MomentEditorScreenState extends State<MomentEditorScreen> {
if (snapshot.hasData) { if (snapshot.hasData) {
var userinfo = snapshot.data; var userinfo = snapshot.data;
return ListTile( return ListTile(
title: Text(userinfo["nick"]), title: Text(userinfo['nick']),
subtitle: Text( subtitle: Text(
AppLocalizations.of(context)!.postIdentityNotify, AppLocalizations.of(context)!.postIdentityNotify,
), ),
leading: AccountAvatar( leading: AccountAvatar(
source: userinfo["picture"], source: userinfo['picture'],
direct: true, direct: true,
), ),
); );

View File

@ -11,25 +11,43 @@ import 'package:solian/widgets/posts/comment_list.dart';
import 'package:solian/widgets/posts/item.dart'; import 'package:solian/widgets/posts/item.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class PostScreen extends StatefulWidget { class PostScreen extends StatelessWidget {
final String dataset; final String dataset;
final String alias; final String alias;
const PostScreen({super.key, required this.alias, required this.dataset}); const PostScreen({super.key, required this.alias, required this.dataset});
@override @override
State<PostScreen> createState() => _PostScreenState(); Widget build(BuildContext context) {
return IndentWrapper(
title: AppLocalizations.of(context)!.post,
noSafeArea: true,
hideDrawer: true,
child: PostScreenWidget(
dataset: dataset,
alias: alias,
),
);
}
} }
class _PostScreenState extends State<PostScreen> { class PostScreenWidget extends StatefulWidget {
final String dataset;
final String alias;
const PostScreenWidget({super.key, required this.dataset, required this.alias});
@override
State<PostScreenWidget> createState() => _PostScreenWidgetState();
}
class _PostScreenWidgetState extends State<PostScreenWidget> {
final _client = http.Client(); final _client = http.Client();
final PagingController<int, Post> _commentPagingController = final PagingController<int, Post> _commentPagingController = PagingController(firstPageKey: 0);
PagingController(firstPageKey: 0);
Future<Post?> fetchPost(BuildContext context) async { Future<Post?> fetchPost(BuildContext context) async {
final uri = getRequestUri( final uri = getRequestUri('interactive', '/api/p/${widget.dataset}/${widget.alias}');
'interactive', '/api/p/${widget.dataset}/${widget.alias}');
final res = await _client.get(uri); final res = await _client.get(uri);
if (res.statusCode != 200) { if (res.statusCode != 200) {
final err = utf8.decode(res.bodyBytes); final err = utf8.decode(res.bodyBytes);
@ -42,11 +60,7 @@ class _PostScreenState extends State<PostScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return IndentWrapper( return FutureBuilder(
noSafeArea: true,
hideDrawer: true,
title: AppLocalizations.of(context)!.post,
child: FutureBuilder(
future: fetchPost(context), future: fetchPost(context),
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.hasData && snapshot.data != null) { if (snapshot.hasData && snapshot.data != null) {
@ -78,7 +92,6 @@ class _PostScreenState extends State<PostScreen> {
); );
} }
}, },
),
); );
} }

View File

@ -14,7 +14,7 @@ class CallOverlay extends StatelessWidget {
final chat = context.watch<ChatProvider>(); final chat = context.watch<ChatProvider>();
if (chat.isShown || chat.call == null) { if (chat.isCallShown || chat.currentCall == null) {
return Container(); return Container();
} }
@ -54,8 +54,8 @@ class CallOverlay extends StatelessWidget {
onTap: () { onTap: () {
router.pushNamed( router.pushNamed(
'chat.channel.call', 'chat.channel.call',
extra: chat.call!.info, extra: chat.currentCall!.info,
pathParameters: {'channel': chat.call!.channel.alias}, pathParameters: {'channel': chat.currentCall!.channel.alias},
); );
}, },
); );

View File

@ -73,9 +73,9 @@ class _ControlsWidgetState extends State<ControlsWidget> {
if (await context.showDisconnectDialog() != true) return; if (await context.showDisconnectDialog() != true) return;
final chat = context.read<ChatProvider>(); final chat = context.read<ChatProvider>();
if (chat.call != null) { if (chat.currentCall != null) {
chat.call!.deactivate(); chat.currentCall!.deactivate();
chat.call!.dispose(); chat.currentCall!.dispose();
router.pop(); router.pop();
} }
} }

View File

@ -55,23 +55,19 @@ class _ChatMaintainerState extends State<ChatMaintainer> {
switch (result.method) { switch (result.method) {
case 'messages.new': case 'messages.new':
final payload = Message.fromJson(result.payload!); final payload = Message.fromJson(result.payload!);
if (payload.channelId == widget.channel.id) if (payload.channelId == widget.channel.id) widget.onInsertMessage(payload);
widget.onInsertMessage(payload);
break; break;
case 'messages.update': case 'messages.update':
final payload = Message.fromJson(result.payload!); final payload = Message.fromJson(result.payload!);
if (payload.channelId == widget.channel.id) if (payload.channelId == widget.channel.id) widget.onUpdateMessage(payload);
widget.onUpdateMessage(payload);
break; break;
case 'messages.burnt': case 'messages.burnt':
final payload = Message.fromJson(result.payload!); final payload = Message.fromJson(result.payload!);
if (payload.channelId == widget.channel.id) if (payload.channelId == widget.channel.id) widget.onDeleteMessage(payload);
widget.onDeleteMessage(payload);
break; break;
case 'calls.new': case 'calls.new':
final payload = Call.fromJson(result.payload!); final payload = Call.fromJson(result.payload!);
if (payload.channelId == widget.channel.id) if (payload.channelId == widget.channel.id) widget.onCallStarted(payload);
widget.onCallStarted(payload);
break; break;
case 'calls.end': case 'calls.end':
final payload = Call.fromJson(result.payload!); final payload = Call.fromJson(result.payload!);

View File

@ -56,7 +56,7 @@ class _ChatMessageEditorState extends State<ChatMessageEditor> {
? getRequestUri('messaging', '/api/channels/${widget.channel}/messages') ? getRequestUri('messaging', '/api/channels/${widget.channel}/messages')
: getRequestUri('messaging', '/api/channels/${widget.channel}/messages/${widget.editing!.id}'); : getRequestUri('messaging', '/api/channels/${widget.channel}/messages/${widget.editing!.id}');
final req = Request(widget.editing == null ? "POST" : "PUT", uri); final req = Request(widget.editing == null ? 'POST' : 'PUT', uri);
req.headers['Content-Type'] = 'application/json'; req.headers['Content-Type'] = 'application/json';
req.body = jsonEncode(<String, dynamic>{ req.body = jsonEncode(<String, dynamic>{
'content': _textController.value.text, 'content': _textController.value.text,
@ -163,7 +163,6 @@ class _ChatMessageEditorState extends State<ChatMessageEditor> {
focusNode: _focusNode, focusNode: _focusNode,
controller: _textController, controller: _textController,
maxLines: null, maxLines: null,
autofocus: true,
autocorrect: true, autocorrect: true,
keyboardType: TextInputType.text, keyboardType: TextInputType.text,
decoration: InputDecoration.collapsed( decoration: InputDecoration.collapsed(

View File

@ -22,7 +22,11 @@ class LayoutWrapper extends StatelessWidget {
final content = child ?? Container(); final content = child ?? Container();
return Scaffold( return Scaffold(
appBar: AppBar(title: Text(title), actions: appBarActions), appBar: AppBar(
title: Text(title),
actions: appBarActions,
centerTitle: false,
),
floatingActionButton: floatingActionButton, floatingActionButton: floatingActionButton,
drawer: const SolianNavigationDrawer(), drawer: const SolianNavigationDrawer(),
body: noSafeArea ? content : SafeArea(child: content), body: noSafeArea ? content : SafeArea(child: content),

23
lib/widgets/empty.dart Normal file
View File

@ -0,0 +1,23 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class SelectionEmptyWidget extends StatelessWidget {
const SelectionEmptyWidget({super.key});
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Image.asset('assets/logo.png', width: 64, height: 64),
const SizedBox(height: 2),
Text(
AppLocalizations.of(context)!.appName,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w900),
),
],
),
);
}
}

View File

@ -9,8 +9,8 @@ extension SolianCommonExtensions on BuildContext {
return message return message
.split(' ') .split(' ')
.map((element) => .map((element) =>
"${element[0].toUpperCase()}${element.substring(1).toLowerCase()}") '${element[0].toUpperCase()}${element.substring(1).toLowerCase()}')
.join(" "); .join(' ');
} }
return showDialog<void>( return showDialog<void>(

View File

@ -5,6 +5,7 @@ import 'package:solian/widgets/navigation_drawer.dart';
class IndentWrapper extends LayoutWrapper { class IndentWrapper extends LayoutWrapper {
final bool hideDrawer; final bool hideDrawer;
final bool fixedAppBarColor;
const IndentWrapper({ const IndentWrapper({
super.key, super.key,
@ -13,6 +14,7 @@ class IndentWrapper extends LayoutWrapper {
super.floatingActionButton, super.floatingActionButton,
super.appBarActions, super.appBarActions,
this.hideDrawer = false, this.hideDrawer = false,
this.fixedAppBarColor = false,
super.noSafeArea = false, super.noSafeArea = false,
}) : super(); }) : super();
@ -30,6 +32,8 @@ class IndentWrapper extends LayoutWrapper {
: null, : null,
title: Text(title), title: Text(title),
actions: appBarActions, actions: appBarActions,
centerTitle: false,
elevation: fixedAppBarColor ? 4 : null,
), ),
floatingActionButton: floatingActionButton, floatingActionButton: floatingActionButton,
drawer: const SolianNavigationDrawer(), drawer: const SolianNavigationDrawer(),

View File

@ -39,21 +39,21 @@ class _SolianNavigationDrawerState extends State<SolianNavigationDrawer> {
icon: const Icon(Icons.explore), icon: const Icon(Icons.explore),
label: Text(AppLocalizations.of(context)!.explore), label: Text(AppLocalizations.of(context)!.explore),
), ),
"explore", 'explore',
), ),
( (
NavigationDrawerDestination( NavigationDrawerDestination(
icon: const Icon(Icons.send), icon: const Icon(Icons.send),
label: Text(AppLocalizations.of(context)!.chat), label: Text(AppLocalizations.of(context)!.chat),
), ),
"chat", 'chat',
), ),
( (
NavigationDrawerDestination( NavigationDrawerDestination(
icon: const Icon(Icons.account_circle), icon: const Icon(Icons.account_circle),
label: Text(AppLocalizations.of(context)!.account), label: Text(AppLocalizations.of(context)!.account),
), ),
"account", 'account',
), ),
]; ];
@ -69,7 +69,7 @@ class _SolianNavigationDrawerState extends State<SolianNavigationDrawer> {
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
children: [ children: [
Image.asset("assets/logo.png", width: 26, height: 26), Image.asset('assets/logo.png', width: 26, height: 26),
const SizedBox(width: 10), const SizedBox(width: 10),
Text( Text(
AppLocalizations.of(context)!.appName, AppLocalizations.of(context)!.appName,

View File

@ -85,7 +85,7 @@ class _NotificationButtonState extends State<NotificationButton> {
child: IconButton( child: IconButton(
icon: const Icon(Icons.notifications), icon: const Icon(Icons.notifications),
onPressed: () { onPressed: () {
router.pushNamed("notification"); router.pushNamed('notification');
}, },
), ),
); );

View File

@ -112,7 +112,7 @@ class _AttachmentEditorState extends State<AttachmentEditor> {
var res = await auth.client!.send(req); var res = await auth.client!.send(req);
if (res.statusCode == 200) { if (res.statusCode == 200) {
var result = Attachment.fromJson( var result = Attachment.fromJson(
jsonDecode(utf8.decode(await res.stream.toBytes()))["info"], jsonDecode(utf8.decode(await res.stream.toBytes()))['info'],
); );
setState(() => _attachments.add(result)); setState(() => _attachments.add(result));
widget.onUpdate(_attachments); widget.onUpdate(_attachments);
@ -252,7 +252,7 @@ class _AttachmentEditorState extends State<AttachmentEditor> {
style: Theme.of(context).textTheme.titleMedium, style: Theme.of(context).textTheme.titleMedium,
), ),
Text( Text(
"${getFileType(element)} · ${formatBytes(element.filesize)}", '${getFileType(element)} · ${formatBytes(element.filesize)}',
), ),
], ],
), ),

View File

@ -121,7 +121,7 @@ class CommentListHeader extends StatelessWidget {
return TextButton( return TextButton(
onPressed: () async { onPressed: () async {
final did = await router.pushNamed( final did = await router.pushNamed(
"posts.comments.editor", 'posts.comments.editor',
extra: CommentPostArguments(related: related), extra: CommentPostArguments(related: related),
); );
if (did == true) paging.refresh(); if (did == true) paging.refresh();

View File

@ -55,7 +55,7 @@ class ArticleContent extends StatelessWidget {
}, },
imageBuilder: (url, _, __) { imageBuilder: (url, _, __) {
Uri uri; Uri uri;
if (url.toString().startsWith("/api/attachments")) { if (url.toString().startsWith('/api/attachments')) {
uri = getRequestUri('interactive', url.toString()); uri = getRequestUri('interactive', url.toString());
} else { } else {
uri = url; uri = url;

View File

@ -30,7 +30,7 @@ class _AttachmentItemState extends State<AttachmentItem> {
late final _videoPlayer = Player( late final _videoPlayer = Player(
configuration: PlayerConfiguration( configuration: PlayerConfiguration(
title: "Attachment #${getTag()}", title: 'Attachment #${getTag()}',
logLevel: MPVLogLevel.error, logLevel: MPVLogLevel.error,
), ),
); );

View File

@ -13,8 +13,8 @@ import 'package:timeago/timeago.dart' as timeago;
class PostItem extends StatefulWidget { class PostItem extends StatefulWidget {
final Post item; final Post item;
final bool? brief; final bool brief;
final bool? ripple; final bool ripple;
final Function? onUpdate; final Function? onUpdate;
final Function? onDelete; final Function? onDelete;
final Function? onTap; final Function? onTap;
@ -22,8 +22,8 @@ class PostItem extends StatefulWidget {
const PostItem({ const PostItem({
super.key, super.key,
required this.item, required this.item,
this.brief, this.brief = true,
this.ripple, this.ripple = true,
this.onUpdate, this.onUpdate,
this.onDelete, this.onDelete,
this.onTap, this.onTap,
@ -79,9 +79,9 @@ class _PostItemState extends State<PostItem> {
Widget renderContent() { Widget renderContent() {
switch (widget.item.modelType) { switch (widget.item.modelType) {
case 'article': case 'article':
return ArticleContent(item: widget.item, brief: widget.brief ?? true); return ArticleContent(item: widget.item, brief: widget.brief);
default: default:
return MomentContent(item: widget.item, brief: widget.brief ?? true); return MomentContent(item: widget.item, brief: widget.brief);
} }
} }
@ -163,7 +163,7 @@ class _PostItemState extends State<PostItem> {
Widget content; Widget content;
if (widget.brief ?? true) { if (widget.brief) {
content = Padding( content = Padding(
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
child: Column( child: Column(
@ -199,7 +199,7 @@ class _PostItemState extends State<PostItem> {
content = Column( content = Column(
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.only(left: 12, right: 12, top: 16), padding: const EdgeInsets.only(left: 20, right: 20, top: 16),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -230,17 +230,17 @@ class _PostItemState extends State<PostItem> {
child: Divider(thickness: 0.3), child: Divider(thickness: 0.3),
), ),
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
child: renderContent(), child: renderContent(),
), ),
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 8), padding: const EdgeInsets.symmetric(horizontal: 16),
child: renderAttachments(), child: renderAttachments(),
), ),
ClipRRect( ClipRRect(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 2),
child: renderReactions(), child: renderReactions(),
), ),
), ),
@ -248,9 +248,7 @@ class _PostItemState extends State<PostItem> {
); );
} }
final ripple = widget.ripple ?? true; if (widget.ripple) {
if (ripple) {
return InkWell( return InkWell(
child: content, child: content,
onTap: () { onTap: () {

View File

@ -134,8 +134,8 @@ class _ReactionActionPopupState extends State<ReactionActionPopup> {
child: ListTile( child: ListTile(
title: Text(info.value.icon), title: Text(info.value.icon),
subtitle: Text( subtitle: Text(
":${info.key}:", ':${info.key}:',
style: const TextStyle(fontFamily: "monospace"), style: const TextStyle(fontFamily: 'monospace'),
), ),
), ),
); );