user artworks; update ui

This commit is contained in:
wgh19
2024-05-13 19:43:24 +08:00
parent 0f225be531
commit 56917f22c1
10 changed files with 291 additions and 179 deletions

View File

@@ -1,38 +0,0 @@
import 'dart:ui';
import 'package:flutter/widgets.dart';
class ColorScheme extends InheritedWidget{
final Brightness brightness;
const ColorScheme({super.key, required this.brightness, required super.child});
static ColorScheme of(BuildContext context){
return context.dependOnInheritedWidgetOfExactType<ColorScheme>()!;
}
bool get _light => brightness == Brightness.light;
Color get primary => _light ? const Color(0xff00538a) : const Color(0xff9ccaff);
Color get primaryContainer => _light ? const Color(0xff5fbdff) : const Color(0xff0079c5);
Color get secondary => _light ? const Color(0xff426182) : const Color(0xffaac9ef);
Color get secondaryContainer => _light ? const Color(0xffc1dcff) : const Color(0xff1f3f5f);
Color get tertiary => _light ? const Color(0xff743192) : const Color(0xffebb2ff);
Color get tertiaryContainer => _light ? const Color(0xffcf9ae8) : const Color(0xff9c58ba);
Color get outline => _light ? const Color(0xff707883) : const Color(0xff89919d);
Color get outlineVariant => _light ? const Color(0xffbfc7d3) : const Color(0xff404752);
Color get errorColor => _light ? const Color(0xffff3131) : const Color(0xfff86a6a);
@override
bool updateShouldNotify(covariant InheritedWidget oldWidget) {
return oldWidget is!ColorScheme || brightness != oldWidget.brightness;
}
}

View File

@@ -89,30 +89,43 @@ abstract class MultiPageLoadingState<T extends StatefulWidget, S extends Object>
});
}
@override
void initState() {
loadData(_page).then((value) {
if(value.success) {
_page++;
setState(() {
_isFirstLoading = false;
_data = value.data;
});
} else {
setState(() {
_isFirstLoading = false;
_error = value.errorMessage!;
});
}
});
super.initState();
}
Widget buildLoading(BuildContext context) {
return const Center(
child: ProgressRing(),
);
}
Widget buildError(BuildContext context, String error) {
return Center(
child: Text(error),
);
}
@override
Widget build(BuildContext context) {
if(_isFirstLoading){
loadData(_page).then((value) {
if(value.success) {
_page++;
setState(() {
_isFirstLoading = false;
_data = value.data;
});
} else {
setState(() {
_isFirstLoading = false;
_error = value.errorMessage!;
});
}
});
return const Center(
child: ProgressRing(),
);
return buildLoading(context);
} else if (_error != null){
return Center(
child: Text(_error!),
);
return buildError(context, _error!);
} else {
return buildContent(context, _data!);
}

View File

@@ -1,3 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter/material.dart' as md;
typedef MdIcons = Icons;
typedef MdIcons = md.Icons;
class ColorScheme {
static md.ColorScheme of(md.BuildContext context) {
return md.Theme.of(context).colorScheme;
}
}

View File

@@ -1,7 +1,7 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:pixes/foundation/app.dart';
import 'color_scheme.dart';
import 'md.dart';
class SegmentedButton<T> extends StatelessWidget {
const SegmentedButton(

View File

@@ -1,12 +1,12 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:pixes/components/animated_image.dart';
import 'package:pixes/components/color_scheme.dart';
import 'package:pixes/foundation/app.dart';
import 'package:pixes/foundation/image_provider.dart';
import 'package:pixes/pages/user_info_page.dart';
import 'package:pixes/utils/translation.dart';
import '../network/network.dart';
import 'md.dart';
class UserPreviewWidget extends StatefulWidget {
const UserPreviewWidget(this.user, {super.key});
@@ -90,7 +90,7 @@ class _UserPreviewWidgetState extends State<UserPreviewWidget> {
else
Button(
onPressed: follow,
child: Text("Unfollow".tl, style: TextStyle(color: ColorScheme.of(context).errorColor),),
child: Text("Unfollow".tl, style: TextStyle(color: ColorScheme.of(context).error),),
),
],
)

View File

@@ -313,4 +313,15 @@ class Network {
return Res.error(res.errorMessage);
}
}
Future<Res<List<Illust>>> getUserIllusts(String uid) async {
var res = await apiGet("/v1/user/illusts?filter=for_android&user_id=$uid&type=illust");
if (res.success) {
return Res(
(res.data["illusts"] as List).map((e) => Illust.fromJson(e)).toList(),
subData: res.data["next_url"]);
} else {
return Res.error(res.errorMessage);
}
}
}

View File

@@ -9,7 +9,8 @@ import 'package:pixes/pages/image_page.dart';
import 'package:pixes/pages/user_info_page.dart';
import 'package:pixes/utils/translation.dart';
import '../components/color_scheme.dart';
import '../components/md.dart';
const _kBottomBarHeight = 64.0;
@@ -304,7 +305,7 @@ class _BottomBarState extends State<_BottomBar> {
else
Button(
onPressed: follow,
child: Text("Unfollow".tl, style: TextStyle(color: ColorScheme.of(context).errorColor),),
child: Text("Unfollow".tl, style: TextStyle(color: ColorScheme.of(context).error),),
),
],
),
@@ -355,7 +356,7 @@ class _BottomBarState extends State<_BottomBar> {
else if(widget.illust.isBookmarked)
Icon(
Icons.favorite,
color: ColorScheme.of(context).errorColor,
color: ColorScheme.of(context).error,
size: 18,
)
else

View File

@@ -2,8 +2,8 @@ import "dart:async";
import "package:fluent_ui/fluent_ui.dart";
import "package:flutter/foundation.dart";
import "package:flutter/material.dart" as md;
import "package:pixes/appdata.dart";
import "package:pixes/components/color_scheme.dart";
import "package:pixes/components/md.dart";
import "package:pixes/foundation/app.dart";
import "package:pixes/network/network.dart";
@@ -73,56 +73,64 @@ class _MainPageState extends State<MainPage> with WindowListener {
content: LoginPage(() => setState(() {})),
);
}
return ColorScheme(
brightness: FluentTheme.of(context).brightness,
child: NavigationView(
appBar: buildAppBar(context, navigatorKey),
pane: NavigationPane(
selected: index,
onChanged: (value) {
setState(() {
index = value;
});
navigate(value);
},
items: [
UserPane(),
PaneItem(
icon: const Icon(MdIcons.search, size: 20,),
title: Text('Search'.tl),
body: const SizedBox.shrink(),
),
PaneItemHeader(header: Text("Artwork".tl).paddingVertical(4).paddingLeft(8)),
PaneItem(
icon: const Icon(MdIcons.star_border, size: 20,),
title: Text('Recommendations'.tl),
body: const SizedBox.shrink(),
),
PaneItem(
icon: const Icon(MdIcons.bookmark_outline, size: 20),
title: Text('Bookmarks'.tl),
body: const SizedBox.shrink(),
),
PaneItemSeparator(),
PaneItem(
icon: const Icon(MdIcons.explore_outlined, size: 20),
title: Text('Explore'.tl),
body: const SizedBox.shrink(),
),
],
footerItems: [
PaneItem(
icon: const Icon(MdIcons.settings_outlined, size: 20),
title: Text('Settings'.tl),
body: const SizedBox.shrink(),
),
],
),
paneBodyBuilder: (pane, child) => Navigator(
key: navigatorKey,
onGenerateRoute: (settings) => AppPageRoute(
builder: (context) => const RecommendationPage()),
)));
return md.Theme(
data: md.ThemeData.from(
useMaterial3: true,
colorScheme: md.ColorScheme.fromSeed(
seedColor: FluentTheme.of(context).accentColor.withOpacity(1),
brightness: FluentTheme.of(context).brightness,
)),
child: DefaultSelectionStyle.merge(
selectionColor: FluentTheme.of(context).selectionColor.withOpacity(0.4),
child: NavigationView(
appBar: buildAppBar(context, navigatorKey),
pane: NavigationPane(
selected: index,
onChanged: (value) {
setState(() {
index = value;
});
navigate(value);
},
items: [
UserPane(),
PaneItem(
icon: const Icon(MdIcons.search, size: 20,),
title: Text('Search'.tl),
body: const SizedBox.shrink(),
),
PaneItemHeader(header: Text("Artwork".tl).paddingVertical(4).paddingLeft(8)),
PaneItem(
icon: const Icon(MdIcons.star_border, size: 20,),
title: Text('Recommendations'.tl),
body: const SizedBox.shrink(),
),
PaneItem(
icon: const Icon(MdIcons.bookmark_outline, size: 20),
title: Text('Bookmarks'.tl),
body: const SizedBox.shrink(),
),
PaneItemSeparator(),
PaneItem(
icon: const Icon(MdIcons.explore_outlined, size: 20),
title: Text('Explore'.tl),
body: const SizedBox.shrink(),
),
],
footerItems: [
PaneItem(
icon: const Icon(MdIcons.settings_outlined, size: 20),
title: Text('Settings'.tl),
body: const SizedBox.shrink(),
),
],
),
paneBodyBuilder: (pane, child) => Navigator(
key: navigatorKey,
onGenerateRoute: (settings) => AppPageRoute(
builder: (context) => const RecommendationPage()),
)),
));
}
static final pageBuilders = [

View File

@@ -10,9 +10,9 @@ import 'package:pixes/pages/user_info_page.dart';
import 'package:pixes/utils/translation.dart';
import '../components/animated_image.dart';
import '../components/color_scheme.dart';
import '../components/grid.dart';
import '../components/illust_widget.dart';
import '../components/md.dart';
import '../foundation/image_provider.dart';
class SearchPage extends StatefulWidget {

View File

@@ -1,5 +1,5 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:pixes/components/color_scheme.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'package:pixes/components/loading.dart';
import 'package:pixes/components/md.dart';
import 'package:pixes/foundation/app.dart';
@@ -8,6 +8,8 @@ import 'package:pixes/network/network.dart';
import 'package:pixes/utils/translation.dart';
import 'package:url_launcher/url_launcher_string.dart';
import '../components/illust_widget.dart';
class UserInfoPage extends StatefulWidget {
const UserInfoPage(this.id, {super.key});
@@ -20,81 +22,190 @@ class UserInfoPage extends StatefulWidget {
class _UserInfoPageState extends LoadingState<UserInfoPage, UserDetails> {
@override
Widget buildContent(BuildContext context, UserDetails data) {
return SingleChildScrollView(
child: Column(
children: [
const SizedBox(height: 16),
Container(
width: 64,
height: 64,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(64),
border: Border.all(color: ColorScheme.of(context).outlineVariant, width: 0.6)),
child: ClipRRect(
borderRadius: BorderRadius.circular(64),
child: Image(
image: CachedImageProvider(data.avatar),
width: 64,
height: 64,
),
),),
const SizedBox(height: 8),
Text(data.name, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w500)),
const SizedBox(height: 4),
Text.rich(
TextSpan(
children: [
TextSpan(text: 'Follows: '.tl),
TextSpan(text: '${data.totalFollowUsers}', style: const TextStyle(fontWeight: FontWeight.w500)),
],
),
style: const TextStyle(fontSize: 14),
),
const SizedBox(height: 8,),
buildHeader("Infomation".tl),
buildItem(icon: MdIcons.comment_outlined, title: "Comment".tl, content: data.comment),
buildItem(icon: MdIcons.cake_outlined, title: "Birthday".tl, content: data.birth),
buildItem(icon: MdIcons.location_city_outlined, title: "Region", content: data.region),
buildItem(icon: MdIcons.work_outline, title: "Job".tl, content: data.job),
buildItem(icon: MdIcons.person_2_outlined, title: "Gender".tl, content: data.gender),
const SizedBox(height: 8,),
buildHeader("Social Network".tl),
buildItem(title: "Webpage", content: data.webpage, onTap: () => launchUrlString(data.webpage!)),
buildItem(title: "Twitter", content: data.twitterUrl, onTap: () => launchUrlString(data.twitterUrl!)),
buildItem(title: "pawoo", content: data.pawooUrl, onTap: () => launchUrlString(data.pawooUrl!))
return ScaffoldPage(
content: CustomScrollView(
slivers: [
buildUser(),
buildInformation(),
SliverToBoxAdapter(child: buildHeader("Artworks"),),
_UserArtworks(data.id.toString(), key: ValueKey(data.id),),
],
),
);
}
Widget buildItem({IconData? icon, required String title, required String? content, VoidCallback? onTap}) {
if(content == null || content.isEmpty) {
return const SizedBox.shrink();
}
return Card(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 2),
padding: EdgeInsets.zero,
child: ListTile(
leading: icon == null ? null : Icon(icon, size: 20,),
title: Text(title),
subtitle: SelectableText(content),
onPressed: onTap,
Widget buildUser() {
return SliverToBoxAdapter(
child: Column(
children: [
Container(
width: 64,
height: 64,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(64),
border: Border.all(color: ColorScheme.of(context).outlineVariant, width: 0.6)),
child: ClipRRect(
borderRadius: BorderRadius.circular(64),
child: Image(
image: CachedImageProvider(data!.avatar),
width: 64,
height: 64,
),
),),
const SizedBox(height: 8),
Text(data!.name, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w500)),
const SizedBox(height: 4),
Text.rich(
TextSpan(
children: [
TextSpan(text: 'Follows: '.tl),
TextSpan(text: '${data!.totalFollowUsers}', style: const TextStyle(fontWeight: FontWeight.w500)),
],
),
style: const TextStyle(fontSize: 14),
),
],
),
);
}
Widget buildHeader(String title) {
return SizedBox(
width: double.infinity,
child: Text(
title,
style: const TextStyle(fontWeight: FontWeight.w600),
).toAlign(Alignment.centerLeft)).paddingLeft(16).paddingVertical(4);
width: double.infinity,
child: Text(
title,
style: const TextStyle(fontWeight: FontWeight.w600),
).toAlign(Alignment.centerLeft)).paddingLeft(16).paddingVertical(4);
}
Widget buildInformation() {
Widget buildItem({IconData? icon, required String title, required String? content, Widget? trailing}) {
if(content == null || content.isEmpty) {
return const SizedBox.shrink();
}
return Card(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 2),
padding: EdgeInsets.zero,
child: ListTile(
leading: icon == null ? null : Icon(icon, size: 20,),
title: Text(title),
subtitle: SelectableText(content),
trailing: trailing,
),
);
}
return SliverToBoxAdapter(
child: Column(
children: [
buildHeader("Infomation".tl),
buildItem(icon: MdIcons.comment_outlined, title: "Comment".tl, content: data!.comment),
buildItem(icon: MdIcons.cake_outlined, title: "Birthday".tl, content: data!.birth),
buildItem(icon: MdIcons.location_city_outlined, title: "Region", content: data!.region),
buildItem(icon: MdIcons.work_outline, title: "Job".tl, content: data!.job),
buildItem(icon: MdIcons.person_2_outlined, title: "Gender".tl, content: data!.gender),
const SizedBox(height: 8,),
buildHeader("Social Network".tl),
buildItem(title: "Webpage",
content: data!.webpage,
trailing: IconButton(
icon: const Icon(MdIcons.open_in_new, size: 18),
onPressed: () => launchUrlString(data!.twitterUrl!)
)),
buildItem(title: "Twitter",
content: data!.twitterUrl,
trailing: IconButton(
icon: const Icon(MdIcons.open_in_new, size: 18),
onPressed: () => launchUrlString(data!.twitterUrl!)
)),
buildItem(title: "pawoo",
content: data!.pawooUrl,
trailing: IconButton(
icon: const Icon(MdIcons.open_in_new, size: 18,),
onPressed: () => launchUrlString(data!.pawooUrl!)
)),
],
),
);
}
@override
Future<Res<UserDetails>> loadData() {
return Network().getUserDetails(widget.id);
}
}
class _UserArtworks extends StatefulWidget {
const _UserArtworks(this.uid, {super.key});
final String uid;
@override
State<_UserArtworks> createState() => _UserArtworksState();
}
class _UserArtworksState extends MultiPageLoadingState<_UserArtworks, Illust> {
@override
Widget buildLoading(BuildContext context) {
return const SliverToBoxAdapter(
child: SizedBox(
child: Center(
child: ProgressRing(),
),
),
);
}
@override
Widget buildError(context, error) {
return SliverToBoxAdapter(
child: SizedBox(
child: Center(
child: Row(
children: [
const Icon(FluentIcons.info),
const SizedBox(width: 4,),
Text(error)
],
),
),
),
);
}
@override
Widget buildContent(BuildContext context, final List<Illust> data) {
return SliverMasonryGrid(
gridDelegate: const SliverSimpleGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 240,
),
delegate: SliverChildBuilderDelegate(
(context, index) {
if(index == data.length - 1){
nextPage();
}
return IllustWidget(data[index]);
},
childCount: data.length,
),
).sliverPaddingHorizontal(8);
}
String? nextUrl;
@override
Future<Res<List<Illust>>> loadData(page) async{
if(nextUrl == "end") {
return Res.error("No more data");
}
var res = nextUrl == null
? await Network().getUserIllusts(widget.uid)
: await Network().getIllustsWithNextUrl(nextUrl!);
if(!res.error) {
nextUrl = res.subData;
nextUrl ??= "end";
}
return res;
}
}