Brings it more into line with Sonarr preferred wordspull/4205/head
parent
da80793204
commit
50d6c5e61e
@ -0,0 +1,6 @@
|
||||
.addCustomFormatMessage {
|
||||
color: $helpTextColor;
|
||||
text-align: center;
|
||||
font-weight: 300;
|
||||
font-size: 20px;
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
.dragPreview {
|
||||
width: 380px;
|
||||
opacity: 0.75;
|
||||
}
|
@ -1,88 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { DragLayer } from 'react-dnd';
|
||||
import dimensions from 'Styles/Variables/dimensions.js';
|
||||
import { QUALITY_PROFILE_FORMAT_ITEM } from 'Helpers/dragTypes';
|
||||
import DragPreviewLayer from 'Components/DragPreviewLayer';
|
||||
import QualityProfileFormatItem from './QualityProfileFormatItem';
|
||||
import styles from './QualityProfileFormatItemDragPreview.css';
|
||||
|
||||
const formGroupSmallWidth = parseInt(dimensions.formGroupSmallWidth);
|
||||
const formLabelLargeWidth = parseInt(dimensions.formLabelLargeWidth);
|
||||
const formLabelRightMarginWidth = parseInt(dimensions.formLabelRightMarginWidth);
|
||||
const dragHandleWidth = parseInt(dimensions.dragHandleWidth);
|
||||
|
||||
function collectDragLayer(monitor) {
|
||||
return {
|
||||
item: monitor.getItem(),
|
||||
itemType: monitor.getItemType(),
|
||||
currentOffset: monitor.getSourceClientOffset()
|
||||
};
|
||||
}
|
||||
|
||||
class QualityProfileFormatItemDragPreview extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
item,
|
||||
itemType,
|
||||
currentOffset
|
||||
} = this.props;
|
||||
|
||||
if (!currentOffset || itemType !== QUALITY_PROFILE_FORMAT_ITEM) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// The offset is shifted because the drag handle is on the right edge of the
|
||||
// list item and the preview is wider than the drag handle.
|
||||
|
||||
const { x, y } = currentOffset;
|
||||
const handleOffset = formGroupSmallWidth - formLabelLargeWidth - formLabelRightMarginWidth - dragHandleWidth;
|
||||
const transform = `translate3d(${x - handleOffset}px, ${y}px, 0)`;
|
||||
|
||||
const style = {
|
||||
position: 'absolute',
|
||||
WebkitTransform: transform,
|
||||
msTransform: transform,
|
||||
transform
|
||||
};
|
||||
|
||||
const {
|
||||
formatId,
|
||||
name,
|
||||
allowed,
|
||||
sortIndex
|
||||
} = item;
|
||||
|
||||
return (
|
||||
<DragPreviewLayer>
|
||||
<div
|
||||
className={styles.dragPreview}
|
||||
style={style}
|
||||
>
|
||||
<QualityProfileFormatItem
|
||||
formatId={formatId}
|
||||
name={name}
|
||||
allowed={allowed}
|
||||
sortIndex={sortIndex}
|
||||
isDragging={false}
|
||||
/>
|
||||
</div>
|
||||
</DragPreviewLayer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
QualityProfileFormatItemDragPreview.propTypes = {
|
||||
item: PropTypes.object,
|
||||
itemType: PropTypes.string,
|
||||
currentOffset: PropTypes.shape({
|
||||
x: PropTypes.number.isRequired,
|
||||
y: PropTypes.number.isRequired
|
||||
})
|
||||
};
|
||||
|
||||
export default DragLayer(collectDragLayer)(QualityProfileFormatItemDragPreview);
|
@ -1,18 +0,0 @@
|
||||
.qualityProfileFormatItemDragSource {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.qualityProfileFormatItemPlaceholder {
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
border: 1px dotted #aaa;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.qualityProfileFormatItemPlaceholderBefore {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.qualityProfileFormatItemPlaceholderAfter {
|
||||
margin-top: 8px;
|
||||
}
|
@ -1,157 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { findDOMNode } from 'react-dom';
|
||||
import { DragSource, DropTarget } from 'react-dnd';
|
||||
import classNames from 'classnames';
|
||||
import { QUALITY_PROFILE_FORMAT_ITEM } from 'Helpers/dragTypes';
|
||||
import QualityProfileFormatItem from './QualityProfileFormatItem';
|
||||
import styles from './QualityProfileFormatItemDragSource.css';
|
||||
|
||||
const qualityProfileFormatItemDragSource = {
|
||||
beginDrag({ formatId, name, allowed, sortIndex }) {
|
||||
return {
|
||||
formatId,
|
||||
name,
|
||||
allowed,
|
||||
sortIndex
|
||||
};
|
||||
},
|
||||
|
||||
endDrag(props, monitor, component) {
|
||||
props.onQualityProfileFormatItemDragEnd(monitor.getItem(), monitor.didDrop());
|
||||
}
|
||||
};
|
||||
|
||||
const qualityProfileFormatItemDropTarget = {
|
||||
hover(props, monitor, component) {
|
||||
const dragIndex = monitor.getItem().sortIndex;
|
||||
const hoverIndex = props.sortIndex;
|
||||
|
||||
const hoverBoundingRect = findDOMNode(component).getBoundingClientRect();
|
||||
const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
|
||||
const clientOffset = monitor.getClientOffset();
|
||||
const hoverClientY = clientOffset.y - hoverBoundingRect.top;
|
||||
|
||||
// Moving up, only trigger if drag position is above 50%
|
||||
if (dragIndex < hoverIndex && hoverClientY > hoverMiddleY) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Moving down, only trigger if drag position is below 50%
|
||||
if (dragIndex > hoverIndex && hoverClientY < hoverMiddleY) {
|
||||
return;
|
||||
}
|
||||
|
||||
props.onQualityProfileFormatItemDragMove(dragIndex, hoverIndex);
|
||||
}
|
||||
};
|
||||
|
||||
function collectDragSource(connect, monitor) {
|
||||
return {
|
||||
connectDragSource: connect.dragSource(),
|
||||
isDragging: monitor.isDragging()
|
||||
};
|
||||
}
|
||||
|
||||
function collectDropTarget(connect, monitor) {
|
||||
return {
|
||||
connectDropTarget: connect.dropTarget(),
|
||||
isOver: monitor.isOver()
|
||||
};
|
||||
}
|
||||
|
||||
class QualityProfileFormatItemDragSource extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
formatId,
|
||||
name,
|
||||
allowed,
|
||||
sortIndex,
|
||||
isDragging,
|
||||
isDraggingUp,
|
||||
isDraggingDown,
|
||||
isOver,
|
||||
connectDragSource,
|
||||
connectDropTarget,
|
||||
onQualityProfileFormatItemAllowedChange
|
||||
} = this.props;
|
||||
|
||||
const isBefore = !isDragging && isDraggingUp && isOver;
|
||||
const isAfter = !isDragging && isDraggingDown && isOver;
|
||||
|
||||
// if (isDragging && !isOver) {
|
||||
// return null;
|
||||
// }
|
||||
|
||||
return connectDropTarget(
|
||||
<div
|
||||
className={classNames(
|
||||
styles.qualityProfileFormatItemDragSource,
|
||||
isBefore && styles.isDraggingUp,
|
||||
isAfter && styles.isDraggingDown
|
||||
)}
|
||||
>
|
||||
{
|
||||
isBefore &&
|
||||
<div
|
||||
className={classNames(
|
||||
styles.qualityProfileFormatItemPlaceholder,
|
||||
styles.qualityProfileFormatItemPlaceholderBefore
|
||||
)}
|
||||
/>
|
||||
}
|
||||
|
||||
<QualityProfileFormatItem
|
||||
formatId={formatId}
|
||||
name={name}
|
||||
allowed={allowed}
|
||||
sortIndex={sortIndex}
|
||||
isDragging={isDragging}
|
||||
isOver={isOver}
|
||||
connectDragSource={connectDragSource}
|
||||
onQualityProfileFormatItemAllowedChange={onQualityProfileFormatItemAllowedChange}
|
||||
/>
|
||||
|
||||
{
|
||||
isAfter &&
|
||||
<div
|
||||
className={classNames(
|
||||
styles.qualityProfileFormatItemPlaceholder,
|
||||
styles.qualityProfileFormatItemPlaceholderAfter
|
||||
)}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
QualityProfileFormatItemDragSource.propTypes = {
|
||||
formatId: PropTypes.number.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
allowed: PropTypes.bool.isRequired,
|
||||
sortIndex: PropTypes.number.isRequired,
|
||||
isDragging: PropTypes.bool,
|
||||
isDraggingUp: PropTypes.bool,
|
||||
isDraggingDown: PropTypes.bool,
|
||||
isOver: PropTypes.bool,
|
||||
connectDragSource: PropTypes.func,
|
||||
connectDropTarget: PropTypes.func,
|
||||
onQualityProfileFormatItemAllowedChange: PropTypes.func.isRequired,
|
||||
onQualityProfileFormatItemDragMove: PropTypes.func.isRequired,
|
||||
onQualityProfileFormatItemDragEnd: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default DropTarget(
|
||||
QUALITY_PROFILE_FORMAT_ITEM,
|
||||
qualityProfileFormatItemDropTarget,
|
||||
collectDropTarget
|
||||
)(DragSource(
|
||||
QUALITY_PROFILE_FORMAT_ITEM,
|
||||
qualityProfileFormatItemDragSource,
|
||||
collectDragSource
|
||||
)(QualityProfileFormatItemDragSource));
|
@ -1,6 +1,31 @@
|
||||
.formats {
|
||||
margin-top: 10px;
|
||||
/* TODO: This should consider the number of languages in the list */
|
||||
min-height: 550px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.headerContainer {
|
||||
display: flex;
|
||||
font-weight: bold;
|
||||
line-height: 35px;
|
||||
}
|
||||
|
||||
.headerTitle {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.headerScore {
|
||||
display: flex;
|
||||
flex-grow: 0;
|
||||
padding-left: 16px;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.addCustomFormatMessage {
|
||||
max-width: $formGroupExtraSmallWidth;
|
||||
color: $helpTextColor;
|
||||
text-align: center;
|
||||
font-weight: 300;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
@ -1,130 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.Languages;
|
||||
using NzbDrone.Core.Profiles;
|
||||
using NzbDrone.Core.Test.CustomFormats;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.Qualities
|
||||
{
|
||||
[TestFixture]
|
||||
public class CustomFormatsComparerFixture : CoreTest
|
||||
{
|
||||
private CustomFormat _customFormat1;
|
||||
private CustomFormat _customFormat2;
|
||||
private CustomFormat _customFormat3;
|
||||
private CustomFormat _customFormat4;
|
||||
|
||||
public CustomFormatsComparer Subject { get; set; }
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
}
|
||||
|
||||
private void GivenDefaultProfileWithFormats()
|
||||
{
|
||||
_customFormat1 = new CustomFormat("My Format 1", new LanguageSpecification { Value = (int)Language.English }) { Id = 1 };
|
||||
_customFormat2 = new CustomFormat("My Format 2", new LanguageSpecification { Value = (int)Language.French }) { Id = 2 };
|
||||
_customFormat3 = new CustomFormat("My Format 3", new LanguageSpecification { Value = (int)Language.Spanish }) { Id = 3 };
|
||||
_customFormat4 = new CustomFormat("My Format 4", new LanguageSpecification { Value = (int)Language.Italian }) { Id = 4 };
|
||||
|
||||
CustomFormatsFixture.GivenCustomFormats(CustomFormat.None, _customFormat1, _customFormat2, _customFormat3, _customFormat4);
|
||||
|
||||
Subject = new CustomFormatsComparer(new Profile { Items = QualityFixture.GetDefaultQualities(), FormatItems = CustomFormatsFixture.GetSampleFormatItems() });
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_lesser_when_first_format_is_worse()
|
||||
{
|
||||
GivenDefaultProfileWithFormats();
|
||||
|
||||
var first = new List<CustomFormat> { _customFormat1 };
|
||||
var second = new List<CustomFormat> { _customFormat2 };
|
||||
|
||||
var compare = Subject.Compare(first, second);
|
||||
|
||||
compare.Should().BeLessThan(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_zero_when_formats_are_equal()
|
||||
{
|
||||
GivenDefaultProfileWithFormats();
|
||||
|
||||
var first = new List<CustomFormat> { _customFormat2 };
|
||||
var second = new List<CustomFormat> { _customFormat2 };
|
||||
|
||||
var compare = Subject.Compare(first, second);
|
||||
|
||||
compare.Should().Be(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_greater_when_first_format_is_better()
|
||||
{
|
||||
GivenDefaultProfileWithFormats();
|
||||
|
||||
var first = new List<CustomFormat> { _customFormat3 };
|
||||
var second = new List<CustomFormat> { _customFormat2 };
|
||||
|
||||
var compare = Subject.Compare(first, second);
|
||||
|
||||
compare.Should().BeGreaterThan(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_greater_when_multiple_formats_better()
|
||||
{
|
||||
GivenDefaultProfileWithFormats();
|
||||
|
||||
var first = new List<CustomFormat> { _customFormat3, _customFormat4 };
|
||||
var second = new List<CustomFormat> { _customFormat2 };
|
||||
|
||||
var compare = Subject.Compare(first, second);
|
||||
|
||||
compare.Should().BeGreaterThan(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_greater_when_best_format_is_better()
|
||||
{
|
||||
GivenDefaultProfileWithFormats();
|
||||
|
||||
var first = new List<CustomFormat> { _customFormat1, _customFormat3 };
|
||||
var second = new List<CustomFormat> { _customFormat2 };
|
||||
|
||||
var compare = Subject.Compare(first, second);
|
||||
|
||||
compare.Should().BeGreaterThan(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_greater_when_best_format_equal_but_more_lower_formats()
|
||||
{
|
||||
GivenDefaultProfileWithFormats();
|
||||
|
||||
var first = new List<CustomFormat> { _customFormat1, _customFormat2 };
|
||||
var second = new List<CustomFormat> { _customFormat2 };
|
||||
|
||||
var compare = Subject.Compare(first, second);
|
||||
|
||||
compare.Should().BeGreaterThan(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_be_greater_when_best_format_worse_but_more_lower_formats()
|
||||
{
|
||||
GivenDefaultProfileWithFormats();
|
||||
|
||||
var first = new List<CustomFormat> { _customFormat1, _customFormat2, _customFormat3 };
|
||||
var second = new List<CustomFormat> { _customFormat4 };
|
||||
|
||||
var compare = Subject.Compare(first, second);
|
||||
|
||||
compare.Should().BeLessThan(0);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Common.EnsureThat;
|
||||
using NzbDrone.Core.Profiles;
|
||||
|
||||
namespace NzbDrone.Core.CustomFormats
|
||||
{
|
||||
public class CustomFormatsComparer : IComparer<List<CustomFormat>>
|
||||
{
|
||||
private readonly Profile _profile;
|
||||
|
||||
public CustomFormatsComparer(Profile profile)
|
||||
{
|
||||
Ensure.That(profile, () => profile).IsNotNull();
|
||||
Ensure.That(profile.Items, () => profile.Items).HasItems();
|
||||
|
||||
_profile = profile;
|
||||
}
|
||||
|
||||
public int Compare(List<CustomFormat> left, List<CustomFormat> right)
|
||||
{
|
||||
var leftIndicies = _profile.GetIndices(left);
|
||||
var rightIndicies = _profile.GetIndices(right);
|
||||
|
||||
// Summing powers of two ensures last format always trumps, but we order correctly if we
|
||||
// have extra formats lower down the list
|
||||
var leftTotal = leftIndicies.Select(x => Math.Pow(2, x)).Sum();
|
||||
var rightTotal = rightIndicies.Select(x => Math.Pow(2, x)).Sum();
|
||||
|
||||
return leftTotal.CompareTo(rightTotal);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,156 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using Dapper;
|
||||
using FluentMigrator;
|
||||
using NzbDrone.Core.Datastore.Converters;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(169)]
|
||||
public class custom_format_scores : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Alter.Table("Profiles").AddColumn("MinFormatScore").AsInt32().WithDefaultValue(0);
|
||||
Alter.Table("Profiles").AddColumn("CutoffFormatScore").AsInt32().WithDefaultValue(0);
|
||||
|
||||
Execute.WithConnection(MigrateOrderToScores);
|
||||
|
||||
Delete.Column("FormatCutoff").FromTable("Profiles");
|
||||
}
|
||||
|
||||
private void MigrateOrderToScores(IDbConnection conn, IDbTransaction tran)
|
||||
{
|
||||
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<List<ProfileFormatItem168>>());
|
||||
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<List<ProfileFormatItem169>>());
|
||||
|
||||
var rows = conn.Query<Profile168>("SELECT Id, FormatCutoff, FormatItems from Profiles", transaction: tran);
|
||||
var newRows = new List<Profile169>();
|
||||
|
||||
foreach (var row in rows)
|
||||
{
|
||||
// Things ranked less than None should have a negative score
|
||||
// Things ranked higher than None have a positive score
|
||||
var allowedBelowNone = new List<ProfileFormatItem168>();
|
||||
var allowedAboveNone = new List<ProfileFormatItem168>();
|
||||
var disallowed = new List<ProfileFormatItem168>();
|
||||
|
||||
var noneEnabled = row.FormatItems.Single(x => x.Format == 0).Allowed;
|
||||
|
||||
// If none was disabled, we count everything as above none
|
||||
var foundNone = !noneEnabled;
|
||||
foreach (var item in row.FormatItems)
|
||||
{
|
||||
if (item.Format == 0)
|
||||
{
|
||||
foundNone = true;
|
||||
}
|
||||
else if (!item.Allowed)
|
||||
{
|
||||
disallowed.Add(item);
|
||||
}
|
||||
else if (foundNone)
|
||||
{
|
||||
allowedAboveNone.Add(item);
|
||||
}
|
||||
else
|
||||
{
|
||||
allowedBelowNone.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
// Set up allowed with scores 1, 2, 4, 8 etc so they replicate existing ranking behaviour
|
||||
var allowedPositive = allowedAboveNone.Select((x, index) => new ProfileFormatItem169
|
||||
{
|
||||
Format = x.Format,
|
||||
Score = (int)Math.Pow(2, index)
|
||||
}).ToList();
|
||||
|
||||
// reverse so we have most wanted first
|
||||
allowedBelowNone.Reverse();
|
||||
var allowedNegative = allowedBelowNone.Select((x, index) => new ProfileFormatItem169
|
||||
{
|
||||
Format = x.Format,
|
||||
Score = -1 * (int)Math.Pow(2, index)
|
||||
}).ToList();
|
||||
|
||||
// The minimum format score should be the minimum score achievable by the allowed formats
|
||||
// By construction, if None disabled then allowedNegative is empty and min is 1
|
||||
// If none was enabled, we could have some below None (with negative score) and
|
||||
// we should set min score negative to allow for these
|
||||
// If someone had no allowed formats and none disabled then keep minScore at 0
|
||||
// (This was a broken config that meant nothing would download)
|
||||
var minScore = 0;
|
||||
if (allowedPositive.Any() && !noneEnabled)
|
||||
{
|
||||
minScore = 1;
|
||||
}
|
||||
else if (allowedNegative.Any())
|
||||
{
|
||||
minScore = ((int)Math.Pow(2, allowedNegative.Count) * -1) + 1;
|
||||
}
|
||||
|
||||
// Previously anything matching a disabled format was banned from downloading
|
||||
// To replicate this, set score negative enough that matching a disabled format
|
||||
// must produce a score below the minimum
|
||||
var disallowedScore = (-1 * (int)Math.Pow(2, allowedPositive.Count)) + Math.Max(minScore, 0);
|
||||
var newDisallowed = disallowed.Select(x => new ProfileFormatItem169
|
||||
{
|
||||
Format = x.Format,
|
||||
Score = disallowedScore
|
||||
});
|
||||
|
||||
var newItems = newDisallowed.Concat(allowedNegative).Concat(allowedPositive).OrderBy(x => x.Score).ToList();
|
||||
|
||||
// Set the cutoff score to be the score associated with old cutoff format.
|
||||
// This can never be achieved by any combination of lesser formats given the 2^n scoring scheme
|
||||
// If the cutoff is None (Id == 0) then set cutoff score to zero
|
||||
var cutoffScore = 0;
|
||||
if (row.FormatCutoff != 0)
|
||||
{
|
||||
cutoffScore = newItems.Single(x => x.Format == row.FormatCutoff).Score;
|
||||
}
|
||||
|
||||
newRows.Add(new Profile169
|
||||
{
|
||||
Id = row.Id,
|
||||
MinFormatScore = minScore,
|
||||
CutoffFormatScore = cutoffScore,
|
||||
FormatItems = newItems
|
||||
});
|
||||
}
|
||||
|
||||
var sql = $"UPDATE Profiles SET MinFormatScore = @MinFormatScore, CutoffFormatScore = @CutoffFormatScore, FormatItems = @FormatItems WHERE Id = @Id";
|
||||
|
||||
conn.Execute(sql, newRows, transaction: tran);
|
||||
}
|
||||
|
||||
private class Profile168 : ModelBase
|
||||
{
|
||||
public int FormatCutoff { get; set; }
|
||||
public List<ProfileFormatItem168> FormatItems { get; set; }
|
||||
}
|
||||
|
||||
private class ProfileFormatItem168
|
||||
{
|
||||
public int Format { get; set; }
|
||||
public bool Allowed { get; set; }
|
||||
}
|
||||
|
||||
private class Profile169 : ModelBase
|
||||
{
|
||||
public int MinFormatScore { get; set; }
|
||||
public int CutoffFormatScore { get; set; }
|
||||
public List<ProfileFormatItem169> FormatItems { get; set; }
|
||||
}
|
||||
|
||||
private class ProfileFormatItem169
|
||||
{
|
||||
public int Format { get; set; }
|
||||
public int Score { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in new issue