浏览代码

Allow attaching a RuleListProfile to a SwitchProfile.

FelisCatus 11 年之前
父节点
当前提交
cdd3bda2cd

+ 48 - 3
omega-i18n/en/messages.json

@@ -55,10 +55,10 @@
     "message": "(Never matches)"
     "message": "(Never matches)"
   },
   },
 
 
-  "rulelistFormat_Switchy": {
+  "ruleListFormat_Switchy": {
     "message": "Switchy"
     "message": "Switchy"
   },
   },
-  "rulelistFormat_AutoProxy": {
+  "ruleListFormat_AutoProxy": {
     "message": "AutoProxy"
     "message": "AutoProxy"
   },
   },
 
 
@@ -340,12 +340,27 @@
   "options_addCondition": {
   "options_addCondition": {
     "message": "Add condition"
     "message": "Add condition"
   },
   },
+  "options_switchAttachedProfileInCondition": {
+    "message": "Rule list rules"
+  },
+  "options_switchAttachedProfileInConditionDetails": {
+    "message": "(Any request matching the rule list below)"
+  },
   "options_switchDefaultProfile": {
   "options_switchDefaultProfile": {
     "message": "Default"
     "message": "Default"
   },
   },
   "options_hostLevelsBetween": {
   "options_hostLevelsBetween": {
     "message": "\u2264 host levels \u2264"
     "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": {
   "options_modalHeader_applyOptions": {
     "message": "Apply Options"
     "message": "Apply Options"
   },
   },
@@ -410,6 +425,24 @@
   "options_resetRules_help" : {
   "options_resetRules_help" : {
     "message": "Set profile for all rules"
     "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" : {
   "options_modalHeader_newProfile" : {
     "message": "New Profile"
     "message": "New Profile"
   },
   },
@@ -438,7 +471,7 @@
     "message": "Applying different profiles automatically on various conditions such as domains or patterns. (Replaces AutoSwitch mode.)"
     "message": "Applying different profiles automatically on various conditions such as domains or patterns. (Replaces AutoSwitch mode.)"
   },
   },
   "options_profileTypeRuleListProfile" : {
   "options_profileTypeRuleListProfile" : {
-    "message": "Rulelist Profile"
+    "message": "Rule List Profile"
   },
   },
   "options_profileDescRuleListProfile" : {
   "options_profileDescRuleListProfile" : {
     "message": "Reusing an online collection of conditions published by others."
     "message": "Reusing an online collection of conditions published by others."
@@ -550,6 +583,18 @@
   "browserAction_titleExternalProxy": {
   "browserAction_titleExternalProxy": {
     "message": "Note: The proxy settings are currently controlled by other app(s)."
     "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": {
   "browserAction_tempRulePrefix": {
     "message": "(TEMP) ",
     "message": "(TEMP) ",
     "description": "The prefix to indicate a temp rule on browserAction title. Should be very short."
     "description": "The prefix to indicate a temp rule on browserAction title. Should be very short."

+ 47 - 2
omega-i18n/zh/messages.json

@@ -55,10 +55,10 @@
     "message": "(不匹配任何请求)"
     "message": "(不匹配任何请求)"
   },
   },
 
 
-  "rulelistFormat_Switchy": {
+  "ruleListFormat_Switchy": {
     "message": "Switchy"
     "message": "Switchy"
   },
   },
-  "rulelistFormat_AutoProxy": {
+  "ruleListFormat_AutoProxy": {
     "message": "AutoProxy"
     "message": "AutoProxy"
   },
   },
 
 
@@ -340,12 +340,27 @@
   "options_addCondition": {
   "options_addCondition": {
     "message": "添加条件"
     "message": "添加条件"
   },
   },
+  "options_switchAttachedProfileInCondition": {
+    "message": "规则列表规则"
+  },
+  "options_switchAttachedProfileInConditionDetails": {
+    "message": "(按照规则列表匹配请求)"
+  },
   "options_switchDefaultProfile": {
   "options_switchDefaultProfile": {
     "message": "默认情景模式"
     "message": "默认情景模式"
   },
   },
   "options_hostLevelsBetween": {
   "options_hostLevelsBetween": {
     "message": "\u2264 主机层数 \u2264"
     "message": "\u2264 主机层数 \u2264"
   },
   },
+  "options_group_attachProfile": {
+    "message": "导入在线规则列表"
+  },
+  "options_attachProfile": {
+    "message": "添加规则列表"
+  },
+  "options_attachProfileHelp": {
+    "message": "可以添加规则列表,以便引用他人在线发布的一组规则。"
+  },
   "options_modalHeader_applyOptions": {
   "options_modalHeader_applyOptions": {
     "message": "应用选项"
     "message": "应用选项"
   },
   },
@@ -410,6 +425,24 @@
   "options_resetRules_help" : {
   "options_resetRules_help" : {
     "message": "批量设置所有规则的情景模式"
     "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" : {
   "options_modalHeader_newProfile" : {
     "message": "新建情景模式"
     "message": "新建情景模式"
   },
   },
@@ -550,6 +583,18 @@
   "browserAction_titleExternalProxy": {
   "browserAction_titleExternalProxy": {
     "message": "注意:其他应用正在控制当前代理设置。"
     "message": "注意:其他应用正在控制当前代理设置。"
   },
   },
+  "browserAction_defaultRuleDetails": {
+    "message": "(默认)",
+    "description": "在图标悬停提示上表示选择了默认情景模式作为结果的文字。"
+  },
+  "browserAction_directResult": {
+    "message": "直接连接",
+    "description": "在图标悬停提示上表示使用了直接连接的文字。"
+  },
+  "browserAction_attachedPrefix": {
+    "message": "(列表) ",
+    "description": "在图标悬停提示上显示规则列表规则的前缀。文字应该非常短。"
+  },
   "browserAction_tempRulePrefix": {
   "browserAction_tempRulePrefix": {
     "message": "(临时) ",
     "message": "(临时) ",
     "description": "在图标悬停提示上显示临时规则的前缀。文字应该非常短。"
     "description": "在图标悬停提示上显示临时规则的前缀。文字应该非常短。"

+ 1 - 1
omega-pac/index.coffee

@@ -2,7 +2,7 @@ module.exports =
   Conditions: require('./src/conditions')
   Conditions: require('./src/conditions')
   PacGenerator: require('./src/pac_generator')
   PacGenerator: require('./src/pac_generator')
   Profiles: require('./src/profiles')
   Profiles: require('./src/profiles')
-  Rulelist: require('./src/rule_list')
+  RuleList: require('./src/rule_list')
   ShexpUtils: require('./src/shexp_utils')
   ShexpUtils: require('./src/shexp_utils')
 
 
 for name, value of require('./src/utils.coffee')
 for name, value of require('./src/utils.coffee')

+ 32 - 6
omega-target-chromium-extension/background.coffee

@@ -39,6 +39,12 @@ drawIcon = (resultColor, profileColor) ->
   icon = ctx.getImageData(0, 0, 19, 19)
   icon = ctx.getImageData(0, 0, 19, 19)
   return iconCache[cacheKey] = icon
   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) ->
 actionForUrl = (url) ->
   options.ready.then(->
   options.ready.then(->
     request = OmegaPac.Conditions.requestFromUrl(url)
     request = OmegaPac.Conditions.requestFromUrl(url)
@@ -47,25 +53,45 @@ actionForUrl = (url) ->
     current = options.currentProfile()
     current = options.currentProfile()
     details = ''
     details = ''
     direct = false
     direct = false
+    attached = false
     for result in results
     for result in results
       if Array.isArray(result)
       if Array.isArray(result)
         if not result[1]?
         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
         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'
         else if typeof result[1] == 'string'
           details += "#{result[1]} => #{result[0]}\n"
           details += "#{result[1]} => #{result[0]}\n"
         else
         else
           condition = (result[1].condition ? result[1]).pattern ? ''
           condition = (result[1].condition ? result[1]).pattern ? ''
+          details += "#{condition} => "
           if result[0] == 'DIRECT'
           if result[0] == 'DIRECT'
+            details += chrome.i18n.getMessage('browserAction_directResult')
+            details += '\n'
             direct = true
             direct = true
-          details += "#{condition} => #{result[0]}\n"
+          else
+            details += "#{result[0]}\n"
       else if result.profileName
       else if result.profileName
         if result.isTempRule
         if result.isTempRule
           details += chrome.i18n.getMessage('browserAction_tempRulePrefix')
           details += chrome.i18n.getMessage('browserAction_tempRulePrefix')
+        else if attached
+          details += chrome.i18n.getMessage('browserAction_attachedPrefix')
+          attached = false
         condition = (result.source ? result.condition.pattern ?
         condition = (result.source ? result.condition.pattern ?
           result.condition.conditionType)
           result.condition.conditionType)
-        details += "#{condition} => #{result.profileName}\n"
+        details += "#{condition} => #{dispName(result.profileName)}\n"
 
 
     icon =
     icon =
       if profile.name == current.name and options.isCurrentProfileStatic()
       if profile.name == current.name and options.isCurrentProfileStatic()
@@ -77,8 +103,8 @@ actionForUrl = (url) ->
         drawIcon(profile.color, current.color)
         drawIcon(profile.color, current.color)
     return {
     return {
       title: chrome.i18n.getMessage('browserAction_titleWithResult', [
       title: chrome.i18n.getMessage('browserAction_titleWithResult', [
-        current.name
-        profile.name
+        dispName(current.name)
+        dispName(profile.name)
         details
         details
       ])
       ])
       icon: icon
       icon: icon

+ 8 - 3
omega-web/src/coffee/popup.coffee

@@ -68,10 +68,11 @@ module.controller 'PopupCtrl', ($scope, $window, $q, omegaTarget,
     $scope.builtinProfiles = []
     $scope.builtinProfiles = []
     $scope.customProfiles = []
     $scope.customProfiles = []
     $scope.availableProfiles = availableProfiles
     $scope.availableProfiles = availableProfiles
+    charCodeUnderscore = '_'.charCodeAt(0)
     for own key, profile of availableProfiles
     for own key, profile of availableProfiles
       if profile.builtin
       if profile.builtin
         $scope.builtinProfiles.push(profile)
         $scope.builtinProfiles.push(profile)
-      else
+      else if profile.name.charCodeAt(0) != charCodeUnderscore
         $scope.customProfiles.push(profile)
         $scope.customProfiles.push(profile)
     $scope.customProfiles.sort(profileOrder)
     $scope.customProfiles.sort(profileOrder)
     $scope.currentProfile = availableProfiles['+' + currentProfileName]
     $scope.currentProfile = availableProfiles['+' + currentProfileName]
@@ -79,8 +80,12 @@ module.controller 'PopupCtrl', ($scope, $window, $q, omegaTarget,
     $scope.isSystemProfile = isSystemProfile
     $scope.isSystemProfile = isSystemProfile
     $scope.externalProfile = externalProfile
     $scope.externalProfile = externalProfile
     refreshOnProfileChange = refreshOnProfileChange
     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) ->
   omegaTarget.getActivePageInfo().then((info) ->
     if info
     if info

+ 10 - 0
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 {
 .fixed-show-advanced {
   td {
   td {
     background-color: transparent !important;
     background-color: transparent !important;

+ 52 - 1
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 =
   $scope.conditionI18n =
     'HostWildcardCondition': 'condition_hostWildcard'
     'HostWildcardCondition': 'condition_hostWildcard'
     'HostRegexCondition': 'condition_hostRegex'
     'HostRegexCondition': 'condition_hostRegex'
@@ -63,3 +65,52 @@ angular.module('omega').controller 'SwitchProfileCtrl', ($scope, $modal) ->
     forceHelperSize: true
     forceHelperSize: true
     forcePlaceholderSize: true
     forcePlaceholderSize: true
     containment: 'parent'
     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]

+ 14 - 0
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}}

+ 1 - 1
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')
       .radio.inline-form-control.no-min-width(ng-repeat='format in ruleListFormats')
         label
         label
           input(type='radio' name='formatInput' value='{{format}}' ng-model='profile.format')
           input(type='radio' name='formatInput' value='{{format}}' ng-model='profile.format')
-          | {{'rulelistFormat_' + format | tr}}
+          | {{'ruleListFormat_' + format | tr}}
   section.settings-group
   section.settings-group
     h3 {{'options_group_ruleListUrl' | tr}}
     h3 {{'options_group_ruleListUrl' | tr}}
     .width-limit(input-group-clear type='url' model='profile.sourceUrl' ng-if='profile')
     .width-limit(input-group-clear type='url' model='profile.sourceUrl' ng-if='profile')

+ 96 - 53
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')