💄 Video player optimized

This commit is contained in:
2025-08-01 20:36:39 +08:00
parent 84b1d6a346
commit b39e2e2d64
24 changed files with 500 additions and 299 deletions

View File

@@ -93,7 +93,7 @@ class _AboutScreenState extends ConsumerState<AboutScreen> {
final theme = Theme.of(context);
return AppScaffold(
noBackground: false,
isNoBackground: false,
appBar: AppBar(title: Text('about'.tr()), elevation: 0),
body:
_isLoading

View File

@@ -64,7 +64,7 @@ class AccountScreen extends HookConsumerWidget {
}
return AppScaffold(
noBackground: isWide,
isNoBackground: isWide,
appBar: AppBar(backgroundColor: Colors.transparent, toolbarHeight: 0),
body: SingleChildScrollView(
padding: getTabbedPadding(context),

View File

@@ -46,7 +46,7 @@ class EventCalanderScreen extends HookConsumerWidget {
}
return AppScaffold(
noBackground: false,
isNoBackground: false,
appBar: AppBar(
leading: const PageBackButton(),
title: Text('eventCalander').tr(),

View File

@@ -12,6 +12,7 @@ import 'package:island/pods/event_calendar.dart';
import 'package:island/pods/network.dart';
import 'package:island/pods/userinfo.dart';
import 'package:island/services/color.dart';
import 'package:island/services/responsive.dart';
import 'package:island/services/time.dart';
import 'package:island/services/timezone/native.dart';
import 'package:island/widgets/account/account_name.dart';
@@ -248,58 +249,12 @@ class AccountProfileScreen extends HookConsumerWidget {
final user = ref.watch(userInfoProvider);
return account.when(
data:
(data) => AppScaffold(
body: CustomScrollView(
slivers: [
SliverAppBar(
foregroundColor: appbarColor.value,
expandedHeight: 180,
pinned: true,
leading: PageBackButton(
color: appbarColor.value,
shadows: [appbarShadow],
),
flexibleSpace: Stack(
children: [
Positioned.fill(
child:
data.profile.background?.id != null
? CloudImageWidget(
file: data.profile.background,
)
: Container(
color:
Theme.of(
context,
).appBarTheme.backgroundColor,
),
),
FlexibleSpaceBar(
title: Text(
data.nick,
style: TextStyle(
color:
appbarColor.value ??
Theme.of(context).appBarTheme.foregroundColor,
shadows: [appbarShadow],
),
),
),
],
),
),
SliverToBoxAdapter(
child: Padding(
Widget accountBasicInfo(SnAccount data) => Padding(
padding: const EdgeInsets.fromLTRB(24, 24, 24, 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ProfilePictureWidget(
file: data.profile.picture,
radius: 32,
),
ProfilePictureWidget(file: data.profile.picture, radius: 32),
const Gap(20),
Expanded(
child: Column(
@@ -307,55 +262,20 @@ class AccountProfileScreen extends HookConsumerWidget {
children: [
Row(
children: [
AccountName(
account: data,
style: TextStyle(fontSize: 20),
),
AccountName(account: data, style: TextStyle(fontSize: 20)),
const Gap(6),
Text(
'@${data.name}',
).fontSize(14).opacity(0.85),
Text('@${data.name}').fontSize(14).opacity(0.85),
],
),
AccountStatusWidget(
uname: name,
padding: EdgeInsets.zero,
),
AccountStatusWidget(uname: name, padding: EdgeInsets.zero),
],
),
),
],
),
),
),
if (data.badges.isNotEmpty)
SliverToBoxAdapter(
child: BadgeList(
badges: data.badges,
).padding(horizontal: 24, bottom: 24),
),
SliverToBoxAdapter(
child: Column(
spacing: 12,
children: [
LevelingProgressCard(
level: data.profile.level,
experience: data.profile.experience,
progress: data.profile.levelingProgress,
),
if (data.profile.verification != null)
VerificationStatusCard(
mark: data.profile.verification!,
),
],
).padding(horizontal: 20),
),
);
SliverToBoxAdapter(
child: const Divider(height: 1).padding(vertical: 24),
),
SliverToBoxAdapter(
child: Column(
Widget accountProfileDetail(SnAccount data) => Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
spacing: 24,
children: [
@@ -393,9 +313,7 @@ class AccountProfileScreen extends HookConsumerWidget {
).$2.formatCustomGlobal('HH:mm'),
),
Text(
getTzInfo(
data.profile.timeZone,
).$1.formatOffsetLocal(),
getTzInfo(data.profile.timeZone).$1.formatOffsetLocal(),
).fontSize(11),
Text(
'UTC${getTzInfo(data.profile.timeZone).$1.formatOffset()}',
@@ -405,18 +323,12 @@ class AccountProfileScreen extends HookConsumerWidget {
],
),
],
).padding(horizontal: 24),
),
).padding(horizontal: 24);
if (user.value != null)
SliverToBoxAdapter(
child: const Divider(
height: 1,
).padding(top: 24, bottom: 12),
),
if (user.value != null)
SliverToBoxAdapter(
child: Row(
Widget accountAction(SnAccount data) => Card(
child: Column(
children: [
Row(
spacing: 8,
children: [
if (accountRelationship.value == null ||
@@ -432,9 +344,7 @@ class AccountProfileScreen extends HookConsumerWidget {
foregroundColor: WidgetStatePropertyAll(
accountRelationship.value == null
? null
: Theme.of(
context,
).colorScheme.onSecondary,
: Theme.of(context).colorScheme.onSecondary,
),
),
onPressed: relationshipAction,
@@ -463,9 +373,7 @@ class AccountProfileScreen extends HookConsumerWidget {
foregroundColor: WidgetStatePropertyAll(
accountRelationship.value == null
? null
: Theme.of(
context,
).colorScheme.onSecondary,
: Theme.of(context).colorScheme.onSecondary,
),
),
onPressed: blockAction,
@@ -483,9 +391,7 @@ class AccountProfileScreen extends HookConsumerWidget {
),
],
).padding(horizontal: 16),
),
SliverToBoxAdapter(
child: Row(
Row(
spacing: 8,
children: [
Expanded(
@@ -519,11 +425,179 @@ class AccountProfileScreen extends HookConsumerWidget {
),
),
],
).padding(horizontal: 16, top: 4),
),
],
).padding(horizontal: 16, vertical: 8),
);
return account.when(
data:
(data) => AppScaffold(
isNoBackground: false,
appBar:
isWideScreen(context)
? AppBar(
foregroundColor: appbarColor.value,
leading: PageBackButton(
color: appbarColor.value,
shadows: [appbarShadow],
),
flexibleSpace: Stack(
children: [
Positioned.fill(
child:
data.profile.background?.id != null
? CloudImageWidget(
file: data.profile.background,
)
: Container(
color:
Theme.of(
context,
).appBarTheme.backgroundColor,
),
),
FlexibleSpaceBar(
title: Text(
data.nick,
style: TextStyle(
color:
appbarColor.value ??
Theme.of(
context,
).appBarTheme.foregroundColor,
shadows: [appbarShadow],
),
),
),
],
),
)
: null,
body:
isWideScreen(context)
? Row(
children: [
Flexible(
child: CustomScrollView(
slivers: [
SliverToBoxAdapter(child: accountBasicInfo(data)),
if (data.badges.isNotEmpty)
SliverToBoxAdapter(
child: BadgeList(
badges: data.badges,
).padding(horizontal: 24, bottom: 24),
),
SliverToBoxAdapter(
child: const Divider(height: 1).padding(top: 12),
child: Column(
spacing: 12,
children: [
LevelingProgressCard(
level: data.profile.level,
experience: data.profile.experience,
progress: data.profile.levelingProgress,
),
if (data.profile.verification != null)
VerificationStatusCard(
mark: data.profile.verification!,
),
],
).padding(horizontal: 20),
),
],
),
),
Flexible(
child: CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: accountProfileDetail(data),
),
if (user.value != null)
SliverToBoxAdapter(child: accountAction(data)),
SliverToBoxAdapter(
child: Card(
child: FortuneGraphWidget(
events: accountEvents,
eventCalanderUser: data.name,
),
).padding(all: 8),
),
],
),
),
],
)
: CustomScrollView(
slivers: [
SliverAppBar(
foregroundColor: appbarColor.value,
expandedHeight: 180,
pinned: true,
leading: PageBackButton(
color: appbarColor.value,
shadows: [appbarShadow],
),
flexibleSpace: Stack(
children: [
Positioned.fill(
child:
data.profile.background?.id != null
? CloudImageWidget(
file: data.profile.background,
)
: Container(
color:
Theme.of(
context,
).appBarTheme.backgroundColor,
),
),
FlexibleSpaceBar(
title: Text(
data.nick,
style: TextStyle(
color:
appbarColor.value ??
Theme.of(
context,
).appBarTheme.foregroundColor,
shadows: [appbarShadow],
),
),
),
],
),
),
SliverToBoxAdapter(child: accountBasicInfo(data)),
if (data.badges.isNotEmpty)
SliverToBoxAdapter(
child: BadgeList(
badges: data.badges,
).padding(horizontal: 24, bottom: 24),
),
SliverToBoxAdapter(
child: Column(
spacing: 12,
children: [
LevelingProgressCard(
level: data.profile.level,
experience: data.profile.experience,
progress: data.profile.levelingProgress,
),
if (data.profile.verification != null)
VerificationStatusCard(
mark: data.profile.verification!,
),
],
).padding(horizontal: 20),
),
SliverToBoxAdapter(child: accountProfileDetail(data)),
if (user.value != null)
SliverToBoxAdapter(child: accountAction(data)),
SliverToBoxAdapter(
child: Column(
children: [

View File

@@ -73,7 +73,7 @@ class CreateAccountScreen extends HookConsumerWidget {
}
return AppScaffold(
noBackground: false,
isNoBackground: false,
appBar: AppBar(
leading: const PageBackButton(),
title: Text('createAccount').tr(),

View File

@@ -55,7 +55,7 @@ class LoginScreen extends HookConsumerWidget {
final factorPicked = useState<SnAuthFactor?>(null);
return AppScaffold(
noBackground: false,
isNoBackground: false,
appBar: AppBar(
leading: const PageBackButton(),
title: Text('login').tr(),

View File

@@ -40,7 +40,7 @@ class CallScreen extends HookConsumerWidget {
);
return AppScaffold(
noBackground: false,
isNoBackground: false,
appBar: AppBar(
leading: PageBackButton(),
title: Column(

View File

@@ -111,7 +111,7 @@ class DeveloperHubScreen extends HookConsumerWidget {
);
return AppScaffold(
noBackground: false,
isNoBackground: false,
appBar: AppBar(
leading: !isWide ? const PageBackButton() : null,
title: Text('developerHub').tr(),

View File

@@ -17,7 +17,7 @@ class DiscoveryRealmsScreen extends HookConsumerWidget {
final currentQuery = useState<String?>(null);
return AppScaffold(
noBackground: false,
isNoBackground: false,
appBar: AppBar(title: Text('discoverRealms'.tr())),
body: Stack(
children: [

View File

@@ -87,7 +87,7 @@ class ExploreScreen extends HookConsumerWidget {
final user = ref.watch(userInfoProvider);
return AppScaffold(
noBackground: false,
isNoBackground: false,
appBar: AppBar(
toolbarHeight: 0,
bottom: PreferredSize(

View File

@@ -53,13 +53,13 @@ class PostEditScreen extends HookConsumerWidget {
data: (post) => PostComposeScreen(originalPost: post),
loading:
() => AppScaffold(
noBackground: false,
isNoBackground: false,
appBar: AppBar(leading: const PageBackButton()),
body: const Center(child: CircularProgressIndicator()),
),
error:
(e, _) => AppScaffold(
noBackground: false,
isNoBackground: false,
appBar: AppBar(leading: const PageBackButton()),
body: Text('Error: $e', textAlign: TextAlign.center),
),
@@ -287,7 +287,7 @@ class PostComposeScreen extends HookConsumerWidget {
}
},
child: AppScaffold(
noBackground: false,
isNoBackground: false,
appBar: AppBar(
leading: const PageBackButton(),
actions: [

View File

@@ -352,7 +352,7 @@ class ArticleComposeScreen extends HookConsumerWidget {
}
},
child: AppScaffold(
noBackground: false,
isNoBackground: false,
appBar: AppBar(
leading: const PageBackButton(),
title: ValueListenableBuilder<TextEditingValue>(

View File

@@ -54,7 +54,7 @@ class PostDetailScreen extends HookConsumerWidget {
final user = ref.watch(userInfoProvider);
return AppScaffold(
noBackground: false,
isNoBackground: false,
appBar: AppBar(title: const Text('Post')),
body: postState.when(
data: (post) {

View File

@@ -110,7 +110,7 @@ class _PostSearchScreenState extends ConsumerState<PostSearchScreen> {
@override
Widget build(BuildContext context) {
return AppScaffold(
noBackground: false,
isNoBackground: false,
appBar: AppBar(
title: TextField(
controller: _searchController,

View File

@@ -259,7 +259,7 @@ class PublisherProfileScreen extends HookConsumerWidget {
return publisher.when(
data:
(data) => AppScaffold(
noBackground: false,
isNoBackground: false,
appBar:
isWideScreen(context)
? AppBar(
@@ -389,13 +389,13 @@ class PublisherProfileScreen extends HookConsumerWidget {
),
error:
(error, stackTrace) => AppScaffold(
noBackground: false,
isNoBackground: false,
appBar: AppBar(leading: const PageBackButton()),
body: Center(child: Text(error.toString())),
),
loading:
() => AppScaffold(
noBackground: false,
isNoBackground: false,
appBar: AppBar(leading: const PageBackButton()),
body: Center(child: CircularProgressIndicator()),
),

View File

@@ -79,7 +79,7 @@ class RealmDetailScreen extends HookConsumerWidget {
);
return AppScaffold(
noBackground: false,
isNoBackground: false,
body: realmState.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, _) => Center(child: Text('Error: $error')),

View File

@@ -41,7 +41,7 @@ class RealmListScreen extends HookConsumerWidget {
final realmInvites = ref.watch(realmInvitesProvider);
return AppScaffold(
noBackground: false,
isNoBackground: false,
appBar: AppBar(
title: const Text('realms').tr(),
actions: [
@@ -279,7 +279,7 @@ class EditRealmScreen extends HookConsumerWidget {
}
return AppScaffold(
noBackground: false,
isNoBackground: false,
appBar: AppBar(
title: Text(slug == null ? 'createRealm'.tr() : 'editRealm'.tr()),
leading: const PageBackButton(),

View File

@@ -552,7 +552,7 @@ class SettingsScreen extends HookConsumerWidget {
}
return AppScaffold(
noBackground: false,
isNoBackground: false,
appBar: AppBar(
title: Text('settings').tr(),
actions:

View File

@@ -165,7 +165,7 @@ class AppScaffold extends StatelessWidget {
final AppBar? appBar;
final DrawerCallback? onDrawerChanged;
final DrawerCallback? onEndDrawerChanged;
final bool? noBackground;
final bool? isNoBackground;
final bool? extendBody;
const AppScaffold({
@@ -181,7 +181,7 @@ class AppScaffold extends StatelessWidget {
this.endDrawer,
this.onDrawerChanged,
this.onEndDrawerChanged,
this.noBackground,
this.isNoBackground,
this.extendBody,
});
@@ -190,7 +190,7 @@ class AppScaffold extends StatelessWidget {
final appBarHeight = appBar?.preferredSize.height ?? 0;
final safeTop = MediaQuery.of(context).padding.top;
final noBackground = this.noBackground ?? isWideScreen(context);
final noBackground = isNoBackground ?? isWideScreen(context);
final content = Column(
children: [

View File

@@ -1,8 +1,11 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/file.dart';
import 'package:island/pods/config.dart';
import 'package:island/services/time.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart';
@@ -45,7 +48,7 @@ class CloudFileWidget extends ConsumerWidget {
),
"video" => AspectRatio(
aspectRatio: ratio,
child: UniversalVideo(uri: uri, aspectRatio: ratio),
child: CloudVideoWidget(item: item),
),
_ => Text('Unable render for ${item.mimeType}'),
};
@@ -58,6 +61,119 @@ class CloudFileWidget extends ConsumerWidget {
}
}
class CloudVideoWidget extends HookConsumerWidget {
final SnCloudFile item;
const CloudVideoWidget({super.key, required this.item});
@override
Widget build(BuildContext context, WidgetRef ref) {
final open = useState(false);
final serverUrl = ref.watch(serverUrlProvider);
final uri = '$serverUrl/drive/files/${item.id}';
var ratio =
item.fileMeta?['ratio'] is num
? item.fileMeta!['ratio'].toDouble()
: 1.0;
if (ratio == 0) ratio = 1.0;
if (open.value) {
return UniversalVideo(uri: uri, aspectRatio: ratio, autoplay: true);
}
return GestureDetector(
child: Stack(
children: [
UniversalImage(uri: '$uri?thumbnail=true'),
Positioned.fill(
child: Center(
child: const Icon(
Symbols.play_arrow,
fill: 1,
size: 32,
shadows: [
BoxShadow(
color: Colors.black54,
offset: Offset(1, 1),
spreadRadius: 8,
blurRadius: 8,
),
],
),
),
),
Positioned(
bottom: 0,
left: 0,
right: 0,
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
spacing: 8,
children: [
if (item.fileMeta?['duration'] != null)
Text(
Duration(
milliseconds:
((item.fileMeta?['duration'] as num) * 1000)
.toInt(),
).formatDuration(),
style: TextStyle(
shadows: [
BoxShadow(
color: Colors.black54,
offset: Offset(1, 1),
spreadRadius: 8,
blurRadius: 8,
),
],
),
),
if (item.fileMeta?['bit_rate'] != null)
Text(
'${int.parse(item.fileMeta?['bit_rate'] as String) ~/ 1000} Kbps',
style: TextStyle(
shadows: [
BoxShadow(
color: Colors.black54,
offset: Offset(1, 1),
spreadRadius: 8,
blurRadius: 8,
),
],
),
),
],
),
Text(
item.name,
style: TextStyle(
fontWeight: FontWeight.bold,
shadows: [
BoxShadow(
color: Colors.black54,
offset: Offset(1, 1),
spreadRadius: 8,
blurRadius: 8,
),
],
),
),
],
),
).padding(horizontal: 16, bottom: 12),
],
),
onTap: () {
open.value = true;
},
);
}
}
class CloudImageWidget extends ConsumerWidget {
final String? fileId;
final SnCloudFile? file;

View File

@@ -11,10 +11,12 @@ import 'package:media_kit_video/media_kit_video.dart';
class UniversalVideo extends ConsumerStatefulWidget {
final String uri;
final double aspectRatio;
final bool autoplay;
const UniversalVideo({
super.key,
required this.uri,
this.aspectRatio = 16 / 9,
this.autoplay = false,
});
@override
@@ -47,7 +49,7 @@ class _UniversalVideoState extends ConsumerState<UniversalVideo> {
log('[MediaPlayer] Hit cache: $url');
}
_player!.open(Media(uri), play: false);
_player!.open(Media(uri), play: widget.autoplay);
}
@override

View File

@@ -27,9 +27,13 @@ class PublisherCard extends ConsumerWidget {
Widget card = Card(
clipBehavior: Clip.antiAlias,
margin: EdgeInsets.zero,
child: InkWell(
onTap: () {
context.pushNamed('publisherProfile', pathParameters: {'name': publisher.name});
context.pushNamed(
'publisherProfile',
pathParameters: {'name': publisher.name},
);
},
child: AspectRatio(
aspectRatio: 16 / 7,

View File

@@ -29,9 +29,13 @@ class RealmCard extends ConsumerWidget {
Widget card = Card(
clipBehavior: Clip.antiAlias,
margin: EdgeInsets.zero,
child: InkWell(
onTap: () {
context.pushNamed('realmDetail', pathParameters: {'slug': realm.slug});
context.pushNamed(
'realmDetail',
pathParameters: {'slug': realm.slug},
);
},
child: AspectRatio(
aspectRatio: 16 / 7,

View File

@@ -28,6 +28,7 @@ class WebArticleCard extends StatelessWidget {
return ConstrainedBox(
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
child: Card(
margin: EdgeInsets.zero,
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: () => _onTap(context),