Status creation & update & clear

This commit is contained in:
LittleSheep 2025-05-10 12:38:34 +08:00
parent d678cc571a
commit fb6e65a0fb
9 changed files with 868 additions and 51 deletions

View File

@ -36,6 +36,7 @@
"editPublisher": "Edit Publisher", "editPublisher": "Edit Publisher",
"syncPublisher": "Use Account Data", "syncPublisher": "Use Account Data",
"create": "Create", "create": "Create",
"update": "Update",
"edit": "Edit", "edit": "Edit",
"delete": "Delete", "delete": "Delete",
"deletePublisher": "Delete Publisher {}", "deletePublisher": "Delete Publisher {}",
@ -117,5 +118,25 @@
"checkInResultLevel4": "Best Luck", "checkInResultLevel4": "Best Luck",
"checkInActivityTitle": "{} checked in on {} and got a {}", "checkInActivityTitle": "{} checked in on {} and got a {}",
"eventCalander": "Event Calander", "eventCalander": "Event Calander",
"eventCalanderEmpty": "No events on that day." "eventCalanderEmpty": "No events on that day.",
"creatorHub": "Creator Hub",
"creatorHubDescription": "Manage posts, analytics, and more.",
"developerPortal": "Developer Portal",
"developerPortalDescription": "Build with Solar Network™.",
"statusCreateHint": "What's on your mind? Add a status.",
"statusCreate": "Add a Status",
"statusUpdate": "Update Status",
"statusLabel": "Status",
"statusAttitude": "Attitude",
"attitudePositive": "Positive",
"attitudeNeutral": "Neutral",
"attitudeNegative": "Negative",
"statusInvisible": "Invisible",
"statusInvisibleDescription": "Your will be showing as offline to others.",
"statusNotDisturb": "Do Not Disturb",
"statusNotDisturbDescription": "Push notification will be disabled.",
"statusClearTime": "Cleared At",
"statusNoAutoClear": "Do not auto clear",
"online": "Online",
"offline": "Offline"
} }

View File

@ -42,3 +42,24 @@ abstract class SnAccountProfile with _$SnAccountProfile {
factory SnAccountProfile.fromJson(Map<String, dynamic> json) => factory SnAccountProfile.fromJson(Map<String, dynamic> json) =>
_$SnAccountProfileFromJson(json); _$SnAccountProfileFromJson(json);
} }
@freezed
abstract class SnAccountStatus with _$SnAccountStatus {
const factory SnAccountStatus({
required String id,
required int attitude,
required bool isOnline,
required bool isInvisible,
required bool isNotDisturb,
required bool isCustomized,
@Default("") String label,
required DateTime? clearedAt,
required int accountId,
required DateTime createdAt,
required DateTime updatedAt,
required DateTime? deletedAt,
}) = _SnAccountStatus;
factory SnAccountStatus.fromJson(Map<String, dynamic> json) =>
_$SnAccountStatusFromJson(json);
}

View File

@ -401,4 +401,170 @@ $SnCloudFileCopyWith<$Res>? get background {
} }
} }
/// @nodoc
mixin _$SnAccountStatus {
String get id; int get attitude; bool get isOnline; bool get isInvisible; bool get isNotDisturb; bool get isCustomized; String get label; DateTime? get clearedAt; int get accountId; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
/// Create a copy of SnAccountStatus
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SnAccountStatusCopyWith<SnAccountStatus> get copyWith => _$SnAccountStatusCopyWithImpl<SnAccountStatus>(this as SnAccountStatus, _$identity);
/// Serializes this SnAccountStatus to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAccountStatus&&(identical(other.id, id) || other.id == id)&&(identical(other.attitude, attitude) || other.attitude == attitude)&&(identical(other.isOnline, isOnline) || other.isOnline == isOnline)&&(identical(other.isInvisible, isInvisible) || other.isInvisible == isInvisible)&&(identical(other.isNotDisturb, isNotDisturb) || other.isNotDisturb == isNotDisturb)&&(identical(other.isCustomized, isCustomized) || other.isCustomized == isCustomized)&&(identical(other.label, label) || other.label == label)&&(identical(other.clearedAt, clearedAt) || other.clearedAt == clearedAt)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,attitude,isOnline,isInvisible,isNotDisturb,isCustomized,label,clearedAt,accountId,createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnAccountStatus(id: $id, attitude: $attitude, isOnline: $isOnline, isInvisible: $isInvisible, isNotDisturb: $isNotDisturb, isCustomized: $isCustomized, label: $label, clearedAt: $clearedAt, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
}
/// @nodoc
abstract mixin class $SnAccountStatusCopyWith<$Res> {
factory $SnAccountStatusCopyWith(SnAccountStatus value, $Res Function(SnAccountStatus) _then) = _$SnAccountStatusCopyWithImpl;
@useResult
$Res call({
String id, int attitude, bool isOnline, bool isInvisible, bool isNotDisturb, bool isCustomized, String label, DateTime? clearedAt, int accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
}
/// @nodoc
class _$SnAccountStatusCopyWithImpl<$Res>
implements $SnAccountStatusCopyWith<$Res> {
_$SnAccountStatusCopyWithImpl(this._self, this._then);
final SnAccountStatus _self;
final $Res Function(SnAccountStatus) _then;
/// Create a copy of SnAccountStatus
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? attitude = null,Object? isOnline = null,Object? isInvisible = null,Object? isNotDisturb = null,Object? isCustomized = null,Object? label = null,Object? clearedAt = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,attitude: null == attitude ? _self.attitude : attitude // ignore: cast_nullable_to_non_nullable
as int,isOnline: null == isOnline ? _self.isOnline : isOnline // ignore: cast_nullable_to_non_nullable
as bool,isInvisible: null == isInvisible ? _self.isInvisible : isInvisible // ignore: cast_nullable_to_non_nullable
as bool,isNotDisturb: null == isNotDisturb ? _self.isNotDisturb : isNotDisturb // ignore: cast_nullable_to_non_nullable
as bool,isCustomized: null == isCustomized ? _self.isCustomized : isCustomized // ignore: cast_nullable_to_non_nullable
as bool,label: null == label ? _self.label : label // ignore: cast_nullable_to_non_nullable
as String,clearedAt: freezed == clearedAt ? _self.clearedAt : clearedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as int,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
));
}
}
/// @nodoc
@JsonSerializable()
class _SnAccountStatus implements SnAccountStatus {
const _SnAccountStatus({required this.id, required this.attitude, required this.isOnline, required this.isInvisible, required this.isNotDisturb, required this.isCustomized, this.label = "", required this.clearedAt, required this.accountId, required this.createdAt, required this.updatedAt, required this.deletedAt});
factory _SnAccountStatus.fromJson(Map<String, dynamic> json) => _$SnAccountStatusFromJson(json);
@override final String id;
@override final int attitude;
@override final bool isOnline;
@override final bool isInvisible;
@override final bool isNotDisturb;
@override final bool isCustomized;
@override@JsonKey() final String label;
@override final DateTime? clearedAt;
@override final int accountId;
@override final DateTime createdAt;
@override final DateTime updatedAt;
@override final DateTime? deletedAt;
/// Create a copy of SnAccountStatus
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SnAccountStatusCopyWith<_SnAccountStatus> get copyWith => __$SnAccountStatusCopyWithImpl<_SnAccountStatus>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$SnAccountStatusToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAccountStatus&&(identical(other.id, id) || other.id == id)&&(identical(other.attitude, attitude) || other.attitude == attitude)&&(identical(other.isOnline, isOnline) || other.isOnline == isOnline)&&(identical(other.isInvisible, isInvisible) || other.isInvisible == isInvisible)&&(identical(other.isNotDisturb, isNotDisturb) || other.isNotDisturb == isNotDisturb)&&(identical(other.isCustomized, isCustomized) || other.isCustomized == isCustomized)&&(identical(other.label, label) || other.label == label)&&(identical(other.clearedAt, clearedAt) || other.clearedAt == clearedAt)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,attitude,isOnline,isInvisible,isNotDisturb,isCustomized,label,clearedAt,accountId,createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnAccountStatus(id: $id, attitude: $attitude, isOnline: $isOnline, isInvisible: $isInvisible, isNotDisturb: $isNotDisturb, isCustomized: $isCustomized, label: $label, clearedAt: $clearedAt, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
}
/// @nodoc
abstract mixin class _$SnAccountStatusCopyWith<$Res> implements $SnAccountStatusCopyWith<$Res> {
factory _$SnAccountStatusCopyWith(_SnAccountStatus value, $Res Function(_SnAccountStatus) _then) = __$SnAccountStatusCopyWithImpl;
@override @useResult
$Res call({
String id, int attitude, bool isOnline, bool isInvisible, bool isNotDisturb, bool isCustomized, String label, DateTime? clearedAt, int accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
}
/// @nodoc
class __$SnAccountStatusCopyWithImpl<$Res>
implements _$SnAccountStatusCopyWith<$Res> {
__$SnAccountStatusCopyWithImpl(this._self, this._then);
final _SnAccountStatus _self;
final $Res Function(_SnAccountStatus) _then;
/// Create a copy of SnAccountStatus
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? attitude = null,Object? isOnline = null,Object? isInvisible = null,Object? isNotDisturb = null,Object? isCustomized = null,Object? label = null,Object? clearedAt = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_SnAccountStatus(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,attitude: null == attitude ? _self.attitude : attitude // ignore: cast_nullable_to_non_nullable
as int,isOnline: null == isOnline ? _self.isOnline : isOnline // ignore: cast_nullable_to_non_nullable
as bool,isInvisible: null == isInvisible ? _self.isInvisible : isInvisible // ignore: cast_nullable_to_non_nullable
as bool,isNotDisturb: null == isNotDisturb ? _self.isNotDisturb : isNotDisturb // ignore: cast_nullable_to_non_nullable
as bool,isCustomized: null == isCustomized ? _self.isCustomized : isCustomized // ignore: cast_nullable_to_non_nullable
as bool,label: null == label ? _self.label : label // ignore: cast_nullable_to_non_nullable
as String,clearedAt: freezed == clearedAt ? _self.clearedAt : clearedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as int,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
));
}
}
// dart format on // dart format on

View File

@ -76,3 +76,41 @@ Map<String, dynamic> _$SnAccountProfileToJson(_SnAccountProfile instance) =>
'updated_at': instance.updatedAt.toIso8601String(), 'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(), 'deleted_at': instance.deletedAt?.toIso8601String(),
}; };
_SnAccountStatus _$SnAccountStatusFromJson(Map<String, dynamic> json) =>
_SnAccountStatus(
id: json['id'] as String,
attitude: (json['attitude'] as num).toInt(),
isOnline: json['is_online'] as bool,
isInvisible: json['is_invisible'] as bool,
isNotDisturb: json['is_not_disturb'] as bool,
isCustomized: json['is_customized'] as bool,
label: json['label'] as String? ?? "",
clearedAt:
json['cleared_at'] == null
? null
: DateTime.parse(json['cleared_at'] as String),
accountId: (json['account_id'] as num).toInt(),
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt:
json['deleted_at'] == null
? null
: DateTime.parse(json['deleted_at'] as String),
);
Map<String, dynamic> _$SnAccountStatusToJson(_SnAccountStatus instance) =>
<String, dynamic>{
'id': instance.id,
'attitude': instance.attitude,
'is_online': instance.isOnline,
'is_invisible': instance.isInvisible,
'is_not_disturb': instance.isNotDisturb,
'is_customized': instance.isCustomized,
'label': instance.label,
'cleared_at': instance.clearedAt?.toIso8601String(),
'account_id': instance.accountId,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
};

View File

@ -59,6 +59,7 @@ class WebSocketService {
Uri.parse(url), Uri.parse(url),
headers: {'Authorization': 'Bearer $atk'}, headers: {'Authorization': 'Bearer $atk'},
); );
// TODO Fix the atk is expired when reconnecting
await _channel!.ready; await _channel!.ready;
_statusStreamController.sink.add(WebSocketState.connected()); _statusStreamController.sink.add(WebSocketState.connected());
_channel!.stream.listen( _channel!.stream.listen(

View File

@ -9,6 +9,7 @@ import 'package:island/pods/message.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/pods/userinfo.dart'; import 'package:island/pods/userinfo.dart';
import 'package:island/route.gr.dart'; import 'package:island/route.gr.dart';
import 'package:island/widgets/account/status.dart';
import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/content/cloud_files.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
@ -31,8 +32,7 @@ class AccountScreen extends HookConsumerWidget {
body: SingleChildScrollView( body: SingleChildScrollView(
child: Column( child: Column(
children: <Widget>[ children: <Widget>[
GestureDetector( Card(
child: Card(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -54,10 +54,17 @@ class AccountScreen extends HookConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
spacing: 16, spacing: 16,
children: [ children: [
ProfilePictureWidget( GestureDetector(
child: ProfilePictureWidget(
fileId: user.value?.profile.pictureId, fileId: user.value?.profile.pictureId,
radius: 24, radius: 24,
), ),
onTap: () {
context.router.push(
AccountProfileRoute(name: user.value!.name),
);
},
),
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -76,23 +83,54 @@ class AccountScreen extends HookConsumerWidget {
], ],
), ),
], ],
).padding(horizontal: 16, vertical: 16), ).padding(horizontal: 16, top: 16),
AccountStatusCreationWidget(uname: user.value!.name),
], ],
), ),
).padding(horizontal: 8), ).padding(horizontal: 8),
onTap: () { Row(
context.router.push( children: [
AccountProfileRoute(name: user.value!.name), Expanded(
); child: Card(
}, child: InkWell(
borderRadius: BorderRadius.circular(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(Symbols.draw, size: 28).padding(bottom: 8),
Text('creatorHub').tr().fontSize(16).bold(),
Text('creatorHubDescription').tr(),
],
).padding(horizontal: 16, vertical: 12),
onTap: () {},
), ),
),
),
Expanded(
child: Card(
child: InkWell(
borderRadius: BorderRadius.circular(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(Symbols.code, size: 28).padding(bottom: 8),
Text('developerPortal').tr().fontSize(16).bold(),
Text('developerPortalDescription').tr(),
],
).padding(horizontal: 16, vertical: 12),
onTap: () {},
),
),
),
],
).padding(horizontal: 8),
const Gap(8), const Gap(8),
ListTile( ListTile(
minTileHeight: 48, minTileHeight: 48,
leading: const Icon(Symbols.public), leading: const Icon(Symbols.public),
trailing: const Icon(Symbols.chevron_right), trailing: const Icon(Symbols.chevron_right),
contentPadding: EdgeInsets.symmetric(horizontal: 24), contentPadding: EdgeInsets.symmetric(horizontal: 24),
title: Text('managedPublisher').tr(), title: Text('publishers').tr(),
onTap: () { onTap: () {
context.router.push(ManagedPublisherRoute()); context.router.push(ManagedPublisherRoute());
}, },

View File

@ -0,0 +1,130 @@
import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/user.dart';
import 'package:island/pods/network.dart';
import 'package:island/widgets/account/status_creation.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:styled_widget/styled_widget.dart';
part 'status.g.dart';
@riverpod
Future<SnAccountStatus?> accountStatus(Ref ref, String uname) async {
final apiClient = ref.watch(apiClientProvider);
try {
final resp = await apiClient.get('/accounts/$uname/statuses');
return SnAccountStatus.fromJson(resp.data);
} catch (err) {
if (err is DioException) {
if (err.response?.statusCode == 404) {
return null;
}
}
rethrow;
}
}
class AccountStatusCreationWidget extends HookConsumerWidget {
final String uname;
final EdgeInsets? padding;
const AccountStatusCreationWidget({
super.key,
required this.uname,
this.padding,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final userStatus = ref.watch(accountStatusProvider(uname));
return InkWell(
borderRadius: BorderRadius.circular(8),
child: userStatus.when(
data:
(status) =>
(status?.isCustomized ?? false)
? AccountStatusWidget(uname: uname)
: Padding(
padding:
padding ??
EdgeInsets.symmetric(horizontal: 27, vertical: 4),
child: Row(
spacing: 4,
children: [
Icon(Symbols.keyboard_arrow_up),
Text('statusCreateHint').tr(),
],
),
).opacity(0.85),
error:
(error, _) => Padding(
padding:
padding ?? EdgeInsets.symmetric(horizontal: 26, vertical: 4),
child: Row(
spacing: 4,
children: [Icon(Symbols.close), Text('Error: $error')],
),
).opacity(0.85),
loading:
() => Padding(
padding:
padding ?? EdgeInsets.symmetric(horizontal: 26, vertical: 4),
child: Row(
spacing: 4,
children: [Icon(Symbols.more_vert), Text('loading').tr()],
),
).opacity(0.85),
),
onTap: () {
showModalBottomSheet(
context: context,
builder:
(context) => AccountStatusCreationSheet(
initialStatus:
(userStatus.value?.isCustomized ?? false)
? userStatus.value
: null,
),
);
},
);
}
}
class AccountStatusWidget extends HookConsumerWidget {
final String uname;
final EdgeInsets? padding;
const AccountStatusWidget({super.key, required this.uname, this.padding});
@override
Widget build(BuildContext context, WidgetRef ref) {
final userStatus = ref.watch(accountStatusProvider(uname));
return Padding(
padding: padding ?? EdgeInsets.symmetric(horizontal: 27, vertical: 4),
child: Row(
spacing: 4,
children: [
if (!(userStatus.value?.isCustomized ?? false))
Icon(Symbols.keyboard_arrow_up)
else if (userStatus.value!.isOnline)
Icon(
Symbols.circle,
fill: 1,
color: Colors.green,
size: 16,
).padding(all: 4)
else
Icon(Symbols.circle, color: Colors.grey, size: 16).padding(all: 4),
if (userStatus.value?.isCustomized ?? false)
Text(userStatus.value?.label ?? 'unknown'.tr())
else
Text((userStatus.value?.label ?? 'offline').toLowerCase()).tr(),
],
),
).opacity((userStatus.value?.isCustomized ?? false) ? 1 : 0.85);
}
}

View File

@ -0,0 +1,153 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'status.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$accountStatusHash() => r'8c3ba5242da1d1e75e3cbf1f2934ff7d5683d0d6';
/// Copied from Dart SDK
class _SystemHash {
_SystemHash._();
static int combine(int hash, int value) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + value);
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
return hash ^ (hash >> 6);
}
static int finish(int hash) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
// ignore: parameter_assignments
hash = hash ^ (hash >> 11);
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
}
}
/// See also [accountStatus].
@ProviderFor(accountStatus)
const accountStatusProvider = AccountStatusFamily();
/// See also [accountStatus].
class AccountStatusFamily extends Family<AsyncValue<SnAccountStatus?>> {
/// See also [accountStatus].
const AccountStatusFamily();
/// See also [accountStatus].
AccountStatusProvider call(String uname) {
return AccountStatusProvider(uname);
}
@override
AccountStatusProvider getProviderOverride(
covariant AccountStatusProvider provider,
) {
return call(provider.uname);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'accountStatusProvider';
}
/// See also [accountStatus].
class AccountStatusProvider
extends AutoDisposeFutureProvider<SnAccountStatus?> {
/// See also [accountStatus].
AccountStatusProvider(String uname)
: this._internal(
(ref) => accountStatus(ref as AccountStatusRef, uname),
from: accountStatusProvider,
name: r'accountStatusProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$accountStatusHash,
dependencies: AccountStatusFamily._dependencies,
allTransitiveDependencies:
AccountStatusFamily._allTransitiveDependencies,
uname: uname,
);
AccountStatusProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.uname,
}) : super.internal();
final String uname;
@override
Override overrideWith(
FutureOr<SnAccountStatus?> Function(AccountStatusRef provider) create,
) {
return ProviderOverride(
origin: this,
override: AccountStatusProvider._internal(
(ref) => create(ref as AccountStatusRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
uname: uname,
),
);
}
@override
AutoDisposeFutureProviderElement<SnAccountStatus?> createElement() {
return _AccountStatusProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is AccountStatusProvider && other.uname == uname;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, uname.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin AccountStatusRef on AutoDisposeFutureProviderRef<SnAccountStatus?> {
/// The parameter `uname` of this provider.
String get uname;
}
class _AccountStatusProviderElement
extends AutoDisposeFutureProviderElement<SnAccountStatus?>
with AccountStatusRef {
_AccountStatusProviderElement(super.provider);
@override
String get uname => (origin as AccountStatusProvider).uname;
}
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@ -0,0 +1,249 @@
import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/user.dart';
import 'package:island/pods/network.dart';
import 'package:island/pods/userinfo.dart';
import 'package:island/widgets/account/status.dart';
import 'package:island/widgets/alert.dart';
import 'package:material_symbols_icons/symbols.dart';
class AccountStatusCreationSheet extends HookConsumerWidget {
final SnAccountStatus? initialStatus;
const AccountStatusCreationSheet({super.key, this.initialStatus});
@override
Widget build(BuildContext context, WidgetRef ref) {
final attitude = useState<int>(initialStatus?.attitude ?? 1);
final isInvisible = useState(initialStatus?.isInvisible ?? false);
final isNotDisturb = useState(initialStatus?.isNotDisturb ?? false);
final clearedAt = useState<DateTime?>(initialStatus?.clearedAt);
final labelController = useTextEditingController(
text: initialStatus?.label ?? '',
);
final submitting = useState(false);
Future<void> clearStatus() async {
try {
submitting.value = true;
final user = ref.watch(userInfoProvider);
final apiClient = ref.read(apiClientProvider);
await apiClient.delete('/accounts/me/statuses');
if (!context.mounted) return;
ref.invalidate(accountStatusProvider(user.value!.name));
Navigator.pop(context);
} catch (err) {
showErrorAlert(err);
} finally {
submitting.value = false;
}
}
Future<void> submitStatus() async {
try {
submitting.value = true;
final user = ref.watch(userInfoProvider);
final apiClient = ref.read(apiClientProvider);
await apiClient.request(
'/accounts/me/statuses',
data: {
'attitude': attitude.value,
'is_invisible': isInvisible.value,
'is_not_disturb': isNotDisturb.value,
'cleared_at': clearedAt.value?.toIso8601String(),
if (labelController.text.isNotEmpty) 'label': labelController.text,
},
options: Options(method: initialStatus == null ? 'POST' : 'PATCH'),
);
if (user.hasValue) {
ref.invalidate(accountStatusProvider(user.value!.name));
}
if (!context.mounted) return;
Navigator.pop(context);
} catch (err) {
showErrorAlert(err);
} finally {
submitting.value = false;
}
}
return Container(
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.8,
),
child: Column(
children: [
Padding(
padding: EdgeInsets.only(top: 16, left: 20, right: 16, bottom: 12),
child: Row(
children: [
Text(
initialStatus == null
? 'statusCreate'.tr()
: 'statusUpdate'.tr(),
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w600,
letterSpacing: -0.5,
),
),
const Spacer(),
TextButton.icon(
onPressed:
submitting.value
? null
: () {
submitStatus();
},
icon: const Icon(Symbols.upload),
label: Text(initialStatus == null ? 'create' : 'update').tr(),
style: ButtonStyle(
visualDensity: VisualDensity(
horizontal: VisualDensity.minimumDensity,
),
foregroundColor: WidgetStatePropertyAll(
Theme.of(context).colorScheme.onSurface,
),
),
),
if (initialStatus != null)
IconButton(
icon: const Icon(Symbols.delete),
onPressed: submitting.value ? null : () => clearStatus(),
style: IconButton.styleFrom(
minimumSize: const Size(36, 36),
),
),
IconButton(
icon: const Icon(Symbols.close),
onPressed: () => Navigator.pop(context),
style: IconButton.styleFrom(minimumSize: const Size(36, 36)),
),
],
),
),
const Divider(height: 1),
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Gap(24),
TextField(
controller: labelController,
decoration: InputDecoration(
labelText: 'statusLabel'.tr(),
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
),
),
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
),
const SizedBox(height: 24),
Text(
'statusAttitude'.tr(),
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
SegmentedButton(
segments: [
ButtonSegment(
value: 0,
icon: const Icon(Symbols.sentiment_satisfied),
label: Text('attitudePositive'.tr()),
),
ButtonSegment(
value: 1,
icon: const Icon(Symbols.sentiment_stressed),
label: Text('attitudeNeutral'.tr()),
),
ButtonSegment(
value: 2,
icon: const Icon(Symbols.sentiment_sad),
label: Text('attitudeNegative'.tr()),
),
],
selected: {attitude.value},
onSelectionChanged: (Set<int> newSelection) {
attitude.value = newSelection.first;
},
),
const Gap(12),
SwitchListTile(
title: Text('statusInvisible'.tr()),
subtitle: Text('statusInvisibleDescription'.tr()),
value: isInvisible.value,
contentPadding: EdgeInsets.symmetric(horizontal: 8),
onChanged: (bool value) {
isInvisible.value = value;
},
),
SwitchListTile(
title: Text('statusNotDisturb'.tr()),
subtitle: Text('statusNotDisturbDescription'.tr()),
value: isNotDisturb.value,
contentPadding: EdgeInsets.symmetric(horizontal: 8),
onChanged: (bool value) {
isNotDisturb.value = value;
},
),
const SizedBox(height: 24),
Text(
'statusClearTime'.tr(),
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
ListTile(
title: Text(
clearedAt.value == null
? 'statusNoAutoClear'.tr()
: DateFormat.yMMMd().add_jm().format(
clearedAt.value!,
),
),
trailing: const Icon(Symbols.schedule),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: BorderSide(
color: Theme.of(context).colorScheme.outline,
),
),
onTap: () async {
final now = DateTime.now();
final date = await showDatePicker(
context: context,
initialDate: now,
firstDate: now,
lastDate: now.add(const Duration(days: 365)),
);
if (date == null) return;
if (!context.mounted) return;
final time = await showTimePicker(
context: context,
initialTime: TimeOfDay.now(),
);
if (time == null) return;
clearedAt.value = DateTime(
date.year,
date.month,
date.day,
time.hour,
time.minute,
);
},
),
Gap(MediaQuery.of(context).padding.bottom + 24),
],
),
),
),
],
),
);
}
}