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(
id: json["id"],
createdAt: DateTime.parse(json["created_at"]),
updatedAt: DateTime.parse(json["updated_at"]),
deletedAt: json["deleted_at"],
name: json["name"],
nick: json["nick"],
avatar: json["avatar"],
banner: json["banner"],
description: json["description"],
emailAddress: json["email_address"],
powerLevel: json["power_level"],
externalId: json["external_id"],
id: json['id'],
createdAt: DateTime.parse(json['created_at']),
updatedAt: DateTime.parse(json['updated_at']),
deletedAt: json['deleted_at'],
name: json['name'],
nick: json['nick'],
avatar: json['avatar'],
banner: json['banner'],
description: json['description'],
emailAddress: json['email_address'],
powerLevel: json['power_level'],
externalId: json['external_id'],
);
Map<String, dynamic> toJson() => {
"id": id,
"created_at": createdAt.toIso8601String(),
"updated_at": updatedAt.toIso8601String(),
"deleted_at": deletedAt,
"name": name,
"nick": nick,
"avatar": avatar,
"banner": banner,
"description": description,
"email_address": emailAddress,
"power_level": powerLevel,
"external_id": externalId,
'id': id,
'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt.toIso8601String(),
'deleted_at': deletedAt,
'name': name,
'nick': nick,
'avatar': avatar,
'banner': banner,
'description': description,
'email_address': emailAddress,
'power_level': powerLevel,
'external_id': externalId,
};
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,10 +8,10 @@ class PaginationResult {
});
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() => {
"count": count,
"data": data,
'count': count,
'data': data,
};
}

View File

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

View File

@ -14,12 +14,12 @@ class AuthProvider extends ChangeNotifier {
final userinfoEndpoint = getRequestUri('passport', '/api/users/me');
final redirectUrl = Uri.parse('solian://auth');
static const clientId = "solian";
static const clientSecret = "_F4%q2Eea3";
static const clientId = 'solian';
static const clientSecret = '_F4%q2Eea3';
static const storage = FlutterSecureStorage();
static const storageKey = "identity";
static const profileKey = "profiles";
static const storageKey = 'identity';
static const profileKey = 'profiles';
/// Before use this variable to make request
/// **MAKE SURE YOU HAVE CALL THE isAuthorized() METHOD**
@ -57,7 +57,7 @@ class AuthProvider extends ChangeNotifier {
password,
identifier: clientId,
secret: clientSecret,
scopes: ["openid"],
scopes: ['openid'],
basicAuth: false,
);
}
@ -115,6 +115,6 @@ class AuthProvider extends ChangeNotifier {
Future<dynamic> getProfiles() async {
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 'package:flutter/material.dart';
import 'package:http/http.dart';
import 'package:livekit_client/livekit_client.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:provider/provider.dart';
@ -17,9 +18,11 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class ChatProvider extends ChangeNotifier {
bool isOpened = false;
bool isShown = false;
bool isCallShown = false;
ChatCallInstance? call;
Call? ongoingCall;
Channel? focusChannel;
ChatCallInstance? currentCall;
Future<WebSocketChannel?> connect(AuthProvider auth) async {
if (auth.client == null) await auth.loadClient();
@ -43,17 +46,51 @@ class ChatProvider extends ChangeNotifier {
return channel;
}
bool handleCall(Call call, Channel channel,
{Function? onUpdate, Function? onDispose}) {
if (this.call != null) return false;
Future<Channel> fetchChannel(String alias) async {
final Client client = Client();
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: () {
notifyListeners();
if (onUpdate != null) onUpdate();
},
onDispose: () {
this.call = null;
currentCall = null;
notifyListeners();
if (onDispose != null) onDispose();
},
@ -64,8 +101,13 @@ class ChatProvider extends ChangeNotifier {
return true;
}
void setShown(bool state) {
isShown = state;
void setOngoingCall(Call? item) {
ongoingCall = item;
notifyListeners();
}
void setCallShown(bool state) {
isCallShown = state;
notifyListeners();
}
}
@ -118,8 +160,9 @@ class ChatCallInstance {
}
Future<void> checkPermissions() async {
if (lkPlatformIs(PlatformType.macOS) || lkPlatformIs(PlatformType.linux))
if (lkPlatformIs(PlatformType.macOS) || lkPlatformIs(PlatformType.linux)) {
return;
}
await Permission.camera.request();
await Permission.microphone.request();
@ -133,7 +176,7 @@ class ChatCallInstance {
final auth = context.read<AuthProvider>();
if (!await auth.isAuthorized()) {
onDispose();
throw Exception("unauthorized");
throw Exception('unauthorized');
}
var uri = getRequestUri(

View File

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

View File

@ -97,7 +97,7 @@ class NameCard extends StatelessWidget {
Future<Widget> renderAvatar(BuildContext context) async {
final auth = context.read<AuthProvider>();
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 {
@ -107,13 +107,13 @@ class NameCard extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
profiles["nick"],
profiles['nick'],
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
Text(profiles["email"])
Text(profiles['email'])
],
);
}

View File

@ -157,8 +157,9 @@ class _FriendScreenState extends State<FriendScreen> {
DismissDirection getDismissDirection(Friendship relation) {
if (relation.status == 2) return DismissDirection.endToStart;
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.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;
ChatCallInstance get _call => _chat.call!;
ChatCallInstance get _call => _chat.currentCall!;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
_chat.setShown(true);
_chat.setCallShown(true);
});
}
@ -40,13 +40,13 @@ class _ChatCallState extends State<ChatCall> {
_chat = context.watch<ChatProvider>();
if (!_isHandled) {
_isHandled = true;
if (_chat.handleCall(widget.call, widget.call.channel)) {
_chat.call?.init();
if (_chat.handleCallJoin(widget.call, widget.call.channel)) {
_chat.currentCall?.init();
}
}
Widget content;
if (_chat.call == null) {
if (_chat.currentCall == null) {
content = const Center(
child: CircularProgressIndicator(),
);
@ -136,7 +136,7 @@ class _ChatCallState extends State<ChatCall> {
@override
void deactivate() {
WidgetsBinding.instance.addPostFrameCallback((_) => _chat.setShown(false));
WidgetsBinding.instance.addPostFrameCallback((_) => _chat.setCallShown(false));
super.deactivate();
}
}

View File

@ -42,7 +42,7 @@ class _ChannelEditorScreenState extends State<ChannelEditorScreen> {
? getRequestUri('messaging', '/api/channels')
: 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.body = jsonEncode(<String, dynamic>{
'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:infinite_scroll_pagination/infinite_scroll_pagination.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/pagination.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/providers/chat.dart';
import 'package:solian/router.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/message.dart';
import 'package:solian/widgets/chat/message_action.dart';
import 'package:solian/widgets/chat/message_editor.dart';
import 'package:solian/widgets/exts.dart';
import 'package:solian/widgets/indent_wrapper.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;
const ChatScreen({super.key, required this.alias});
@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> {
Call? _ongoingCall;
Channel? _channelMeta;
class ChatScreenWidget extends StatefulWidget {
final String alias;
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 http.Client _client = http.Client();
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;
}
}
late final ChatProvider _chat;
Future<void> fetchMessages(int pageKey, BuildContext context) async {
final auth = context.read<AuthProvider>();
@ -142,11 +125,6 @@ class _ChatScreenState extends State<ChatScreen> {
@override
void initState() {
Future.delayed(Duration.zero, () {
fetchMetadata();
fetchCall();
});
_pagingController.addPageRequestListener((pageKey) => fetchMessages(pageKey, context));
super.initState();
@ -180,6 +158,11 @@ class _ChatScreenState extends State<ChatScreen> {
);
}
if (!_isReady) {
_isReady = true;
_chat = context.watch<ChatProvider>();
}
final callBanner = MaterialBanner(
padding: const EdgeInsets.only(top: 4, bottom: 4, left: 20),
leading: const Icon(Icons.call_received),
@ -192,7 +175,7 @@ class _ChatScreenState extends State<ChatScreen> {
onPressed: () {
router.pushNamed(
'chat.channel.call',
extra: _ongoingCall,
extra: _chat.ongoingCall,
pathParameters: {'channel': widget.alias},
);
},
@ -200,69 +183,52 @@ class _ChatScreenState extends State<ChatScreen> {
],
);
return IndentWrapper(
hideDrawer: true,
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) {
if (!snapshot.hasData || snapshot.data == null) {
return const Center(child: CircularProgressIndicator());
}
return FutureBuilder(
future: _chat.fetchChannel(widget.alias),
builder: (context, snapshot) {
if (!snapshot.hasData || snapshot.data == null) {
return const Center(child: CircularProgressIndicator());
}
return ChatMaintainer(
channel: snapshot.data!,
child: Stack(
children: [
Column(
children: [
Expanded(
child: PagedListView<int, Message>(
reverse: true,
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate<Message>(
animateTransitions: true,
transitionDuration: 500.ms,
itemBuilder: chatHistoryBuilder,
noItemsFoundIndicatorBuilder: (_) => Container(),
),
return ChatMaintainer(
channel: snapshot.data!,
child: Stack(
children: [
Column(
children: [
Expanded(
child: PagedListView<int, Message>(
reverse: true,
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate<Message>(
animateTransitions: true,
transitionDuration: 500.ms,
itemBuilder: chatHistoryBuilder,
noItemsFoundIndicatorBuilder: (_) => Container(),
),
),
ChatMessageEditor(
channel: widget.alias,
editing: _editingItem,
replying: _replyingItem,
onReset: () => setState(() {
_editingItem = null;
_replyingItem = null;
}),
),
],
),
_ongoingCall != null ? callBanner.animate().slideY() : Container(),
],
),
onInsertMessage: (message) => addMessage(message),
onUpdateMessage: (message) => updateMessage(message),
onDeleteMessage: (message) => deleteMessage(message),
onCallStarted: (call) => setState(() => _ongoingCall = call),
onCallEnded: () => setState(() => _ongoingCall = null),
);
},
),
),
ChatMessageEditor(
channel: widget.alias,
editing: _editingItem,
replying: _replyingItem,
onReset: () => setState(() {
_editingItem = null;
_replyingItem = null;
}),
),
],
),
_chat.ongoingCall != null ? callBanner.animate().slideY() : Container(),
],
),
onInsertMessage: (message) => addMessage(message),
onUpdateMessage: (message) => updateMessage(message),
onDeleteMessage: (message) => deleteMessage(message),
onCallStarted: (call) => _chat.setOngoingCall(call),
onCallEnded: () => _chat.setOngoingCall(null),
);
},
);
}
}

View File

@ -5,8 +5,10 @@ import 'package:provider/provider.dart';
import 'package:solian/models/channel.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/router.dart';
import 'package:solian/screens/chat/chat.dart';
import 'package:solian/utils/service_url.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/indent_wrapper.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
@ -21,6 +23,68 @@ class ChatIndexScreen extends StatefulWidget {
}
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();
Future<void> fetchChannels() async {
@ -61,9 +125,7 @@ class _ChatIndexScreenState extends State<ChatIndexScreen> {
Widget build(BuildContext context) {
final auth = context.read<AuthProvider>();
return IndentWrapper(
title: AppLocalizations.of(context)!.chat,
appBarActions: const [NotificationButton()],
return Scaffold(
floatingActionButton: FutureBuilder(
future: auth.isAuthorized(),
builder: (context, snapshot) {
@ -78,43 +140,33 @@ class _ChatIndexScreenState extends State<ChatIndexScreen> {
}
},
),
child: FutureBuilder(
future: auth.isAuthorized(),
builder: (context, snapshot) {
if (!snapshot.hasData || !snapshot.data!) {
return const SignInRequiredScreen();
}
body: FutureBuilder(
future: auth.isAuthorized(),
builder: (context, snapshot) {
if (!snapshot.hasData || !snapshot.data!) {
return const SignInRequiredScreen();
}
return RefreshIndicator(
onRefresh: () => fetchChannels(),
child: ListView.builder(
itemCount: _channels.length,
itemBuilder: (context, index) {
final element = _channels[index];
return ListTile(
leading: const CircleAvatar(
backgroundColor: Colors.indigo,
child: Icon(Icons.tag, color: Colors.white),
),
title: Text(element.name),
subtitle: Text(element.description),
onTap: () async {
final result = await router.pushNamed(
'chat.channel',
pathParameters: {
'channel': element.alias,
},
);
switch (result) {
case 'refresh':
fetchChannels();
}
},
);
},
),
);
}),
return RefreshIndicator(
onRefresh: () => fetchChannels(),
child: ListView.builder(
itemCount: _channels.length,
itemBuilder: (context, index) {
final element = _channels[index];
return ListTile(
leading: const CircleAvatar(
backgroundColor: Colors.indigo,
child: Icon(Icons.tag, color: Colors.white),
),
title: Text(element.name),
subtitle: Text(element.description),
onTap: () => widget.onSelect(element),
);
},
),
);
},
),
);
}
}

View File

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

View File

@ -97,7 +97,7 @@ class NotificationItem extends StatelessWidget {
padding: const EdgeInsets.only(
left: 16, right: 16, top: 34, bottom: 12),
child: Text(
"Links",
'Links',
style: Theme.of(context).textTheme.headlineSmall,
),
),
@ -151,7 +151,7 @@ class NotificationItem extends StatelessWidget {
text: item.subject,
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/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.body = jsonEncode(<String, dynamic>{
'alias': _alias,
@ -140,12 +140,12 @@ class _CommentEditorScreenState extends State<CommentEditorScreen> {
if (snapshot.hasData) {
var userinfo = snapshot.data;
return ListTile(
title: Text(userinfo["nick"]),
title: Text(userinfo['nick']),
subtitle: Text(
AppLocalizations.of(context)!.postIdentityNotify,
),
leading: AccountAvatar(
source: userinfo["picture"],
source: userinfo['picture'],
direct: true,
),
);

View File

@ -55,7 +55,7 @@ class _MomentEditorScreenState extends State<MomentEditorScreen> {
? getRequestUri('interactive', '/api/p/moments')
: 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.body = jsonEncode(<String, dynamic>{
'alias': _alias,
@ -130,12 +130,12 @@ class _MomentEditorScreenState extends State<MomentEditorScreen> {
if (snapshot.hasData) {
var userinfo = snapshot.data;
return ListTile(
title: Text(userinfo["nick"]),
title: Text(userinfo['nick']),
subtitle: Text(
AppLocalizations.of(context)!.postIdentityNotify,
),
leading: AccountAvatar(
source: userinfo["picture"],
source: userinfo['picture'],
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:flutter_gen/gen_l10n/app_localizations.dart';
class PostScreen extends StatefulWidget {
class PostScreen extends StatelessWidget {
final String dataset;
final String alias;
const PostScreen({super.key, required this.alias, required this.dataset});
@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 PagingController<int, Post> _commentPagingController =
PagingController(firstPageKey: 0);
final PagingController<int, Post> _commentPagingController = PagingController(firstPageKey: 0);
Future<Post?> fetchPost(BuildContext context) async {
final uri = getRequestUri(
'interactive', '/api/p/${widget.dataset}/${widget.alias}');
final uri = getRequestUri('interactive', '/api/p/${widget.dataset}/${widget.alias}');
final res = await _client.get(uri);
if (res.statusCode != 200) {
final err = utf8.decode(res.bodyBytes);
@ -42,43 +60,38 @@ class _PostScreenState extends State<PostScreen> {
@override
Widget build(BuildContext context) {
return IndentWrapper(
noSafeArea: true,
hideDrawer: true,
title: AppLocalizations.of(context)!.post,
child: FutureBuilder(
future: fetchPost(context),
builder: (context, snapshot) {
if (snapshot.hasData && snapshot.data != null) {
return CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: PostItem(
item: snapshot.data!,
brief: false,
ripple: false,
),
return FutureBuilder(
future: fetchPost(context),
builder: (context, snapshot) {
if (snapshot.hasData && snapshot.data != null) {
return CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: PostItem(
item: snapshot.data!,
brief: false,
ripple: false,
),
SliverToBoxAdapter(
child: CommentListHeader(
related: snapshot.data!,
paging: _commentPagingController,
),
),
CommentList(
),
SliverToBoxAdapter(
child: CommentListHeader(
related: snapshot.data!,
dataset: widget.dataset,
paging: _commentPagingController,
),
],
);
} else {
return const Center(
child: CircularProgressIndicator(),
);
}
},
),
),
CommentList(
related: snapshot.data!,
dataset: widget.dataset,
paging: _commentPagingController,
),
],
);
} else {
return const Center(
child: CircularProgressIndicator(),
);
}
},
);
}

View File

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

View File

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

View File

@ -55,23 +55,19 @@ class _ChatMaintainerState extends State<ChatMaintainer> {
switch (result.method) {
case 'messages.new':
final payload = Message.fromJson(result.payload!);
if (payload.channelId == widget.channel.id)
widget.onInsertMessage(payload);
if (payload.channelId == widget.channel.id) widget.onInsertMessage(payload);
break;
case 'messages.update':
final payload = Message.fromJson(result.payload!);
if (payload.channelId == widget.channel.id)
widget.onUpdateMessage(payload);
if (payload.channelId == widget.channel.id) widget.onUpdateMessage(payload);
break;
case 'messages.burnt':
final payload = Message.fromJson(result.payload!);
if (payload.channelId == widget.channel.id)
widget.onDeleteMessage(payload);
if (payload.channelId == widget.channel.id) widget.onDeleteMessage(payload);
break;
case 'calls.new':
final payload = Call.fromJson(result.payload!);
if (payload.channelId == widget.channel.id)
widget.onCallStarted(payload);
if (payload.channelId == widget.channel.id) widget.onCallStarted(payload);
break;
case 'calls.end':
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/${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.body = jsonEncode(<String, dynamic>{
'content': _textController.value.text,
@ -163,7 +163,6 @@ class _ChatMessageEditorState extends State<ChatMessageEditor> {
focusNode: _focusNode,
controller: _textController,
maxLines: null,
autofocus: true,
autocorrect: true,
keyboardType: TextInputType.text,
decoration: InputDecoration.collapsed(

View File

@ -22,7 +22,11 @@ class LayoutWrapper extends StatelessWidget {
final content = child ?? Container();
return Scaffold(
appBar: AppBar(title: Text(title), actions: appBarActions),
appBar: AppBar(
title: Text(title),
actions: appBarActions,
centerTitle: false,
),
floatingActionButton: floatingActionButton,
drawer: const SolianNavigationDrawer(),
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
.split(' ')
.map((element) =>
"${element[0].toUpperCase()}${element.substring(1).toLowerCase()}")
.join(" ");
'${element[0].toUpperCase()}${element.substring(1).toLowerCase()}')
.join(' ');
}
return showDialog<void>(

View File

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

View File

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

View File

@ -85,7 +85,7 @@ class _NotificationButtonState extends State<NotificationButton> {
child: IconButton(
icon: const Icon(Icons.notifications),
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);
if (res.statusCode == 200) {
var result = Attachment.fromJson(
jsonDecode(utf8.decode(await res.stream.toBytes()))["info"],
jsonDecode(utf8.decode(await res.stream.toBytes()))['info'],
);
setState(() => _attachments.add(result));
widget.onUpdate(_attachments);
@ -252,7 +252,7 @@ class _AttachmentEditorState extends State<AttachmentEditor> {
style: Theme.of(context).textTheme.titleMedium,
),
Text(
"${getFileType(element)} · ${formatBytes(element.filesize)}",
'${getFileType(element)} · ${formatBytes(element.filesize)}',
),
],
),

View File

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

View File

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

View File

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

View File

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

View File

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