Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

expose-licence-diff

..._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