✨ Status creation & update & clear
This commit is contained in:
130
lib/widgets/account/status.dart
Normal file
130
lib/widgets/account/status.dart
Normal 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);
|
||||
}
|
||||
}
|
153
lib/widgets/account/status.g.dart
Normal file
153
lib/widgets/account/status.g.dart
Normal 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
|
249
lib/widgets/account/status_creation.dart
Normal file
249
lib/widgets/account/status_creation.dart
Normal 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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user