✨ Profile decoration
This commit is contained in:
@@ -16,6 +16,7 @@ import 'package:island/widgets/account/leveling_progress.dart';
|
|||||||
import 'package:island/widgets/alert.dart';
|
import 'package:island/widgets/alert.dart';
|
||||||
import 'package:island/widgets/app_scaffold.dart';
|
import 'package:island/widgets/app_scaffold.dart';
|
||||||
import 'package:island/widgets/content/cloud_files.dart';
|
import 'package:island/widgets/content/cloud_files.dart';
|
||||||
|
import 'package:island/widgets/content/profile_decoration.dart';
|
||||||
import 'package:island/widgets/debug_sheet.dart';
|
import 'package:island/widgets/debug_sheet.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
@@ -98,6 +99,10 @@ class AccountScreen extends HookConsumerWidget {
|
|||||||
child: ProfilePictureWidget(
|
child: ProfilePictureWidget(
|
||||||
file: user.value?.profile.picture,
|
file: user.value?.profile.picture,
|
||||||
radius: 32,
|
radius: 32,
|
||||||
|
decoration: ProfileDecoration(
|
||||||
|
text: '#OpenToWork',
|
||||||
|
color: Colors.green,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
context.pushNamed(
|
context.pushNamed(
|
||||||
@@ -331,23 +336,21 @@ class AccountScreen extends HookConsumerWidget {
|
|||||||
if (availableWidth > totalMin) {
|
if (availableWidth > totalMin) {
|
||||||
return Row(
|
return Row(
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
children:
|
children: children
|
||||||
children
|
.map((child) => Expanded(child: child))
|
||||||
.map((child) => Expanded(child: child))
|
.toList(),
|
||||||
.toList(),
|
|
||||||
).padding(horizontal: 12).height(48);
|
).padding(horizontal: 12).height(48);
|
||||||
} else {
|
} else {
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
child: Row(
|
child: Row(
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
children:
|
children: children
|
||||||
children
|
.map(
|
||||||
.map(
|
(child) =>
|
||||||
(child) =>
|
SizedBox(width: minWidth, child: child),
|
||||||
SizedBox(width: minWidth, child: child),
|
)
|
||||||
)
|
.toList(),
|
||||||
.toList(),
|
|
||||||
).padding(horizontal: 12),
|
).padding(horizontal: 12),
|
||||||
).height(48);
|
).height(48);
|
||||||
}
|
}
|
||||||
@@ -495,97 +498,96 @@ class _UnauthorizedAccountScreen extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
appBar: AppBar(title: const Text('account').tr()),
|
appBar: AppBar(title: const Text('account').tr()),
|
||||||
body:
|
body: ConstrainedBox(
|
||||||
ConstrainedBox(
|
constraints: const BoxConstraints(maxWidth: 360),
|
||||||
constraints: const BoxConstraints(maxWidth: 360),
|
child: Column(
|
||||||
child: Column(
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
child: Card(
|
||||||
|
child: InkWell(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
|
onTap: () {
|
||||||
|
context.pushNamed('createAccount');
|
||||||
|
},
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Icon(Symbols.person_add, size: 48),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text('createAccount').tr().bold(),
|
||||||
|
Text('createAccountDescription').tr(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
child: Card(
|
||||||
|
child: InkWell(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
|
onTap: () {
|
||||||
|
context.pushNamed('login');
|
||||||
|
},
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Icon(Symbols.login, size: 48),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text('login').tr().bold(),
|
||||||
|
Text('loginDescription').tr(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: <Widget>[
|
children: [
|
||||||
Padding(
|
IconButton(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
onPressed: () {
|
||||||
child: Card(
|
context.pushNamed('about');
|
||||||
child: InkWell(
|
},
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
iconSize: 18,
|
||||||
onTap: () {
|
color: Theme.of(context).colorScheme.secondary,
|
||||||
context.pushNamed('createAccount');
|
icon: const Icon(Icons.info, fill: 1),
|
||||||
},
|
tooltip: 'about'.tr(),
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Icon(Symbols.person_add, size: 48),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text('createAccount').tr().bold(),
|
|
||||||
Text('createAccountDescription').tr(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const Gap(8),
|
IconButton(
|
||||||
Padding(
|
icon: const Icon(Icons.bug_report, fill: 1),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
onPressed: () {
|
||||||
child: Card(
|
showModalBottomSheet(
|
||||||
child: InkWell(
|
context: context,
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
builder: (context) => DebugSheet(),
|
||||||
onTap: () {
|
);
|
||||||
context.pushNamed('login');
|
},
|
||||||
},
|
iconSize: 18,
|
||||||
child: Padding(
|
color: Theme.of(context).colorScheme.secondary,
|
||||||
padding: const EdgeInsets.all(16),
|
tooltip: 'debugOptions'.tr(),
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Icon(Symbols.login, size: 48),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text('login').tr().bold(),
|
|
||||||
Text('loginDescription').tr(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const Gap(8),
|
IconButton(
|
||||||
Row(
|
onPressed: () {
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
context.pushNamed('settings');
|
||||||
children: [
|
},
|
||||||
IconButton(
|
icon: const Icon(Icons.settings, fill: 1),
|
||||||
onPressed: () {
|
iconSize: 18,
|
||||||
context.pushNamed('about');
|
color: Theme.of(context).colorScheme.secondary,
|
||||||
},
|
tooltip: 'appSettings'.tr(),
|
||||||
iconSize: 18,
|
|
||||||
color: Theme.of(context).colorScheme.secondary,
|
|
||||||
icon: const Icon(Icons.info, fill: 1),
|
|
||||||
tooltip: 'about'.tr(),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.bug_report, fill: 1),
|
|
||||||
onPressed: () {
|
|
||||||
showModalBottomSheet(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => DebugSheet(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
iconSize: 18,
|
|
||||||
color: Theme.of(context).colorScheme.secondary,
|
|
||||||
tooltip: 'debugOptions'.tr(),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
onPressed: () {
|
|
||||||
context.pushNamed('settings');
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.settings, fill: 1),
|
|
||||||
iconSize: 18,
|
|
||||||
color: Theme.of(context).colorScheme.secondary,
|
|
||||||
tooltip: 'appSettings'.tr(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
).center(),
|
],
|
||||||
|
),
|
||||||
|
).center(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,11 @@ import 'package:island/models/file.dart';
|
|||||||
import 'package:island/pods/config.dart';
|
import 'package:island/pods/config.dart';
|
||||||
import 'package:island/services/time.dart';
|
import 'package:island/services/time.dart';
|
||||||
import 'package:island/utils/format.dart';
|
import 'package:island/utils/format.dart';
|
||||||
|
import 'package:island/widgets/content/profile_decoration.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
import 'dart:math' as math;
|
||||||
|
import 'dart:ui' as ui;
|
||||||
import 'package:island/widgets/data_saving_gate.dart';
|
import 'package:island/widgets/data_saving_gate.dart';
|
||||||
|
|
||||||
import 'file_viewer_contents.dart';
|
import 'file_viewer_contents.dart';
|
||||||
@@ -258,17 +261,15 @@ class CloudFileWidget extends HookConsumerWidget {
|
|||||||
var content = switch (item.mimeType?.split('/').firstOrNull) {
|
var content = switch (item.mimeType?.split('/').firstOrNull) {
|
||||||
'image' => AspectRatio(
|
'image' => AspectRatio(
|
||||||
aspectRatio: ratio,
|
aspectRatio: ratio,
|
||||||
child:
|
child: (useInternalGate && dataSaving && !unlocked.value)
|
||||||
(useInternalGate && dataSaving && !unlocked.value)
|
? dataPlaceHolder(Symbols.image)
|
||||||
? dataPlaceHolder(Symbols.image)
|
: cloudImage(),
|
||||||
: cloudImage(),
|
|
||||||
),
|
),
|
||||||
'video' => AspectRatio(
|
'video' => AspectRatio(
|
||||||
aspectRatio: ratio,
|
aspectRatio: ratio,
|
||||||
child:
|
child: (useInternalGate && dataSaving && !unlocked.value)
|
||||||
(useInternalGate && dataSaving && !unlocked.value)
|
? dataPlaceHolder(Symbols.play_arrow)
|
||||||
? dataPlaceHolder(Symbols.play_arrow)
|
: cloudVideo(),
|
||||||
: cloudVideo(),
|
|
||||||
),
|
),
|
||||||
'audio' => AudioFileContent(item: item, uri: uri),
|
'audio' => AudioFileContent(item: item, uri: uri),
|
||||||
_ => Builder(
|
_ => Builder(
|
||||||
@@ -383,10 +384,9 @@ class CloudVideoWidget extends HookConsumerWidget {
|
|||||||
final serverUrl = ref.watch(serverUrlProvider);
|
final serverUrl = ref.watch(serverUrlProvider);
|
||||||
final uri = '$serverUrl/drive/files/${item.id}';
|
final uri = '$serverUrl/drive/files/${item.id}';
|
||||||
|
|
||||||
var ratio =
|
var ratio = item.fileMeta?['ratio'] is num
|
||||||
item.fileMeta?['ratio'] is num
|
? item.fileMeta!['ratio'].toDouble()
|
||||||
? item.fileMeta!['ratio'].toDouble()
|
: 1.0;
|
||||||
: 1.0;
|
|
||||||
if (ratio == 0) ratio = 1.0;
|
if (ratio == 0) ratio = 1.0;
|
||||||
|
|
||||||
if (open.value) {
|
if (open.value) {
|
||||||
@@ -533,10 +533,9 @@ class CloudImageWidget extends ConsumerWidget {
|
|||||||
|
|
||||||
return AspectRatio(
|
return AspectRatio(
|
||||||
aspectRatio: aspectRatio,
|
aspectRatio: aspectRatio,
|
||||||
child:
|
child: file != null
|
||||||
file != null
|
? CloudFileWidget(item: file!, fit: fit)
|
||||||
? CloudFileWidget(item: file!, fit: fit)
|
: UniversalImage(uri: uri, blurHash: blurHash, fit: fit),
|
||||||
: UniversalImage(uri: uri, blurHash: blurHash, fit: fit),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -545,10 +544,9 @@ class CloudImageWidget extends ConsumerWidget {
|
|||||||
required String serverUrl,
|
required String serverUrl,
|
||||||
bool original = false,
|
bool original = false,
|
||||||
}) {
|
}) {
|
||||||
final uri =
|
final uri = original
|
||||||
original
|
? '$serverUrl/drive/files/$fileId?original=true'
|
||||||
? '$serverUrl/drive/files/$fileId?original=true'
|
: '$serverUrl/drive/files/$fileId';
|
||||||
: '$serverUrl/drive/files/$fileId';
|
|
||||||
return CachedNetworkImageProvider(uri);
|
return CachedNetworkImageProvider(uri);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -560,6 +558,7 @@ class ProfilePictureWidget extends ConsumerWidget {
|
|||||||
final double? borderRadius;
|
final double? borderRadius;
|
||||||
final IconData? fallbackIcon;
|
final IconData? fallbackIcon;
|
||||||
final Color? fallbackColor;
|
final Color? fallbackColor;
|
||||||
|
final ProfileDecoration? decoration;
|
||||||
const ProfilePictureWidget({
|
const ProfilePictureWidget({
|
||||||
super.key,
|
super.key,
|
||||||
this.fileId,
|
this.fileId,
|
||||||
@@ -568,6 +567,7 @@ class ProfilePictureWidget extends ConsumerWidget {
|
|||||||
this.borderRadius,
|
this.borderRadius,
|
||||||
this.fallbackIcon,
|
this.fallbackIcon,
|
||||||
this.fallbackColor,
|
this.fallbackColor,
|
||||||
|
this.decoration,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -575,36 +575,49 @@ class ProfilePictureWidget extends ConsumerWidget {
|
|||||||
final serverUrl = ref.watch(serverUrlProvider);
|
final serverUrl = ref.watch(serverUrlProvider);
|
||||||
final String? id = file?.id ?? fileId;
|
final String? id = file?.id ?? fileId;
|
||||||
|
|
||||||
final fallback =
|
final fallback = Icon(
|
||||||
Icon(
|
fallbackIcon ?? Symbols.account_circle,
|
||||||
fallbackIcon ?? Symbols.account_circle,
|
size: radius,
|
||||||
size: radius,
|
color: fallbackColor ?? Theme.of(context).colorScheme.onPrimaryContainer,
|
||||||
color:
|
).center();
|
||||||
fallbackColor ?? Theme.of(context).colorScheme.onPrimaryContainer,
|
|
||||||
).center();
|
final image = id == null
|
||||||
|
? fallback
|
||||||
|
: DataSavingGate(
|
||||||
|
bypass: true,
|
||||||
|
placeholder: fallback,
|
||||||
|
content: () => UniversalImage(
|
||||||
|
uri: '$serverUrl/drive/files/$id',
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget content = Container(
|
||||||
|
width: radius * 2,
|
||||||
|
height: radius * 2,
|
||||||
|
color: Theme.of(context).colorScheme.primaryContainer,
|
||||||
|
child: decoration != null
|
||||||
|
? Stack(
|
||||||
|
fit: StackFit.expand,
|
||||||
|
children: [
|
||||||
|
image,
|
||||||
|
CustomPaint(
|
||||||
|
painter: _ProfileDecorationPainter(
|
||||||
|
text: decoration!.text,
|
||||||
|
color: decoration!.color,
|
||||||
|
textColor: decoration!.textColor ?? Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: image,
|
||||||
|
);
|
||||||
|
|
||||||
return ClipRRect(
|
return ClipRRect(
|
||||||
borderRadius:
|
borderRadius: borderRadius == null
|
||||||
borderRadius == null
|
? BorderRadius.all(Radius.circular(radius))
|
||||||
? BorderRadius.all(Radius.circular(radius))
|
: BorderRadius.all(Radius.circular(borderRadius!)),
|
||||||
: BorderRadius.all(Radius.circular(borderRadius!)),
|
child: content,
|
||||||
child: Container(
|
|
||||||
width: radius * 2,
|
|
||||||
height: radius * 2,
|
|
||||||
color: Theme.of(context).colorScheme.primaryContainer,
|
|
||||||
child:
|
|
||||||
id == null
|
|
||||||
? fallback
|
|
||||||
: DataSavingGate(
|
|
||||||
bypass: true,
|
|
||||||
placeholder: fallback,
|
|
||||||
content:
|
|
||||||
() => UniversalImage(
|
|
||||||
uri: '$serverUrl/drive/files/$id',
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -716,32 +729,29 @@ class SplitAvatarWidget extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child:
|
child: filesId.length > 4
|
||||||
filesId.length > 4
|
? Container(
|
||||||
? Container(
|
color: Theme.of(
|
||||||
color:
|
context,
|
||||||
Theme.of(
|
).colorScheme.primaryContainer,
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
'+${filesId.length - 3}',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: radius * 0.4,
|
||||||
|
color: Theme.of(
|
||||||
context,
|
context,
|
||||||
).colorScheme.primaryContainer,
|
).colorScheme.onPrimaryContainer,
|
||||||
child: Center(
|
|
||||||
child: Text(
|
|
||||||
'+${filesId.length - 3}',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: radius * 0.4,
|
|
||||||
color:
|
|
||||||
Theme.of(
|
|
||||||
context,
|
|
||||||
).colorScheme.onPrimaryContainer,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
|
||||||
: _buildQuadrant(
|
|
||||||
context,
|
|
||||||
filesId[3],
|
|
||||||
ref,
|
|
||||||
radius,
|
|
||||||
),
|
),
|
||||||
|
)
|
||||||
|
: _buildQuadrant(
|
||||||
|
context,
|
||||||
|
filesId[3],
|
||||||
|
ref,
|
||||||
|
radius,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -765,14 +775,12 @@ class SplitAvatarWidget extends ConsumerWidget {
|
|||||||
width: radius,
|
width: radius,
|
||||||
height: radius,
|
height: radius,
|
||||||
color: Theme.of(context).colorScheme.primaryContainer,
|
color: Theme.of(context).colorScheme.primaryContainer,
|
||||||
child:
|
child: Icon(
|
||||||
Icon(
|
fallbackIcon,
|
||||||
fallbackIcon,
|
size: radius * 0.6,
|
||||||
size: radius * 0.6,
|
color:
|
||||||
color:
|
fallbackColor ?? Theme.of(context).colorScheme.onPrimaryContainer,
|
||||||
fallbackColor ??
|
).center(),
|
||||||
Theme.of(context).colorScheme.onPrimaryContainer,
|
|
||||||
).center(),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -786,3 +794,106 @@ class SplitAvatarWidget extends ConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _ProfileDecorationPainter extends CustomPainter {
|
||||||
|
final String text;
|
||||||
|
final Color color;
|
||||||
|
final Color textColor;
|
||||||
|
|
||||||
|
_ProfileDecorationPainter({
|
||||||
|
required this.text,
|
||||||
|
required this.color,
|
||||||
|
required this.textColor,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
if (text.isEmpty) return;
|
||||||
|
|
||||||
|
final radius = size.width / 2;
|
||||||
|
final center = Offset(size.width / 2, size.height / 2);
|
||||||
|
|
||||||
|
final strokeWidth = radius * 0.4; // Increased thickness
|
||||||
|
final centerAngle = 3 * math.pi / 4;
|
||||||
|
final sweepAngle = math.pi / 1;
|
||||||
|
final startAngle = centerAngle - (sweepAngle / 2);
|
||||||
|
|
||||||
|
final arcRadius = radius - (strokeWidth / 2);
|
||||||
|
final rect = Rect.fromCircle(center: center, radius: arcRadius);
|
||||||
|
|
||||||
|
final paint = Paint()
|
||||||
|
..style = PaintingStyle.stroke
|
||||||
|
..strokeWidth = strokeWidth
|
||||||
|
..shader = SweepGradient(
|
||||||
|
startAngle: startAngle,
|
||||||
|
endAngle: startAngle + sweepAngle,
|
||||||
|
colors: [color.withOpacity(0), color, color, color.withOpacity(0)],
|
||||||
|
stops: const [0.0, 0.25, 0.75, 1.0],
|
||||||
|
).createShader(rect);
|
||||||
|
|
||||||
|
canvas.drawArc(rect, startAngle, sweepAngle, false, paint);
|
||||||
|
|
||||||
|
_drawTextOnArc(canvas, center, arcRadius, text, centerAngle);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _drawTextOnArc(
|
||||||
|
Canvas canvas,
|
||||||
|
Offset center,
|
||||||
|
double radius,
|
||||||
|
String text,
|
||||||
|
double centerAngle,
|
||||||
|
) {
|
||||||
|
final textStyle = TextStyle(
|
||||||
|
color: textColor,
|
||||||
|
fontSize: radius * 0.28,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
);
|
||||||
|
|
||||||
|
double totalAngle = 0;
|
||||||
|
List<double> charAngles = [];
|
||||||
|
|
||||||
|
// Calculate total angle occupied by text
|
||||||
|
for (int i = 0; i < text.length; i++) {
|
||||||
|
final char = text[i];
|
||||||
|
final span = TextSpan(text: char, style: textStyle);
|
||||||
|
final tp = TextPainter(text: span, textDirection: ui.TextDirection.ltr);
|
||||||
|
tp.layout();
|
||||||
|
final charWidth = tp.width;
|
||||||
|
final angle = charWidth / radius;
|
||||||
|
charAngles.add(angle);
|
||||||
|
totalAngle += angle;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start from "Left" of the center (High angle)
|
||||||
|
// We want to traverse from centerAngle + total/2 to centerAngle - total/2
|
||||||
|
double currentAngle = centerAngle + (totalAngle / 2);
|
||||||
|
|
||||||
|
for (int i = 0; i < text.length; i++) {
|
||||||
|
final char = text[i];
|
||||||
|
final span = TextSpan(text: char, style: textStyle);
|
||||||
|
final tp = TextPainter(text: span, textDirection: ui.TextDirection.ltr);
|
||||||
|
tp.layout();
|
||||||
|
|
||||||
|
final charAngle = charAngles[i];
|
||||||
|
final midCharAngle = currentAngle - charAngle / 2;
|
||||||
|
|
||||||
|
final x = center.dx + radius * math.cos(midCharAngle);
|
||||||
|
final y = center.dy + radius * math.sin(midCharAngle);
|
||||||
|
|
||||||
|
canvas.save();
|
||||||
|
canvas.translate(x, y);
|
||||||
|
canvas.rotate(midCharAngle - math.pi / 2);
|
||||||
|
|
||||||
|
tp.paint(canvas, Offset(-tp.width / 2, -tp.height / 2));
|
||||||
|
|
||||||
|
canvas.restore();
|
||||||
|
|
||||||
|
currentAngle -= charAngle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(covariant CustomPainter oldDelegate) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
13
lib/widgets/content/profile_decoration.dart
Normal file
13
lib/widgets/content/profile_decoration.dart
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
part 'profile_decoration.freezed.dart';
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
sealed class ProfileDecoration with _$ProfileDecoration {
|
||||||
|
const factory ProfileDecoration({
|
||||||
|
required String text,
|
||||||
|
required Color color,
|
||||||
|
Color? textColor,
|
||||||
|
}) = _ProfileDecoration;
|
||||||
|
}
|
||||||
277
lib/widgets/content/profile_decoration.freezed.dart
Normal file
277
lib/widgets/content/profile_decoration.freezed.dart
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
// coverage:ignore-file
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||||
|
|
||||||
|
part of 'profile_decoration.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// FreezedGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
// dart format off
|
||||||
|
T _$identity<T>(T value) => value;
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$ProfileDecoration {
|
||||||
|
|
||||||
|
String get text; Color get color; Color? get textColor;
|
||||||
|
/// Create a copy of ProfileDecoration
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$ProfileDecorationCopyWith<ProfileDecoration> get copyWith => _$ProfileDecorationCopyWithImpl<ProfileDecoration>(this as ProfileDecoration, _$identity);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is ProfileDecoration&&(identical(other.text, text) || other.text == text)&&(identical(other.color, color) || other.color == color)&&(identical(other.textColor, textColor) || other.textColor == textColor));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,text,color,textColor);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'ProfileDecoration(text: $text, color: $color, textColor: $textColor)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class $ProfileDecorationCopyWith<$Res> {
|
||||||
|
factory $ProfileDecorationCopyWith(ProfileDecoration value, $Res Function(ProfileDecoration) _then) = _$ProfileDecorationCopyWithImpl;
|
||||||
|
@useResult
|
||||||
|
$Res call({
|
||||||
|
String text, Color color, Color? textColor
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class _$ProfileDecorationCopyWithImpl<$Res>
|
||||||
|
implements $ProfileDecorationCopyWith<$Res> {
|
||||||
|
_$ProfileDecorationCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final ProfileDecoration _self;
|
||||||
|
final $Res Function(ProfileDecoration) _then;
|
||||||
|
|
||||||
|
/// Create a copy of ProfileDecoration
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline') @override $Res call({Object? text = null,Object? color = null,Object? textColor = freezed,}) {
|
||||||
|
return _then(_self.copyWith(
|
||||||
|
text: null == text ? _self.text : text // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,color: null == color ? _self.color : color // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Color,textColor: freezed == textColor ? _self.textColor : textColor // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Color?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Adds pattern-matching-related methods to [ProfileDecoration].
|
||||||
|
extension ProfileDecorationPatterns on ProfileDecoration {
|
||||||
|
/// A variant of `map` that fallback to returning `orElse`.
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case final Subclass value:
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return orElse();
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _ProfileDecoration value)? $default,{required TResult orElse(),}){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _ProfileDecoration() when $default != null:
|
||||||
|
return $default(_that);case _:
|
||||||
|
return orElse();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A `switch`-like method, using callbacks.
|
||||||
|
///
|
||||||
|
/// Callbacks receives the raw object, upcasted.
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case final Subclass value:
|
||||||
|
/// return ...;
|
||||||
|
/// case final Subclass2 value:
|
||||||
|
/// return ...;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _ProfileDecoration value) $default,){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _ProfileDecoration():
|
||||||
|
return $default(_that);case _:
|
||||||
|
throw StateError('Unexpected subclass');
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A variant of `map` that fallback to returning `null`.
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case final Subclass value:
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return null;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _ProfileDecoration value)? $default,){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _ProfileDecoration() when $default != null:
|
||||||
|
return $default(_that);case _:
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A variant of `when` that fallback to an `orElse` callback.
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case Subclass(:final field):
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return orElse();
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String text, Color color, Color? textColor)? $default,{required TResult orElse(),}) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _ProfileDecoration() when $default != null:
|
||||||
|
return $default(_that.text,_that.color,_that.textColor);case _:
|
||||||
|
return orElse();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A `switch`-like method, using callbacks.
|
||||||
|
///
|
||||||
|
/// As opposed to `map`, this offers destructuring.
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case Subclass(:final field):
|
||||||
|
/// return ...;
|
||||||
|
/// case Subclass2(:final field2):
|
||||||
|
/// return ...;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String text, Color color, Color? textColor) $default,) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _ProfileDecoration():
|
||||||
|
return $default(_that.text,_that.color,_that.textColor);case _:
|
||||||
|
throw StateError('Unexpected subclass');
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A variant of `when` that fallback to returning `null`
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case Subclass(:final field):
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return null;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String text, Color color, Color? textColor)? $default,) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _ProfileDecoration() when $default != null:
|
||||||
|
return $default(_that.text,_that.color,_that.textColor);case _:
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
|
||||||
|
|
||||||
|
class _ProfileDecoration implements ProfileDecoration {
|
||||||
|
const _ProfileDecoration({required this.text, required this.color, this.textColor});
|
||||||
|
|
||||||
|
|
||||||
|
@override final String text;
|
||||||
|
@override final Color color;
|
||||||
|
@override final Color? textColor;
|
||||||
|
|
||||||
|
/// Create a copy of ProfileDecoration
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$ProfileDecorationCopyWith<_ProfileDecoration> get copyWith => __$ProfileDecorationCopyWithImpl<_ProfileDecoration>(this, _$identity);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ProfileDecoration&&(identical(other.text, text) || other.text == text)&&(identical(other.color, color) || other.color == color)&&(identical(other.textColor, textColor) || other.textColor == textColor));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,text,color,textColor);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'ProfileDecoration(text: $text, color: $color, textColor: $textColor)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class _$ProfileDecorationCopyWith<$Res> implements $ProfileDecorationCopyWith<$Res> {
|
||||||
|
factory _$ProfileDecorationCopyWith(_ProfileDecoration value, $Res Function(_ProfileDecoration) _then) = __$ProfileDecorationCopyWithImpl;
|
||||||
|
@override @useResult
|
||||||
|
$Res call({
|
||||||
|
String text, Color color, Color? textColor
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class __$ProfileDecorationCopyWithImpl<$Res>
|
||||||
|
implements _$ProfileDecorationCopyWith<$Res> {
|
||||||
|
__$ProfileDecorationCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final _ProfileDecoration _self;
|
||||||
|
final $Res Function(_ProfileDecoration) _then;
|
||||||
|
|
||||||
|
/// Create a copy of ProfileDecoration
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @pragma('vm:prefer-inline') $Res call({Object? text = null,Object? color = null,Object? textColor = freezed,}) {
|
||||||
|
return _then(_ProfileDecoration(
|
||||||
|
text: null == text ? _self.text : text // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,color: null == color ? _self.color : color // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Color,textColor: freezed == textColor ? _self.textColor : textColor // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Color?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// dart format on
|
||||||
Reference in New Issue
Block a user