⚡ Optimize
This commit is contained in:
parent
57ff5e44ba
commit
1eb42fa351
@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/providers/content/attachment_list.dart';
|
||||
import 'package:solian/providers/content/attachment.dart';
|
||||
import 'package:solian/router.dart';
|
||||
import 'package:solian/theme.dart';
|
||||
import 'package:solian/translations.dart';
|
||||
@ -28,7 +28,7 @@ class SolianApp extends StatelessWidget {
|
||||
fallbackLocale: const Locale('en', 'US'),
|
||||
onInit: () {
|
||||
Get.lazyPut(() => AuthProvider());
|
||||
Get.lazyPut(() => AttachmentListProvider());
|
||||
Get.lazyPut(() => AttachmentProvider());
|
||||
},
|
||||
builder: (context, child) {
|
||||
return ScaffoldMessenger(
|
||||
|
114
lib/providers/content/attachment.dart
Normal file
114
lib/providers/content/attachment.dart
Normal file
@ -0,0 +1,114 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/services.dart';
|
||||
import 'package:image/image.dart' as img;
|
||||
|
||||
Future<String> calculateFileSha256(File file) async {
|
||||
final bytes = await Isolate.run(() => file.readAsBytesSync());
|
||||
final digest = await Isolate.run(() => sha256.convert(bytes));
|
||||
return digest.toString();
|
||||
}
|
||||
|
||||
Future<double> calculateFileAspectRatio(File file) async {
|
||||
final bytes = await Isolate.run(() => file.readAsBytesSync());
|
||||
final decoder = await Isolate.run(() => img.findDecoderForData(bytes));
|
||||
if (decoder == null) return 1;
|
||||
final image = await Isolate.run(() => decoder.decode(bytes));
|
||||
if (image == null) return 1;
|
||||
return image.width / image.height;
|
||||
}
|
||||
|
||||
class AttachmentProvider extends GetConnect {
|
||||
@override
|
||||
void onInit() {
|
||||
httpClient.baseUrl = ServiceFinder.services['paperclip'];
|
||||
}
|
||||
|
||||
Future<Response> getMetadata(int id) => get('/api/attachments/$id/meta');
|
||||
|
||||
Future<Response> createAttachment(File file, String hash, String usage,
|
||||
{double? ratio}) async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (!await auth.isAuthorized) throw Exception('unauthorized');
|
||||
|
||||
final client = GetConnect();
|
||||
client.httpClient.baseUrl = ServiceFinder.services['paperclip'];
|
||||
client.httpClient.addAuthenticator(auth.reqAuthenticator);
|
||||
|
||||
final filePayload =
|
||||
MultipartFile(await file.readAsBytes(), filename: basename(file.path));
|
||||
final fileAlt = basename(file.path).contains('.')
|
||||
? basename(file.path).substring(0, basename(file.path).lastIndexOf('.'))
|
||||
: basename(file.path);
|
||||
|
||||
final resp = await client.post(
|
||||
'/api/attachments',
|
||||
FormData({
|
||||
'alt': fileAlt,
|
||||
'file': filePayload,
|
||||
'hash': hash,
|
||||
'usage': usage,
|
||||
'metadata': jsonEncode({
|
||||
if (ratio != null) 'ratio': ratio,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
if (resp.statusCode == 200) {
|
||||
return resp;
|
||||
}
|
||||
|
||||
throw Exception(resp.bodyString);
|
||||
}
|
||||
|
||||
Future<Response> updateAttachment(
|
||||
int id,
|
||||
String alt,
|
||||
String usage, {
|
||||
double? ratio,
|
||||
bool isMature = false,
|
||||
}) async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (!await auth.isAuthorized) throw Exception('unauthorized');
|
||||
|
||||
final client = GetConnect();
|
||||
client.httpClient.baseUrl = ServiceFinder.services['paperclip'];
|
||||
client.httpClient.addAuthenticator(auth.reqAuthenticator);
|
||||
|
||||
var resp = await client.put('/api/attachments/$id', {
|
||||
'metadata': {
|
||||
if (ratio != null) 'ratio': ratio,
|
||||
},
|
||||
'alt': alt,
|
||||
'usage': usage,
|
||||
'is_mature': isMature,
|
||||
});
|
||||
|
||||
if (resp.statusCode != 200) {
|
||||
throw Exception(resp.bodyString);
|
||||
}
|
||||
|
||||
return resp.body;
|
||||
}
|
||||
|
||||
Future<Response> deleteAttachment(int id) async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (!await auth.isAuthorized) throw Exception('unauthorized');
|
||||
|
||||
final client = GetConnect();
|
||||
client.httpClient.baseUrl = ServiceFinder.services['paperclip'];
|
||||
client.httpClient.addAuthenticator(auth.reqAuthenticator);
|
||||
|
||||
var resp = await client.delete('/api/attachments/$id');
|
||||
if (resp.statusCode != 200) {
|
||||
throw Exception(resp.bodyString);
|
||||
}
|
||||
|
||||
return resp;
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/services.dart';
|
||||
|
||||
class AttachmentListProvider extends GetConnect {
|
||||
@override
|
||||
void onInit() {
|
||||
httpClient.baseUrl = ServiceFinder.services['paperclip'];
|
||||
}
|
||||
|
||||
Future<Response> getMetadata(int id) => get('/api/attachments/$id/meta');
|
||||
}
|
@ -1,9 +1,11 @@
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:solian/screens/account.dart';
|
||||
import 'package:solian/screens/account/personalize.dart';
|
||||
import 'package:solian/screens/auth/signin.dart';
|
||||
import 'package:solian/screens/auth/signup.dart';
|
||||
import 'package:solian/screens/home.dart';
|
||||
import 'package:solian/screens/posts/publish.dart';
|
||||
import 'package:solian/shells/basic_shell.dart';
|
||||
import 'package:solian/shells/nav_shell.dart';
|
||||
|
||||
abstract class AppRouter {
|
||||
@ -14,30 +16,41 @@ abstract class AppRouter {
|
||||
NavShell(state: state, child: child),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: "/",
|
||||
name: "home",
|
||||
path: '/',
|
||||
name: 'home',
|
||||
builder: (context, state) => const HomeScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: "/account",
|
||||
name: "account",
|
||||
path: '/account',
|
||||
name: 'account',
|
||||
builder: (context, state) => const AccountScreen(),
|
||||
),
|
||||
],
|
||||
),
|
||||
ShellRoute(
|
||||
builder: (context, state, child) =>
|
||||
BasicShell(state: state, child: child),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: "/auth/sign-in",
|
||||
name: "signin",
|
||||
path: '/account/personalize',
|
||||
name: 'accountPersonalize',
|
||||
builder: (context, state) => const PersonalizeScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/auth/sign-in',
|
||||
name: 'signin',
|
||||
builder: (context, state) => const SignInScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: "/auth/sign-up",
|
||||
name: "signup",
|
||||
path: '/auth/sign-up',
|
||||
name: 'signup',
|
||||
builder: (context, state) => const SignUpScreen(),
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRoute(
|
||||
path: "/posts/publish",
|
||||
name: "postPublishing",
|
||||
path: '/posts/publish',
|
||||
name: 'postPublishing',
|
||||
builder: (context, state) {
|
||||
final arguments = state.extra as PostPublishingArguments?;
|
||||
return PostPublishingScreen(
|
||||
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/router.dart';
|
||||
import 'package:solian/theme.dart';
|
||||
import 'package:solian/widgets/account/account_avatar.dart';
|
||||
|
||||
class AccountScreen extends StatefulWidget {
|
||||
@ -15,8 +16,12 @@ class _AccountScreenState extends State<AccountScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final actionItems = [
|
||||
// (const Icon(Icons.color_lens), 'personalize'.tr, 'account.personalize'),
|
||||
// (const Icon(Icons.diversity_1), 'friend'.tr, 'account.friend'),
|
||||
(
|
||||
const Icon(Icons.color_lens),
|
||||
'accountPersonalize'.tr,
|
||||
'accountPersonalize'
|
||||
),
|
||||
(const Icon(Icons.diversity_1), 'accountFriend'.tr, 'accountFriend'),
|
||||
];
|
||||
|
||||
final AuthProvider provider = Get.find();
|
||||
@ -101,14 +106,19 @@ class AccountNameCard extends StatelessWidget {
|
||||
}
|
||||
|
||||
return Material(
|
||||
elevation: 2,
|
||||
child: ListTile(
|
||||
contentPadding:
|
||||
const EdgeInsets.only(left: 22, right: 34, top: 4, bottom: 4),
|
||||
leading: AccountAvatar(
|
||||
content: snapshot.data!.body?['avatar'], radius: 24),
|
||||
title: Text(snapshot.data!.body?['nick']),
|
||||
subtitle: Text(snapshot.data!.body?['email']),
|
||||
child: Card(
|
||||
child: ListTile(
|
||||
contentPadding:
|
||||
const EdgeInsets.only(left: 22, right: 34, top: 4, bottom: 4),
|
||||
leading: AccountAvatar(
|
||||
content: snapshot.data!.body?['avatar'], radius: 24),
|
||||
title: Text(snapshot.data!.body?['nick']),
|
||||
subtitle: Text(snapshot.data!.body?['email']),
|
||||
),
|
||||
).paddingOnly(
|
||||
left: 16,
|
||||
right: 16,
|
||||
top: SolianTheme.isLargeScreen(context) ? 8 : 0,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
294
lib/screens/account/personalize.dart
Normal file
294
lib/screens/account/personalize.dart
Normal file
@ -0,0 +1,294 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:solian/exts.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/providers/content/attachment.dart';
|
||||
import 'package:solian/services.dart';
|
||||
import 'package:solian/widgets/account/account_avatar.dart';
|
||||
|
||||
class PersonalizeScreen extends StatefulWidget {
|
||||
const PersonalizeScreen({super.key});
|
||||
|
||||
@override
|
||||
State<PersonalizeScreen> createState() => _PersonalizeScreenState();
|
||||
}
|
||||
|
||||
class _PersonalizeScreenState extends State<PersonalizeScreen> {
|
||||
final _imagePicker = ImagePicker();
|
||||
|
||||
final _usernameController = TextEditingController();
|
||||
final _nicknameController = TextEditingController();
|
||||
final _firstNameController = TextEditingController();
|
||||
final _lastNameController = TextEditingController();
|
||||
final _descriptionController = TextEditingController();
|
||||
final _birthdayController = TextEditingController();
|
||||
|
||||
int? _avatar;
|
||||
int? _banner;
|
||||
DateTime? _birthday;
|
||||
|
||||
bool _isBusy = false;
|
||||
|
||||
void selectBirthday() async {
|
||||
final DateTime? picked = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: _birthday,
|
||||
firstDate: DateTime(DateTime.now().year - 200),
|
||||
lastDate: DateTime(DateTime.now().year + 200),
|
||||
);
|
||||
if (picked != null && picked != _birthday) {
|
||||
setState(() {
|
||||
_birthday = picked;
|
||||
_birthdayController.text =
|
||||
DateFormat('yyyy-MM-dd hh:mm').format(_birthday!);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void syncWidget() async {
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
final AuthProvider auth = Get.find();
|
||||
final prof = await auth.getProfile(noCache: true);
|
||||
setState(() {
|
||||
_usernameController.text = prof.body['name'];
|
||||
_nicknameController.text = prof.body['nick'];
|
||||
_descriptionController.text = prof.body['description'];
|
||||
_firstNameController.text = prof.body['profile']['first_name'];
|
||||
_lastNameController.text = prof.body['profile']['last_name'];
|
||||
_avatar = prof.body['avatar'];
|
||||
_banner = prof.body['banner'];
|
||||
if (prof.body['profile']['birthday'] != null) {
|
||||
_birthday = DateTime.parse(prof.body['profile']['birthday']);
|
||||
_birthdayController.text =
|
||||
DateFormat('yyyy-MM-dd hh:mm').format(_birthday!);
|
||||
}
|
||||
|
||||
_isBusy = false;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> updateImage(String position) async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (!await auth.isAuthorized) return;
|
||||
|
||||
final image = await _imagePicker.pickImage(source: ImageSource.gallery);
|
||||
if (image == null) return;
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
final AttachmentProvider provider = Get.find();
|
||||
|
||||
late Response attachResp;
|
||||
try {
|
||||
final file = File(image.path);
|
||||
final hash = await calculateFileSha256(file);
|
||||
attachResp = await provider.createAttachment(
|
||||
file,
|
||||
hash,
|
||||
'p.$position',
|
||||
ratio: await calculateFileAspectRatio(file),
|
||||
);
|
||||
} catch (e) {
|
||||
setState(() => _isBusy = false);
|
||||
context.showErrorDialog(e);
|
||||
}
|
||||
|
||||
final client = GetConnect();
|
||||
client.httpClient.baseUrl = ServiceFinder.services['passport'];
|
||||
client.httpClient.addAuthenticator(auth.reqAuthenticator);
|
||||
|
||||
final resp = await client.put(
|
||||
'/api/users/me/$position',
|
||||
{'attachment': attachResp.body['id']},
|
||||
);
|
||||
if (resp.statusCode == 200) {
|
||||
syncWidget();
|
||||
|
||||
context.showSnackbar('accountPersonalizeApplied'.tr);
|
||||
} else {
|
||||
context.showErrorDialog(resp.bodyString);
|
||||
}
|
||||
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
Future.delayed(Duration.zero, () => syncWidget());
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
child: ListView(
|
||||
children: [
|
||||
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
|
||||
const SizedBox(height: 24),
|
||||
Stack(
|
||||
children: [
|
||||
AccountAvatar(content: _avatar, radius: 40),
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
left: 40,
|
||||
child: FloatingActionButton.small(
|
||||
heroTag: const Key('avatar-editor'),
|
||||
onPressed: () => updateImage('avatar'),
|
||||
child: const Icon(
|
||||
Icons.camera,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Stack(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 16 / 9,
|
||||
child: Container(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
child: _banner != null
|
||||
? Image.network(
|
||||
'${ServiceFinder.services['paperclip']}/api/attachments/$_banner',
|
||||
fit: BoxFit.cover,
|
||||
loadingBuilder: (BuildContext context,
|
||||
Widget child,
|
||||
ImageChunkEvent? loadingProgress) {
|
||||
if (loadingProgress == null) return child;
|
||||
return Center(
|
||||
child: CircularProgressIndicator(
|
||||
value: loadingProgress.expectedTotalBytes !=
|
||||
null
|
||||
? loadingProgress
|
||||
.cumulativeBytesLoaded /
|
||||
loadingProgress.expectedTotalBytes!
|
||||
: null,
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
: Container(),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 16,
|
||||
right: 16,
|
||||
child: FloatingActionButton(
|
||||
heroTag: const Key('banner-editor'),
|
||||
onPressed: () => updateImage('banner'),
|
||||
child: const Icon(
|
||||
Icons.camera_alt,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
children: [
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: TextField(
|
||||
readOnly: true,
|
||||
controller: _usernameController,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: 'username'.tr,
|
||||
prefixText: '@',
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: TextField(
|
||||
controller: _nicknameController,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: 'nickname'.tr,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: TextField(
|
||||
controller: _firstNameController,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: 'firstName'.tr,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: TextField(
|
||||
controller: _lastNameController,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: 'lastName'.tr,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: _descriptionController,
|
||||
keyboardType: TextInputType.multiline,
|
||||
maxLines: null,
|
||||
minLines: 3,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: 'description'.tr,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: _birthdayController,
|
||||
readOnly: true,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: 'birthday'.tr,
|
||||
),
|
||||
onTap: () => selectBirthday(),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: null,
|
||||
child: Text('reset'.tr),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: null,
|
||||
child: Text('apply'.tr),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -7,9 +7,9 @@ import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/router.dart';
|
||||
import 'package:solian/services.dart';
|
||||
import 'package:solian/widgets/account/account_avatar.dart';
|
||||
import 'package:solian/shells/nav_shell.dart' as shell;
|
||||
import 'package:solian/widgets/attachments/attachment_publish.dart';
|
||||
import 'package:solian/widgets/posts/post_item.dart';
|
||||
import 'package:solian/widgets/prev_page.dart';
|
||||
|
||||
class PostPublishingArguments {
|
||||
final Post? edit;
|
||||
@ -122,7 +122,7 @@ class _PostPublishingScreenState extends State<PostPublishingScreen> {
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('postPublishing'.tr),
|
||||
leading: const shell.BackButton(),
|
||||
leading: const PrevPageButton(),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: Text('postAction'.tr.toUpperCase()),
|
||||
|
30
lib/shells/basic_shell.dart
Normal file
30
lib/shells/basic_shell.dart
Normal file
@ -0,0 +1,30 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:solian/router.dart';
|
||||
import 'package:solian/theme.dart';
|
||||
import 'package:solian/widgets/prev_page.dart';
|
||||
|
||||
class BasicShell extends StatelessWidget {
|
||||
final GoRouterState state;
|
||||
final Widget child;
|
||||
|
||||
const BasicShell({super.key, required this.child, required this.state});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final canPop = AppRouter.instance.canPop();
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(state.topRoute?.name?.tr ?? 'page'.tr),
|
||||
centerTitle: false,
|
||||
titleSpacing: canPop ? null : 24,
|
||||
elevation: SolianTheme.isLargeScreen(context) ? 1 : 0,
|
||||
leading: canPop ? const PrevPageButton() : null,
|
||||
automaticallyImplyLeading: false,
|
||||
),
|
||||
body: child,
|
||||
);
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@ import 'package:get/get.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:solian/router.dart';
|
||||
import 'package:solian/theme.dart';
|
||||
import 'package:solian/widgets/prev_page.dart';
|
||||
import 'package:solian/widgets/navigation/app_navigation_bottom_bar.dart';
|
||||
import 'package:solian/widgets/navigation/app_navigation_rail.dart';
|
||||
|
||||
@ -22,9 +23,12 @@ class NavShell extends StatelessWidget {
|
||||
centerTitle: false,
|
||||
titleSpacing: canPop ? null : 24,
|
||||
elevation: SolianTheme.isLargeScreen(context) ? 1 : 0,
|
||||
leading: canPop ? const BackButton() : null,
|
||||
leading: canPop ? const PrevPageButton() : null,
|
||||
automaticallyImplyLeading: false,
|
||||
),
|
||||
bottomNavigationBar: SolianTheme.isLargeScreen(context) ? null : const AppNavigationBottomBar(),
|
||||
bottomNavigationBar: SolianTheme.isLargeScreen(context)
|
||||
? null
|
||||
: const AppNavigationBottomBar(),
|
||||
body: SolianTheme.isLargeScreen(context)
|
||||
? Row(
|
||||
children: [
|
||||
@ -37,20 +41,3 @@ class NavShell extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class BackButton extends StatelessWidget {
|
||||
const BackButton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
tooltip: MaterialLocalizations.of(context).backButtonTooltip,
|
||||
onPressed: () {
|
||||
if (AppRouter.instance.canPop()) {
|
||||
AppRouter.instance.pop();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ class SolianMessages extends Translations {
|
||||
'hide': 'Hide',
|
||||
'okay': 'Okay',
|
||||
'next': 'Next',
|
||||
'reset': 'Reset',
|
||||
'page': 'Page',
|
||||
'home': 'Home',
|
||||
'apply': 'Apply',
|
||||
@ -21,9 +22,15 @@ class SolianMessages extends Translations {
|
||||
'username': 'Username',
|
||||
'nickname': 'Nickname',
|
||||
'password': 'Password',
|
||||
'description': 'Description',
|
||||
'birthday': 'Birthday',
|
||||
'firstName': 'First Name',
|
||||
'lastName': 'Last Name',
|
||||
'account': 'Account',
|
||||
'personalize': 'Personalize',
|
||||
'friend': 'Friend',
|
||||
'accountPersonalize': 'Personalize',
|
||||
'accountPersonalizeApplied':
|
||||
'Account personalize settings has been saved.',
|
||||
'accountFriend': 'Friend',
|
||||
'aspectRatio': 'Aspect Ratio',
|
||||
'aspectRatioSquare': 'Square',
|
||||
'aspectRatioPortrait': 'Portrait',
|
||||
@ -71,6 +78,7 @@ class SolianMessages extends Translations {
|
||||
'hide': '隐藏',
|
||||
'okay': '确认',
|
||||
'next': '下一步',
|
||||
'reset': '重置',
|
||||
'cancel': '取消',
|
||||
'confirm': '确认',
|
||||
'edit': '编辑',
|
||||
@ -85,9 +93,14 @@ class SolianMessages extends Translations {
|
||||
'username': '用户名',
|
||||
'nickname': '显示名',
|
||||
'password': '密码',
|
||||
'description': '简介',
|
||||
'birthday': '生日',
|
||||
'firstName': '名称',
|
||||
'lastName': '姓氏',
|
||||
'account': '账号',
|
||||
'personalize': '个性化',
|
||||
'friend': '好友',
|
||||
'accountPersonalize': '个性化',
|
||||
'accountPersonalizeApplied': '账户的个性化设置已保存。',
|
||||
'accountFriend': '好友',
|
||||
'aspectRatio': '纵横比',
|
||||
'aspectRatioSquare': '方型',
|
||||
'aspectRatioPortrait': '竖型',
|
||||
|
@ -4,8 +4,8 @@ import 'package:carousel_slider/carousel_slider.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/models/attachment.dart';
|
||||
import 'package:solian/providers/content/attachment_item.dart';
|
||||
import 'package:solian/providers/content/attachment_list.dart';
|
||||
import 'package:solian/widgets/attachments/attachment_item.dart';
|
||||
import 'package:solian/providers/content/attachment.dart';
|
||||
import 'package:solian/widgets/attachments/attachment_list_fullscreen.dart';
|
||||
|
||||
class AttachmentList extends StatefulWidget {
|
||||
@ -26,7 +26,7 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
List<Attachment?> _attachmentsMeta = List.empty();
|
||||
|
||||
void getMetadataList() {
|
||||
final AttachmentListProvider provider = Get.find();
|
||||
final AttachmentProvider provider = Get.find();
|
||||
|
||||
if (widget.attachmentsId.isEmpty) {
|
||||
return;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:solian/models/attachment.dart';
|
||||
import 'package:solian/providers/content/attachment_item.dart';
|
||||
import 'package:solian/widgets/attachments/attachment_item.dart';
|
||||
|
||||
class AttachmentListFullscreen extends StatefulWidget {
|
||||
final Attachment attachment;
|
||||
|
@ -1,6 +1,4 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
@ -8,19 +6,10 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:solian/exts.dart';
|
||||
import 'package:solian/models/attachment.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:solian/providers/content/attachment_list.dart';
|
||||
import 'package:solian/services.dart';
|
||||
|
||||
Future<String> calculateFileSha256(File file) async {
|
||||
final bytes = await file.readAsBytes();
|
||||
final digest = await Isolate.run(() => sha256.convert(bytes));
|
||||
return digest.toString();
|
||||
}
|
||||
import 'package:solian/providers/content/attachment.dart';
|
||||
|
||||
class AttachmentPublishingPopup extends StatefulWidget {
|
||||
final String usage;
|
||||
@ -59,11 +48,13 @@ class _AttachmentPublishingPopupState extends State<AttachmentPublishingPopup> {
|
||||
for (final media in medias) {
|
||||
final file = File(media.path);
|
||||
final hash = await calculateFileSha256(file);
|
||||
final image = await decodeImageFromList(await file.readAsBytes());
|
||||
final ratio = image.width / image.height;
|
||||
|
||||
try {
|
||||
await uploadAttachment(file, hash, ratio: ratio);
|
||||
await uploadAttachment(
|
||||
file,
|
||||
hash,
|
||||
ratio: await calculateFileAspectRatio(file),
|
||||
);
|
||||
} catch (err) {
|
||||
this.context.showErrorDialog(err);
|
||||
}
|
||||
@ -102,10 +93,10 @@ class _AttachmentPublishingPopupState extends State<AttachmentPublishingPopup> {
|
||||
await FilePicker.platform.pickFiles(allowMultiple: true);
|
||||
if (result == null) return;
|
||||
|
||||
List<File> files = result.paths.map((path) => File(path!)).toList();
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
List<File> files = result.paths.map((path) => File(path!)).toList();
|
||||
|
||||
for (final file in files) {
|
||||
final hash = await calculateFileSha256(file);
|
||||
try {
|
||||
@ -139,8 +130,7 @@ class _AttachmentPublishingPopupState extends State<AttachmentPublishingPopup> {
|
||||
if (isVideo) {
|
||||
ratio = 16 / 9;
|
||||
} else {
|
||||
final image = await decodeImageFromList(await file.readAsBytes());
|
||||
ratio = image.width / image.height;
|
||||
ratio = await calculateFileAspectRatio(file);
|
||||
}
|
||||
|
||||
try {
|
||||
@ -153,36 +143,19 @@ class _AttachmentPublishingPopupState extends State<AttachmentPublishingPopup> {
|
||||
}
|
||||
|
||||
Future<void> uploadAttachment(File file, String hash, {double? ratio}) async {
|
||||
final AuthProvider auth = Get.find();
|
||||
|
||||
final client = GetConnect();
|
||||
client.httpClient.baseUrl = ServiceFinder.services['paperclip'];
|
||||
client.httpClient.addAuthenticator(auth.reqAuthenticator);
|
||||
|
||||
final filePayload =
|
||||
MultipartFile(await file.readAsBytes(), filename: basename(file.path));
|
||||
final fileAlt = basename(file.path).contains('.')
|
||||
? basename(file.path).substring(0, basename(file.path).lastIndexOf('.'))
|
||||
: basename(file.path);
|
||||
|
||||
final resp = await client.post(
|
||||
'/api/attachments',
|
||||
FormData({
|
||||
'alt': fileAlt,
|
||||
'file': filePayload,
|
||||
'hash': hash,
|
||||
'usage': widget.usage,
|
||||
'metadata': jsonEncode({
|
||||
if (ratio != null) 'ratio': ratio,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
if (resp.statusCode == 200) {
|
||||
final AttachmentProvider provider = Get.find();
|
||||
try {
|
||||
final resp = await provider.createAttachment(
|
||||
file,
|
||||
hash,
|
||||
widget.usage,
|
||||
ratio: ratio,
|
||||
);
|
||||
var result = Attachment.fromJson(resp.body);
|
||||
setState(() => _attachments.add(result));
|
||||
widget.onUpdate(_attachments.map((e) => e!.id).toList());
|
||||
} else {
|
||||
throw Exception(resp.bodyString);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@ -206,7 +179,7 @@ class _AttachmentPublishingPopupState extends State<AttachmentPublishingPopup> {
|
||||
}
|
||||
|
||||
void revertMetadataList() {
|
||||
final AttachmentListProvider provider = Get.find();
|
||||
final AttachmentProvider provider = Get.find();
|
||||
|
||||
if (widget.current.isEmpty) {
|
||||
_isFirstTimeBusy = false;
|
||||
@ -299,7 +272,7 @@ class _AttachmentPublishingPopupState extends State<AttachmentPublishingPopup> {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AttachmentEditingPopup(
|
||||
return AttachmentEditingDialog(
|
||||
item: element,
|
||||
onDelete: () {
|
||||
setState(
|
||||
@ -379,22 +352,23 @@ class _AttachmentPublishingPopupState extends State<AttachmentPublishingPopup> {
|
||||
}
|
||||
}
|
||||
|
||||
class AttachmentEditingPopup extends StatefulWidget {
|
||||
class AttachmentEditingDialog extends StatefulWidget {
|
||||
final Attachment item;
|
||||
final Function onDelete;
|
||||
final Function(Attachment item) onUpdate;
|
||||
|
||||
const AttachmentEditingPopup(
|
||||
const AttachmentEditingDialog(
|
||||
{super.key,
|
||||
required this.item,
|
||||
required this.onDelete,
|
||||
required this.onUpdate});
|
||||
|
||||
@override
|
||||
State<AttachmentEditingPopup> createState() => _AttachmentEditingPopupState();
|
||||
State<AttachmentEditingDialog> createState() =>
|
||||
_AttachmentEditingDialogState();
|
||||
}
|
||||
|
||||
class _AttachmentEditingPopupState extends State<AttachmentEditingPopup> {
|
||||
class _AttachmentEditingDialogState extends State<AttachmentEditingDialog> {
|
||||
final _ratioController = TextEditingController();
|
||||
final _altController = TextEditingController();
|
||||
|
||||
@ -402,49 +376,40 @@ class _AttachmentEditingPopupState extends State<AttachmentEditingPopup> {
|
||||
bool _isMature = false;
|
||||
bool _hasAspectRatio = false;
|
||||
|
||||
Future<Attachment?> applyAttachment() async {
|
||||
final AuthProvider auth = Get.find();
|
||||
|
||||
final client = GetConnect();
|
||||
client.httpClient.baseUrl = ServiceFinder.services['paperclip'];
|
||||
client.httpClient.addAuthenticator(auth.reqAuthenticator);
|
||||
Future<Attachment?> updateAttachment() async {
|
||||
final AttachmentProvider provider = Get.find();
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
var resp = await client.put('/api/attachments/${widget.item.id}', {
|
||||
'metadata': {
|
||||
if (_hasAspectRatio)
|
||||
'ratio': double.tryParse(_ratioController.value.text) ?? 1,
|
||||
},
|
||||
'alt': _altController.value.text,
|
||||
'usage': widget.item.usage,
|
||||
'is_mature': _isMature,
|
||||
});
|
||||
|
||||
setState(() => _isBusy = false);
|
||||
|
||||
if (resp.statusCode != 200) {
|
||||
this.context.showErrorDialog(resp.bodyString);
|
||||
return null;
|
||||
} else {
|
||||
try {
|
||||
final resp = await provider.updateAttachment(
|
||||
widget.item.id,
|
||||
_altController.value.text,
|
||||
widget.item.usage,
|
||||
ratio: _hasAspectRatio
|
||||
? (double.tryParse(_ratioController.value.text) ?? 1)
|
||||
: null,
|
||||
isMature: _isMature,
|
||||
);
|
||||
return Attachment.fromJson(resp.body);
|
||||
} catch (e) {
|
||||
this.context.showErrorDialog(e);
|
||||
return null;
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteAttachment() async {
|
||||
final AuthProvider auth = Get.find();
|
||||
|
||||
final client = GetConnect();
|
||||
client.httpClient.baseUrl = ServiceFinder.services['paperclip'];
|
||||
client.httpClient.addAuthenticator(auth.reqAuthenticator);
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
var resp = await client.delete('/api/attachments/${widget.item.id}');
|
||||
if (resp.statusCode == 200) {
|
||||
try {
|
||||
final AttachmentProvider provider = Get.find();
|
||||
await provider.deleteAttachment(widget.item.id);
|
||||
widget.onDelete();
|
||||
} else {
|
||||
this.context.showErrorDialog(resp.bodyString);
|
||||
} catch (e) {
|
||||
this.context.showErrorDialog(e);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
|
||||
void syncWidget() {
|
||||
@ -586,7 +551,7 @@ class _AttachmentEditingPopupState extends State<AttachmentEditingPopup> {
|
||||
TextButton(
|
||||
child: Text('apply'.tr),
|
||||
onPressed: () {
|
||||
applyAttachment().then((value) {
|
||||
updateAttachment().then((value) {
|
||||
if (value != null) {
|
||||
widget.onUpdate(value);
|
||||
Navigator.pop(context);
|
||||
|
19
lib/widgets/prev_page.dart
Normal file
19
lib/widgets/prev_page.dart
Normal file
@ -0,0 +1,19 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:solian/router.dart';
|
||||
|
||||
class PrevPageButton extends StatelessWidget {
|
||||
const PrevPageButton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
tooltip: MaterialLocalizations.of(context).backButtonTooltip,
|
||||
onPressed: () {
|
||||
if (AppRouter.instance.canPop()) {
|
||||
AppRouter.instance.pop();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
34
pubspec.lock
34
pubspec.lock
@ -1,6 +1,14 @@
|
||||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
archive:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: archive
|
||||
sha256: "6bd38d335f0954f5fad9c79e614604fbf03a0e5b975923dd001b6ea965ef5b4b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.6.0"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -280,6 +288,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.2"
|
||||
image:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: image
|
||||
sha256: "4c68bfd5ae83e700b5204c1e74451e7bf3cf750e6843c6e158289cf56bda018e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.7"
|
||||
image_picker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -353,7 +369,7 @@ packages:
|
||||
source: hosted
|
||||
version: "4.0.0"
|
||||
intl:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: intl
|
||||
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
|
||||
@ -512,6 +528,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.1"
|
||||
petitparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: petitparser
|
||||
sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.2"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -709,6 +733,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
xml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xml
|
||||
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.5.0"
|
||||
sdks:
|
||||
dart: ">=3.3.4 <4.0.0"
|
||||
flutter: ">=3.19.0"
|
||||
|
@ -49,6 +49,8 @@ dependencies:
|
||||
file_picker: ^8.0.3
|
||||
crypto: ^3.0.3
|
||||
path: ^1.9.0
|
||||
intl: ^0.19.0
|
||||
image: ^4.1.7
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
Loading…
Reference in New Issue
Block a user