nc-photos/app/lib/widget/account_picker_dialog.dart
2023-07-29 02:50:57 +08:00

450 lines
13 KiB
Dart

import 'dart:async';
import 'dart:math';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:clock/clock.dart';
import 'package:copy_with/copy_with.dart';
import 'package:event_bus/event_bus.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:kiwi/kiwi.dart';
import 'package:logging/logging.dart';
import 'package:mutex/mutex.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/api/api_util.dart' as api_util;
import 'package:nc_photos/app_localizations.dart';
import 'package:nc_photos/bloc_util.dart';
import 'package:nc_photos/controller/account_controller.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/pref.dart';
import 'package:nc_photos/entity/server_status.dart';
import 'package:nc_photos/entity/sqlite/database.dart' as sql;
import 'package:nc_photos/event/event.dart';
import 'package:nc_photos/exception_event.dart';
import 'package:nc_photos/exception_util.dart' as exception_util;
import 'package:nc_photos/help_utils.dart' as help_util;
import 'package:nc_photos/k.dart' as k;
import 'package:nc_photos/theme.dart';
import 'package:nc_photos/toast.dart';
import 'package:nc_photos/url_launcher_util.dart';
import 'package:nc_photos/widget/home.dart';
import 'package:nc_photos/widget/settings.dart';
import 'package:nc_photos/widget/settings/account_settings.dart';
import 'package:nc_photos/widget/sign_in.dart';
import 'package:np_codegen/np_codegen.dart';
import 'package:to_string/to_string.dart';
part 'account_picker_dialog.g.dart';
part 'account_picker_dialog/bloc.dart';
part 'account_picker_dialog/state_event.dart';
typedef _BlocBuilder = BlocBuilder<_Bloc, _State>;
typedef _BlocListener = BlocListener<_Bloc, _State>;
class AccountPickerDialog extends StatelessWidget {
const AccountPickerDialog({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => _Bloc(
container: KiwiContainer().resolve(),
accountController: context.read(),
),
child: const _WrappedAccountPickerDialog(),
);
}
}
class _WrappedAccountPickerDialog extends StatelessWidget {
const _WrappedAccountPickerDialog();
@override
Widget build(BuildContext context) {
return MultiBlocListener(
listeners: [
_BlocListener(
listenWhen: (previous, current) =>
previous.newSelectAccount != current.newSelectAccount,
listener: (context, state) {
if (state.newSelectAccount != null) {
Navigator.of(context).pushNamedAndRemoveUntil(
Home.routeName,
(_) => false,
arguments: HomeArguments(state.newSelectAccount!),
);
}
},
),
_BlocListener(
listenWhen: (previous, current) => previous.error != current.error,
listener: (context, state) {
if (state.error != null) {
AppToast.showToast(
context,
msg: exception_util.toUserString(state.error!.error),
duration: k.snackBarDurationNormal,
);
}
},
),
],
child: Dialog(
child: Padding(
padding: const EdgeInsets.all(8),
child: ClipRRect(
borderRadius: BorderRadius.circular(24),
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 8),
Stack(
alignment: Alignment.center,
children: [
Text(
L10n.global().appTitle,
style: Theme.of(context).textTheme.headlineSmall,
),
if (!Pref().isFollowSystemThemeOr(false))
Align(
alignment: AlignmentDirectional.centerEnd,
child: _DarkModeSwitch(
onChanged: _onDarkModeChanged,
),
),
],
),
const SizedBox(height: 8),
ClipRRect(
borderRadius: BorderRadius.circular(24),
child: Container(
color: Theme.of(context).colorScheme.background,
child: Material(
type: MaterialType.transparency,
child: _BlocBuilder(
buildWhen: (previous, current) =>
previous.isOpenDropdown !=
current.isOpenDropdown ||
previous.accounts != current.accounts,
builder: (context, state) {
final bloc = context.read<_Bloc>();
return Column(
mainAxisSize: MainAxisSize.min,
children: [
const _AccountDropdown(),
if (state.isOpenDropdown) ...[
...state.accounts
.where(
(a) => a.id != bloc.activeAccount.id)
.map((a) => _AccountView(account: a)),
const _NewAccountView(),
] else
const _AccountSettingsView(),
],
);
},
),
),
),
),
_IconTile(
icon: const Icon(Icons.settings_outlined),
title: Text(L10n.global().settingsMenuLabel),
onTap: () {
Navigator.of(context)
..pop()
..pushNamed(
Settings.routeName,
arguments: SettingsArguments(
context.read<_Bloc>().activeAccount),
);
},
),
_IconTile(
icon: const Icon(Icons.groups_outlined),
title: Text(L10n.global().contributorsTooltip),
onTap: () {
Navigator.of(context).pop();
launch(help_util.contributorsUrl);
},
),
_IconTile(
icon: const Icon(Icons.help_outline),
title: Text(L10n.global().helpTooltip),
onTap: () {
Navigator.of(context).pop();
launch(help_util.mainUrl);
},
),
const _AboutChin(),
],
),
),
),
),
),
);
}
void _onDarkModeChanged(bool value) {
Pref().setDarkTheme(value).then((_) {
KiwiContainer().resolve<EventBus>().fire(ThemeChangedEvent());
});
}
}
class _DarkModeSwitch extends StatelessWidget {
const _DarkModeSwitch({
this.onChanged,
});
@override
Widget build(BuildContext context) {
return DarkModeSwitchTheme(
child: Switch(
value: Theme.of(context).brightness == Brightness.dark,
onChanged: onChanged,
activeThumbImage:
const AssetImage("assets/ic_dark_mode_switch_24dp.png"),
inactiveThumbImage:
const AssetImage("assets/ic_dark_mode_switch_24dp.png"),
),
);
}
final ValueChanged<bool>? onChanged;
}
class _AccountDropdown extends StatelessWidget {
const _AccountDropdown();
@override
Widget build(BuildContext context) {
return _AccountTile(
account: context.read<_Bloc>().activeAccount,
trailing: _BlocBuilder(
builder: (_, state) {
return AnimatedRotation(
turns: state.isOpenDropdown ? .5 : 0,
duration: k.animationDurationShort,
child: Icon(
Icons.keyboard_arrow_down_outlined,
color: state.isOpenDropdown
? Theme.of(context).colorScheme.primary
: null,
),
);
},
),
onTap: () {
context.read<_Bloc>().add(const _ToggleDropdown());
},
);
}
}
class _AccountTile extends StatelessWidget {
const _AccountTile({
required this.account,
this.trailing,
this.onTap,
});
@override
Widget build(BuildContext context) {
final accountLabel = AccountPref.of(account).getAccountLabel();
return ListTile(
leading: SizedBox.square(
dimension: 40,
child: Center(child: _AccountIcon(account)),
),
title: accountLabel != null
? SizedBox(
height: 64,
child: Align(
alignment: AlignmentDirectional.centerStart,
child: Text(
accountLabel,
maxLines: 1,
overflow: TextOverflow.clip,
),
),
)
: Text(
account.address,
maxLines: 1,
overflow: TextOverflow.clip,
),
subtitle: accountLabel == null ? Text(account.username2) : null,
trailing: trailing,
onTap: onTap,
);
}
final Account account;
final Widget? trailing;
final VoidCallback? onTap;
}
class _AccountIcon extends StatelessWidget {
const _AccountIcon(this.account);
@override
Widget build(BuildContext context) {
return ClipRRect(
borderRadius: BorderRadius.circular(24),
child: CachedNetworkImage(
imageUrl: api_util.getAccountAvatarUrl(account, 64),
fadeInDuration: const Duration(),
filterQuality: FilterQuality.high,
),
);
}
final Account account;
}
class _IconTile extends StatelessWidget {
const _IconTile({
required this.icon,
required this.title,
this.onTap,
});
@override
Widget build(BuildContext context) {
return ListTile(
leading: SizedBox.square(
dimension: 40,
child: Center(child: icon),
),
title: title,
onTap: onTap,
);
}
final Widget icon;
final Widget title;
final VoidCallback? onTap;
}
class _AccountView extends StatelessWidget {
const _AccountView({
required this.account,
});
@override
Widget build(BuildContext context) {
return _AccountTile(
account: account,
trailing: IconButton(
icon: const Icon(Icons.close),
tooltip: L10n.global().deleteTooltip,
onPressed: () {
context.read<_Bloc>().add(_DeleteAccount(account));
},
),
onTap: () {
context.read<_Bloc>().add(_SwitchAccount(account));
},
);
}
final Account account;
}
class _AccountSettingsView extends StatelessWidget {
const _AccountSettingsView();
@override
Widget build(BuildContext context) {
return _IconTile(
icon: const Icon(Icons.manage_accounts_outlined),
title: Text(L10n.global().accountSettingsTooltip),
onTap: () {
Navigator.of(context)
..pop()
..pushNamed(AccountSettings.routeName);
},
);
}
}
class _NewAccountView extends StatelessWidget {
const _NewAccountView();
@override
Widget build(BuildContext context) {
return _IconTile(
icon: const Icon(Icons.add),
title: Text(L10n.global().addServerTooltip),
onTap: () {
Navigator.of(context)
..pop()
..pushNamed(SignIn.routeName);
},
);
}
}
class _AboutChin extends StatelessWidget {
const _AboutChin();
@override
Widget build(BuildContext context) {
return StreamBuilder<ServerStatus?>(
stream: context.read<_Bloc>().accountController.serverController.status,
initialData: context
.read<_Bloc>()
.accountController
.serverController
.status
.valueOrNull,
builder: (context, snapshot) {
var text = "${L10n.global().appTitle} ${k.versionStr}";
if (snapshot.hasData) {
final status = snapshot.requireData!;
text +=
" ${_getSymbol()} ${status.productName} ${status.versionName}";
}
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Text(
text,
style: Theme.of(context).textTheme.bodySmall,
),
);
},
);
}
String _getSymbol() {
final today = clock.now();
if (today.month == 1 && today.day == 1) {
// firework
return "\u{1f386}";
} else if (today.month == 4 && today.day == 10) {
// initial commit!
return "\u{1f382}";
} else {
const symbols = [
// cloud
"\u2601",
// heart
"\u2665",
// star
"\u2b50",
// rainbow
"\u{1f308}",
// globe
"\u{1f310}",
// clover
"\u{1f340}",
];
return symbols[Random(_seed).nextInt(symbols.length)];
}
}
static final _seed = Random().nextInt(65536);
}