Realm avatar, banner

This commit is contained in:
LittleSheep 2024-09-17 14:21:37 +08:00
parent c9fbe47337
commit 565a8e41cc
10 changed files with 219 additions and 25 deletions

View File

@ -228,6 +228,8 @@
"realmDescription": "Description",
"realmPublic": "Public Realm",
"realmCommunity": "Community Realm",
"realmAvatar": "Realm avatar",
"realmBanner": "Realm banner",
"realmDetail": "Realm detail",
"realmMember": "Realm member",
"realmMembers": "Realm members",

View File

@ -229,6 +229,8 @@
"realmDescription": "领域简介",
"realmPublic": "公开领域",
"realmCommunity": "社区领域",
"realmAvatar": "领域头像",
"realmBanner": "领域横幅",
"realmDetail": "领域详情",
"realmMember": "领域成员",
"realmMembers": "领域成员",

View File

@ -5,7 +5,7 @@ import 'package:solian/models/realm.dart';
import 'package:solian/screens/about.dart';
import 'package:solian/screens/account.dart';
import 'package:solian/screens/account/friend.dart';
import 'package:solian/screens/account/personalize.dart';
import 'package:solian/screens/account/profile_edit.dart';
import 'package:solian/screens/account/profile_page.dart';
import 'package:solian/screens/auth/signin.dart';
import 'package:solian/screens/auth/signup.dart';

View File

@ -114,7 +114,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
ListTile(
leading: const Icon(Icons.settings),
trailing: const Icon(Icons.chevron_right),
title: Text('channelSettings'.tr.capitalize!),
title: Text('channelSettings'.tr),
onTap: () async {
AppRouter.instance
.pushNamed(
@ -173,7 +173,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
children: [
ListTile(
leading: const Icon(Icons.notifications_active),
title: Text('channelNotifyLevel'.tr.capitalize!),
title: Text('channelNotifyLevel'.tr),
trailing: DropdownButtonHideUnderline(
child: DropdownButton2<int>(
isExpanded: true,
@ -208,7 +208,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
ListTile(
leading: const Icon(Icons.supervisor_account),
trailing: const Icon(Icons.chevron_right),
title: Text('channelMembers'.tr.capitalize!),
title: Text('channelMembers'.tr),
onTap: () => showMemberList(),
),
...(_isOwned ? ownerActions : List.empty()),

View File

@ -97,6 +97,14 @@ class _ChannelOrganizeScreenState extends State<ChannelOrganizeScreen> {
super.initState();
}
@override
void dispose() {
_aliasController.dispose();
_nameController.dispose();
_descriptionController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final notifyBannerActions = [

View File

@ -7,10 +7,13 @@ import 'package:solian/providers/auth.dart';
import 'package:solian/providers/content/realm.dart';
import 'package:solian/router.dart';
import 'package:solian/screens/account/notification.dart';
import 'package:solian/services.dart';
import 'package:solian/theme.dart';
import 'package:solian/widgets/account/account_avatar.dart';
import 'package:solian/widgets/account/signin_required_overlay.dart';
import 'package:solian/widgets/app_bar_leading.dart';
import 'package:solian/widgets/app_bar_title.dart';
import 'package:solian/widgets/auto_cache_image.dart';
import 'package:solian/widgets/current_state_action.dart';
import 'package:solian/widgets/sized_container.dart';
@ -128,19 +131,34 @@ class _RealmListScreenState extends State<RealmListScreen> {
children: [
Container(
color: Theme.of(context).colorScheme.surfaceContainer,
child: (element.banner?.isEmpty ?? true)
? const SizedBox.shrink()
: AutoCacheImage(
ServiceFinder.buildUrl(
'uc',
'/attachments/${element.banner}',
),
fit: BoxFit.cover,
),
),
const Positioned(
Positioned(
bottom: -30,
left: 18,
child: CircleAvatar(
radius: 24,
backgroundColor: Colors.indigo,
child: FaIcon(
FontAwesomeIcons.globe,
color: Colors.white,
size: 18,
),
),
child: (element.avatar?.isEmpty ?? true)
? CircleAvatar(
radius: 24,
backgroundColor:
Theme.of(context).colorScheme.primary,
child: const FaIcon(
FontAwesomeIcons.globe,
color: Colors.white,
size: 18,
),
)
: AccountAvatar(
content: element.avatar!,
bgColor: Theme.of(context).colorScheme.primary,
),
),
],
),

View File

@ -69,7 +69,8 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> {
ListTile(
leading: const Icon(Icons.settings),
trailing: const Icon(Icons.chevron_right),
title: Text('realmSettings'.tr.capitalize!),
title: Text('realmSettings'.tr),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
onTap: () async {
AppRouter.instance
.pushNamed(
@ -120,14 +121,16 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> {
child: ListView(
children: [
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Icons.supervisor_account),
trailing: const Icon(Icons.chevron_right),
title: Text('realmMembers'.tr.capitalize!),
title: Text('realmMembers'.tr),
onTap: () => showMemberList(),
),
...(_isOwned ? ownerActions : List.empty()),
const Divider(thickness: 0.3),
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: _isOwned
? const Icon(Icons.delete)
: const Icon(Icons.exit_to_app),

View File

@ -1,9 +1,15 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:get/get.dart';
import 'package:image_cropper/image_cropper.dart';
import 'package:image_picker/image_picker.dart';
import 'package:solian/exts.dart';
import 'package:solian/models/attachment.dart';
import 'package:solian/models/realm.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/providers/content/attachment.dart';
import 'package:solian/router.dart';
import 'package:solian/theme.dart';
import 'package:solian/widgets/app_bar_leading.dart';
@ -29,17 +35,19 @@ class _RealmOrganizeScreenState extends State<RealmOrganizeScreen> {
bool _isBusy = false;
final _aliasController = TextEditingController();
final _avatarController = TextEditingController();
final _bannerController = TextEditingController();
final _nameController = TextEditingController();
final _descriptionController = TextEditingController();
bool _isCommunity = false;
bool _isPublic = false;
void applyRealm() async {
void _applyRealm() async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) return;
if (_aliasController.value.text.isEmpty) randomizeAlias();
if (_aliasController.value.text.isEmpty) _randomizeAlias();
setState(() => _isBusy = true);
@ -49,6 +57,8 @@ class _RealmOrganizeScreenState extends State<RealmOrganizeScreen> {
'alias': _aliasController.value.text.toLowerCase(),
'name': _nameController.value.text,
'description': _descriptionController.value.text,
'avatar': _avatarController.value.text,
'banner': _bannerController.value.text,
'is_public': _isPublic,
'is_community': _isCommunity,
};
@ -68,31 +78,110 @@ class _RealmOrganizeScreenState extends State<RealmOrganizeScreen> {
setState(() => _isBusy = false);
}
void randomizeAlias() {
final _imagePicker = ImagePicker();
Future<void> _editImage(String position) async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) return;
final image = await _imagePicker.pickImage(source: ImageSource.gallery);
if (image == null) return;
CroppedFile? croppedFile = await ImageCropper().cropImage(
sourcePath: image.path,
uiSettings: [
AndroidUiSettings(
toolbarTitle: 'cropImage'.tr,
toolbarColor: Theme.of(context).colorScheme.primary,
toolbarWidgetColor: Theme.of(context).colorScheme.onPrimary,
aspectRatioPresets: [
if (position == 'avatar') CropAspectRatioPreset.square,
if (position == 'banner') _BannerCropAspectRatioPreset(),
],
),
IOSUiSettings(
title: 'cropImage'.tr,
aspectRatioPresets: [
if (position == 'avatar') CropAspectRatioPreset.square,
if (position == 'banner') _BannerCropAspectRatioPreset(),
],
),
WebUiSettings(
context: context,
),
],
);
if (croppedFile == null) return;
final file = File(croppedFile.path);
setState(() => _isBusy = true);
final AttachmentProvider attach = Get.find();
Attachment? attachResult;
try {
attachResult = await attach.createAttachmentDirectly(
await file.readAsBytes(),
file.path,
'avatar',
null,
);
} catch (e) {
setState(() => _isBusy = false);
context.showErrorDialog(e);
return;
}
switch (position) {
case 'avatar':
_avatarController.text = attachResult.rid;
break;
case 'banner':
_bannerController.text = attachResult.rid;
break;
}
setState(() => _isBusy = false);
}
void _randomizeAlias() {
_aliasController.text =
const Uuid().v4().replaceAll('-', '').substring(0, 12);
}
void syncWidget() {
void _syncWidget() {
if (widget.edit != null) {
_aliasController.text = widget.edit!.alias;
_nameController.text = widget.edit!.name;
_descriptionController.text = widget.edit!.description;
_avatarController.text = widget.edit!.avatar ?? '';
_bannerController.text = widget.edit!.banner ?? '';
_isPublic = widget.edit!.isPublic;
_isCommunity = widget.edit!.isCommunity;
}
}
void cancelAction() {
void _cancelAction() {
AppRouter.instance.pop();
}
@override
void initState() {
syncWidget();
_syncWidget();
super.initState();
}
@override
void dispose() {
_aliasController.dispose();
_avatarController.dispose();
_bannerController.dispose();
_nameController.dispose();
_descriptionController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Material(
@ -105,7 +194,7 @@ class _RealmOrganizeScreenState extends State<RealmOrganizeScreen> {
toolbarHeight: AppTheme.toolbarHeight(context),
actions: [
TextButton(
onPressed: _isBusy ? null : () => applyRealm(),
onPressed: _isBusy ? null : () => _applyRealm(),
child: Text('apply'.tr.toUpperCase()),
)
],
@ -126,7 +215,7 @@ class _RealmOrganizeScreenState extends State<RealmOrganizeScreen> {
),
actions: [
TextButton(
onPressed: cancelAction,
onPressed: _cancelAction,
child: Text('cancel'.tr),
),
],
@ -150,7 +239,7 @@ class _RealmOrganizeScreenState extends State<RealmOrganizeScreen> {
visualDensity:
const VisualDensity(horizontal: -2, vertical: -2),
),
onPressed: () => randomizeAlias(),
onPressed: () => _randomizeAlias(),
child: const Icon(Icons.refresh),
)
],
@ -166,6 +255,55 @@ class _RealmOrganizeScreenState extends State<RealmOrganizeScreen> {
FocusManager.instance.primaryFocus?.unfocus(),
).paddingSymmetric(horizontal: 16, vertical: 8),
const Divider(thickness: 0.3),
Row(
children: [
Expanded(
child: TextField(
autofocus: true,
controller: _avatarController,
decoration: InputDecoration.collapsed(
hintText: 'realmAvatar'.tr,
),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
),
TextButton(
style: TextButton.styleFrom(
shape: const CircleBorder(),
visualDensity:
const VisualDensity(horizontal: -2, vertical: -2),
),
onPressed: _isBusy ? null : () => _editImage('avatar'),
child: const Icon(Icons.upload),
)
],
).paddingSymmetric(horizontal: 16, vertical: 2),
Row(
children: [
Expanded(
child: TextField(
autofocus: true,
controller: _bannerController,
decoration: InputDecoration.collapsed(
hintText: 'realmBanner'.tr,
),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
),
TextButton(
style: TextButton.styleFrom(
shape: const CircleBorder(),
visualDensity:
const VisualDensity(horizontal: -2, vertical: -2),
),
onPressed: _isBusy ? null : () => _editImage('banner'),
child: const Icon(Icons.upload),
)
],
).paddingSymmetric(horizontal: 16, vertical: 2),
const Divider(thickness: 0.3),
Expanded(
child: TextField(
minLines: 5,
@ -202,3 +340,11 @@ class _RealmOrganizeScreenState extends State<RealmOrganizeScreen> {
);
}
}
class _BannerCropAspectRatioPreset extends CropAspectRatioPresetData {
@override
(int, int)? get data => (16, 7);
@override
String get name => '16x7';
}

View File

@ -6,7 +6,9 @@ import 'package:solian/providers/auth.dart';
import 'package:solian/providers/content/channel.dart';
import 'package:solian/providers/content/realm.dart';
import 'package:solian/providers/navigation.dart';
import 'package:solian/services.dart';
import 'package:solian/widgets/account/account_avatar.dart';
import 'package:solian/widgets/auto_cache_image.dart';
import 'package:solian/widgets/channel/channel_list.dart';
class AppNavigationRegion extends StatefulWidget {
@ -169,6 +171,19 @@ class _AppNavigationRegionState extends State<AppNavigationRegion> {
)
: Column(
children: [
if (!widget.isCollapsed &&
(navState.focusedRealm.value!.banner?.isNotEmpty ??
false))
AspectRatio(
aspectRatio: 16 / 7,
child: AutoCacheImage(
ServiceFinder.buildUrl(
'uc',
'/attachments/${navState.focusedRealm.value!.banner}',
),
fit: BoxFit.cover,
),
),
if (widget.isCollapsed)
Tooltip(
message: navState.focusedRealm.value!.name,