@@ -428,6 +428,9 @@ | |||
<genjar jarfile="manager.jar"> | |||
<resource file="${basedir}/src/com/gitblit/client/splash.png" /> | |||
<resource file="${basedir}/resources/gitblt-favicon.png" /> | |||
<resource file="${basedir}/resources/gitweb-favicon.png" /> | |||
<resource file="${basedir}/resources/user_16x16.png" /> | |||
<resource file="${basedir}/resources/settings_16x16.png" /> | |||
<resource file="${basedir}/resources/lock_go_16x16.png" /> | |||
<resource file="${basedir}/resources/lock_pull_16x16.png" /> | |||
<resource file="${basedir}/resources/shield_16x16.png" /> |
@@ -20,12 +20,18 @@ import java.awt.Dimension; | |||
import java.awt.EventQueue; | |||
import java.awt.Font; | |||
import java.awt.GridLayout; | |||
import java.awt.Point; | |||
import java.awt.event.ActionEvent; | |||
import java.awt.event.ActionListener; | |||
import java.awt.event.KeyEvent; | |||
import java.awt.event.WindowAdapter; | |||
import java.awt.event.WindowEvent; | |||
import java.io.File; | |||
import java.io.IOException; | |||
import java.util.ArrayList; | |||
import java.util.List; | |||
import java.text.MessageFormat; | |||
import java.util.LinkedHashMap; | |||
import java.util.Map; | |||
import java.util.Set; | |||
import javax.swing.ImageIcon; | |||
import javax.swing.JFrame; | |||
@@ -41,11 +47,17 @@ import javax.swing.JTextField; | |||
import javax.swing.KeyStroke; | |||
import javax.swing.UIManager; | |||
import org.eclipse.jgit.errors.ConfigInvalidException; | |||
import org.eclipse.jgit.lib.StoredConfig; | |||
import org.eclipse.jgit.storage.file.FileBasedConfig; | |||
import org.eclipse.jgit.util.Base64; | |||
import org.eclipse.jgit.util.FS; | |||
import com.gitblit.Constants; | |||
import com.gitblit.utils.StringUtils; | |||
/** | |||
* Sample RPC application. | |||
* Gitblit Manager issues JSON RPC requests to a Gitblit server. | |||
* | |||
* @author James Moger | |||
* | |||
@@ -54,10 +66,11 @@ public class GitblitManager extends JFrame { | |||
private static final long serialVersionUID = 1L; | |||
private JTabbedPane serverTabs; | |||
private File configFile = new File(System.getProperty("user.home"), ".gitblit/config"); | |||
private GitblitRegistration localhost = new GitblitRegistration("default", | |||
"https://localhost:8443", "admin", "admin".toCharArray()); | |||
private List<GitblitRegistration> registrations = new ArrayList<GitblitRegistration>(); | |||
private Map<String, GitblitRegistration> registrations = new LinkedHashMap<String, GitblitRegistration>(); | |||
private JMenu recentMenu; | |||
private GitblitManager() { | |||
@@ -67,10 +80,66 @@ public class GitblitManager extends JFrame { | |||
private void initialize() { | |||
setContentPane(getCenterPanel()); | |||
setIconImage(new ImageIcon(getClass().getResource("/gitblt-favicon.png")).getImage()); | |||
setTitle("Gitblit Manager v" + Constants.VERSION + " (" + Constants.VERSION_DATE + ")"); | |||
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); | |||
setSize(800, 500); | |||
addWindowListener(new WindowAdapter() { | |||
@Override | |||
public void windowClosing(WindowEvent event) { | |||
saveSizeAndPosition(); | |||
} | |||
}); | |||
setSizeAndPosition(); | |||
loadRegistrations(); | |||
rebuildRecentMenu(); | |||
} | |||
private void setSizeAndPosition() { | |||
String sz = null; | |||
String pos = null; | |||
try { | |||
StoredConfig config = getConfig(); | |||
sz = config.getString("ui", null, "size"); | |||
pos = config.getString("ui", null, "position"); | |||
} catch (Throwable t) { | |||
t.printStackTrace(); | |||
} | |||
// try to restore saved window size | |||
if (StringUtils.isEmpty(sz)) { | |||
setSize(850, 500); | |||
} else { | |||
String[] chunks = sz.split("x"); | |||
int width = Integer.parseInt(chunks[0]); | |||
int height = Integer.parseInt(chunks[1]); | |||
setSize(width, height); | |||
} | |||
// try to restore saved window position | |||
if (StringUtils.isEmpty(pos)) { | |||
setLocationRelativeTo(null); | |||
} else { | |||
String[] chunks = pos.split(","); | |||
int x = Integer.parseInt(chunks[0]); | |||
int y = Integer.parseInt(chunks[1]); | |||
setLocation(x, y); | |||
} | |||
} | |||
private void saveSizeAndPosition() { | |||
try { | |||
// save window size and position | |||
StoredConfig config = getConfig(); | |||
Dimension sz = GitblitManager.this.getSize(); | |||
config.setString("ui", null, "size", | |||
MessageFormat.format("{0,number,0}x{1,number,0}", sz.width, sz.height)); | |||
Point pos = GitblitManager.this.getLocationOnScreen(); | |||
config.setString("ui", null, "position", | |||
MessageFormat.format("{0,number,0},{1,number,0}", pos.x, pos.y)); | |||
config.save(); | |||
} catch (Throwable t) { | |||
Utils.showException(GitblitManager.this, t); | |||
} | |||
} | |||
public void setVisible(boolean value) { | |||
@@ -80,10 +149,10 @@ public class GitblitManager extends JFrame { | |||
loginPrompt(localhost); | |||
} else if (registrations.size() == 1) { | |||
// single registration prompt | |||
loginPrompt(registrations.get(0)); | |||
GitblitRegistration reg = registrations.values().iterator().next(); | |||
loginPrompt(reg); | |||
} | |||
super.setVisible(value); | |||
setLocationRelativeTo(null); | |||
} | |||
} | |||
@@ -147,7 +216,7 @@ public class GitblitManager extends JFrame { | |||
reg = new GitblitRegistration(nameField.getText(), url, accountField.getText(), | |||
passwordField.getPassword()); | |||
boolean success = login(reg); | |||
registrations.add(0, reg); | |||
registrations.put(reg.name, reg); | |||
rebuildRecentMenu(); | |||
return success; | |||
} | |||
@@ -161,6 +230,7 @@ public class GitblitManager extends JFrame { | |||
serverTabs.setSelectedIndex(idx); | |||
serverTabs.setTabComponentAt(idx, new ClosableTabComponent(reg.name, null, serverTabs, | |||
panel)); | |||
saveRegistration(reg); | |||
return true; | |||
} catch (IOException e) { | |||
JOptionPane.showMessageDialog(GitblitManager.this, e.getMessage(), | |||
@@ -171,17 +241,54 @@ public class GitblitManager extends JFrame { | |||
private void rebuildRecentMenu() { | |||
recentMenu.removeAll(); | |||
for (final GitblitRegistration reg : registrations) { | |||
JMenuItem item = new JMenuItem(reg.name); | |||
ImageIcon icon = new ImageIcon(getClass().getResource("/gitblt-favicon.png")); | |||
for (final GitblitRegistration reg : registrations.values()) { | |||
JMenuItem item = new JMenuItem(reg.name, icon); | |||
item.addActionListener(new ActionListener() { | |||
public void actionPerformed(ActionEvent e) { | |||
login(reg); | |||
loginPrompt(reg); | |||
} | |||
}); | |||
recentMenu.add(item); | |||
} | |||
} | |||
private void loadRegistrations() { | |||
try { | |||
StoredConfig config = getConfig(); | |||
Set<String> servers = config.getSubsections("servers"); | |||
for (String server : servers) { | |||
String url = config.getString("servers", server, "url"); | |||
String account = config.getString("servers", server, "account"); | |||
char[] password = new String(Base64.decode(config.getString("servers", server, | |||
"password"))).toCharArray(); | |||
GitblitRegistration reg = new GitblitRegistration(server, url, account, password); | |||
registrations.put(reg.name, reg); | |||
} | |||
} catch (Throwable t) { | |||
Utils.showException(GitblitManager.this, t); | |||
} | |||
} | |||
private void saveRegistration(GitblitRegistration reg) { | |||
try { | |||
StoredConfig config = getConfig(); | |||
config.setString("servers", reg.name, "url", reg.url); | |||
config.setString("servers", reg.name, "account", reg.account); | |||
config.setString("servers", reg.name, "password", | |||
Base64.encodeBytes(new String(reg.password).getBytes("UTF-8"))); | |||
config.save(); | |||
} catch (Throwable t) { | |||
Utils.showException(GitblitManager.this, t); | |||
} | |||
} | |||
private StoredConfig getConfig() throws IOException, ConfigInvalidException { | |||
FileBasedConfig config = new FileBasedConfig(configFile, FS.detect()); | |||
config.load(); | |||
return config; | |||
} | |||
public static void main(String[] args) { | |||
EventQueue.invokeLater(new Runnable() { | |||
public void run() { |
@@ -80,6 +80,10 @@ public class GitblitPanel extends JPanel implements CloseTabListener { | |||
private UsersModel usersModel; | |||
private JTable settingsTable; | |||
private SettingsModel settingsModel; | |||
private JButton createRepository; | |||
private JButton delRepository; | |||
@@ -96,6 +100,8 @@ public class GitblitPanel extends JPanel implements CloseTabListener { | |||
private TableRowSorter<UsersModel> defaultUsersSorter; | |||
private TableRowSorter<SettingsModel> defaultSettingsSorter; | |||
private JButton editRepository; | |||
public GitblitPanel(GitblitRegistration reg) { | |||
@@ -105,6 +111,17 @@ public class GitblitPanel extends JPanel implements CloseTabListener { | |||
public GitblitPanel(String url, String account, char[] password) { | |||
this.gitblit = new GitblitModel(url, account, password); | |||
tabs = new JTabbedPane(JTabbedPane.BOTTOM); | |||
tabs.addTab(Translation.get("gb.repositories"), createRepositoriesPanel()); | |||
tabs.addTab(Translation.get("gb.users"), createUsersPanel()); | |||
tabs.addTab(Translation.get("gb.federation"), new JPanel()); | |||
tabs.addTab(Translation.get("gb.settings"), createSettingsPanel()); | |||
setLayout(new BorderLayout()); | |||
add(tabs, BorderLayout.CENTER); | |||
} | |||
private JPanel createRepositoriesPanel() { | |||
final JButton browseRepository = new JButton(Translation.get("gb.browse")); | |||
browseRepository.setEnabled(false); | |||
browseRepository.addActionListener(new ActionListener() { | |||
@@ -168,10 +185,10 @@ public class GitblitPanel extends JPanel implements CloseTabListener { | |||
repositoriesTable.setRowSorter(defaultRepositoriesSorter); | |||
repositoriesTable.getRowSorter().toggleSortOrder(RepositoriesModel.Columns.Name.ordinal()); | |||
setRepositoryRenderer(RepositoriesModel.Columns.Name, nameRenderer); | |||
setRepositoryRenderer(RepositoriesModel.Columns.Indicators, typeRenderer); | |||
setRepositoryRenderer(RepositoriesModel.Columns.Owner, ownerRenderer); | |||
setRepositoryRenderer(RepositoriesModel.Columns.Size, sizeRenderer); | |||
setRepositoryRenderer(RepositoriesModel.Columns.Name, nameRenderer, -1); | |||
setRepositoryRenderer(RepositoriesModel.Columns.Indicators, typeRenderer, 100); | |||
setRepositoryRenderer(RepositoriesModel.Columns.Owner, ownerRenderer, -1); | |||
setRepositoryRenderer(RepositoriesModel.Columns.Size, sizeRenderer, 60); | |||
repositoriesTable.getSelectionModel().addListSelectionListener(new ListSelectionListener() { | |||
@Override | |||
@@ -231,11 +248,25 @@ public class GitblitPanel extends JPanel implements CloseTabListener { | |||
return insets; | |||
} | |||
}; | |||
repositoriesPanel.add(new HeaderPanel(Translation.get("gb.repositories")), | |||
BorderLayout.NORTH); | |||
repositoriesPanel.add(new HeaderPanel(Translation.get("gb.repositories"), | |||
"gitweb-favicon.png"), BorderLayout.NORTH); | |||
repositoriesPanel.add(repositoryTablePanel, BorderLayout.CENTER); | |||
repositoriesPanel.add(repositoryControls, BorderLayout.SOUTH); | |||
return repositoriesPanel; | |||
} | |||
private void setRepositoryRenderer(RepositoriesModel.Columns col, TableCellRenderer renderer, | |||
int maxWidth) { | |||
String name = repositoriesTable.getColumnName(col.ordinal()); | |||
repositoriesTable.getColumn(name).setCellRenderer(renderer); | |||
if (maxWidth > 0) { | |||
repositoriesTable.getColumn(name).setMinWidth(maxWidth); | |||
repositoriesTable.getColumn(name).setMaxWidth(maxWidth); | |||
} | |||
} | |||
private JPanel createUsersPanel() { | |||
JButton refreshUsers = new JButton(Translation.get("gb.refresh")); | |||
refreshUsers.addActionListener(new ActionListener() { | |||
public void actionPerformed(ActionEvent e) { | |||
@@ -322,22 +353,73 @@ public class GitblitPanel extends JPanel implements CloseTabListener { | |||
return insets; | |||
} | |||
}; | |||
usersPanel.add(new HeaderPanel(Translation.get("gb.users")), BorderLayout.NORTH); | |||
usersPanel.add(new HeaderPanel(Translation.get("gb.users"), "user_16x16.png"), | |||
BorderLayout.NORTH); | |||
usersPanel.add(userTablePanel, BorderLayout.CENTER); | |||
usersPanel.add(userControls, BorderLayout.SOUTH); | |||
tabs = new JTabbedPane(JTabbedPane.BOTTOM); | |||
tabs.addTab(Translation.get("gb.repositories"), repositoriesPanel); | |||
tabs.addTab(Translation.get("gb.users"), usersPanel); | |||
tabs.addTab(Translation.get("gb.federation"), new JPanel()); | |||
setLayout(new BorderLayout()); | |||
add(tabs, BorderLayout.CENTER); | |||
return usersPanel; | |||
} | |||
private void setRepositoryRenderer(RepositoriesModel.Columns col, TableCellRenderer renderer) { | |||
String name = repositoriesTable.getColumnName(col.ordinal()); | |||
repositoriesTable.getColumn(name).setCellRenderer(renderer); | |||
private JPanel createSettingsPanel() { | |||
settingsModel = new SettingsModel(); | |||
defaultSettingsSorter = new TableRowSorter<SettingsModel>(settingsModel); | |||
settingsTable = Utils.newTable(settingsModel); | |||
String name = settingsTable.getColumnName(UsersModel.Columns.Name.ordinal()); | |||
settingsTable.setRowHeight(nameRenderer.getFont().getSize() + 8); | |||
settingsTable.getColumn(name).setCellRenderer(nameRenderer); | |||
settingsTable.setRowSorter(defaultSettingsSorter); | |||
settingsTable.getRowSorter().toggleSortOrder(SettingsModel.Columns.Name.ordinal()); | |||
settingsTable.getSelectionModel().addListSelectionListener(new ListSelectionListener() { | |||
@Override | |||
public void valueChanged(ListSelectionEvent e) { | |||
if (e.getValueIsAdjusting()) { | |||
return; | |||
} | |||
boolean selected = settingsTable.getSelectedRow() > -1; | |||
boolean singleSelection = settingsTable.getSelectedRows().length == 1; | |||
// TODO enable/disable setting buttons | |||
} | |||
}); | |||
final JTextField settingFilter = new JTextField(); | |||
settingFilter.addActionListener(new ActionListener() { | |||
public void actionPerformed(ActionEvent e) { | |||
filterSettings(settingFilter.getText()); | |||
} | |||
}); | |||
settingFilter.addKeyListener(new KeyAdapter() { | |||
public void keyReleased(KeyEvent e) { | |||
filterSettings(settingFilter.getText()); | |||
} | |||
}); | |||
JPanel settingFilterPanel = new JPanel(new BorderLayout(margin, margin)); | |||
settingFilterPanel.add(new JLabel(Translation.get("gb.filter")), BorderLayout.WEST); | |||
settingFilterPanel.add(settingFilter, BorderLayout.CENTER); | |||
JPanel settingsTablePanel = new JPanel(new BorderLayout(margin, margin)); | |||
settingsTablePanel.add(settingFilterPanel, BorderLayout.NORTH); | |||
settingsTablePanel.add(new JScrollPane(settingsTable), BorderLayout.CENTER); | |||
JPanel settingsControls = new JPanel(new FlowLayout(FlowLayout.CENTER, 5, 0)); | |||
// TODO update setting? | |||
JPanel settingsPanel = new JPanel(new BorderLayout(margin, margin)) { | |||
private static final long serialVersionUID = 1L; | |||
public Insets getInsets() { | |||
return insets; | |||
} | |||
}; | |||
settingsPanel.add(new HeaderPanel(Translation.get("gb.settings"), "settings_16x16.png"), | |||
BorderLayout.NORTH); | |||
settingsPanel.add(settingsTablePanel, BorderLayout.CENTER); | |||
settingsPanel.add(settingsControls, BorderLayout.SOUTH); | |||
return settingsPanel; | |||
} | |||
public void login() throws IOException { | |||
@@ -348,6 +430,7 @@ public class GitblitPanel extends JPanel implements CloseTabListener { | |||
if (gitblit.allowAdmin()) { | |||
updateUsersTable(); | |||
updateSettingsTable(); | |||
} else { | |||
// user does not have administrator privileges | |||
// hide admin repository buttons | |||
@@ -374,6 +457,11 @@ public class GitblitPanel extends JPanel implements CloseTabListener { | |||
usersModel.fireTableDataChanged(); | |||
} | |||
private void updateSettingsTable() { | |||
settingsModel.setSettings(gitblit.getSettings()); | |||
settingsModel.fireTableDataChanged(); | |||
} | |||
private void filterRepositories(final String fragment) { | |||
if (StringUtils.isEmpty(fragment)) { | |||
repositoriesTable.setRowSorter(defaultRepositoriesSorter); | |||
@@ -415,6 +503,26 @@ public class GitblitPanel extends JPanel implements CloseTabListener { | |||
usersTable.setRowSorter(sorter); | |||
} | |||
private void filterSettings(final String fragment) { | |||
if (StringUtils.isEmpty(fragment)) { | |||
settingsTable.setRowSorter(defaultSettingsSorter); | |||
return; | |||
} | |||
RowFilter<SettingsModel, Object> containsFilter = new RowFilter<SettingsModel, Object>() { | |||
public boolean include(Entry<? extends SettingsModel, ? extends Object> entry) { | |||
for (int i = entry.getValueCount() - 1; i >= 0; i--) { | |||
if (entry.getStringValue(i).toLowerCase().contains(fragment.toLowerCase())) { | |||
return true; | |||
} | |||
} | |||
return false; | |||
} | |||
}; | |||
TableRowSorter<SettingsModel> sorter = new TableRowSorter<SettingsModel>(settingsModel); | |||
sorter.setRowFilter(containsFilter); | |||
settingsTable.setRowSorter(sorter); | |||
} | |||
private List<RepositoryModel> getSelectedRepositories() { | |||
List<RepositoryModel> repositories = new ArrayList<RepositoryModel>(); | |||
for (int viewRow : repositoriesTable.getSelectedRows()) { | |||
@@ -572,6 +680,7 @@ public class GitblitPanel extends JPanel implements CloseTabListener { | |||
success &= gitblit.deleteRepository(repository); | |||
} | |||
if (success) { | |||
gitblit.refreshRepositories(); | |||
gitblit.refreshUsers(); | |||
} | |||
return success; |
@@ -15,6 +15,7 @@ | |||
*/ | |||
package com.gitblit.client; | |||
import java.awt.BasicStroke; | |||
import java.awt.Color; | |||
import java.awt.FlowLayout; | |||
import java.awt.GradientPaint; | |||
@@ -24,21 +25,27 @@ import java.awt.Paint; | |||
import java.awt.geom.Point2D; | |||
import java.awt.geom.Rectangle2D; | |||
import javax.swing.ImageIcon; | |||
import javax.swing.JLabel; | |||
import javax.swing.JPanel; | |||
import com.gitblit.utils.StringUtils; | |||
public class HeaderPanel extends JPanel { | |||
private static final long serialVersionUID = 1L; | |||
private Color lightColor = new Color(0, 0, 0x60); | |||
public HeaderPanel(String text) { | |||
public HeaderPanel(String text, String icon) { | |||
super(new FlowLayout(FlowLayout.LEFT), true); | |||
setOpaque(true); | |||
setBackground(new Color(0, 0, 0x20)); | |||
JLabel label = new JLabel(text); | |||
if (!StringUtils.isEmpty(icon)) { | |||
label.setIcon(new ImageIcon(getClass().getResource("/" + icon))); | |||
} | |||
label.setForeground(Color.white); | |||
label.setFont(label.getFont().deriveFont(14f)); | |||
add(label); | |||
@@ -53,5 +60,9 @@ public class HeaderPanel extends JPanel { | |||
false); | |||
g.setPaint(gradientPaint); | |||
g.fill(new Rectangle2D.Double(0, 0, getWidth(), getHeight())); | |||
g.setColor(new Color(0xff, 0x99, 0x00)); | |||
int stroke = 2; | |||
g.setStroke(new BasicStroke(stroke)); | |||
g.drawLine(0, getHeight() - 1, getWidth(), getHeight() - 1); | |||
} | |||
} |
@@ -0,0 +1,110 @@ | |||
/* | |||
* Copyright 2011 gitblit.com. | |||
* | |||
* Licensed under the Apache License, Version 2.0 (the "License"); | |||
* you may not use this file except in compliance with the License. | |||
* You may obtain a copy of the License at | |||
* | |||
* http://www.apache.org/licenses/LICENSE-2.0 | |||
* | |||
* Unless required by applicable law or agreed to in writing, software | |||
* distributed under the License is distributed on an "AS IS" BASIS, | |||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||
* See the License for the specific language governing permissions and | |||
* limitations under the License. | |||
*/ | |||
package com.gitblit.client; | |||
import java.util.ArrayList; | |||
import java.util.Collections; | |||
import java.util.List; | |||
import javax.swing.table.AbstractTableModel; | |||
import com.gitblit.IStoredSettings; | |||
/** | |||
* Table model of IStoredSettings. | |||
* | |||
* @author James Moger | |||
* | |||
*/ | |||
public class SettingsModel extends AbstractTableModel { | |||
private static final long serialVersionUID = 1L; | |||
IStoredSettings settings; | |||
List<String> keys; | |||
enum Columns { | |||
Name, Value; | |||
@Override | |||
public String toString() { | |||
return name().replace('_', ' '); | |||
} | |||
} | |||
public SettingsModel() { | |||
this(null); | |||
} | |||
public SettingsModel(IStoredSettings settings) { | |||
setSettings(settings); | |||
} | |||
public void setSettings(IStoredSettings settings) { | |||
this.settings = settings; | |||
if (settings == null) { | |||
keys = new ArrayList<String>(); | |||
} else { | |||
keys = new ArrayList<String>(settings.getAllKeys(null)); | |||
Collections.sort(keys); | |||
} | |||
} | |||
@Override | |||
public int getRowCount() { | |||
return keys.size(); | |||
} | |||
@Override | |||
public int getColumnCount() { | |||
return Columns.values().length; | |||
} | |||
@Override | |||
public String getColumnName(int column) { | |||
Columns col = Columns.values()[column]; | |||
switch (col) { | |||
case Name: | |||
return Translation.get("gb.name"); | |||
} | |||
return ""; | |||
} | |||
/** | |||
* Returns <code>Object.class</code> regardless of <code>columnIndex</code>. | |||
* | |||
* @param columnIndex | |||
* the column being queried | |||
* @return the Object.class | |||
*/ | |||
public Class<?> getColumnClass(int columnIndex) { | |||
return String.class; | |||
} | |||
@Override | |||
public Object getValueAt(int rowIndex, int columnIndex) { | |||
String key = keys.get(rowIndex); | |||
Columns col = Columns.values()[columnIndex]; | |||
switch (col) { | |||
case Name: | |||
return key; | |||
case Value: | |||
return settings.getString(key, ""); | |||
} | |||
return null; | |||
} | |||
} |
@@ -56,6 +56,7 @@ public class Utils { | |||
public static void showException(Component c, Throwable t) { | |||
// TODO show the unexpected exception | |||
t.printStackTrace(); | |||
} | |||
public static void packColumns(JTable table, int margin) { |
@@ -155,4 +155,5 @@ gb.size = size | |||
gb.downloading = downloading | |||
gb.loading = loading | |||
gb.starting = starting | |||
gb.general = general | |||
gb.general = general | |||
gb.settings = settings |