diff --git a/omega-i18n/en/messages.json b/omega-i18n/en/messages.json index eab46ad..fbada70 100644 --- a/omega-i18n/en/messages.json +++ b/omega-i18n/en/messages.json @@ -55,10 +55,10 @@ "message": "(Never matches)" }, - "rulelistFormat_Switchy": { + "ruleListFormat_Switchy": { "message": "Switchy" }, - "rulelistFormat_AutoProxy": { + "ruleListFormat_AutoProxy": { "message": "AutoProxy" }, @@ -340,12 +340,27 @@ "options_addCondition": { "message": "Add condition" }, + "options_switchAttachedProfileInCondition": { + "message": "Rule list rules" + }, + "options_switchAttachedProfileInConditionDetails": { + "message": "(Any request matching the rule list below)" + }, "options_switchDefaultProfile": { "message": "Default" }, "options_hostLevelsBetween": { "message": "\u2264 host levels \u2264" }, + "options_group_attachProfile": { + "message": "Import online rule lists" + }, + "options_attachProfile": { + "message": "Add a rule list" + }, + "options_attachProfileHelp": { + "message": "You can reuse an online collection of conditions published by others by adding a rule list." + }, "options_modalHeader_applyOptions": { "message": "Apply Options" }, @@ -410,6 +425,24 @@ "options_resetRules_help" : { "message": "Set profile for all rules" }, + "options_modalHeader_deleteAttached": { + "message": "Remove Rule List" + }, + "options_deleteAttachedConfirm": { + "message": "Do you really want to remove the rule list from the current profile?" + }, + "options_ruleListLineCount": { + "message": "$COUNT$ line(s) of rules", + "placeholders": { + "count": { + "content": "$1", + "example": "42" + } + } + }, + "options_deleteAttached": { + "message": "Remove rule list" + }, "options_modalHeader_newProfile" : { "message": "New Profile" }, @@ -438,7 +471,7 @@ "message": "Applying different profiles automatically on various conditions such as domains or patterns. (Replaces AutoSwitch mode.)" }, "options_profileTypeRuleListProfile" : { - "message": "Rulelist Profile" + "message": "Rule List Profile" }, "options_profileDescRuleListProfile" : { "message": "Reusing an online collection of conditions published by others." @@ -550,6 +583,18 @@ "browserAction_titleExternalProxy": { "message": "Note: The proxy settings are currently controlled by other app(s)." }, + "browserAction_defaultRuleDetails": { + "message": "(default)", + "description": "Representation of the default profile being selected on browserAction title." + }, + "browserAction_directResult": { + "message": "DIRECT", + "description": "Representation of direct connection being used on browserAction title." + }, + "browserAction_attachedPrefix": { + "message": "(RL) ", + "description": "The prefix to indicate a rule list rule on browserAction title. Should be very short." + }, "browserAction_tempRulePrefix": { "message": "(TEMP) ", "description": "The prefix to indicate a temp rule on browserAction title. Should be very short." diff --git a/omega-i18n/zh/messages.json b/omega-i18n/zh/messages.json index 040edc8..698eb83 100644 --- a/omega-i18n/zh/messages.json +++ b/omega-i18n/zh/messages.json @@ -55,10 +55,10 @@ "message": "(不匹配任何请求)" }, - "rulelistFormat_Switchy": { + "ruleListFormat_Switchy": { "message": "Switchy" }, - "rulelistFormat_AutoProxy": { + "ruleListFormat_AutoProxy": { "message": "AutoProxy" }, @@ -340,12 +340,27 @@ "options_addCondition": { "message": "添加条件" }, + "options_switchAttachedProfileInCondition": { + "message": "规则列表规则" + }, + "options_switchAttachedProfileInConditionDetails": { + "message": "(按照规则列表匹配请求)" + }, "options_switchDefaultProfile": { "message": "默认情景模式" }, "options_hostLevelsBetween": { "message": "\u2264 主机层数 \u2264" }, + "options_group_attachProfile": { + "message": "导入在线规则列表" + }, + "options_attachProfile": { + "message": "添加规则列表" + }, + "options_attachProfileHelp": { + "message": "可以添加规则列表,以便引用他人在线发布的一组规则。" + }, "options_modalHeader_applyOptions": { "message": "应用选项" }, @@ -410,6 +425,24 @@ "options_resetRules_help" : { "message": "批量设置所有规则的情景模式" }, + "options_modalHeader_deleteAttached": { + "message": "移除规则列表" + }, + "options_deleteAttachedConfirm": { + "message": "真的要移除当前情景模式的在线规则列表吗?" + }, + "options_ruleListLineCount": { + "message": "共计$COUNT$行规则", + "placeholders": { + "count": { + "content": "$1", + "example": "42" + } + } + }, + "options_deleteAttached": { + "message": "Remove rule list" + }, "options_modalHeader_newProfile" : { "message": "新建情景模式" }, @@ -550,6 +583,18 @@ "browserAction_titleExternalProxy": { "message": "注意:其他应用正在控制当前代理设置。" }, + "browserAction_defaultRuleDetails": { + "message": "(默认)", + "description": "在图标悬停提示上表示选择了默认情景模式作为结果的文字。" + }, + "browserAction_directResult": { + "message": "直接连接", + "description": "在图标悬停提示上表示使用了直接连接的文字。" + }, + "browserAction_attachedPrefix": { + "message": "(列表) ", + "description": "在图标悬停提示上显示规则列表规则的前缀。文字应该非常短。" + }, "browserAction_tempRulePrefix": { "message": "(临时) ", "description": "在图标悬停提示上显示临时规则的前缀。文字应该非常短。" diff --git a/omega-pac/index.coffee b/omega-pac/index.coffee index 6f58934..60fa4d4 100644 --- a/omega-pac/index.coffee +++ b/omega-pac/index.coffee @@ -2,7 +2,7 @@ module.exports = Conditions: require('./src/conditions') PacGenerator: require('./src/pac_generator') Profiles: require('./src/profiles') - Rulelist: require('./src/rule_list') + RuleList: require('./src/rule_list') ShexpUtils: require('./src/shexp_utils') for name, value of require('./src/utils.coffee') diff --git a/omega-target-chromium-extension/background.coffee b/omega-target-chromium-extension/background.coffee index c06b27d..fcc89f1 100644 --- a/omega-target-chromium-extension/background.coffee +++ b/omega-target-chromium-extension/background.coffee @@ -39,6 +39,12 @@ drawIcon = (resultColor, profileColor) -> icon = ctx.getImageData(0, 0, 19, 19) return iconCache[cacheKey] = icon +charCodeUnderscore = '_'.charCodeAt(0) +isHidden = (name) -> (name.charCodeAt(0) == charCodeUnderscore and + name.charCodeAt(1) == charCodeUnderscore) + +dispName = (name) -> chrome.i18n.getMessage('profile_' + name) || name + actionForUrl = (url) -> options.ready.then(-> request = OmegaPac.Conditions.requestFromUrl(url) @@ -47,25 +53,45 @@ actionForUrl = (url) -> current = options.currentProfile() details = '' direct = false + attached = false for result in results if Array.isArray(result) if not result[1]? - details += "(default) => #{result[0]}\n" + attached = false + name = result[0] + if name[0] == '+' + name = name.substr(1) + if isHidden(name) + attached = true + else + details += chrome.i18n.getMessage 'browserAction_defaultRuleDetails' + details += " => #{dispName(name)}\n" else if result[1].length == 0 - details += "#{result[0]}\n" + if result[0] == 'DIRECT' + details += chrome.i18n.getMessage('browserAction_directResult') + details += '\n' + else + details += "#{result[0]}\n" else if typeof result[1] == 'string' details += "#{result[1]} => #{result[0]}\n" else condition = (result[1].condition ? result[1]).pattern ? '' + details += "#{condition} => " if result[0] == 'DIRECT' + details += chrome.i18n.getMessage('browserAction_directResult') + details += '\n' direct = true - details += "#{condition} => #{result[0]}\n" + else + details += "#{result[0]}\n" else if result.profileName if result.isTempRule details += chrome.i18n.getMessage('browserAction_tempRulePrefix') + else if attached + details += chrome.i18n.getMessage('browserAction_attachedPrefix') + attached = false condition = (result.source ? result.condition.pattern ? result.condition.conditionType) - details += "#{condition} => #{result.profileName}\n" + details += "#{condition} => #{dispName(result.profileName)}\n" icon = if profile.name == current.name and options.isCurrentProfileStatic() @@ -77,8 +103,8 @@ actionForUrl = (url) -> drawIcon(profile.color, current.color) return { title: chrome.i18n.getMessage('browserAction_titleWithResult', [ - current.name - profile.name + dispName(current.name) + dispName(profile.name) details ]) icon: icon diff --git a/omega-web/src/coffee/popup.coffee b/omega-web/src/coffee/popup.coffee index 76351e8..dfcddd4 100644 --- a/omega-web/src/coffee/popup.coffee +++ b/omega-web/src/coffee/popup.coffee @@ -68,10 +68,11 @@ module.controller 'PopupCtrl', ($scope, $window, $q, omegaTarget, $scope.builtinProfiles = [] $scope.customProfiles = [] $scope.availableProfiles = availableProfiles + charCodeUnderscore = '_'.charCodeAt(0) for own key, profile of availableProfiles if profile.builtin $scope.builtinProfiles.push(profile) - else + else if profile.name.charCodeAt(0) != charCodeUnderscore $scope.customProfiles.push(profile) $scope.customProfiles.sort(profileOrder) $scope.currentProfile = availableProfiles['+' + currentProfileName] @@ -79,8 +80,12 @@ module.controller 'PopupCtrl', ($scope, $window, $q, omegaTarget, $scope.isSystemProfile = isSystemProfile $scope.externalProfile = externalProfile refreshOnProfileChange = refreshOnProfileChange - $scope.validResultProfiles = validResultProfiles.map (name) -> - availableProfiles['+' + name] + $scope.validResultProfiles = [] + for name in validResultProfiles + shown = (name.charCodeAt(0) != charCodeUnderscore or + name.charCodeAt(1) != charCodeUnderscore) + if shown + $scope.validResultProfiles.push(availableProfiles['+' + name]) omegaTarget.getActivePageInfo().then((info) -> if info diff --git a/omega-web/src/less/options.less b/omega-web/src/less/options.less index 41be77a..af57085 100644 --- a/omega-web/src/less/options.less +++ b/omega-web/src/less/options.less @@ -271,6 +271,16 @@ main { } } +.switch-attached { + > tr > td { + background-color: #F9F9F9; + + &:first-child { + text-align: center; + } + } +} + .fixed-show-advanced { td { background-color: transparent !important; diff --git a/omega-web/src/omega/controllers/switch_profile.coffee b/omega-web/src/omega/controllers/switch_profile.coffee index 5157b1d..d334347 100644 --- a/omega-web/src/omega/controllers/switch_profile.coffee +++ b/omega-web/src/omega/controllers/switch_profile.coffee @@ -1,4 +1,6 @@ -angular.module('omega').controller 'SwitchProfileCtrl', ($scope, $modal) -> +angular.module('omega').controller 'SwitchProfileCtrl', ($scope, $modal, + profileIcons) -> + $scope.conditionI18n = 'HostWildcardCondition': 'condition_hostWildcard' 'HostRegexCondition': 'condition_hostRegex' @@ -63,3 +65,52 @@ angular.module('omega').controller 'SwitchProfileCtrl', ($scope, $modal) -> forceHelperSize: true forcePlaceholderSize: true containment: 'parent' + + $scope.ruleListFormats = OmegaPac.Profiles.ruleListFormats + + $scope.$watch 'profile.name', (name) -> + $scope.attachedName = '__ruleListOf_' + name + $scope.attachedKey = OmegaPac.Profiles.nameAsKey('__ruleListOf_' + name) + + $scope.$watch 'options[attachedKey]', (attached) -> + $scope.attached = attached + + onAttachedChange = (profile, oldProfile) -> + return profile if profile == oldProfile or not profile or not oldProfile + OmegaPac.Profiles.updateRevision(profile) + return profile + $scope.omegaWatchAndChange 'options[attachedKey]', onAttachedChange, true + + $scope.$watch 'profile.defaultProfileName', (name) -> + if not $scope.attached + $scope.defaultProfileName = name + + $scope.$watch 'attached.defaultProfileName', (name) -> + if name + $scope.defaultProfileName = name + + $scope.$watch 'defaultProfileName', (name) -> + ($scope.attached || $scope.profile).defaultProfileName = name + + $scope.attachNew = -> + $scope.attached = OmegaPac.Profiles.create( + name: $scope.attachedName + defaultProfileName: $scope.profile.defaultProfileName + profileType: 'RuleListProfile' + color: $scope.profile.color + ) + OmegaPac.Profiles.updateRevision($scope.attached) + $scope.options[$scope.attachedKey] = $scope.attached + $scope.profile.defaultProfileName = $scope.attachedName + + $scope.removeAttached = -> + return unless $scope.attached + scope = $scope.$new('isolate') + scope.attached = $scope.attached + scope.profileIcons = profileIcons + $modal.open( + templateUrl: 'partials/delete_attached.html' + scope: scope + ).result.then -> + $scope.profile.defaultProfileName = $scope.attached.defaultProfileName + delete $scope.options[$scope.attachedKey] diff --git a/omega-web/src/partials/delete_attached.jade b/omega-web/src/partials/delete_attached.jade new file mode 100644 index 0000000..b89d1eb --- /dev/null +++ b/omega-web/src/partials/delete_attached.jade @@ -0,0 +1,14 @@ +.modal-header + button.close(type='button' ng-click='$dismiss()') + span(aria-hidden='true') × + span.sr-only {{'dialog_close' | tr}} + h4.modal-title {{'options_modalHeader_deleteAttached' | tr}} +.modal-body + p {{'options_deleteAttachedConfirm' | tr}} + .well + span.glyphicon(class='{{profileIcons[attached.profileType]}}') + = ' ' + | {{attached.sourceUrl || ('options_ruleListLineCount' | tr:[attached.ruleList.split('\r?\n').length])}} +.modal-footer + button.btn.btn-default(ng-click='$dismiss()') {{'dialog_cancel' | tr}} + button.btn.btn-danger(type='button' ng-click='$close("ok")') {{'options_deleteAttached' | tr}} diff --git a/omega-web/src/partials/profile_rule_list.jade b/omega-web/src/partials/profile_rule_list.jade index fde1c2b..24adcb0 100644 --- a/omega-web/src/partials/profile_rule_list.jade +++ b/omega-web/src/partials/profile_rule_list.jade @@ -16,7 +16,7 @@ div(ng-controller='RuleListProfileCtrl') .radio.inline-form-control.no-min-width(ng-repeat='format in ruleListFormats') label input(type='radio' name='formatInput' value='{{format}}' ng-model='profile.format') - | {{'rulelistFormat_' + format | tr}} + | {{'ruleListFormat_' + format | tr}} section.settings-group h3 {{'options_group_ruleListUrl' | tr}} .width-limit(input-group-clear type='url' model='profile.sourceUrl' ng-if='profile') diff --git a/omega-web/src/partials/profile_switch.jade b/omega-web/src/partials/profile_switch.jade index 0958a56..9913626 100644 --- a/omega-web/src/partials/profile_switch.jade +++ b/omega-web/src/partials/profile_switch.jade @@ -1,53 +1,96 @@ -section.settings-group(ng-controller='SwitchProfileCtrl') - h3 {{'options_group_switchRules' | tr}} - .table-responsive - table.switch-rules.table.table-bordered.table-condensed.width-limit-xl - thead - tr - th(style='white-space: nowrap') {{'options_sort' | tr}} - th {{'options_conditionType' | tr}} - th {{'options_conditionDetails' | tr}} - th {{'options_resultProfile' | tr}} - th {{'options_conditionActions' | tr}} - tbody(ui-sortable='sortableOptions' ng-model='profile.rules') - tr(ng-repeat='rule in profile.rules') - td.sort-bar - span.glyphicon.glyphicon-sort - td - select.form-control(ng-model='rule.condition.conditionType' - ng-options='cond as (i18n | tr) for (cond, i18n) in conditionI18n') - td(ng-switch='rule.condition.conditionType') - span(ng-switch-when='AlwaysCondition') {{'condition_always_details' | tr}} - span(ng-switch-when='NeverCondition') {{'condition_never_details' | tr}} - span.host-levels-details(ng-switch-when='HostLevelsCondition') - input.form-control(type='number' min='1' max='99' ng-model='rule.condition.minValue' required) - = ' ' - span {{'options_hostLevelsBetween' | tr}} - = ' ' - input.form-control(type='number' max='99' min='1' ng-model='rule.condition.maxValue' required) - input.form-control(ng-model='rule.condition.pattern' ng-switch-default required - ui-validate='{pattern: "validateCondition(rule.condition, $value)"}') - td - div.form-control(omega-profile-select='options | profiles:profile' ng-model='rule.profileName' - disp-name='$profile.name | dispName') - td - button.btn.btn-danger.btn-sm(title="{{'options_deleteRule' | tr}}" ng-click='removeRule($index)') - span.glyphicon.glyphicon-trash - tbody - tr - td(style='border-right: none;') - td(style='border-left: none;', colspan='4') - button.btn.btn-default.btn-sm(ng-click='addRule()') - span.glyphicon.glyphicon-plus - = ' ' - span {{'options_addCondition' | tr}} - tbody - tr - td - td(colspan='2') {{'options_switchDefaultProfile' | tr}} - td - div.form-control(omega-profile-select='options | profiles:profile' ng-model='profile.defaultProfileName' - disp-name='$profile.name | dispName') - td - button.btn.btn-info.btn-sm(title="{{'options_resetRules_help' | tr}}" ng-click='resetRules()') - span.glyphicon.glyphicon-chevron-up +div(ng-controller='SwitchProfileCtrl') + section.settings-group + h3 {{'options_group_switchRules' | tr}} + .table-responsive + table.switch-rules.table.table-bordered.table-condensed.width-limit-xl + thead + tr + th(style='white-space: nowrap') {{'options_sort' | tr}} + th {{'options_conditionType' | tr}} + th {{'options_conditionDetails' | tr}} + th {{'options_resultProfile' | tr}} + th {{'options_conditionActions' | tr}} + tbody(ui-sortable='sortableOptions' ng-model='profile.rules') + tr(ng-repeat='rule in profile.rules') + td.sort-bar + span.glyphicon.glyphicon-sort + td + select.form-control(ng-model='rule.condition.conditionType' + ng-options='cond as (i18n | tr) for (cond, i18n) in conditionI18n') + td(ng-switch='rule.condition.conditionType') + span(ng-switch-when='AlwaysCondition') {{'condition_always_details' | tr}} + span(ng-switch-when='NeverCondition') {{'condition_never_details' | tr}} + span.host-levels-details(ng-switch-when='HostLevelsCondition') + input.form-control(type='number' min='1' max='99' ng-model='rule.condition.minValue' required) + = ' ' + span {{'options_hostLevelsBetween' | tr}} + = ' ' + input.form-control(type='number' max='99' min='1' ng-model='rule.condition.maxValue' required) + input.form-control(ng-model='rule.condition.pattern' ng-switch-default required + ui-validate='{pattern: "validateCondition(rule.condition, $value)"}') + td + div.form-control(omega-profile-select='options | profiles:profile' ng-model='rule.profileName' + disp-name='$profile.name | dispName') + td + button.btn.btn-danger.btn-sm(title="{{'options_deleteRule' | tr}}" ng-click='removeRule($index)') + span.glyphicon.glyphicon-trash + tbody + tr + td(style='border-right: none;') + td(style='border-left: none;', colspan='4') + button.btn.btn-default.btn-sm(ng-click='addRule()') + span.glyphicon.glyphicon-plus + = ' ' + span {{'options_addCondition' | tr}} + tbody.switch-attached(ng-if='attached') + tr + td(style='border-right: none;') + span.glyphicon(class='{{profileIcons["RuleListProfile"]}}') + td(style='border-left: none;') {{'options_switchAttachedProfileInCondition' | tr}} + td + span {{'options_switchAttachedProfileInConditionDetails' | tr}} + td + div.form-control(omega-profile-select='options | profiles:profile' ng-model='attached.matchProfileName' + disp-name='$profile.name | dispName') + td + button.btn.btn-danger.btn-sm(title="{{'options_deleteAttached' | tr}}" ng-click='removeAttached()') + span.glyphicon.glyphicon-trash + tbody + tr + td + td(colspan='2') {{'options_switchDefaultProfile' | tr}} + td + div.form-control(omega-profile-select='options | profiles:profile' ng-model='defaultProfileName' + disp-name='$profile.name | dispName') + td + button.btn.btn-info.btn-sm(title="{{'options_resetRules_help' | tr}}" ng-click='resetRules()') + span.glyphicon.glyphicon-chevron-up + section.settings-group(ng-if='!attached') + h3 {{'options_group_attachProfile' | tr}} + p.help-block {{'options_attachProfileHelp' | tr}} + button.btn.btn-default(ng-click='attachNew()') + span.glyphicon.glyphicon-plus + = ' ' + | {{'options_attachProfile' | tr}} + section.settings-group(ng-if='attached') + h3 {{'options_group_ruleListConfig' | tr}} + form + .form-group + label {{'options_ruleListFormat' | tr}} + .radio.inline-form-control.no-min-width(ng-repeat='format in ruleListFormats') + label + input(type='radio' name='formatInput' value='{{format}}' ng-model='attached.format') + | {{'ruleListFormat_' + format | tr}} + .form-group + label {{'options_group_ruleListUrl' | tr}} + .width-limit.inline-form-control(input-group-clear type='url' model='attached.sourceUrl' + style='vertical-align: middle') + p.help-block {{'options_ruleListUrlHelp' | tr}} + section.settings-group(ng-if='attached') + h3 {{'options_group_ruleListText' | tr}} + p + button.btn.btn-default(ng-disabled='!attached.sourceUrl' ng-click='updateProfile(attached.name)' + ladda='updatingProfile[attached.name]' data-spinner-color="#000000") + | #[span.glyphicon.glyphicon-download-alt] {{'options_downloadProfileNow' | tr}} + textarea.form-control.width-limit(ng-model='attached.ruleList' rows=20 + ng-disabled='!!attached.sourceUrl')