Merge branch 'master' into fix-navigation-bar
This commit is contained in:
commit
aabc270a3b
14
CHANGELOG.md
14
CHANGELOG.md
|
@ -1,3 +1,17 @@
|
|||
## v0.2.3 - 2021-02-09
|
||||
|
||||
Lemmur is now available on the [play store](https://play.google.com/store/apps/details?id=com.krawieck.lemmur) and [f-droid](https://f-droid.org/packages/com.krawieck.lemmur)
|
||||
|
||||
### Changed
|
||||
|
||||
- Posts with large amount of text are now truncated in infinite scroll views
|
||||
- Changed image viewer dismissal to be more fun. The image now also moves on the x axis, changes scale and rotates a bit for more user enjoyment
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed issue where the "About lemmur" tile would not appear on Windows/Linux
|
||||
- Added a bigger bottom margin in the comment section to prevent the floating action button from covering the last comment
|
||||
|
||||
## v0.2.2 - 2021-02-03
|
||||
|
||||
Minimum Lemmy version supported: `v0.9.4`
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
Lemmur is now available on the [play store](https://play.google.com/store/apps/details?id=com.krawieck.lemmur) and [f-droid](https://f-droid.org/packages/com.krawieck.lemmur)
|
||||
|
||||
### Changed
|
||||
|
||||
- Posts with large amount of text are now truncated in infinite scroll views
|
||||
- Changed image viewer dismissal to be more fun. The image now also moves on the x axis, changes scale and rotates a bit for more user enjoyment
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed issue where the "About lemmur" tile would not appear on Windows/Linux
|
||||
- Added a bigger bottom margin in the comment section to prevent the floating action button from covering the last comment
|
|
@ -4,7 +4,6 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:url_launcher/url_launcher.dart' as ul;
|
||||
|
||||
import 'hooks/stores.dart';
|
||||
import 'pages/communities_tab.dart';
|
||||
|
@ -15,6 +14,7 @@ import 'pages/search_tab.dart';
|
|||
import 'stores/accounts_store.dart';
|
||||
import 'stores/config_store.dart';
|
||||
import 'util/extensions/brightness.dart';
|
||||
import 'theme.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
@ -46,51 +46,17 @@ class MyApp extends HookWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final configStore = useConfigStore();
|
||||
final maybeAmoledColor = configStore.amoledDarkMode ? Colors.black : null;
|
||||
|
||||
return MaterialApp(
|
||||
title: 'Lemmur',
|
||||
title: 'lemmur',
|
||||
themeMode: configStore.theme,
|
||||
darkTheme: ThemeData.dark().copyWith(
|
||||
scaffoldBackgroundColor: maybeAmoledColor,
|
||||
backgroundColor: maybeAmoledColor,
|
||||
canvasColor: maybeAmoledColor,
|
||||
cardColor: maybeAmoledColor,
|
||||
splashColor: maybeAmoledColor,
|
||||
),
|
||||
theme: ThemeData(
|
||||
visualDensity: VisualDensity.adaptivePlatformDensity,
|
||||
),
|
||||
darkTheme: configStore.amoledDarkMode ? amoledTheme : darkTheme,
|
||||
theme: lightTheme,
|
||||
home: const MyHomePage(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TemporarySearchTab extends HookWidget {
|
||||
const TemporarySearchTab();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final accStore = useAccountsStore();
|
||||
return ListView(
|
||||
children: [
|
||||
const ListTile(
|
||||
title: Center(
|
||||
child: Text('🚧 this tab is still under construction 🚧\n'
|
||||
'but you can open your instances in a browser '
|
||||
' for missing functionality')),
|
||||
),
|
||||
const Divider(),
|
||||
for (final inst in accStore.instances)
|
||||
ListTile(
|
||||
title: Text(inst),
|
||||
onTap: () => ul.launch('https://$inst/'),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MyHomePage extends HookWidget {
|
||||
const MyHomePage();
|
||||
|
||||
|
|
|
@ -7,8 +7,8 @@ import 'package:url_launcher/url_launcher.dart' as ul;
|
|||
|
||||
import '../hooks/delayed_loading.dart';
|
||||
import '../hooks/stores.dart';
|
||||
import '../widgets/bottom_modal.dart';
|
||||
import '../widgets/fullscreenable_image.dart';
|
||||
import '../widgets/radio_picker.dart';
|
||||
import 'add_instance.dart';
|
||||
|
||||
/// A modal where an account can be added for a given instance
|
||||
|
@ -22,15 +22,14 @@ class AddAccountPage extends HookWidget {
|
|||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
final usernameController = useTextEditingController();
|
||||
final passwordController = useTextEditingController();
|
||||
useValueListenable(usernameController);
|
||||
useValueListenable(passwordController);
|
||||
final usernameController = useListenable(useTextEditingController());
|
||||
final passwordController = useListenable(useTextEditingController());
|
||||
final accountsStore = useAccountsStore();
|
||||
|
||||
final loading = useDelayedLoading();
|
||||
final selectedInstance = useState(instanceHost);
|
||||
final icon = useState<String>(null);
|
||||
|
||||
useEffect(() {
|
||||
LemmyApiV2(selectedInstance.value)
|
||||
.run(GetSite())
|
||||
|
@ -38,46 +37,6 @@ class AddAccountPage extends HookWidget {
|
|||
return null;
|
||||
}, [selectedInstance.value]);
|
||||
|
||||
/// show a modal with a list of instance checkboxes
|
||||
selectInstance() async {
|
||||
final val = await showModalBottomSheet<String>(
|
||||
backgroundColor: Colors.transparent,
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (context) => BottomModal(
|
||||
title: 'select instance',
|
||||
child: Column(children: [
|
||||
for (final i in accountsStore.instances)
|
||||
RadioListTile<String>(
|
||||
value: i,
|
||||
groupValue: selectedInstance.value,
|
||||
onChanged: (val) {
|
||||
Navigator.of(context).pop(val);
|
||||
},
|
||||
title: Text(i),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Padding(
|
||||
padding: EdgeInsets.all(8),
|
||||
child: Icon(Icons.add),
|
||||
),
|
||||
title: const Text('Add instance'),
|
||||
onTap: () async {
|
||||
final val = await showCupertinoModalPopup<String>(
|
||||
context: context,
|
||||
builder: (context) => AddInstancePage(),
|
||||
);
|
||||
Navigator.of(context).pop(val);
|
||||
},
|
||||
),
|
||||
]),
|
||||
),
|
||||
);
|
||||
if (val != null) {
|
||||
selectedInstance.value = val;
|
||||
}
|
||||
}
|
||||
|
||||
handleOnAdd() async {
|
||||
try {
|
||||
loading.start();
|
||||
|
@ -99,107 +58,91 @@ class AddAccountPage extends HookWidget {
|
|||
key: scaffoldKey,
|
||||
appBar: AppBar(
|
||||
leading: const CloseButton(),
|
||||
actionsIconTheme: theme.iconTheme,
|
||||
iconTheme: theme.iconTheme,
|
||||
textTheme: theme.textTheme,
|
||||
brightness: theme.brightness,
|
||||
centerTitle: true,
|
||||
title: const Text('Add account'),
|
||||
backgroundColor: theme.canvasColor,
|
||||
shadowColor: Colors.transparent,
|
||||
),
|
||||
body: Padding(
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.all(15),
|
||||
child: ListView(
|
||||
children: [
|
||||
if (icon.value == null)
|
||||
const SizedBox(height: 150)
|
||||
else
|
||||
SizedBox(
|
||||
height: 150,
|
||||
child: FullscreenableImage(
|
||||
url: icon.value,
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: icon.value,
|
||||
errorWidget: (_, __, ___) => const SizedBox.shrink(),
|
||||
),
|
||||
children: [
|
||||
if (icon.value == null)
|
||||
const SizedBox(height: 150)
|
||||
else
|
||||
SizedBox(
|
||||
height: 150,
|
||||
child: FullscreenableImage(
|
||||
url: icon.value,
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: icon.value,
|
||||
errorWidget: (_, __, ___) => const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
FlatButton(
|
||||
onPressed: selectInstance,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
RadioPicker<String>(
|
||||
title: 'select instance',
|
||||
values: accountsStore.instances.toList(),
|
||||
groupValue: selectedInstance.value,
|
||||
onChanged: (value) => selectedInstance.value = value,
|
||||
buttonBuilder: (context, displayValue, onPressed) => TextButton(
|
||||
onPressed: onPressed,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(selectedInstance.value),
|
||||
Text(displayValue),
|
||||
const Icon(Icons.arrow_drop_down),
|
||||
],
|
||||
),
|
||||
),
|
||||
// TODO: add support for password managers
|
||||
TextField(
|
||||
autofocus: true,
|
||||
controller: usernameController,
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
labelText: 'Username or email',
|
||||
trailing: ListTile(
|
||||
leading: const Padding(
|
||||
padding: EdgeInsets.all(8),
|
||||
child: Icon(Icons.add),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
TextField(
|
||||
controller: passwordController,
|
||||
obscureText: true,
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
labelText: 'Password',
|
||||
),
|
||||
),
|
||||
RaisedButton(
|
||||
color: theme.accentColor,
|
||||
padding: const EdgeInsets.all(0),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
onPressed: usernameController.text.isEmpty ||
|
||||
passwordController.text.isEmpty
|
||||
? null
|
||||
: loading.pending
|
||||
? () {}
|
||||
: handleOnAdd,
|
||||
child: !loading.loading
|
||||
? const Text('Sign in')
|
||||
: SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
valueColor:
|
||||
AlwaysStoppedAnimation<Color>(theme.canvasColor),
|
||||
),
|
||||
),
|
||||
),
|
||||
FlatButton(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
onPressed: () {
|
||||
ul.launch('https://${selectedInstance.value}/login');
|
||||
title: const Text('Add instance'),
|
||||
onTap: () async {
|
||||
final value = await showCupertinoModalPopup<String>(
|
||||
context: context,
|
||||
builder: (context) => AddInstancePage(),
|
||||
);
|
||||
Navigator.of(context).pop(value);
|
||||
},
|
||||
child: const Text('Register'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// TODO: add support for password managers
|
||||
TextField(
|
||||
autofocus: true,
|
||||
controller: usernameController,
|
||||
decoration: const InputDecoration(labelText: 'Username or email'),
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
TextField(
|
||||
controller: passwordController,
|
||||
obscureText: true,
|
||||
decoration: const InputDecoration(labelText: 'Password'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: usernameController.text.isEmpty ||
|
||||
passwordController.text.isEmpty
|
||||
? null
|
||||
: loading.pending
|
||||
? () {}
|
||||
: handleOnAdd,
|
||||
child: !loading.loading
|
||||
? const Text('Sign in')
|
||||
: SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
valueColor:
|
||||
AlwaysStoppedAnimation<Color>(theme.canvasColor),
|
||||
),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
ul.launch('https://${selectedInstance.value}/login');
|
||||
},
|
||||
child: const Text('Register'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -63,14 +63,7 @@ class AddInstancePage extends HookWidget {
|
|||
return Scaffold(
|
||||
key: scaffoldKey,
|
||||
appBar: AppBar(
|
||||
backgroundColor: theme.scaffoldBackgroundColor,
|
||||
brightness: theme.brightness,
|
||||
shadowColor: Colors.transparent,
|
||||
iconTheme: theme.iconTheme,
|
||||
centerTitle: true,
|
||||
leading: const CloseButton(),
|
||||
actionsIconTheme: theme.iconTheme,
|
||||
textTheme: theme.textTheme,
|
||||
title: const Text('Add instance'),
|
||||
),
|
||||
body: ListView(
|
||||
|
@ -107,13 +100,7 @@ class AddInstancePage extends HookWidget {
|
|||
autofocus: true,
|
||||
controller: instanceController,
|
||||
autocorrect: false,
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
labelText: 'instance url',
|
||||
),
|
||||
decoration: const InputDecoration(labelText: 'instance url'),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -122,11 +109,7 @@ class AddInstancePage extends HookWidget {
|
|||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
child: RaisedButton(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
color: theme.accentColor,
|
||||
child: ElevatedButton(
|
||||
onPressed: isSite.value == true ? handleOnAdd : null,
|
||||
child: !debounce.loading
|
||||
? const Text('Add')
|
||||
|
|
|
@ -25,11 +25,8 @@ class CommunitiesListPage extends StatelessWidget {
|
|||
final theme = Theme.of(context);
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
brightness: theme.brightness,
|
||||
title: Text(title, style: theme.textTheme.headline6),
|
||||
centerTitle: true,
|
||||
backgroundColor: theme.cardColor,
|
||||
iconTheme: theme.iconTheme,
|
||||
title: Text(title),
|
||||
),
|
||||
body: SortableInfiniteList<CommunityView>(
|
||||
fetcher: fetcher,
|
||||
|
|
|
@ -159,8 +159,6 @@ class CommunitiesTab extends HookWidget {
|
|||
textAlign: TextAlign.center,
|
||||
decoration: InputDecoration(
|
||||
suffixIcon: filterIcon,
|
||||
isDense: true,
|
||||
border: const OutlineInputBorder(),
|
||||
hintText: 'Filter', // TODO: hint with an filter icon
|
||||
),
|
||||
),
|
||||
|
@ -247,7 +245,7 @@ class CommunitiesTab extends HookWidget {
|
|||
const SizedBox(width: 30),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
'''!${comm.community.name}${comm.community.local ? '' : '@${comm.community.originInstanceHost}'}''',
|
||||
'!${comm.community.name}${comm.community.local ? '' : '@${comm.community.originInstanceHost}'}',
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
@ -11,11 +11,10 @@ import '../hooks/logged_in_action.dart';
|
|||
import '../hooks/memo_future.dart';
|
||||
import '../hooks/stores.dart';
|
||||
import '../util/extensions/api.dart';
|
||||
import '../util/extensions/spaced.dart';
|
||||
import '../util/goto.dart';
|
||||
import '../util/intl.dart';
|
||||
import '../util/more_icon.dart';
|
||||
import '../util/text_color.dart';
|
||||
import '../widgets/badge.dart';
|
||||
import '../widgets/bottom_modal.dart';
|
||||
import '../widgets/fullscreenable_image.dart';
|
||||
import '../widgets/info_table_popup.dart';
|
||||
|
@ -69,8 +68,6 @@ class CommunityPage extends HookWidget {
|
|||
}
|
||||
});
|
||||
|
||||
final colorOnCard = textColorBasedOnBackground(theme.cardColor);
|
||||
|
||||
final community = () {
|
||||
if (fullCommunitySnap.hasData) {
|
||||
return fullCommunitySnap.data.communityView;
|
||||
|
@ -85,12 +82,7 @@ class CommunityPage extends HookWidget {
|
|||
|
||||
if (community == null) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
iconTheme: theme.iconTheme,
|
||||
brightness: theme.brightness,
|
||||
backgroundColor: theme.cardColor,
|
||||
elevation: 0,
|
||||
),
|
||||
appBar: AppBar(),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
|
@ -114,35 +106,31 @@ class CommunityPage extends HookWidget {
|
|||
Share.text('Share instance', community.community.actorId, 'text/plain');
|
||||
|
||||
void _openMoreMenu() {
|
||||
showModalBottomSheet(
|
||||
backgroundColor: Colors.transparent,
|
||||
showBottomModal(
|
||||
context: context,
|
||||
builder: (context) => BottomModal(
|
||||
child: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.open_in_browser),
|
||||
title: const Text('Open in browser'),
|
||||
onTap: () async => await ul
|
||||
.canLaunch(community.community.actorId)
|
||||
? ul.launch(community.community.actorId)
|
||||
: Scaffold.of(context).showSnackBar(
|
||||
const SnackBar(content: Text("can't open in browser"))),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.info_outline),
|
||||
title: const Text('Nerd stuff'),
|
||||
onTap: () {
|
||||
showInfoTablePopup(context, {
|
||||
'id': community.community.id,
|
||||
'actorId': community.community.actorId,
|
||||
'created by': '@${community.creator.name}',
|
||||
'published': community.community.published,
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
builder: (context) => Column(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.open_in_browser),
|
||||
title: const Text('Open in browser'),
|
||||
onTap: () async => await ul.canLaunch(community.community.actorId)
|
||||
? ul.launch(community.community.actorId)
|
||||
: Scaffold.of(context).showSnackBar(
|
||||
const SnackBar(content: Text("can't open in browser"))),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.info_outline),
|
||||
title: const Text('Nerd stuff'),
|
||||
onTap: () {
|
||||
showInfoTablePopup(context, {
|
||||
'id': community.community.id,
|
||||
'actorId': community.community.actorId,
|
||||
'created by': '@${community.creator.name}',
|
||||
'published': community.community.published,
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -152,16 +140,11 @@ class CommunityPage extends HookWidget {
|
|||
length: 3,
|
||||
child: NestedScrollView(
|
||||
headerSliverBuilder: (context, innerBoxIsScrolled) => <Widget>[
|
||||
// TODO: change top section to be more flexible
|
||||
SliverAppBar(
|
||||
expandedHeight: 300,
|
||||
expandedHeight: community.community.icon == null ? 220 : 300,
|
||||
pinned: true,
|
||||
elevation: 0,
|
||||
backgroundColor: theme.cardColor,
|
||||
brightness: theme.brightness,
|
||||
iconTheme: theme.iconTheme,
|
||||
title: Text('!${community.community.name}',
|
||||
style: TextStyle(color: colorOnCard)),
|
||||
title: Text('!${community.community.name}'),
|
||||
actions: [
|
||||
IconButton(icon: const Icon(Icons.share), onPressed: _share),
|
||||
IconButton(icon: Icon(moreIcon), onPressed: _openMoreMenu),
|
||||
|
@ -173,20 +156,19 @@ class CommunityPage extends HookWidget {
|
|||
onlineUsers: fullCommunitySnap.data?.online,
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverPersistentHeader(
|
||||
delegate: _SliverAppBarDelegate(
|
||||
TabBar(
|
||||
labelColor: theme.textTheme.bodyText1.color,
|
||||
unselectedLabelColor: Colors.grey,
|
||||
tabs: const [
|
||||
Tab(text: 'Posts'),
|
||||
Tab(text: 'Comments'),
|
||||
Tab(text: 'About'),
|
||||
],
|
||||
bottom: PreferredSize(
|
||||
preferredSize: const TabBar(tabs: []).preferredSize,
|
||||
child: Material(
|
||||
color: theme.cardColor,
|
||||
child: const TabBar(
|
||||
tabs: [
|
||||
Tab(text: 'Posts'),
|
||||
Tab(text: 'Comments'),
|
||||
Tab(text: 'About'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
pinned: true,
|
||||
),
|
||||
],
|
||||
body: TabBarView(
|
||||
|
@ -251,12 +233,15 @@ class _CommunityOverview extends StatelessWidget {
|
|||
width: 90,
|
||||
height: 90,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.7), blurRadius: 3)
|
||||
]),
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.7),
|
||||
blurRadius: 3,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 83,
|
||||
|
@ -329,15 +314,16 @@ class _CommunityOverview extends StatelessWidget {
|
|||
),
|
||||
// TITLE/MOTTO
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 8, left: 20, right: 20),
|
||||
child: Text(
|
||||
community.community.title,
|
||||
textAlign: TextAlign.center,
|
||||
style:
|
||||
TextStyle(fontWeight: FontWeight.w300, shadows: [shadow]),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 8, left: 20, right: 20),
|
||||
child: Text(
|
||||
community.community.title,
|
||||
textAlign: TextAlign.center,
|
||||
style:
|
||||
TextStyle(fontWeight: FontWeight.w300, shadows: [shadow]),
|
||||
),
|
||||
),
|
||||
)),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 20),
|
||||
child: Stack(
|
||||
|
@ -353,9 +339,7 @@ class _CommunityOverview extends StatelessWidget {
|
|||
child: Icon(Icons.people, size: 20),
|
||||
),
|
||||
Text(compactNumber(community.counts.subscribers)),
|
||||
const Spacer(
|
||||
flex: 4,
|
||||
),
|
||||
const Spacer(flex: 4),
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(right: 3),
|
||||
child: Icon(Icons.record_voice_over, size: 20),
|
||||
|
@ -378,27 +362,6 @@ class _CommunityOverview extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
|
||||
final TabBar _tabBar;
|
||||
|
||||
const _SliverAppBarDelegate(this._tabBar);
|
||||
|
||||
@override
|
||||
double get minExtent => _tabBar.preferredSize.height;
|
||||
@override
|
||||
double get maxExtent => _tabBar.preferredSize.height;
|
||||
|
||||
@override
|
||||
Widget build(
|
||||
BuildContext context, double shrinkOffset, bool overlapsContent) {
|
||||
final theme = Theme.of(context);
|
||||
return Container(color: theme.cardColor, child: _tabBar);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRebuild(_SliverAppBarDelegate oldDelegate) => false;
|
||||
}
|
||||
|
||||
class _AboutTab extends StatelessWidget {
|
||||
final CommunityView community;
|
||||
final List<CommunityModeratorView> moderators;
|
||||
|
@ -435,34 +398,28 @@ class _AboutTab extends StatelessWidget {
|
|||
const _Divider(),
|
||||
],
|
||||
SizedBox(
|
||||
height: 25,
|
||||
height: 32,
|
||||
child: ListView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||
children: [
|
||||
// TODO: consider using Chips
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 7),
|
||||
child: _Badge('${onlineUsers ?? 'X'} users online'),
|
||||
),
|
||||
_Badge(
|
||||
'''${community.counts.subscribers} subscriber${pluralS(community.counts.subscribers)}'''),
|
||||
_Badge(
|
||||
'''${community.counts.posts} post${pluralS(community.counts.posts)}'''),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 15),
|
||||
child: _Badge(
|
||||
'''${community.counts.comments} comment${pluralS(community.counts.comments)}'''),
|
||||
),
|
||||
],
|
||||
Chip(label: Text('${onlineUsers ?? 'X'} users online')),
|
||||
Chip(
|
||||
label: Text(
|
||||
'${community.counts.subscribers} subscriber${pluralS(community.counts.subscribers)}')),
|
||||
Chip(
|
||||
label: Text(
|
||||
'${community.counts.posts} post${pluralS(community.counts.posts)}')),
|
||||
Chip(
|
||||
label: Text(
|
||||
'${community.counts.comments} comment${pluralS(community.counts.comments)}')),
|
||||
].spaced(8),
|
||||
),
|
||||
),
|
||||
const _Divider(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||
child: OutlineButton(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: OutlinedButton(
|
||||
onPressed: goToCategories,
|
||||
child: Text(community.category.name),
|
||||
),
|
||||
|
@ -470,10 +427,7 @@ class _AboutTab extends StatelessWidget {
|
|||
const _Divider(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||
child: OutlineButton(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: OutlinedButton(
|
||||
onPressed: goToModlog,
|
||||
child: const Text('Modlog'),
|
||||
),
|
||||
|
@ -498,29 +452,6 @@ class _AboutTab extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
class _Badge extends StatelessWidget {
|
||||
final String text;
|
||||
final bool noPad;
|
||||
|
||||
const _Badge(this.text, {this.noPad = false});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Padding(
|
||||
padding: noPad ? const EdgeInsets.all(0) : const EdgeInsets.only(left: 8),
|
||||
child: Badge(
|
||||
child: Text(
|
||||
text,
|
||||
style:
|
||||
TextStyle(color: textColorBasedOnBackground(theme.accentColor)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Divider extends StatelessWidget {
|
||||
const _Divider();
|
||||
|
||||
|
@ -541,11 +472,9 @@ class _FollowButton extends HookWidget {
|
|||
final theme = Theme.of(context);
|
||||
|
||||
final isSubbed = useState(community.subscribed ?? false);
|
||||
final delayed = useDelayedLoading();
|
||||
final delayed = useDelayedLoading(Duration.zero);
|
||||
final loggedInAction = useLoggedInAction(community.instanceHost);
|
||||
|
||||
final colorOnTopOfAccent = textColorBasedOnBackground(theme.accentColor);
|
||||
|
||||
subscribe(Jwt token) async {
|
||||
delayed.start();
|
||||
try {
|
||||
|
@ -570,40 +499,35 @@ class _FollowButton extends HookWidget {
|
|||
delayed.cancel();
|
||||
}
|
||||
|
||||
return Center(
|
||||
child: SizedBox(
|
||||
height: 27,
|
||||
width: 160,
|
||||
child: delayed.loading
|
||||
? RaisedButton(
|
||||
onPressed: null,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
return ElevatedButtonTheme(
|
||||
data: ElevatedButtonThemeData(
|
||||
style: theme.elevatedButtonTheme.style.copyWith(
|
||||
shape: MaterialStateProperty.all(const StadiumBorder()),
|
||||
textStyle: MaterialStateProperty.all(theme.textTheme.subtitle1),
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: SizedBox(
|
||||
height: 27,
|
||||
width: 160,
|
||||
child: delayed.loading
|
||||
? const ElevatedButton(
|
||||
onPressed: null,
|
||||
child: SizedBox(
|
||||
height: 15,
|
||||
width: 15,
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
)
|
||||
: ElevatedButton.icon(
|
||||
onPressed:
|
||||
loggedInAction(delayed.pending ? (_) {} : subscribe),
|
||||
icon: isSubbed.value
|
||||
? const Icon(Icons.remove, size: 18)
|
||||
: const Icon(Icons.add, size: 18),
|
||||
label: Text('${isSubbed.value ? 'un' : ''}subscribe'),
|
||||
),
|
||||
child: const SizedBox(
|
||||
height: 15,
|
||||
width: 15,
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
)
|
||||
: RaisedButton.icon(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 5, horizontal: 20),
|
||||
onPressed: loggedInAction(delayed.pending ? (_) {} : subscribe),
|
||||
icon: isSubbed.value
|
||||
? Icon(Icons.remove, size: 18, color: colorOnTopOfAccent)
|
||||
: Icon(Icons.add, size: 18, color: colorOnTopOfAccent),
|
||||
color: theme.accentColor,
|
||||
label: Text(
|
||||
'${isSubbed.value ? 'un' : ''}subscribe',
|
||||
style: TextStyle(
|
||||
color: colorOnTopOfAccent,
|
||||
fontSize: theme.textTheme.subtitle1.fontSize),
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ import '../util/goto.dart';
|
|||
import '../util/pictrs.dart';
|
||||
import '../util/unawaited.dart';
|
||||
import '../widgets/markdown_text.dart';
|
||||
import '../widgets/radio_picker.dart';
|
||||
import 'full_post.dart';
|
||||
|
||||
/// Fab that triggers the [CreatePost] modal
|
||||
|
@ -110,21 +111,18 @@ class CreatePostPage extends HookWidget {
|
|||
urlController.text = '';
|
||||
}
|
||||
|
||||
// TODO: use drop down from AddAccountPage
|
||||
final instanceDropdown = InputDecorator(
|
||||
decoration: const InputDecoration(
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 1, horizontal: 20),
|
||||
border: OutlineInputBorder()),
|
||||
child: DropdownButtonHideUnderline(
|
||||
child: DropdownButton<String>(
|
||||
value: selectedInstance.value,
|
||||
onChanged: (val) => selectedInstance.value = val,
|
||||
items: accStore.loggedInInstances
|
||||
.map((instance) => DropdownMenuItem(
|
||||
value: instance,
|
||||
child: Text(instance),
|
||||
))
|
||||
.toList(),
|
||||
final instanceDropdown = RadioPicker<String>(
|
||||
values: accStore.loggedInInstances.toList(),
|
||||
groupValue: selectedInstance.value,
|
||||
onChanged: (value) => selectedInstance.value = value,
|
||||
buttonBuilder: (context, displayValue, onPressed) => TextButton(
|
||||
onPressed: onPressed,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(displayValue),
|
||||
const Icon(Icons.arrow_drop_down),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -133,7 +131,8 @@ class CreatePostPage extends HookWidget {
|
|||
final communitiesDropdown = InputDecorator(
|
||||
decoration: const InputDecoration(
|
||||
contentPadding: EdgeInsets.symmetric(vertical: 1, horizontal: 20),
|
||||
border: OutlineInputBorder()),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(10)))),
|
||||
child: DropdownButtonHideUnderline(
|
||||
child: DropdownButton<int>(
|
||||
value: selectedCommunity.value?.community?.id,
|
||||
|
@ -166,9 +165,9 @@ class CreatePostPage extends HookWidget {
|
|||
enabled: pictrsDeleteToken.value == null,
|
||||
controller: urlController,
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
labelText: 'URL',
|
||||
suffixIcon: Icon(Icons.link)),
|
||||
labelText: 'URL',
|
||||
suffixIcon: Icon(Icons.link),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 5),
|
||||
|
@ -189,8 +188,7 @@ class CreatePostPage extends HookWidget {
|
|||
controller: titleController,
|
||||
minLines: 1,
|
||||
maxLines: 2,
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(), labelText: 'Title'),
|
||||
decoration: const InputDecoration(labelText: 'Title'),
|
||||
);
|
||||
|
||||
final body = IndexedStack(
|
||||
|
@ -201,9 +199,7 @@ class CreatePostPage extends HookWidget {
|
|||
keyboardType: TextInputType.multiline,
|
||||
maxLines: null,
|
||||
minLines: 5,
|
||||
textAlignVertical: TextAlignVertical.top,
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(), labelText: 'Body'),
|
||||
decoration: const InputDecoration(labelText: 'Body'),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
|
@ -250,10 +246,7 @@ class CreatePostPage extends HookWidget {
|
|||
return Scaffold(
|
||||
key: scaffoldKey,
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: Navigator.of(context).pop,
|
||||
),
|
||||
leading: const CloseButton(),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(showFancy.value ? Icons.build : Icons.brush),
|
||||
|
@ -285,7 +278,7 @@ class CreatePostPage extends HookWidget {
|
|||
],
|
||||
),
|
||||
),
|
||||
FlatButton(
|
||||
TextButton(
|
||||
onPressed: delayed.pending ? () {} : handleSubmit,
|
||||
child: delayed.loading
|
||||
? const CircularProgressIndicator()
|
||||
|
|
|
@ -94,7 +94,6 @@ class FullPostPage extends HookWidget {
|
|||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: const BackButton(),
|
||||
actions: [
|
||||
IconButton(icon: const Icon(Icons.share), onPressed: sharePost),
|
||||
SavePostButton(post),
|
||||
|
|
|
@ -66,123 +66,119 @@ class HomeTab extends HookWidget {
|
|||
]);
|
||||
|
||||
handleListChange() async {
|
||||
final val = await showModalBottomSheet<_SelectedList>(
|
||||
backgroundColor: Colors.transparent,
|
||||
isScrollControlled: true,
|
||||
final val = await showBottomModal<_SelectedList>(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
pop(_SelectedList thing) => Navigator.of(context).pop(thing);
|
||||
|
||||
return BottomModal(
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 5),
|
||||
const ListTile(
|
||||
title: Text('EVERYTHING'),
|
||||
return Column(
|
||||
children: [
|
||||
const SizedBox(height: 5),
|
||||
const ListTile(
|
||||
title: Text('EVERYTHING'),
|
||||
dense: true,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
visualDensity:
|
||||
VisualDensity(vertical: VisualDensity.minimumDensity),
|
||||
leading: SizedBox.shrink(),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(
|
||||
'Subscribed',
|
||||
style: TextStyle(
|
||||
color: accStore.hasNoAccount
|
||||
? theme.textTheme.bodyText1.color.withOpacity(0.4)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
onTap: accStore.hasNoAccount
|
||||
? null
|
||||
: () => pop(
|
||||
const _SelectedList(
|
||||
listingType: PostListingType.subscribed,
|
||||
),
|
||||
),
|
||||
leading: const SizedBox(width: 20),
|
||||
),
|
||||
for (final listingType in [
|
||||
PostListingType.local,
|
||||
PostListingType.all,
|
||||
])
|
||||
ListTile(
|
||||
title: Text(listingType.value),
|
||||
leading: const SizedBox(width: 20, height: 20),
|
||||
onTap: () => pop(_SelectedList(listingType: listingType)),
|
||||
),
|
||||
for (final instance in accStore.instances) ...[
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 10),
|
||||
child: Divider(),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(
|
||||
instance.toUpperCase(),
|
||||
style: TextStyle(
|
||||
color:
|
||||
theme.textTheme.bodyText1.color.withOpacity(0.7)),
|
||||
),
|
||||
onTap: () => goToInstance(context, instance),
|
||||
dense: true,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
visualDensity:
|
||||
VisualDensity(vertical: VisualDensity.minimumDensity),
|
||||
leading: SizedBox.shrink(),
|
||||
visualDensity: const VisualDensity(
|
||||
vertical: VisualDensity.minimumDensity),
|
||||
leading: (instancesIcons.hasData &&
|
||||
instancesIcons.data[instance] != null)
|
||||
? Padding(
|
||||
padding: const EdgeInsets.only(left: 20),
|
||||
child: SizedBox(
|
||||
width: 25,
|
||||
height: 25,
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: instancesIcons.data[instance],
|
||||
height: 25,
|
||||
width: 25,
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox(width: 30),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(
|
||||
'Subscribed',
|
||||
style: TextStyle(
|
||||
color: accStore.hasNoAccount
|
||||
? theme.textTheme.bodyText1.color.withOpacity(0.4)
|
||||
: null,
|
||||
),
|
||||
color: accStore.isAnonymousFor(instance)
|
||||
? theme.textTheme.bodyText1.color.withOpacity(0.4)
|
||||
: null),
|
||||
),
|
||||
onTap: accStore.hasNoAccount
|
||||
? null
|
||||
: () => pop(
|
||||
const _SelectedList(
|
||||
listingType: PostListingType.subscribed,
|
||||
),
|
||||
),
|
||||
onTap: accStore.isAnonymousFor(instance)
|
||||
? () => showCupertinoModalPopup(
|
||||
context: context,
|
||||
builder: (_) =>
|
||||
AddAccountPage(instanceHost: instance))
|
||||
: () => pop(_SelectedList(
|
||||
listingType: PostListingType.subscribed,
|
||||
instanceHost: instance,
|
||||
)),
|
||||
leading: const SizedBox(width: 20),
|
||||
),
|
||||
ListTile(
|
||||
title: const Text('Local'),
|
||||
onTap: () => pop(_SelectedList(
|
||||
listingType: PostListingType.local,
|
||||
instanceHost: instance,
|
||||
)),
|
||||
leading: const SizedBox(width: 20),
|
||||
),
|
||||
ListTile(
|
||||
title: const Text('All'),
|
||||
onTap: () => pop(_SelectedList(
|
||||
listingType: PostListingType.all,
|
||||
instanceHost: instance,
|
||||
)),
|
||||
leading: const SizedBox(width: 20),
|
||||
),
|
||||
for (final listingType in [
|
||||
PostListingType.local,
|
||||
PostListingType.all,
|
||||
])
|
||||
ListTile(
|
||||
title: Text(listingType.value),
|
||||
leading: const SizedBox(width: 20, height: 20),
|
||||
onTap: () => pop(_SelectedList(listingType: listingType)),
|
||||
),
|
||||
for (final instance in accStore.instances) ...[
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 10),
|
||||
child: Divider(),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(
|
||||
instance.toUpperCase(),
|
||||
style: TextStyle(
|
||||
color:
|
||||
theme.textTheme.bodyText1.color.withOpacity(0.7)),
|
||||
),
|
||||
onTap: () => goToInstance(context, instance),
|
||||
dense: true,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
visualDensity: const VisualDensity(
|
||||
vertical: VisualDensity.minimumDensity),
|
||||
leading: (instancesIcons.hasData &&
|
||||
instancesIcons.data[instance] != null)
|
||||
? Padding(
|
||||
padding: const EdgeInsets.only(left: 20),
|
||||
child: SizedBox(
|
||||
width: 25,
|
||||
height: 25,
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: instancesIcons.data[instance],
|
||||
height: 25,
|
||||
width: 25,
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox(width: 30),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(
|
||||
'Subscribed',
|
||||
style: TextStyle(
|
||||
color: accStore.isAnonymousFor(instance)
|
||||
? theme.textTheme.bodyText1.color.withOpacity(0.4)
|
||||
: null),
|
||||
),
|
||||
onTap: accStore.isAnonymousFor(instance)
|
||||
? () => showCupertinoModalPopup(
|
||||
context: context,
|
||||
builder: (_) =>
|
||||
AddAccountPage(instanceHost: instance))
|
||||
: () => pop(_SelectedList(
|
||||
listingType: PostListingType.subscribed,
|
||||
instanceHost: instance,
|
||||
)),
|
||||
leading: const SizedBox(width: 20),
|
||||
),
|
||||
ListTile(
|
||||
title: const Text('Local'),
|
||||
onTap: () => pop(_SelectedList(
|
||||
listingType: PostListingType.local,
|
||||
instanceHost: instance,
|
||||
)),
|
||||
leading: const SizedBox(width: 20),
|
||||
),
|
||||
ListTile(
|
||||
title: const Text('All'),
|
||||
onTap: () => pop(_SelectedList(
|
||||
listingType: PostListingType.all,
|
||||
instanceHost: instance,
|
||||
)),
|
||||
leading: const SizedBox(width: 20),
|
||||
),
|
||||
]
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
@ -221,14 +217,9 @@ class HomeTab extends HookWidget {
|
|||
onPressed: () => goTo(context, (_) => const InboxPage()),
|
||||
)
|
||||
],
|
||||
centerTitle: true,
|
||||
title: TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(10))),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||
primary: theme.buttonColor,
|
||||
textStyle: theme.primaryTextTheme.headline6,
|
||||
),
|
||||
onPressed: handleListChange,
|
||||
child: Row(
|
||||
|
@ -238,14 +229,11 @@ class HomeTab extends HookWidget {
|
|||
Flexible(
|
||||
child: Text(
|
||||
title,
|
||||
style: theme.primaryTextTheme.headline6,
|
||||
style: theme.appBarTheme.textTheme.headline6,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
Icons.arrow_drop_down,
|
||||
color: theme.primaryTextTheme.headline6.color,
|
||||
),
|
||||
const Icon(Icons.arrow_drop_down),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -339,7 +327,8 @@ class InfiniteHomeList extends HookWidget {
|
|||
prepend: Column(
|
||||
children: [
|
||||
PostListOptions(
|
||||
onChange: changeSorting,
|
||||
sortValue: sort.value,
|
||||
onSortChanged: changeSorting,
|
||||
styleButton: onStyleChange != null,
|
||||
),
|
||||
],
|
||||
|
|
|
@ -8,10 +8,10 @@ import 'package:url_launcher/url_launcher.dart' as ul;
|
|||
|
||||
import '../hooks/stores.dart';
|
||||
import '../util/extensions/api.dart';
|
||||
import '../util/extensions/spaced.dart';
|
||||
import '../util/goto.dart';
|
||||
import '../util/more_icon.dart';
|
||||
import '../util/text_color.dart';
|
||||
import '../widgets/badge.dart';
|
||||
import '../widgets/bottom_modal.dart';
|
||||
import '../widgets/fullscreenable_image.dart';
|
||||
import '../widgets/info_table_popup.dart';
|
||||
|
@ -44,12 +44,7 @@ class InstancePage extends HookWidget {
|
|||
|
||||
if (!siteSnap.hasData) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
iconTheme: theme.iconTheme,
|
||||
brightness: theme.brightness,
|
||||
backgroundColor: theme.cardColor,
|
||||
elevation: 0,
|
||||
),
|
||||
appBar: AppBar(),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
|
@ -71,38 +66,35 @@ class InstancePage extends HookWidget {
|
|||
final site = siteSnap.data;
|
||||
|
||||
void _openMoreMenu(BuildContext c) {
|
||||
showModalBottomSheet(
|
||||
backgroundColor: Colors.transparent,
|
||||
showBottomModal(
|
||||
context: context,
|
||||
builder: (context) => BottomModal(
|
||||
child: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.open_in_browser),
|
||||
title: const Text('Open in browser'),
|
||||
onTap: () async => await ul
|
||||
.canLaunch('https://${site.instanceHost}')
|
||||
? ul.launch('https://${site.instanceHost}')
|
||||
: Scaffold.of(context).showSnackBar(
|
||||
const SnackBar(content: Text("can't open in browser"))),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.info_outline),
|
||||
title: const Text('Nerd stuff'),
|
||||
onTap: () {
|
||||
showInfoTablePopup(context, {
|
||||
'url': instanceHost,
|
||||
'creator': '@${site.siteView.creator.name}',
|
||||
'version': site.version,
|
||||
'enableDownvotes': site.siteView.site.enableDownvotes,
|
||||
'enableNsfw': site.siteView.site.enableNsfw,
|
||||
'published': site.siteView.site.published,
|
||||
'updated': site.siteView.site.updated,
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
builder: (context) => Column(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.open_in_browser),
|
||||
title: const Text('Open in browser'),
|
||||
onTap: () async => await ul
|
||||
.canLaunch('https://${site.instanceHost}')
|
||||
? ul.launch('https://${site.instanceHost}')
|
||||
: Scaffold.of(context).showSnackBar(
|
||||
const SnackBar(content: Text("can't open in browser"))),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.info_outline),
|
||||
title: const Text('Nerd stuff'),
|
||||
onTap: () {
|
||||
showInfoTablePopup(context, {
|
||||
'url': instanceHost,
|
||||
'creator': '@${site.siteView.creator.name}',
|
||||
'version': site.version,
|
||||
'enableDownvotes': site.siteView.site.enableDownvotes,
|
||||
'enableNsfw': site.siteView.site.enableNsfw,
|
||||
'published': site.siteView.site.published,
|
||||
'updated': site.siteView.site.updated,
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -113,12 +105,9 @@ class InstancePage extends HookWidget {
|
|||
child: NestedScrollView(
|
||||
headerSliverBuilder: (context, innerBoxIsScrolled) => <Widget>[
|
||||
SliverAppBar(
|
||||
brightness: theme.brightness,
|
||||
expandedHeight: 200,
|
||||
expandedHeight: 250,
|
||||
pinned: true,
|
||||
elevation: 0,
|
||||
backgroundColor: theme.cardColor,
|
||||
iconTheme: theme.iconTheme,
|
||||
title: Text(
|
||||
site.siteView.site.name,
|
||||
style: TextStyle(color: colorOnCard),
|
||||
|
@ -167,20 +156,19 @@ class InstancePage extends HookWidget {
|
|||
),
|
||||
]),
|
||||
),
|
||||
),
|
||||
SliverPersistentHeader(
|
||||
delegate: _SliverAppBarDelegate(
|
||||
TabBar(
|
||||
labelColor: theme.textTheme.bodyText1.color,
|
||||
unselectedLabelColor: Colors.grey,
|
||||
tabs: const [
|
||||
Tab(text: 'Posts'),
|
||||
Tab(text: 'Comments'),
|
||||
Tab(text: 'About'),
|
||||
],
|
||||
bottom: PreferredSize(
|
||||
preferredSize: const TabBar(tabs: []).preferredSize,
|
||||
child: Material(
|
||||
color: theme.cardColor,
|
||||
child: const TabBar(
|
||||
tabs: [
|
||||
Tab(text: 'Posts'),
|
||||
Tab(text: 'Comments'),
|
||||
Tab(text: 'About'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
pinned: true,
|
||||
),
|
||||
],
|
||||
body: TabBarView(
|
||||
|
@ -215,27 +203,6 @@ class InstancePage extends HookWidget {
|
|||
}
|
||||
}
|
||||
|
||||
class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
|
||||
final TabBar _tabBar;
|
||||
|
||||
const _SliverAppBarDelegate(this._tabBar);
|
||||
|
||||
@override
|
||||
double get minExtent => _tabBar.preferredSize.height;
|
||||
@override
|
||||
double get maxExtent => _tabBar.preferredSize.height;
|
||||
|
||||
@override
|
||||
Widget build(
|
||||
BuildContext context, double shrinkOffset, bool overlapsContent) {
|
||||
final theme = Theme.of(context);
|
||||
return Container(color: theme.cardColor, child: _tabBar);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRebuild(_SliverAppBarDelegate oldDelegate) => false;
|
||||
}
|
||||
|
||||
class _AboutTab extends HookWidget {
|
||||
final FullSiteView site;
|
||||
final Future<List<CommunityView>> communitiesFuture;
|
||||
|
@ -296,18 +263,20 @@ class _AboutTab extends HookWidget {
|
|||
),
|
||||
const _Divider(),
|
||||
SizedBox(
|
||||
height: 25,
|
||||
height: 32,
|
||||
child: ListView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||
children: [
|
||||
const SizedBox(width: 7),
|
||||
_Badge('${site.online} users online'),
|
||||
_Badge('${site.siteView.counts.users} users'),
|
||||
_Badge('${site.siteView.counts.communities} communities'),
|
||||
_Badge('${site.siteView.counts.posts} posts'),
|
||||
_Badge('${site.siteView.counts.comments} comments'),
|
||||
const SizedBox(width: 15),
|
||||
],
|
||||
Chip(label: Text('${site.online} users online')),
|
||||
Chip(label: Text('${site.siteView.counts.users} users')),
|
||||
Chip(
|
||||
label: Text(
|
||||
'${site.siteView.counts.communities} communities')),
|
||||
Chip(label: Text('${site.siteView.counts.posts} posts')),
|
||||
Chip(
|
||||
label: Text('${site.siteView.counts.comments} comments')),
|
||||
].spaced(8),
|
||||
),
|
||||
),
|
||||
const _Divider(),
|
||||
|
@ -407,28 +376,6 @@ class _AboutTab extends HookWidget {
|
|||
}
|
||||
}
|
||||
|
||||
class _Badge extends StatelessWidget {
|
||||
final String text;
|
||||
|
||||
const _Badge(this.text);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 8),
|
||||
child: Badge(
|
||||
child: Text(
|
||||
text,
|
||||
style:
|
||||
TextStyle(color: textColorBasedOnBackground(theme.accentColor)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Divider extends StatelessWidget {
|
||||
const _Divider();
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ import '../hooks/ref.dart';
|
|||
import '../hooks/stores.dart';
|
||||
import '../util/pictrs.dart';
|
||||
import '../widgets/bottom_safe.dart';
|
||||
import '../widgets/radio_picker.dart';
|
||||
|
||||
/// Page for managing things like username, email, avatar etc
|
||||
/// This page will assume the manage account is logged in and
|
||||
|
@ -27,7 +28,6 @@ class ManageAccountPage extends HookWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final accountStore = useAccountsStore();
|
||||
final theme = Theme.of(context);
|
||||
|
||||
final userFuture = useMemoized(() async {
|
||||
final site = await LemmyApiV2(instanceHost).run(
|
||||
|
@ -38,13 +38,7 @@ class ManageAccountPage extends HookWidget {
|
|||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: theme.scaffoldBackgroundColor,
|
||||
brightness: theme.brightness,
|
||||
shadowColor: Colors.transparent,
|
||||
iconTheme: theme.iconTheme,
|
||||
title:
|
||||
Text('@$instanceHost@$username', style: theme.textTheme.headline6),
|
||||
centerTitle: true,
|
||||
title: Text('@$instanceHost@$username'),
|
||||
),
|
||||
body: FutureBuilder<UserSafeSettings>(
|
||||
future: userFuture,
|
||||
|
@ -159,8 +153,8 @@ class _ManageAccount extends HookWidget {
|
|||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'''Are you sure you want to remove @${user.instanceHost}@${user.name}? '''
|
||||
'''WARNING: this removes your account COMPLETELY, not from lemmur only''',
|
||||
'Are you sure you want to remove @${user.instanceHost}@${user.name}? '
|
||||
'WARNING: this removes your account COMPLETELY, not from lemmur only',
|
||||
),
|
||||
TextField(
|
||||
controller: deleteAccountPasswordController,
|
||||
|
@ -170,11 +164,11 @@ class _ManageAccount extends HookWidget {
|
|||
],
|
||||
),
|
||||
actions: [
|
||||
FlatButton(
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: const Text('no'),
|
||||
),
|
||||
FlatButton(
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
child: const Text('yes'),
|
||||
),
|
||||
|
@ -226,98 +220,37 @@ class _ManageAccount extends HookWidget {
|
|||
),
|
||||
const SizedBox(height: 8),
|
||||
Text('Display Name', style: theme.textTheme.headline6),
|
||||
TextField(
|
||||
controller: displayNameController,
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
),
|
||||
TextField(controller: displayNameController),
|
||||
const SizedBox(height: 8),
|
||||
Text('Bio', style: theme.textTheme.headline6),
|
||||
TextField(
|
||||
controller: bioController,
|
||||
minLines: 4,
|
||||
maxLines: 10,
|
||||
decoration: InputDecoration(
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text('Email', style: theme.textTheme.headline6),
|
||||
TextField(
|
||||
controller: emailController,
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
),
|
||||
TextField(controller: emailController),
|
||||
const SizedBox(height: 8),
|
||||
Text('Matrix User', style: theme.textTheme.headline6),
|
||||
TextField(
|
||||
controller: matrixUserController,
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
),
|
||||
TextField(controller: matrixUserController),
|
||||
const SizedBox(height: 8),
|
||||
Text('New password', style: theme.textTheme.headline6),
|
||||
TextField(
|
||||
controller: newPasswordController,
|
||||
obscureText: true,
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text('Verify password', style: theme.textTheme.headline6),
|
||||
TextField(
|
||||
controller: newPasswordVerifyController,
|
||||
obscureText: true,
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text('Old password', style: theme.textTheme.headline6),
|
||||
TextField(
|
||||
controller: oldPasswordController,
|
||||
obscureText: true,
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
|
@ -333,21 +266,15 @@ class _ManageAccount extends HookWidget {
|
|||
)
|
||||
],
|
||||
),
|
||||
DropdownButton<PostListingType>(
|
||||
items: [
|
||||
for (final postListingType in [
|
||||
PostListingType.all,
|
||||
PostListingType.local,
|
||||
PostListingType.subscribed,
|
||||
])
|
||||
DropdownMenuItem(
|
||||
value: postListingType,
|
||||
child: Text(postListingType.value),
|
||||
)
|
||||
RadioPicker<PostListingType>(
|
||||
values: const [
|
||||
PostListingType.all,
|
||||
PostListingType.local,
|
||||
PostListingType.subscribed,
|
||||
],
|
||||
groupValue: defaultListingType.value,
|
||||
onChanged: (value) => defaultListingType.value = value,
|
||||
value: defaultListingType.value,
|
||||
isDense: true,
|
||||
mapValueToString: (value) => value.value,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -365,17 +292,11 @@ class _ManageAccount extends HookWidget {
|
|||
)
|
||||
],
|
||||
),
|
||||
DropdownButton<SortType>(
|
||||
items: [
|
||||
for (final defaultSortType in SortType.values)
|
||||
DropdownMenuItem(
|
||||
value: defaultSortType,
|
||||
child: Text(defaultSortType.value),
|
||||
)
|
||||
],
|
||||
RadioPicker<SortType>(
|
||||
values: SortType.values,
|
||||
groupValue: defaultSortType.value,
|
||||
onChanged: (value) => defaultSortType.value = value,
|
||||
value: defaultSortType.value,
|
||||
isDense: true,
|
||||
mapValueToString: (value) => value.value,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -405,12 +326,6 @@ class _ManageAccount extends HookWidget {
|
|||
const SizedBox(height: 8),
|
||||
ElevatedButton(
|
||||
onPressed: saveDelayedLoading.loading ? null : handleSubmit,
|
||||
style: ElevatedButton.styleFrom(
|
||||
visualDensity: VisualDensity.comfortable,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
child: saveDelayedLoading.loading
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
|
@ -424,10 +339,6 @@ class _ManageAccount extends HookWidget {
|
|||
onPressed: deleteAccountDialog,
|
||||
style: ElevatedButton.styleFrom(
|
||||
primary: Colors.red,
|
||||
visualDensity: VisualDensity.comfortable,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
child: const Text('DELETE ACCOUNT'),
|
||||
),
|
||||
|
@ -533,12 +444,6 @@ class _ImagePicker extends HookWidget {
|
|||
if (pictrsDeleteToken.value == null)
|
||||
ElevatedButton(
|
||||
onPressed: delayedLoading.loading ? null : uploadImage,
|
||||
style: ElevatedButton.styleFrom(
|
||||
visualDensity: VisualDensity.comfortable,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
child: delayedLoading.loading
|
||||
? const SizedBox(
|
||||
height: 20,
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import 'dart:math' show max, min;
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:esys_flutter_share/esys_flutter_share.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:matrix4_transform/matrix4_transform.dart';
|
||||
import 'package:photo_view/photo_view.dart';
|
||||
|
||||
import '../widgets/bottom_modal.dart';
|
||||
|
@ -11,6 +13,8 @@ import '../widgets/bottom_modal.dart';
|
|||
class MediaViewPage extends HookWidget {
|
||||
final String url;
|
||||
final GlobalKey<ScaffoldState> _key = GlobalKey();
|
||||
static const yThreshold = 150;
|
||||
static const speedThreshold = 45;
|
||||
|
||||
MediaViewPage(this.url);
|
||||
|
||||
|
@ -18,57 +22,43 @@ class MediaViewPage extends HookWidget {
|
|||
Widget build(BuildContext context) {
|
||||
final showButtons = useState(true);
|
||||
final isZoomedOut = useState(true);
|
||||
final scaleIsInitial = useState(true);
|
||||
|
||||
final isDragging = useState(false);
|
||||
|
||||
final offset = useState(Offset.zero);
|
||||
final prevOffset = usePrevious(offset.value);
|
||||
|
||||
notImplemented() {
|
||||
_key.currentState.showSnackBar(const SnackBar(
|
||||
content: Text("this feature hasn't been implemented yet 😰")));
|
||||
}
|
||||
|
||||
useEffect(() {
|
||||
if (showButtons.value) {
|
||||
SystemChrome.setEnabledSystemUIOverlays([
|
||||
SystemUiOverlay.bottom,
|
||||
SystemUiOverlay.top,
|
||||
]);
|
||||
} else {
|
||||
SystemChrome.setEnabledSystemUIOverlays([]);
|
||||
}
|
||||
return null;
|
||||
}, [showButtons.value]);
|
||||
|
||||
useEffect(
|
||||
() => () => SystemChrome.setEnabledSystemUIOverlays([
|
||||
SystemUiOverlay.bottom,
|
||||
SystemUiOverlay.top,
|
||||
]),
|
||||
[]);
|
||||
// TODO: hide navbar and topbar on android without a content jump
|
||||
|
||||
share() {
|
||||
showModalBottomSheet(
|
||||
backgroundColor: Colors.transparent,
|
||||
showBottomModal(
|
||||
context: context,
|
||||
builder: (context) => BottomModal(
|
||||
child: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.link),
|
||||
title: const Text('Share link'),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
Share.text('Share image url', url, 'text/plain');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.image),
|
||||
title: const Text('Share file'),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
notImplemented();
|
||||
// TODO: share file
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
builder: (context) => Column(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.link),
|
||||
title: const Text('Share link'),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
Share.text('Share image url', url, 'text/plain');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.image),
|
||||
title: const Text('Share file'),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
notImplemented();
|
||||
// TODO: share file
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -77,10 +67,11 @@ class MediaViewPage extends HookWidget {
|
|||
key: _key,
|
||||
extendBodyBehindAppBar: true,
|
||||
extendBody: true,
|
||||
backgroundColor:
|
||||
Colors.black.withOpacity(max(0, 1.0 - (offset.value.dy.abs() / 200))),
|
||||
appBar: showButtons.value
|
||||
? AppBar(
|
||||
backgroundColor: Colors.black38,
|
||||
shadowColor: Colors.transparent,
|
||||
leading: const CloseButton(),
|
||||
actions: [
|
||||
IconButton(
|
||||
|
@ -96,26 +87,67 @@ class MediaViewPage extends HookWidget {
|
|||
],
|
||||
)
|
||||
: null,
|
||||
body: GestureDetector(
|
||||
onTapUp: (details) => showButtons.value = !showButtons.value,
|
||||
onVerticalDragEnd: isZoomedOut.value
|
||||
? (details) {
|
||||
if (details.primaryVelocity.abs() > 1000) {
|
||||
body: Listener(
|
||||
onPointerMove: scaleIsInitial.value
|
||||
? (event) {
|
||||
if (!isDragging.value &&
|
||||
event.delta.dx.abs() > event.delta.dy.abs()) return;
|
||||
isDragging.value = true;
|
||||
offset.value += event.delta;
|
||||
}
|
||||
: (_) => isDragging.value = false,
|
||||
onPointerCancel: (_) => offset.value = Offset.zero,
|
||||
onPointerUp: isZoomedOut.value
|
||||
? (_) {
|
||||
if (!isDragging.value) {
|
||||
showButtons.value = !showButtons.value;
|
||||
return;
|
||||
}
|
||||
|
||||
isDragging.value = false;
|
||||
final speed = (offset.value - prevOffset).distance;
|
||||
if (speed > speedThreshold ||
|
||||
offset.value.dy.abs() > yThreshold) {
|
||||
Navigator.of(context).pop();
|
||||
} else {
|
||||
offset.value = Offset.zero;
|
||||
}
|
||||
}
|
||||
: null,
|
||||
child: PhotoView(
|
||||
scaleStateChangedCallback: (value) {
|
||||
isZoomedOut.value = value == PhotoViewScaleState.zoomedOut ||
|
||||
value == PhotoViewScaleState.initial;
|
||||
},
|
||||
minScale: PhotoViewComputedScale.contained,
|
||||
initialScale: PhotoViewComputedScale.contained,
|
||||
imageProvider: CachedNetworkImageProvider(url),
|
||||
heroAttributes: PhotoViewHeroAttributes(tag: url),
|
||||
loadingBuilder: (context, event) =>
|
||||
const Center(child: CircularProgressIndicator()),
|
||||
: (_) {
|
||||
offset.value = Offset.zero;
|
||||
isDragging.value = false;
|
||||
},
|
||||
child: AnimatedContainer(
|
||||
transform: Matrix4Transform()
|
||||
.scale(max(0.9, 1 - offset.value.dy.abs() / 1000))
|
||||
.translateOffset(offset.value)
|
||||
.rotate(min(-offset.value.dx / 2000, 0.1))
|
||||
.matrix4,
|
||||
duration: isDragging.value
|
||||
? Duration.zero
|
||||
: const Duration(milliseconds: 200),
|
||||
child: PhotoView(
|
||||
backgroundDecoration:
|
||||
const BoxDecoration(color: Colors.transparent),
|
||||
scaleStateChangedCallback: (value) {
|
||||
isZoomedOut.value = value == PhotoViewScaleState.zoomedOut ||
|
||||
value == PhotoViewScaleState.initial;
|
||||
showButtons.value = isZoomedOut.value;
|
||||
|
||||
scaleIsInitial.value = value == PhotoViewScaleState.initial;
|
||||
isDragging.value = false;
|
||||
offset.value = Offset.zero;
|
||||
},
|
||||
onTapUp: isZoomedOut.value
|
||||
? null
|
||||
: (_, __, ___) => showButtons.value = !showButtons.value,
|
||||
minScale: PhotoViewComputedScale.contained,
|
||||
initialScale: PhotoViewComputedScale.contained,
|
||||
imageProvider: CachedNetworkImageProvider(url),
|
||||
heroAttributes: PhotoViewHeroAttributes(tag: url),
|
||||
loadingBuilder: (context, event) =>
|
||||
const Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
@ -4,7 +4,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||
|
||||
import '../hooks/stores.dart';
|
||||
import '../util/goto.dart';
|
||||
import '../widgets/bottom_modal.dart';
|
||||
import '../widgets/radio_picker.dart';
|
||||
import '../widgets/user_profile.dart';
|
||||
import 'settings.dart';
|
||||
|
||||
|
@ -29,18 +29,13 @@ class UserProfileTab extends HookWidget {
|
|||
|
||||
if (accountsStore.hasNoAccount) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
actions: actions,
|
||||
backgroundColor: Colors.transparent,
|
||||
iconTheme: theme.iconTheme,
|
||||
shadowColor: Colors.transparent,
|
||||
),
|
||||
appBar: AppBar(actions: actions),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Text('No account was added.'),
|
||||
FlatButton.icon(
|
||||
TextButton.icon(
|
||||
onPressed: () {
|
||||
goTo(context, (_) => AccountsConfigPage());
|
||||
},
|
||||
|
@ -55,63 +50,37 @@ class UserProfileTab extends HookWidget {
|
|||
|
||||
return Scaffold(
|
||||
extendBodyBehindAppBar: true,
|
||||
// TODO: this is not visible in light mode when the sliver app bar
|
||||
// in UserProfile is folded
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
shadowColor: Colors.transparent,
|
||||
centerTitle: true,
|
||||
title: FlatButton(
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (ctx) {
|
||||
final userTags = <String>[
|
||||
for (final instanceHost in accountsStore.loggedInInstances)
|
||||
for (final username
|
||||
in accountsStore.usernamesFor(instanceHost))
|
||||
'$username@$instanceHost'
|
||||
];
|
||||
|
||||
return BottomModal(
|
||||
title: 'account',
|
||||
child: Column(
|
||||
children: [
|
||||
for (final tag in userTags)
|
||||
RadioListTile<String>(
|
||||
value: tag,
|
||||
title: Text(tag),
|
||||
groupValue: '${accountsStore.defaultUsername}'
|
||||
'@${accountsStore.defaultInstanceHost}',
|
||||
onChanged: (selected) {
|
||||
final userTag = selected.split('@');
|
||||
accountsStore.setDefaultAccount(
|
||||
userTag[1], userTag[0]);
|
||||
Navigator.of(ctx).pop();
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
title: RadioPicker<String>(
|
||||
title: 'account',
|
||||
values: accountsStore.loggedInInstances
|
||||
.expand(
|
||||
(instanceHost) => accountsStore
|
||||
.usernamesFor(instanceHost)
|
||||
.map((username) => '$username@$instanceHost'),
|
||||
)
|
||||
.toList(),
|
||||
groupValue:
|
||||
'${accountsStore.defaultUsername}@${accountsStore.defaultInstanceHost}',
|
||||
onChanged: (value) {
|
||||
final userTag = value.split('@');
|
||||
accountsStore.setDefaultAccount(userTag[1], userTag[0]);
|
||||
},
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
// TODO: fix overflow issues
|
||||
'@${accountsStore.defaultUsername}',
|
||||
style: theme.primaryTextTheme.headline6,
|
||||
overflow: TextOverflow.fade,
|
||||
),
|
||||
Icon(
|
||||
Icons.expand_more,
|
||||
color: theme.primaryIconTheme.color,
|
||||
),
|
||||
],
|
||||
buttonBuilder: (context, displayValue, onPressed) => TextButton(
|
||||
onPressed: onPressed,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
// TODO: fix overflow issues
|
||||
displayValue,
|
||||
style: theme.appBarTheme.textTheme.headline6,
|
||||
overflow: TextOverflow.fade,
|
||||
),
|
||||
const Icon(Icons.expand_more),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: actions,
|
||||
|
|
|
@ -27,7 +27,6 @@ class SearchResultsPage extends HookWidget {
|
|||
length: 4,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
centerTitle: true,
|
||||
title: Text('Looking for "$query"'),
|
||||
bottom: const TabBar(
|
||||
isScrollable: true,
|
||||
|
|
|
@ -4,7 +4,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||
|
||||
import '../hooks/stores.dart';
|
||||
import '../util/goto.dart';
|
||||
import '../widgets/bottom_modal.dart';
|
||||
import '../widgets/radio_picker.dart';
|
||||
import 'search_results.dart';
|
||||
|
||||
class SearchTab extends HookWidget {
|
||||
|
@ -22,20 +22,14 @@ class SearchTab extends HookWidget {
|
|||
|
||||
if (instanceHost.value == null) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
shadowColor: Colors.transparent,
|
||||
),
|
||||
appBar: AppBar(),
|
||||
body: const Center(
|
||||
child: Text('You do not have any instances added'),
|
||||
),
|
||||
);
|
||||
}
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
shadowColor: Colors.transparent,
|
||||
),
|
||||
appBar: AppBar(),
|
||||
body: GestureDetector(
|
||||
onTapDown: (_) => primaryFocus.unfocus(),
|
||||
child: ListView(
|
||||
|
@ -44,15 +38,7 @@ class SearchTab extends HookWidget {
|
|||
TextField(
|
||||
controller: searchInputController,
|
||||
textAlign: TextAlign.center,
|
||||
decoration: InputDecoration(
|
||||
fillColor: Colors.grey,
|
||||
isDense: true,
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 10),
|
||||
hintText: 'search',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
decoration: const InputDecoration(hintText: 'search'),
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
Row(
|
||||
|
@ -63,9 +49,10 @@ class SearchTab extends HookWidget {
|
|||
style: Theme.of(context).textTheme.subtitle1),
|
||||
),
|
||||
Expanded(
|
||||
child: SelectInstanceButton(
|
||||
instanceHost: instanceHost.value,
|
||||
onChange: (s) => instanceHost.value = s,
|
||||
child: RadioPicker<String>(
|
||||
values: accStore.instances.toList(),
|
||||
groupValue: instanceHost.value,
|
||||
onChanged: (value) => instanceHost.value = value,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -86,58 +73,3 @@ class SearchTab extends HookWidget {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SelectInstanceButton extends HookWidget {
|
||||
final ValueChanged<String> onChange;
|
||||
final String instanceHost;
|
||||
const SelectInstanceButton(
|
||||
{@required this.onChange, @required this.instanceHost})
|
||||
: assert(instanceHost != null);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final accStore = useAccountsStore();
|
||||
return OutlinedButton(
|
||||
onPressed: () async {
|
||||
final val = await showModalBottomSheet<String>(
|
||||
backgroundColor: Colors.transparent,
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (context) => BottomModal(
|
||||
child: Column(
|
||||
children: [
|
||||
for (final inst in accStore.instances)
|
||||
ListTile(
|
||||
leading: inst == instanceHost
|
||||
? Icon(
|
||||
Icons.radio_button_on,
|
||||
color: theme.accentColor,
|
||||
)
|
||||
: const Icon(Icons.radio_button_off),
|
||||
title: Text(inst),
|
||||
onTap: () => Navigator.of(context).pop(inst),
|
||||
)
|
||||
],
|
||||
),
|
||||
));
|
||||
if (val != null) {
|
||||
onChange?.call(val);
|
||||
}
|
||||
},
|
||||
style: OutlinedButton.styleFrom(
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(10))),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||
primary: theme.textTheme.bodyText1.color,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(instanceHost),
|
||||
const Icon(Icons.arrow_drop_down),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,39 +17,30 @@ class SettingsPage extends StatelessWidget {
|
|||
const SettingsPage();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
brightness: theme.brightness,
|
||||
backgroundColor: theme.scaffoldBackgroundColor,
|
||||
shadowColor: Colors.transparent,
|
||||
iconTheme: theme.iconTheme,
|
||||
title: Text('Settings', style: theme.textTheme.headline6),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: ListView(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.person),
|
||||
title: const Text('Accounts'),
|
||||
onTap: () {
|
||||
goTo(context, (_) => AccountsConfigPage());
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.color_lens),
|
||||
title: const Text('Appearance'),
|
||||
onTap: () {
|
||||
goTo(context, (_) => const AppearanceConfigPage());
|
||||
},
|
||||
),
|
||||
const AboutTile()
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Settings'),
|
||||
),
|
||||
body: ListView(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.person),
|
||||
title: const Text('Accounts'),
|
||||
onTap: () {
|
||||
goTo(context, (_) => AccountsConfigPage());
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.color_lens),
|
||||
title: const Text('Appearance'),
|
||||
onTap: () {
|
||||
goTo(context, (_) => const AppearanceConfigPage());
|
||||
},
|
||||
),
|
||||
const AboutTile()
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Settings for theme color, AMOLED switch
|
||||
|
@ -58,17 +49,11 @@ class AppearanceConfigPage extends HookWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final configStore = useConfigStore();
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
brightness: theme.brightness,
|
||||
backgroundColor: theme.scaffoldBackgroundColor,
|
||||
shadowColor: Colors.transparent,
|
||||
iconTheme: theme.iconTheme,
|
||||
title: Text('Appearance', style: theme.textTheme.headline6),
|
||||
centerTitle: true,
|
||||
title: const Text('Appearance'),
|
||||
),
|
||||
body: ListView(
|
||||
children: [
|
||||
|
@ -110,11 +95,11 @@ class AccountsConfigPage extends HookWidget {
|
|||
title: const Text('Remove instance?'),
|
||||
content: Text('Are you sure you want to remove $instanceHost?'),
|
||||
actions: [
|
||||
FlatButton(
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: const Text('no'),
|
||||
),
|
||||
FlatButton(
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
child: const Text('yes'),
|
||||
),
|
||||
|
@ -134,11 +119,11 @@ class AccountsConfigPage extends HookWidget {
|
|||
content: Text(
|
||||
'Are you sure you want to remove $username@$instanceHost?'),
|
||||
actions: [
|
||||
FlatButton(
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: const Text('no'),
|
||||
),
|
||||
FlatButton(
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
child: const Text('yes'),
|
||||
),
|
||||
|
@ -153,12 +138,7 @@ class AccountsConfigPage extends HookWidget {
|
|||
return Scaffold(
|
||||
key: _scaffoldKey,
|
||||
appBar: AppBar(
|
||||
backgroundColor: theme.scaffoldBackgroundColor,
|
||||
brightness: theme.brightness,
|
||||
shadowColor: Colors.transparent,
|
||||
iconTheme: theme.iconTheme,
|
||||
title: Text('Accounts', style: theme.textTheme.headline6),
|
||||
centerTitle: true,
|
||||
title: const Text('Accounts'),
|
||||
),
|
||||
floatingActionButton: SpeedDial(
|
||||
animatedIcon: AnimatedIcons.menu_close, // TODO: change to + => x
|
||||
|
@ -193,16 +173,14 @@ class AccountsConfigPage extends HookWidget {
|
|||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 100),
|
||||
child: FlatButton.icon(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
onPressed: () => showCupertinoModalPopup(
|
||||
context: context,
|
||||
builder: (_) => AddInstancePage(),
|
||||
),
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Add instance')),
|
||||
child: TextButton.icon(
|
||||
onPressed: () => showCupertinoModalPopup(
|
||||
context: context,
|
||||
builder: (_) => AddInstancePage(),
|
||||
),
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Add instance'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
@ -41,8 +41,6 @@ class UserPage extends HookWidget {
|
|||
return Scaffold(
|
||||
extendBodyBehindAppBar: true,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
shadowColor: Colors.transparent,
|
||||
actions: [
|
||||
if (userDetailsSnap.hasData) ...[
|
||||
IconButton(
|
||||
|
|
|
@ -22,10 +22,8 @@ class UsersListPage extends StatelessWidget {
|
|||
// TODO: change to infinite scroll
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(title ?? '', style: theme.textTheme.headline6),
|
||||
centerTitle: true,
|
||||
backgroundColor: theme.cardColor,
|
||||
iconTheme: theme.iconTheme,
|
||||
title: Text(title ?? ''),
|
||||
),
|
||||
body: ListView.builder(
|
||||
itemBuilder: (context, i) => UsersListItem(user: users[i]),
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'util/text_color.dart';
|
||||
|
||||
ThemeData _themeFactory({bool dark = false, bool amoled = false}) {
|
||||
assert(dark || !amoled, "Can't have amoled without dark mode");
|
||||
|
||||
final theme = dark ? ThemeData.dark() : ThemeData.light();
|
||||
final maybeAmoledColor = amoled ? Colors.black : null;
|
||||
|
||||
return theme.copyWith(
|
||||
scaffoldBackgroundColor: maybeAmoledColor,
|
||||
backgroundColor: maybeAmoledColor,
|
||||
canvasColor: maybeAmoledColor,
|
||||
cardColor: maybeAmoledColor,
|
||||
splashColor: maybeAmoledColor,
|
||||
visualDensity: VisualDensity.adaptivePlatformDensity,
|
||||
appBarTheme: AppBarTheme(
|
||||
elevation: 0,
|
||||
brightness: theme.brightness,
|
||||
color: Colors.transparent,
|
||||
shadowColor: Colors.transparent,
|
||||
centerTitle: true,
|
||||
iconTheme: IconThemeData(color: theme.colorScheme.onSurface),
|
||||
textTheme: TextTheme(
|
||||
headline6: theme.textTheme.headline6
|
||||
.copyWith(fontSize: 20, fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
tabBarTheme: TabBarTheme(
|
||||
unselectedLabelColor: Colors.grey,
|
||||
labelColor: theme.colorScheme.onSurface,
|
||||
),
|
||||
chipTheme: ChipThemeData(
|
||||
backgroundColor: theme.accentColor,
|
||||
disabledColor: Colors.amber,
|
||||
selectedColor: Colors.amber,
|
||||
secondarySelectedColor: Colors.amber,
|
||||
padding: EdgeInsets.zero,
|
||||
shape: const StadiumBorder(),
|
||||
labelStyle:
|
||||
TextStyle(color: textColorBasedOnBackground(theme.accentColor)),
|
||||
secondaryLabelStyle: const TextStyle(color: Colors.amber),
|
||||
brightness: theme.brightness,
|
||||
labelPadding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
),
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
contentPadding: const EdgeInsets.all(10),
|
||||
isDense: true,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ElevatedButton.styleFrom(
|
||||
primary: theme.accentColor,
|
||||
onPrimary: textColorBasedOnBackground(theme.accentColor),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
),
|
||||
textButtonTheme: TextButtonThemeData(
|
||||
style: TextButton.styleFrom(
|
||||
primary: theme.colorScheme.onSurface,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
),
|
||||
outlinedButtonTheme: OutlinedButtonThemeData(
|
||||
style: TextButton.styleFrom(
|
||||
primary: theme.colorScheme.onSurface,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final lightTheme = _themeFactory();
|
||||
final darkTheme = _themeFactory(dark: true);
|
||||
final amoledTheme = _themeFactory(dark: true, amoled: true);
|
|
@ -67,6 +67,7 @@ void goToMedia(BuildContext context, String url) => Navigator.push(
|
|||
PageRouteBuilder(
|
||||
pageBuilder: (_, __, ___) => MediaViewPage(url),
|
||||
transitionDuration: const Duration(milliseconds: 300),
|
||||
opaque: false,
|
||||
transitionsBuilder: (_, animation, __, child) =>
|
||||
FadeTransition(opacity: animation, child: child),
|
||||
),
|
||||
|
|
|
@ -34,22 +34,21 @@ class AboutTile extends HookWidget {
|
|||
return AboutListTile(
|
||||
icon: const Icon(Icons.info),
|
||||
aboutBoxChildren: [
|
||||
FlatButton.icon(
|
||||
TextButton.icon(
|
||||
icon: const Icon(Icons.subject),
|
||||
label: const Text('changelog'),
|
||||
onPressed: () => showModalBottomSheet(
|
||||
onPressed: () => showBottomModal(
|
||||
context: context,
|
||||
builder: (_) => BottomModal(
|
||||
child: MarkdownBody(data: changelog),
|
||||
),
|
||||
padding: const EdgeInsets.all(8),
|
||||
builder: (_) => MarkdownBody(data: changelog),
|
||||
),
|
||||
),
|
||||
FlatButton.icon(
|
||||
TextButton.icon(
|
||||
icon: const Icon(Icons.code),
|
||||
label: const Text('source code'),
|
||||
onPressed: () => openInBrowser('https://github.com/krawieck/lemmur'),
|
||||
),
|
||||
FlatButton.icon(
|
||||
TextButton.icon(
|
||||
icon: const Icon(Icons.monetization_on),
|
||||
label: const Text('support development'),
|
||||
onPressed: () {
|
||||
|
@ -59,12 +58,12 @@ class AboutTile extends HookWidget {
|
|||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
FlatButton(
|
||||
TextButton(
|
||||
onPressed: () =>
|
||||
openInBrowser('https://patreon.com/lemmur'),
|
||||
child: const Text('Patreon'),
|
||||
),
|
||||
FlatButton(
|
||||
TextButton(
|
||||
onPressed: () =>
|
||||
openInBrowser('https://buymeacoff.ee/lemmur'),
|
||||
child: const Text('Buy Me a Coffee'),
|
||||
|
|
|
@ -1,29 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
/// A badge with accent color as background
|
||||
class Badge extends StatelessWidget {
|
||||
final Widget child;
|
||||
final BorderRadiusGeometry borderRadius;
|
||||
|
||||
const Badge({
|
||||
@required this.child,
|
||||
this.borderRadius = const BorderRadius.all(Radius.circular(10)),
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Container(
|
||||
height: 25,
|
||||
decoration: BoxDecoration(
|
||||
color: theme.accentColor,
|
||||
borderRadius: borderRadius,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,11 +1,18 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
|
||||
|
||||
/// Should be spawned with a showModalBottomSheet, not routed to.
|
||||
/// Should be spawned with a [showBottomModal], not routed to.
|
||||
class BottomModal extends StatelessWidget {
|
||||
final Widget child;
|
||||
final String title;
|
||||
final EdgeInsets padding;
|
||||
final Widget child;
|
||||
|
||||
const BottomModal({@required this.child, this.title});
|
||||
const BottomModal({
|
||||
this.title,
|
||||
this.padding = EdgeInsets.zero,
|
||||
@required this.child,
|
||||
}) : assert(padding != null),
|
||||
assert(child != null);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -14,36 +21,42 @@ class BottomModal extends StatelessWidget {
|
|||
return SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: SingleChildScrollView(
|
||||
child: Container(
|
||||
padding: title != null ? const EdgeInsets.only(top: 10) : null,
|
||||
decoration: BoxDecoration(
|
||||
color: theme.scaffoldBackgroundColor,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(10)),
|
||||
border: Border.all(
|
||||
color: Colors.grey.withOpacity(0.5),
|
||||
width: 0.2,
|
||||
)),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (title != null) ...[
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 70),
|
||||
child: Text(
|
||||
title,
|
||||
style: theme.textTheme.subtitle2,
|
||||
textAlign: TextAlign.left,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Colors.grey.withOpacity(0.5),
|
||||
width: 0.2,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Material(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (title != null) ...[
|
||||
const SizedBox(height: 10),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 70),
|
||||
child: Text(
|
||||
title,
|
||||
style: theme.textTheme.subtitle2,
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
),
|
||||
const Divider(
|
||||
indent: 20,
|
||||
endIndent: 20,
|
||||
)
|
||||
],
|
||||
Padding(
|
||||
padding: padding,
|
||||
child: child,
|
||||
),
|
||||
const Divider(
|
||||
indent: 20,
|
||||
endIndent: 20,
|
||||
)
|
||||
],
|
||||
child,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -51,3 +64,23 @@ class BottomModal extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper function for showing a [BottomModal]
|
||||
Future<T> showBottomModal<T>({
|
||||
@required BuildContext context,
|
||||
@required WidgetBuilder builder,
|
||||
String title,
|
||||
EdgeInsets padding = EdgeInsets.zero,
|
||||
}) =>
|
||||
showCustomModalBottomSheet<T>(
|
||||
context: context,
|
||||
animationCurve: Curves.easeInOutCubic,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: builder,
|
||||
containerWidget: (context, animation, child) => BottomModal(
|
||||
title: title,
|
||||
padding: padding,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
|
|
|
@ -74,7 +74,7 @@ class CommentWidget extends HookWidget {
|
|||
'downvotes': com.counts.downvotes,
|
||||
'score': com.counts.score,
|
||||
'% of upvotes':
|
||||
'''${100 * (com.counts.upvotes / (com.counts.upvotes + com.counts.downvotes))}%''',
|
||||
'${100 * (com.counts.upvotes / (com.counts.upvotes + com.counts.downvotes))}%',
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -127,63 +127,60 @@ class CommentWidget extends HookWidget {
|
|||
pop() => Navigator.of(context).pop();
|
||||
|
||||
final com = commentTree.comment;
|
||||
showModalBottomSheet(
|
||||
backgroundColor: Colors.transparent,
|
||||
showBottomModal(
|
||||
context: context,
|
||||
builder: (context) => BottomModal(
|
||||
child: Column(
|
||||
children: [
|
||||
builder: (context) => Column(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.open_in_browser),
|
||||
title: const Text('Open in browser'),
|
||||
onTap: () async => await ul.canLaunch(com.comment.link)
|
||||
? ul.launch(com.comment.link)
|
||||
: Scaffold.of(context).showSnackBar(
|
||||
const SnackBar(content: Text("can't open in browser"))),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.share),
|
||||
title: const Text('Share url'),
|
||||
onTap: () => Share.text(
|
||||
'Share comment url', com.comment.link, 'text/plain'),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.share),
|
||||
title: const Text('Share text'),
|
||||
onTap: () => Share.text(
|
||||
'Share comment text', com.comment.content, 'text/plain'),
|
||||
),
|
||||
ListTile(
|
||||
leading:
|
||||
Icon(selectable.value ? Icons.assignment : Icons.content_cut),
|
||||
title:
|
||||
Text('Make text ${selectable.value ? 'un' : ''}selectable'),
|
||||
onTap: () {
|
||||
selectable.value = !selectable.value;
|
||||
pop();
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: Icon(showRaw.value ? Icons.brush : Icons.build),
|
||||
title: Text('Show ${showRaw.value ? 'fancy' : 'raw'} text'),
|
||||
onTap: () {
|
||||
showRaw.value = !showRaw.value;
|
||||
pop();
|
||||
},
|
||||
),
|
||||
if (isMine)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.open_in_browser),
|
||||
title: const Text('Open in browser'),
|
||||
onTap: () async => await ul.canLaunch(com.comment.link)
|
||||
? ul.launch(com.comment.link)
|
||||
: Scaffold.of(context).showSnackBar(
|
||||
const SnackBar(content: Text("can't open in browser"))),
|
||||
leading: Icon(isDeleted.value ? Icons.restore : Icons.delete),
|
||||
title: Text(isDeleted.value ? 'Restore' : 'Delete'),
|
||||
onTap: loggedInAction(handleDelete),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.share),
|
||||
title: const Text('Share url'),
|
||||
onTap: () => Share.text(
|
||||
'Share comment url', com.comment.link, 'text/plain'),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.share),
|
||||
title: const Text('Share text'),
|
||||
onTap: () => Share.text(
|
||||
'Share comment text', com.comment.content, 'text/plain'),
|
||||
),
|
||||
ListTile(
|
||||
leading: Icon(
|
||||
selectable.value ? Icons.assignment : Icons.content_cut),
|
||||
title:
|
||||
Text('Make text ${selectable.value ? 'un' : ''}selectable'),
|
||||
onTap: () {
|
||||
selectable.value = !selectable.value;
|
||||
pop();
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: Icon(showRaw.value ? Icons.brush : Icons.build),
|
||||
title: Text('Show ${showRaw.value ? 'fancy' : 'raw'} text'),
|
||||
onTap: () {
|
||||
showRaw.value = !showRaw.value;
|
||||
pop();
|
||||
},
|
||||
),
|
||||
if (isMine)
|
||||
ListTile(
|
||||
leading: Icon(isDeleted.value ? Icons.restore : Icons.delete),
|
||||
title: Text(isDeleted.value ? 'Restore' : 'Delete'),
|
||||
onTap: loggedInAction(handleDelete),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.info_outline),
|
||||
title: const Text('Nerd stuff'),
|
||||
onTap: () => _showCommentInfo(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.info_outline),
|
||||
title: const Text('Nerd stuff'),
|
||||
onTap: () => _showCommentInfo(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -50,34 +50,29 @@ class CommentSection extends HookWidget {
|
|||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 15),
|
||||
child: Row(
|
||||
children: [
|
||||
OutlineButton(
|
||||
OutlinedButton(
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
backgroundColor: Colors.transparent,
|
||||
context: context,
|
||||
builder: (context) => BottomModal(
|
||||
title: 'sort by',
|
||||
child: Column(
|
||||
children: [
|
||||
for (final e in sortPairs.entries)
|
||||
ListTile(
|
||||
leading: Icon(e.value[0] as IconData),
|
||||
title: Text(e.value[1] as String),
|
||||
trailing: sorting.value == e.key
|
||||
? const Icon(Icons.check)
|
||||
: null,
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
sortComments(e.key);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
));
|
||||
showBottomModal(
|
||||
title: 'sort by',
|
||||
context: context,
|
||||
builder: (context) => Column(
|
||||
children: [
|
||||
for (final e in sortPairs.entries)
|
||||
ListTile(
|
||||
leading: Icon(e.value[0] as IconData),
|
||||
title: Text(e.value[1] as String),
|
||||
trailing: sorting.value == e.key
|
||||
? const Icon(Icons.check)
|
||||
: null,
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
sortComments(e.key);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(sortPairs[sorting.value][1] as String),
|
||||
|
@ -107,7 +102,7 @@ class CommentSection extends HookWidget {
|
|||
else
|
||||
for (final com in comments)
|
||||
CommentWidget(com, postCreatorId: postCreatorId),
|
||||
const BottomSafe(50),
|
||||
const BottomSafe(kMinInteractiveDimension + kFloatingActionButtonMargin),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -59,40 +59,37 @@ class PostWidget extends HookWidget {
|
|||
// == ACTIONS ==
|
||||
|
||||
static void showMoreMenu(BuildContext context, PostView post) {
|
||||
showModalBottomSheet(
|
||||
backgroundColor: Colors.transparent,
|
||||
showBottomModal(
|
||||
context: context,
|
||||
builder: (context) => BottomModal(
|
||||
child: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.open_in_browser),
|
||||
title: const Text('Open in browser'),
|
||||
onTap: () async => await ul.canLaunch(post.post.apId)
|
||||
? ul.launch(post.post.apId)
|
||||
: Scaffold.of(context).showSnackBar(
|
||||
const SnackBar(content: Text("can't open in browser"))),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.info_outline),
|
||||
title: const Text('Nerd stuff'),
|
||||
onTap: () {
|
||||
showInfoTablePopup(context, {
|
||||
'id': post.post.id,
|
||||
'apId': post.post.apId,
|
||||
'upvotes': post.counts.upvotes,
|
||||
'downvotes': post.counts.downvotes,
|
||||
'score': post.counts.score,
|
||||
'% of upvotes':
|
||||
'''${(100 * (post.counts.upvotes / (post.counts.upvotes + post.counts.downvotes))).toInt()}%''',
|
||||
'local': post.post.local,
|
||||
'published': post.post.published,
|
||||
'updated': post.post.updated ?? 'never',
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
builder: (context) => Column(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.open_in_browser),
|
||||
title: const Text('Open in browser'),
|
||||
onTap: () async => await ul.canLaunch(post.post.apId)
|
||||
? ul.launch(post.post.apId)
|
||||
: Scaffold.of(context).showSnackBar(
|
||||
const SnackBar(content: Text("can't open in browser"))),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.info_outline),
|
||||
title: const Text('Nerd stuff'),
|
||||
onTap: () {
|
||||
showInfoTablePopup(context, {
|
||||
'id': post.post.id,
|
||||
'apId': post.post.apId,
|
||||
'upvotes': post.counts.upvotes,
|
||||
'downvotes': post.counts.downvotes,
|
||||
'score': post.counts.score,
|
||||
'% of upvotes':
|
||||
'${(100 * (post.counts.upvotes / (post.counts.upvotes + post.counts.downvotes))).toInt()}%',
|
||||
'local': post.post.local,
|
||||
'published': post.post.published,
|
||||
'updated': post.post.updated ?? 'never',
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -219,7 +216,7 @@ class PostWidget extends HookWidget {
|
|||
),
|
||||
TextSpan(
|
||||
text:
|
||||
''' · ${timeago.format(post.post.published, locale: 'en_short')}'''),
|
||||
' · ${timeago.format(post.post.published, locale: 'en_short')}'),
|
||||
if (post.post.locked)
|
||||
const TextSpan(text: ' · 🔒'),
|
||||
if (post.post.stickied)
|
||||
|
|
|
@ -1,78 +1,41 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:lemmy_api_client/v2.dart';
|
||||
|
||||
import 'bottom_modal.dart';
|
||||
import 'radio_picker.dart';
|
||||
|
||||
/// Dropdown filters where you can change sorting or viewing type
|
||||
class PostListOptions extends HookWidget {
|
||||
final void Function(SortType sort) onChange;
|
||||
final SortType defaultSort;
|
||||
class PostListOptions extends StatelessWidget {
|
||||
final ValueChanged<SortType> onSortChanged;
|
||||
final SortType sortValue;
|
||||
final bool styleButton;
|
||||
|
||||
const PostListOptions({
|
||||
@required this.onChange,
|
||||
@required this.onSortChanged,
|
||||
@required this.sortValue,
|
||||
this.styleButton = true,
|
||||
this.defaultSort = SortType.active,
|
||||
});
|
||||
}) : assert(sortValue != null);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final sort = useState(defaultSort);
|
||||
|
||||
void selectSortType(BuildContext context) {
|
||||
showModalBottomSheet(
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) => BottomModal(
|
||||
title: 'sort by',
|
||||
child: Column(
|
||||
children: [
|
||||
for (final x in SortType.values)
|
||||
RadioListTile<SortType>(
|
||||
value: x,
|
||||
groupValue: sort.value,
|
||||
title: Text(x.value),
|
||||
onChanged: (val) {
|
||||
sort.value = val;
|
||||
onChange(val);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
)),
|
||||
Widget build(BuildContext context) => Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 10),
|
||||
child: Row(
|
||||
children: [
|
||||
RadioPicker<SortType>(
|
||||
title: 'sort by',
|
||||
values: SortType.values,
|
||||
groupValue: sortValue,
|
||||
onChanged: onSortChanged,
|
||||
mapValueToString: (value) => value.value,
|
||||
),
|
||||
const Spacer(),
|
||||
if (styleButton)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.view_stream),
|
||||
// TODO: create compact post and dropdown for selecting
|
||||
onPressed: () => print('TBD'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 10),
|
||||
child: Row(
|
||||
children: [
|
||||
OutlineButton(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
onPressed: () => selectSortType(context),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Text(sort.value.value),
|
||||
const SizedBox(width: 8),
|
||||
const Icon(Icons.arrow_drop_down),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
if (styleButton)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.view_stream),
|
||||
// TODO: create compact post and dropdown for selecting
|
||||
onPressed: () => print('TBD'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'bottom_modal.dart';
|
||||
|
||||
/// A picker with radio values (only one value can be picked at once)
|
||||
class RadioPicker<T> extends StatelessWidget {
|
||||
final List<T> values;
|
||||
final T groupValue;
|
||||
final ValueChanged<T> onChanged;
|
||||
|
||||
/// Map a given value to a string for display
|
||||
final String Function(T) mapValueToString;
|
||||
final String title;
|
||||
|
||||
/// custom button builder. When null, an OutlinedButton is used
|
||||
final Widget Function(
|
||||
BuildContext context, String displayValue, VoidCallback onPressed)
|
||||
buttonBuilder;
|
||||
|
||||
final Widget trailing;
|
||||
|
||||
const RadioPicker({
|
||||
Key key,
|
||||
@required this.values,
|
||||
@required this.groupValue,
|
||||
@required this.onChanged,
|
||||
this.mapValueToString,
|
||||
this.buttonBuilder,
|
||||
this.title,
|
||||
this.trailing,
|
||||
}) : assert(values != null),
|
||||
assert(groupValue != null),
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final mapValueToString =
|
||||
this.mapValueToString ?? (value) => value.toString();
|
||||
|
||||
final buttonBuilder = this.buttonBuilder ??
|
||||
(context, displayString, onPressed) => OutlinedButton(
|
||||
onPressed: onPressed,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(displayString),
|
||||
const Icon(Icons.arrow_drop_down),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
Future<void> onPressed() async {
|
||||
final value = await showBottomModal<T>(
|
||||
context: context,
|
||||
title: title,
|
||||
builder: (context) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
for (final value in values)
|
||||
RadioListTile<T>(
|
||||
value: value,
|
||||
groupValue: groupValue,
|
||||
title: Text(mapValueToString(value)),
|
||||
onChanged: (value) => Navigator.of(context).pop(value),
|
||||
),
|
||||
if (trailing != null) trailing
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (value != null) {
|
||||
onChanged?.call(value);
|
||||
}
|
||||
}
|
||||
|
||||
return buttonBuilder(context, mapValueToString(groupValue), onPressed);
|
||||
}
|
||||
}
|
|
@ -35,7 +35,8 @@ class SortableInfiniteList<T> extends HookWidget {
|
|||
|
||||
return InfiniteScroll<T>(
|
||||
prepend: PostListOptions(
|
||||
onChange: changeSorting,
|
||||
sortValue: sort.value,
|
||||
onSortChanged: changeSorting,
|
||||
styleButton: onStyleChange != null,
|
||||
),
|
||||
builder: builder,
|
||||
|
|
|
@ -12,7 +12,6 @@ import '../util/extensions/api.dart';
|
|||
import '../util/goto.dart';
|
||||
import '../util/intl.dart';
|
||||
import '../util/text_color.dart';
|
||||
import 'badge.dart';
|
||||
import 'fullscreenable_image.dart';
|
||||
import 'markdown_text.dart';
|
||||
import 'sortable_infinite_list.dart';
|
||||
|
@ -71,22 +70,24 @@ class UserProfile extends HookWidget {
|
|||
headerSliverBuilder: (_, __) => [
|
||||
SliverAppBar(
|
||||
pinned: true,
|
||||
expandedHeight: 265,
|
||||
expandedHeight: 300,
|
||||
toolbarHeight: 0,
|
||||
forceElevated: true,
|
||||
elevation: 0,
|
||||
backgroundColor: theme.cardColor,
|
||||
brightness: theme.brightness,
|
||||
iconTheme: theme.iconTheme,
|
||||
flexibleSpace:
|
||||
FlexibleSpaceBar(background: _UserOverview(userView)),
|
||||
bottom: TabBar(
|
||||
labelColor: theme.textTheme.bodyText1.color,
|
||||
tabs: const [
|
||||
Tab(text: 'Posts'),
|
||||
Tab(text: 'Comments'),
|
||||
Tab(text: 'About'),
|
||||
],
|
||||
bottom: PreferredSize(
|
||||
preferredSize: const TabBar(tabs: []).preferredSize,
|
||||
child: Material(
|
||||
color: theme.cardColor,
|
||||
child: const TabBar(
|
||||
tabs: [
|
||||
Tab(text: 'Posts'),
|
||||
Tab(text: 'Comments'),
|
||||
Tab(text: 'About'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -181,7 +182,7 @@ class _UserOverview extends HookWidget {
|
|||
topRight: Radius.circular(40),
|
||||
topLeft: Radius.circular(40),
|
||||
),
|
||||
color: theme.scaffoldBackgroundColor,
|
||||
color: theme.cardColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -195,7 +196,6 @@ class _UserOverview extends HookWidget {
|
|||
width: 80,
|
||||
height: 80,
|
||||
child: Container(
|
||||
// clipBehavior: Clip.antiAlias,
|
||||
decoration: BoxDecoration(
|
||||
boxShadow: const [
|
||||
BoxShadow(blurRadius: 6, color: Colors.black54)
|
||||
|
@ -215,117 +215,94 @@ class _UserOverview extends HookWidget {
|
|||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: userView.user.avatar != null
|
||||
? const EdgeInsets.only(top: 8)
|
||||
: const EdgeInsets.only(top: 70),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: userView.user.avatar == null ? 10 : 0),
|
||||
child: Text(
|
||||
userView.user.displayName,
|
||||
style: theme.textTheme.headline6,
|
||||
),
|
||||
),
|
||||
if (userView.user.avatar != null)
|
||||
const SizedBox(height: 8)
|
||||
else
|
||||
const SizedBox(height: 80),
|
||||
Text(
|
||||
userView.user.displayName,
|
||||
style: theme.textTheme.headline6,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'@${userView.user.name}@',
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'@${userView.user.name}@',
|
||||
style: theme.textTheme.caption,
|
||||
),
|
||||
InkWell(
|
||||
onTap: () =>
|
||||
goToInstance(context, userView.user.originInstanceHost),
|
||||
child: Text(
|
||||
userView.user.originInstanceHost,
|
||||
style: theme.textTheme.caption,
|
||||
),
|
||||
InkWell(
|
||||
onTap: () => goToInstance(
|
||||
context, userView.user.originInstanceHost),
|
||||
child: Text(
|
||||
userView.user.originInstanceHost,
|
||||
style: theme.textTheme.caption,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 15),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Badge(
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.article,
|
||||
size: 15,
|
||||
color: colorOnTopOfAccentColor,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 4),
|
||||
child: Text(
|
||||
'${compactNumber(userView.counts.postCount)}'
|
||||
' Post${pluralS(userView.counts.postCount)}',
|
||||
style: TextStyle(color: colorOnTopOfAccentColor),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16),
|
||||
child: Badge(
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.comment,
|
||||
size: 15,
|
||||
color: colorOnTopOfAccentColor,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 4),
|
||||
child: Text(
|
||||
'${compactNumber(userView.counts.commentCount)}'
|
||||
''' Comment${pluralS(userView.counts.commentCount)}''',
|
||||
style:
|
||||
TextStyle(color: colorOnTopOfAccentColor),
|
||||
),
|
||||
),
|
||||
],
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Chip(
|
||||
label: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.article,
|
||||
size: 15,
|
||||
color: colorOnTopOfAccentColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'${compactNumber(userView.counts.postCount)}'
|
||||
' Post${pluralS(userView.counts.postCount)}',
|
||||
style: TextStyle(color: colorOnTopOfAccentColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 15),
|
||||
child: Text(
|
||||
'Joined ${timeago.format(userView.user.published)}',
|
||||
style: theme.textTheme.bodyText1,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.cake,
|
||||
size: 13,
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Chip(
|
||||
label: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.comment,
|
||||
size: 15,
|
||||
color: colorOnTopOfAccentColor,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'${compactNumber(userView.counts.commentCount)} Comment${pluralS(userView.counts.commentCount)}',
|
||||
style: TextStyle(color: colorOnTopOfAccentColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 4),
|
||||
child: Text(
|
||||
DateFormat('MMM dd, yyyy')
|
||||
.format(userView.user.published),
|
||||
style: theme.textTheme.bodyText1,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
// Expanded(child: tabs())
|
||||
const SizedBox(height: 15),
|
||||
Text(
|
||||
'Joined ${timeago.format(userView.user.published)}',
|
||||
style: theme.textTheme.bodyText1,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.cake,
|
||||
size: 13,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 4),
|
||||
child: Text(
|
||||
DateFormat('MMM dd, yyyy')
|
||||
.format(userView.user.published),
|
||||
style: theme.textTheme.bodyText1,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
@ -78,10 +78,7 @@ class WriteComment extends HookWidget {
|
|||
return Scaffold(
|
||||
key: scaffoldKey,
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: Navigator.of(context).pop,
|
||||
),
|
||||
leading: const CloseButton(),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(showFancy.value ? Icons.build : Icons.brush),
|
||||
|
@ -108,8 +105,6 @@ class WriteComment extends HookWidget {
|
|||
autofocus: true,
|
||||
minLines: 5,
|
||||
maxLines: null,
|
||||
textAlignVertical: TextAlignVertical.top,
|
||||
decoration: const InputDecoration(border: OutlineInputBorder()),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
|
@ -123,7 +118,7 @@ class WriteComment extends HookWidget {
|
|||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
FlatButton(
|
||||
TextButton(
|
||||
onPressed: delayed.pending ? () {} : handleSubmit,
|
||||
child: delayed.loading
|
||||
? const CircularProgressIndicator()
|
||||
|
|
14
pubspec.lock
14
pubspec.lock
|
@ -282,6 +282,13 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.12.10-nullsafety.1"
|
||||
matrix4_transform:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: matrix4_transform
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.7"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -289,6 +296,13 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.3.0-nullsafety.3"
|
||||
modal_bottom_sheet:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: modal_bottom_sheet
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.0+1"
|
||||
nested:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
name: lemmur
|
||||
description: A new Flutter project.
|
||||
description: A mobile client for Lemmy - a federated reddit alternative
|
||||
|
||||
# The following line prevents the package from being accidentally published to
|
||||
# pub.dev using `pub publish`. This is preferred for private packages.
|
||||
|
@ -15,7 +15,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
|||
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
||||
# Read more about iOS versioning at
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
version: 0.2.2+10
|
||||
version: 0.2.3+11
|
||||
|
||||
environment:
|
||||
sdk: ">=2.7.0 <3.0.0"
|
||||
|
@ -28,6 +28,7 @@ dependencies:
|
|||
markdown: ^2.1.8
|
||||
flutter_markdown: ^0.4.3
|
||||
cached_network_image: ^2.2.0+1
|
||||
modal_bottom_sheet: ^1.0.0+1
|
||||
|
||||
# native
|
||||
esys_flutter_share: ^1.0.2
|
||||
|
@ -44,6 +45,7 @@ dependencies:
|
|||
timeago: ^2.0.27
|
||||
fuzzy: <1.0.0
|
||||
lemmy_api_client: ^0.10.2
|
||||
matrix4_transform: ^1.1.7
|
||||
|
||||
flutter:
|
||||
sdk: flutter
|
||||
|
|
|
@ -90,7 +90,7 @@ BEGIN
|
|||
BLOCK "040904e4"
|
||||
BEGIN
|
||||
VALUE "CompanyName", "com.krawieck" "\0"
|
||||
VALUE "FileDescription", "A new Flutter project." "\0"
|
||||
VALUE "FileDescription", "lemmur" "\0"
|
||||
VALUE "FileVersion", VERSION_AS_STRING "\0"
|
||||
VALUE "InternalName", "lemmur" "\0"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2020 com.krawieck. All rights reserved." "\0"
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 401 KiB |
Loading…
Reference in New Issue