Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions app/lib/frontend/handlers/experimental.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const _publicFlags = <PublicFlag>{

final _allFlags = <String>{
'dark-as-default',
'license',
..._publicFlags.map((x) => x.name),
};

Expand Down Expand Up @@ -88,6 +89,8 @@ class ExperimentalFlags {

bool get isDarkModeDefault => isEnabled('dark-as-default');

late final isLicenseEnabled = isEnabled('license');

String encodedAsCookie() => _enabled.join(':');

@override
Expand Down
88 changes: 84 additions & 4 deletions app/lib/frontend/templates/package.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import 'package:_pub_shared/data/page_data.dart';
import 'package:_pub_shared/search/tags.dart';
import 'package:collection/collection.dart' show IterableExtension;
import 'package:pana/pana.dart';
import 'package:pub_dev/frontend/request_context.dart';
import 'package:pub_dev/frontend/templates/views/pkg/liked_package_list.dart';

import '../../package/models.dart';
Expand Down Expand Up @@ -378,13 +380,91 @@ Tab _installTab(PackagePageData data) {
}

Tab _licenseTab(PackagePageData data) {
final license = data.hasLicense
? renderFile(data.asset!, urlResolverFn: data.urlResolverFn)
: d.text('No license file found.');
final licenses = data.scoreCard.panaReport?.licenses;
final hasEditOpData =
licenses != null &&
licenses.isNotEmpty &&
licenses.any((l) => l.operations?.isNotEmpty ?? false);
late d.Node content;
if (!data.hasLicense) {
content = d.text('No license file found.');
} else if (hasEditOpData &&
requestContext.experimentalFlags.isLicenseEnabled) {
final text = data.asset!.textContent!;
final opAndLicensePairs =
licenses
.expand((l) => (l.operations ?? []).map((op) => (op, l)))
.toList()
..sort((a, b) => a.$1.start.compareTo(b.$1.start));
final nodes = <d.Node>[];
var offset = 0;
for (final (op, _) in opAndLicensePairs) {
if (offset < op.start) {
nodes.add(
d.span(
classes: ['license-op-insert'],
text: text.substring(offset, op.start),
),
);
offset = op.start;
}
switch (op.type) {
case TextOpType.delete:
nodes.add(
d.span(
classes: ['license-op-delete', 'license-op-delete-hidden'],
children: [
d.span(
classes: ['license-op-delete-icon'],
text: '✄',
attributes: {'tabindex': '-1'},
),
d.span(
classes: ['license-op-delete-content'],
text: op.content,
),
],
),
);
break;
case TextOpType.insert:
final end = op.start + op.length;
nodes.add(
d.span(
classes: ['license-op-insert'],
text: text.substring(op.start, end),
),
);
offset = end;
break;
case TextOpType.match:
final end = op.start + op.length;
nodes.add(
d.span(
classes: ['license-op-match'],
text: text.substring(op.start, end),
),
);
offset = end;
break;
}
}
if (offset < text.length) {
nodes.add(
d.span(classes: ['license-op-insert'], text: text.substring(offset)),
);
}
content = d.div(
classes: ['highlight'],
child: d.pre(children: nodes),
);
} else {
content = renderFile(data.asset!, urlResolverFn: data.urlResolverFn);
}
return Tab.withContent(
id: 'license',
title: 'License',
contentNode: d.fragment([d.h2(text: 'License'), license]),
contentNode: d.fragment([d.h2(text: 'License'), content]),
isMarkdown: true,
);
}
Expand Down
13 changes: 13 additions & 0 deletions pkg/web_app/lib/src/foldable.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import 'web_util.dart';
void setupFoldable() {
_setEventForFoldable();
_setEventForCheckboxToggle();
_setEventForLicenseDeleteIcons();
}

/// Elements with the `foldable` class provide a folding content:
Expand Down Expand Up @@ -106,3 +107,15 @@ void _setEventForCheckboxToggle() {
});
}
}

/// Setup a toggle event for the delete operation icons in licenses.
void _setEventForLicenseDeleteIcons() {
final icons = document.body!
.querySelectorAll('.license-op-delete-icon')
.toElementList();
for (final icon in icons) {
icon.onClick.listen((event) {
icon.parentElement!.classList.toggle('license-op-delete-hidden');
});
}
}
22 changes: 22 additions & 0 deletions pkg/web_css/lib/src/_pkg.scss
Original file line number Diff line number Diff line change
Expand Up @@ -582,3 +582,25 @@
}
}
}

.license-op-delete {
background: var(--pub-license-editop-delete);

&.license-op-delete-hidden {
.license-op-delete-content {
display: none;
}
}

.license-op-delete-icon {
cursor: pointer;
}
}

.license-op-insert {
background: var(--pub-license-editop-insert);
}

.license-op-match {
background: var(--pub-license-editop-match);
}
5 changes: 5 additions & 0 deletions pkg/web_css/lib/src/_variables.scss
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,11 @@
// Opacity values used to display monochrome icons.
--pub-monochrome-opacity-initial: 0.6;
--pub-monochrome-opacity-hover: 1.0;

// Incomplete colors for license text edit ops.
--pub-license-editop-delete: rgba(255, 0, 0, 0.2);
--pub-license-editop-insert: rgba(255, 255, 0, 0.2);
--pub-license-editop-match: rgba(0, 255, 0, 0.2);
}

/// Variables that are specific to the light theme.
Expand Down