mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-01-22 16:56:19 +01:00
450 lines
13 KiB
Dart
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);
|
|
}
|