Merge branch 'master' into fix-navigation-bar

This commit is contained in:
Filip Krawczyk 2021-02-09 21:42:43 +01:00 committed by GitHub
commit aabc270a3b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 1061 additions and 1379 deletions

View File

@ -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`

View File

@ -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

View File

@ -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();

View File

@ -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'),
),
],
),
);
}

View File

@ -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')

View File

@ -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,

View File

@ -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}'}',
),
],
),

View File

@ -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),
),
),
),
),
);
}

View File

@ -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()

View File

@ -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),

View File

@ -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,
),
],

View File

@ -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();

View File

@ -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,

View File

@ -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()),
),
),
),
);

View File

@ -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,

View File

@ -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,

View File

@ -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),
],
),
);
}
}

View File

@ -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'),
),
),
],
),

View File

@ -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(

View File

@ -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]),

84
lib/theme.dart Normal file
View File

@ -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);

View File

@ -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),
),

View File

@ -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'),

View File

@ -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,
),
);
}
}

View File

@ -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,
),
);

View File

@ -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),
),
],
),
);
}

View File

@ -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),
]);
}
}

View File

@ -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)

View File

@ -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'),
),
],
),
);
}
}

View File

@ -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);
}
}

View File

@ -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,

View File

@ -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),
],
),
),

View File

@ -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()

View File

@ -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:

View File

@ -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

View File

@ -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