{*************************************************************************************
  This file is part of Transmission Remote GUI.
  Copyright (c) 2008-2019 by Yury Sidorov and Transmission Remote GUI working group.

  Transmission Remote GUI is free software; you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation; either version 2 of the License, or
  (at your option) any later version.

  Transmission Remote GUI is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with Transmission Remote GUI; if not, write to the Free Software
  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
*************************************************************************************}

unit Main;
{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, FileUtil, zstream, LResources, Forms, Controls,
  {$ifdef windows}
  windows, ShellApi,
  {$else}
  lclintf,
  {$endif windows}
  Graphics, Dialogs, ComCtrls, Menus, ActnList, LCLVersion,
  httpsend, StdCtrls, fpjson, jsonparser, ExtCtrls, rpc, syncobjs, variants, varlist, IpResolver,
  zipper, ResTranslator, VarGrid, StrUtils, LCLProc, Grids, BaseForm, utils, AddTorrent, Types,
  LazFileUtils, LazUTF8, StringToVK, passwcon, GContnrs,lineinfo;

const
  AppName = 'Transmission Remote GUI';
  AppVersion = '5.18.0';

resourcestring
  sAll = 'All torrents';
  sWaiting = 'Waiting';
  sVerifying = 'Verifying';
  sDownloading = 'Downloading';
  sSeeding = 'Seeding';
  sFinished = 'Finished';
  sStopped = 'Stopped';
  sUnknown = 'Unknown';
  sCompleted = 'Completed';
  sConnected = 'connected';
  sActive = 'Active';
  sInactive = 'Inactive';
  sErrorState = 'Error';
  sUpdating = 'Updating...';
  sFinishedDownload = '''%s'' has finished downloading';
  sDownloadComplete = 'Download complete';
  sUpdateComplete = 'Update complete.';
  sTorrentVerification = 'Torrent verification may take a long time.' + LineEnding + 'Are you sure to start verification of torrent ''%s''?';
  sTorrentsVerification = 'Torrents verification may take a long time.' + LineEnding + 'Are you sure to start verification of %d torrents?';
  sReconnect = 'Reconnect in %d seconds.';
  sDisconnected = 'Disconnected';
  sConnectingToDaemon = 'Connecting to daemon...';
  sLanguagePathFile = 'Language pathfile';
  sLanguagePath = 'Language path';
  sLanguageList = 'Language list';
  sSecs = '%ds';
  sMins = '%dm';
  sHours = '%dh';
  sDays = '%dd';
  sMonths = '%dmo';
  sYears  = '%dy';
  sDownloadingSeeding = '%s%s%d downloading, %d seeding%s%s, %s';
  sDownSpeed = 'D: %s/s';
  sUpSpeed = 'U: %s/s';
  SFreeSpace = 'Free: %s';
  sNoPathMapping = 'Unable to find path mapping.'+LineEnding+'Use the application''s options to setup path mappings.';
  sGeoIPConfirm = 'Geo IP database is needed to resolve country by IP address.' + LineEnding + 'Download this database now?';
  sFlagArchiveConfirm = 'Flag images archive is needed to display country flags.' + LineEnding + 'Download this archive now?';
  sInSwarm = 'in swarm';
  sHashfails = '%s (%d hashfails)';
  sDone = '%s (%s done)';
  sHave = '%d x %s (have %d)';
  sUnableExtractFlag = 'Unable to extract flag image.';
  sTrackerWorking = 'Working';
  sTrackerUpdating = 'Updating';
  sRestartRequired = 'You need to restart the application to apply changes.';
  sRemoveTorrentData = 'Are you sure to remove torrent ''%s'' and all associated DATA?';
  sRemoveTorrentDataMulti = 'Are you sure to remove %d selected torrents and all their associated DATA?';
  sRemoveTorrent = 'Are you sure to remove torrent ''%s''?';
  sRemoveTorrentMulti = 'Are you sure to remove %d selected torrents?';
  sUnableGetFilesList = 'Unable to get files list';
  sTrackerError = 'Tracker';
  sSkip = 'skip';
  sLow = 'low';
  sNormal = 'normal';
  sHigh = 'high';
  sByte = 'b';
  sKByte = 'KB';
  sMByte = 'MB';
  sGByte = 'GB';
  sTByte = 'TB';
  sPerSecond = '/s';
  sOf = 'of';
  sNoTracker = 'No tracker';
  sTorrents = 'Torrents';
  sBlocklistUpdateComplete = 'The block list has been updated successfully.' + LineEnding + 'The list entries count: %d.';
  sSeveralTorrents = '%d torrents';
  sUnableToExecute = 'Unable to execute "%s".';
  sSSLLoadError = 'Unable to load OpenSSL library files: %s and %s';
  SRemoveTracker = 'Are you sure to remove tracker ''%s''?';
  SUnlimited = 'Unlimited';
  SAverage = 'average';
  SCheckNewVersion = 'Do you wish to enable automatic checking for a new version of %s?';
  SDuplicateTorrent = 'Torrent already exists in the list';
  SUpdateTrackers = 'Update trackers for the existing torrent?';
  SDownloadingTorrent = 'Downloading torrent file...';
  SConnectTo = 'Connect to %s';
  SEnterPassword = 'Please enter a password to connect to %s:';

  SDownloaded = 'Downloaded';
  SUploaded = 'Uploaded';
  SFilesAdded = 'Files added';
  SActiveTime = 'Active time';
  STotalSize = 'Total: %s';
  sTotalSizeToDownload = 'Selected: %s';
  sTotalDownloaded = 'Done: %s';
  sTotalRemain = 'Remaining: %s';

  sUserMenu = 'User Menu';

  sBiDiDefault = 'Default';
  sBiDiLeftRight = 'Left->Right';
  sBiDiRightLeft = 'Right->Left';
  sBiDiRightLeftNoAlign = 'Right->Left (No Align)';
  sBiDiRightLeftReadOnly = 'Right->Left (Reading Only)';

  sPrivateOn = 'ON';
  sPrivateOff = 'OFF';

type

  { TMyHashMap example from hashmapdemo }
  TMyHashMap = class(specialize TGenHashMap<Integer, Integer>)
    function DefaultHashKey(const Key: Integer): Integer; override;
    function DefaultKeysEqual(const A, B: Integer): Boolean; override;
    function DefaultKeyToString(const Key: Integer): String; override;
    function DefaultItemToString(const Item: Integer): String; override;
  end;

  // for torrent folder
  FolderData = class
  public
    Hit: integer;
    Ext: string;
    Txt: string;
    Lst: TDate;
  end;


  { TProgressImage }

  TProgressImage = class(TGraphicControl)
  private
    FBmp: TBitmap;
    FBorderColor: TColor;
    FEndIndex: integer;
    FImageIndex: integer;
    FImages: TImageList;
    FStartIndex: integer;
    FTimer: TTimer;
    function GetFrameDelay: integer;
    procedure SetBorderColor(const AValue: TColor);
    procedure SetEndIndex(const AValue: integer);
    procedure SetFrameDelay(const AValue: integer);
    procedure SetImageIndex(const AValue: integer);
    procedure SetImages(const AValue: TImageList);
    procedure SetStartIndex(const AValue: integer);
    procedure UpdateIndex;
    procedure DoTimer(Sender: TObject);
  protected
    procedure Paint; override;
    procedure VisibleChanged; override;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    property Images: TImageList read FImages write SetImages;
    property ImageIndex: integer read FImageIndex write SetImageIndex;
    property StartIndex: integer read FStartIndex write SetStartIndex;
    property EndIndex: integer read FEndIndex write SetEndIndex;
    property FrameDelay: integer read GetFrameDelay write SetFrameDelay;
    property BorderColor: TColor read FBorderColor write SetBorderColor;
  end;

  { TMainForm }

  TMainForm = class(TBaseForm)
    acConnect: TAction;
    acAddTorrent: TAction;
    acExport: TAction;
    acImport: TAction;
    acStopTorrent: TAction;
    acRemoveTorrent: TAction;
    acStartTorrent: TAction;
    acSetHighPriority: TAction;
    acSetNormalPriority: TAction;
    acSetLowPriority: TAction;
    acSetNotDownload: TAction;
    acOptions: TAction;
    acDaemonOptions: TAction;
    acStartAllTorrents: TAction;
    acStopAllTorrents: TAction;
    acExit: TAction;
    acResolveHost: TAction;
    acResolveCountry: TAction;
    acShowCountryFlag: TAction;
    acSetupColumns: TAction;
    acRemoveTorrentAndData: TAction;
    acOpenFile: TAction;
    acOpenContainingFolder: TAction;
    acAddLink: TAction;
    acReannounceTorrent: TAction;
    acMoveTorrent: TAction;
    acSelectAll: TAction;
    acShowApp: TAction;
    acHideApp: TAction;
    acAddTracker: TAction;
    acEditTracker: TAction;
    acDelTracker: TAction;
    acConnOptions: TAction;
    acNewConnection: TAction;
    acDisconnect: TAction;
    acAltSpeed: TAction;
    acForceStartTorrent: TAction;
    acQMoveTop: TAction;
    acQMoveUp: TAction;
    acQMoveDown: TAction;
    acQMoveBottom: TAction;
    acCheckNewVersion: TAction;
    acFolderGrouping: TAction;
    acAdvEditTrackers: TAction;
    acFilterPane: TAction;
    acMenuShow :  TAction;
    acBigToolbar: TAction;
    ImageList32: TImageList;
    MenuItem103: TMenuItem;
    MenuShow: TAction;
    ActionList1: TActionList;
    acToolbarShow :  TAction;
    acInfoPane: TAction;
    acStatusBar: TAction;
    acCopyPath: TAction;
    acRename: TAction;
    acStatusBarSizes: TAction;
    acTrackerGrouping: TAction;
    acUpdateBlocklist: TAction;
    acUpdateGeoIP: TAction;
    acTorrentProps: TAction;
    acVerifyTorrent: TAction;
    ActionList: TActionList;
    ApplicationProperties: TApplicationProperties;
    MenuItem501: TMenuItem;
    MenuItem502: TMenuItem;
    SearchToolbar: TToolBar;
    tbSearchCancel: TToolButton;
    LocalWatchTimer: TTimer;
    txMagLabel: TLabel;
    txMagnetLink: TEdit;
    MenuItem101: TMenuItem;
    MenuItem102: TMenuItem;
    edSearch: TEdit;
    imgSearch: TImage;
    imgFlags: TImageList;
    ImageList16: TImageList;
    FilterTimer: TTimer;
    MenuItem100: TMenuItem;
    MenuItem122: TMenuItem;
    MenuItem1888: TMenuItem;
    MenuItem1889: TMenuItem;
    MenuItem1890: TMenuItem;
    MenuItem68: TMenuItem;
    MenuItem93: TMenuItem;
    MenuItem94: TMenuItem;
    MenuItem95: TMenuItem;
    MenuItem96: TMenuItem;
    MenuItem97: TMenuItem;
    MenuItem98: TMenuItem;
    MenuItem99: TMenuItem;
    miExport: TMenuItem;
    miImport: TMenuItem;
    OpenDialog1: TOpenDialog;
    panDetailsWait: TPanel;
    SaveDialog1: TSaveDialog;
    ToolButton5: TToolButton;
    txGlobalStats: TLabel;
    lvFilter: TVarGrid;
    lvTrackers: TVarGrid;
    MenuItem25: TMenuItem;
    MenuItem34: TMenuItem;
    MenuItem35: TMenuItem;
    MenuItem40: TMenuItem;
    MenuItem41: TMenuItem;
    MenuItem43: TMenuItem;
    MenuItem63: TMenuItem;
    MenuItem64: TMenuItem;
    MenuItem77: TMenuItem;
    MenuItem78: TMenuItem;
    MenuItem79: TMenuItem;
    MenuItem80: TMenuItem;
    MenuItem81: TMenuItem;
    MenuItem82: TMenuItem;
    MenuItem83: TMenuItem;
    MenuItem84: TMenuItem;
    MenuItem85: TMenuItem;
    MenuItem86: TMenuItem;
    MenuItem87: TMenuItem;
    MenuItem88: TMenuItem;
    MenuItem89: TMenuItem;
    MenuItem90: TMenuItem;
    MenuItem91: TMenuItem;
    MenuItem92: TMenuItem;
    miView: TMenuItem;
    goDevelopmentSite: TMenuItem;
    miHomePage: TMenuItem;
    pmiQueue: TMenuItem;
    miQueue: TMenuItem;
    pmFilter: TPopupMenu;
    sepTrackers: TMenuItem;
    MenuItem65: TMenuItem;
    MenuItem66: TMenuItem;
    MenuItem67: TMenuItem;
    MenuItem69: TMenuItem;
    MenuItem70: TMenuItem;
    MenuItem72: TMenuItem;
    MenuItem76: TMenuItem;
    pmiUpSpeedLimit: TMenuItem;
    pmiDownSpeedLimit: TMenuItem;
    pmDownSpeeds: TPopupMenu;
    pmUpSpeeds: TPopupMenu;
    sepCon2: TMenuItem;
    MenuItem71: TMenuItem;
    sepCon1: TMenuItem;
    MenuItem73: TMenuItem;
    MenuItem74: TMenuItem;
    MenuItem75: TMenuItem;
    pmSepOpen2: TMenuItem;
    MenuItem42: TMenuItem;
    pmSepOpen1: TMenuItem;
    MenuItem44: TMenuItem;
    MenuItem45: TMenuItem;
    MenuItem46: TMenuItem;
    MenuItem47: TMenuItem;
    MenuItem48: TMenuItem;
    MenuItem49: TMenuItem;
    MenuItem50: TMenuItem;
    MenuItem51: TMenuItem;
    MenuItem52: TMenuItem;
    MenuItem53: TMenuItem;
    MenuItem54: TMenuItem;
    MenuItem55: TMenuItem;
    MenuItem56: TMenuItem;
    MenuItem58: TMenuItem;
    MenuItem59: TMenuItem;
    MenuItem60: TMenuItem;
    MenuItem61: TMenuItem;
    MenuItem62: TMenuItem;
    miPriority: TMenuItem;
    pmiPriority: TMenuItem;
    MenuItem57: TMenuItem;
    pbDownloaded: TPaintBox;
    pmTrackers: TPopupMenu;
    pmConnections: TPopupMenu;
    tabStats: TTabSheet;
    tabTrackers: TTabSheet;
    tbConnect: TToolButton;
    tbtAltSpeed: TToolButton;
    sepAltSpeed: TToolButton;
    sepQueue: TToolButton;
    tbQMoveUp: TToolButton;
    tbQMoveDown: TToolButton;
    ToolButton9: TToolButton;
    txConnErrorLabel: TLabel;
    panSearch: TPanel;
    panFilter: TPanel;
    panReconnectFrame: TShape;
    txReconnectSecs: TLabel;
    txConnError: TLabel;
    MenuItem38: TMenuItem;
    MenuItem39: TMenuItem;
    panReconnect: TPanel;
    txLastActive: TLabel;
    txLastActiveLabel: TLabel;
    txTracker: TLabel;
    txTrackerLabel: TLabel;
    txCompletedOn: TLabel;
    txCompletedOnLabel: TLabel;
    txAddedOn: TLabel;
    txAddedOnLabel: TLabel;
    MenuItem19: TMenuItem;
    MenuItem20: TMenuItem;
    MenuItem21: TMenuItem;
    MenuItem22: TMenuItem;
    MenuItem23: TMenuItem;
    MenuItem24: TMenuItem;
    miTSep1: TMenuItem;
    MenuItem26: TMenuItem;
    MenuItem27: TMenuItem;
    MenuItem28: TMenuItem;
    MenuItem29: TMenuItem;
    MenuItem30: TMenuItem;
    MenuItem31: TMenuItem;
    MenuItem32: TMenuItem;
    MenuItem33: TMenuItem;
    MenuItem36: TMenuItem;
    MenuItem37: TMenuItem;
    miAbout: TMenuItem;
    miHelp: TMenuItem;
    panTop: TPanel;
    pmTray: TPopupMenu;
    HSplitter: TSplitter;
    pmPeers: TPopupMenu;
    TrayIcon: TTrayIcon;
    txCreated: TLabel;
    txCreatedLabel: TLabel;
    txTorrentHeader: TPanel;
    txTorrentName: TLabel;
    txTorrentNameLabel: TLabel;
    txDownProgress: TLabel;
    txDownProgressLabel: TLabel;
    panProgress: TPanel;
    txMaxPeers: TLabel;
    txMaxPeersLabel: TLabel;
    txPeers: TLabel;
    txPeersLabel: TLabel;
    txSeeds: TLabel;
    txSeedsLabel: TLabel;
    txDummy2: TLabel;
    txTrackerUpdate: TLabel;
    txDummy1: TLabel;
    txRemaining: TLabel;
    txRemainingLabel: TLabel;
    txStatus: TLabel;
    txStatusLabel: TLabel;
    txRatio: TLabel;
    txRatioLabel: TLabel;
    txDownLimit: TLabel;
    txDownLimitLabel: TLabel;
    txTrackerUpdateLabel: TLabel;
    txTransferHeader: TPanel;
    txUpSpeed: TLabel;
    txUpLimit: TLabel;
    txUpSpeedLabel: TLabel;
    txDownSpeed: TLabel;
    txDownSpeedLabel: TLabel;
    txUploaded: TLabel;
    txUploadedLabel: TLabel;
    txDownloaded: TLabel;
    txDownloadedLabel: TLabel;
    txUpLimitLabel: TLabel;
    txWasted: TLabel;
    txWastedLabel: TLabel;
    miCopyLabel: TMenuItem;
    pmLabels: TPopupMenu;
    txError: TLabel;
    txErrorLabel: TLabel;
    MenuItem17: TMenuItem;
    MenuItem18: TMenuItem;
    miTools: TMenuItem;
    TickTimer: TTimer;
    MainToolBar: TToolBar;
    panTransfer: TPanel;
    ToolButton1: TToolButton;
    ToolButton2: TToolButton;
    ToolButton3: TToolButton;
    ToolButton4: TToolButton;
    tbStopTorrent: TToolButton;
    ToolButton6: TToolButton;
    ToolButton7: TToolButton;
    ToolButton8: TToolButton;
    txComment: TLabel;
    txCommentLabel: TLabel;
    txHash: TLabel;
    txHashLabel: TLabel;
    panGeneralInfo: TPanel;
    lvFiles: TVarGrid;
    lvPeers: TVarGrid;
    MainMenu: TMainMenu;
    MenuItem1: TMenuItem;
    MenuItem10: TMenuItem;
    MenuItem11: TMenuItem;
    MenuItem12: TMenuItem;
    MenuItem13: TMenuItem;
    MenuItem14: TMenuItem;
    MenuItem15: TMenuItem;
    MenuItem16: TMenuItem;
    MenuItem2: TMenuItem;
    MenuItem3: TMenuItem;
    MenuItem4: TMenuItem;
    MenuItem5: TMenuItem;
    MenuItem6: TMenuItem;
    MenuItem7: TMenuItem;
    MenuItem8: TMenuItem;
    MenuItem9: TMenuItem;
    miConnect: TMenuItem;
    miExit: TMenuItem;
    miTorrent: TMenuItem;
    OpenTorrentDlg: TOpenDialog;
    PageInfo: TPageControl;
    pmTorrents: TPopupMenu;
    pmFiles: TPopupMenu;
    sbGenInfo: TScrollBox;
    txPieces: TLabel;
    txPiecesLabel: TLabel;
    txTotalSize: TLabel;
    txTotalSizeLabel: TLabel;
    gTorrents: TVarGrid;
    gStats: TVarGrid;
    VSplitter: TSplitter;
    StatusBar: TStatusBar;
    tabPeers: TTabSheet;
    tabGeneral: TTabSheet;
    TorrentsListTimer: TTimer;
    tabFiles: TTabSheet;
    procedure acAddLinkExecute(Sender: TObject);
    procedure acAddTorrentExecute(Sender: TObject);
    procedure acAddTrackerExecute(Sender: TObject);
    procedure acAdvEditTrackersExecute(Sender: TObject);
    procedure acAltSpeedExecute(Sender: TObject);
    procedure acBigToolbarExecute(Sender: TObject);
    procedure acCheckNewVersionExecute(Sender: TObject);
    procedure acConnectExecute(Sender: TObject);
    procedure acConnOptionsExecute(Sender: TObject);
    procedure acCopyPathExecute(Sender: TObject);
    procedure acDelTrackerExecute(Sender: TObject);
    procedure acEditTrackerExecute(Sender: TObject);
    procedure acFilterPaneExecute(Sender: TObject);
    procedure acFolderGroupingExecute(Sender: TObject);
    procedure acForceStartTorrentExecute(Sender: TObject);
    procedure acHideAppExecute(Sender: TObject);
    procedure acInfoPaneExecute(Sender: TObject);
    procedure acMenuShowExecute(Sender: TObject);
    procedure acMoveTorrentExecute(Sender: TObject);
    procedure acNewConnectionExecute(Sender: TObject);
    procedure acOpenContainingFolderExecute(Sender: TObject);
    procedure acOpenFileExecute(Sender: TObject);
    procedure acOptionsExecute(Sender: TObject);
    procedure acDisconnectExecute(Sender: TObject);
    procedure acExportExecute(Sender: TObject);
    procedure acImportExecute(Sender: TObject);
    procedure acExitExecute(Sender: TObject);
    procedure acDaemonOptionsExecute(Sender: TObject);
    procedure acQMoveBottomExecute(Sender: TObject);
    procedure acQMoveDownExecute(Sender: TObject);
    procedure acQMoveTopExecute(Sender: TObject);
    procedure acQMoveUpExecute(Sender: TObject);
    procedure acReannounceTorrentExecute(Sender: TObject);
    procedure acRemoveTorrentAndDataExecute(Sender: TObject);
    procedure acRemoveTorrentExecute(Sender: TObject);
    procedure acRenameExecute(Sender: TObject);
    procedure acResolveCountryExecute(Sender: TObject);
    procedure acResolveHostExecute(Sender: TObject);
    procedure acSelectAllExecute(Sender: TObject);
    procedure acSetHighPriorityExecute(Sender: TObject);
    procedure acSetLowPriorityExecute(Sender: TObject);
    procedure acSetNormalPriorityExecute(Sender: TObject);
    procedure acSetNotDownloadExecute(Sender: TObject);
    procedure acSetupColumnsExecute(Sender: TObject);
    procedure acShowAppExecute(Sender: TObject);
    procedure acShowCountryFlagExecute(Sender: TObject);
    procedure acStartAllTorrentsExecute(Sender: TObject);
    procedure acStartTorrentExecute(Sender: TObject);
    procedure acStatusBarExecute(Sender: TObject);
    procedure acStatusBarSizesExecute(Sender: TObject);
    procedure acStopAllTorrentsExecute(Sender: TObject);
    procedure acStopTorrentExecute(Sender: TObject);
    procedure gTorrentsMouseMove(Sender: TObject; Shift: TShiftState; X,
      Y: Integer);
    procedure gTorrentsMouseUp(Sender: TObject; Button: TMouseButton;
      Shift: TShiftState; X, Y: Integer);
    procedure LocalWatchTimerTimer(Sender: TObject);
    procedure lvFilesMouseUp(Sender: TObject; Button: TMouseButton;
      Shift: TShiftState; X, Y: Integer);
    procedure MenuShowExecute(Sender: TObject);
    procedure acToolbarShowExecute(Sender: TObject);
    procedure acTorrentPropsExecute(Sender: TObject);
    procedure acTrackerGroupingExecute(Sender: TObject);
    procedure acUpdateBlocklistExecute(Sender: TObject);
    procedure acUpdateGeoIPExecute(Sender: TObject);
    procedure acVerifyTorrentExecute(Sender: TObject);
    procedure ApplicationPropertiesEndSession(Sender: TObject);
    procedure ApplicationPropertiesException(Sender: TObject; E: Exception);
    procedure ApplicationPropertiesIdle(Sender: TObject; var Done: Boolean);
    procedure ApplicationPropertiesMinimize(Sender: TObject);
    procedure ApplicationPropertiesRestore(Sender: TObject);
    procedure edSearchChange(Sender: TObject);
    procedure edSearchKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState );
    procedure FormActivate(Sender: TObject);
    procedure FormDropFiles(Sender: TObject; const FileNames: array of String);
    procedure FormKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
    procedure FormWindowStateChange(Sender: TObject);
    procedure gTorrentsCellAttributes(Sender: TVarGrid; ACol, ARow, ADataCol: integer; AState: TGridDrawState; var CellAttribs: TCellAttributes);
    procedure gTorrentsClick(Sender: TObject);
    procedure gTorrentsDblClick(Sender: TObject);
    procedure gTorrentsDrawCell(Sender: TVarGrid; ACol, ARow, ADataCol: integer; AState: TGridDrawState; const R: TRect; var ADefaultDrawing: boolean);
    procedure gTorrentsEditorHide(Sender: TObject);
    procedure gTorrentsEditorShow(Sender: TObject);
    procedure gTorrentsQuickSearch(Sender: TVarGrid; var SearchText: string; var ARow: integer);
    procedure gTorrentsResize(Sender: TObject);
    procedure gTorrentsSetEditText(Sender: TObject; ACol, ARow: Integer; const Value: string);
    procedure gTorrentsSortColumn(Sender: TVarGrid; var ASortCol: integer);
    procedure HSplitterChangeBounds(Sender: TObject);
    procedure lvFilesDblClick(Sender: TObject);
    procedure lvFilesEditorHide(Sender: TObject);
    procedure lvFilesEditorShow(Sender: TObject);
    procedure lvFilesSetEditText(Sender: TObject; ACol, ARow: Integer; const Value: string);
    procedure lvFilterCellAttributes(Sender: TVarGrid; ACol, ARow, ADataCol: integer; AState: TGridDrawState; var CellAttribs: TCellAttributes);
    procedure lvFilterClick(Sender: TObject);
    procedure lvFilterDrawCell(Sender: TVarGrid; ACol, ARow, ADataCol: integer; AState: TGridDrawState; const R: TRect; var ADefaultDrawing: boolean);
    procedure lvPeersCellAttributes(Sender: TVarGrid; ACol, ARow, ADataCol: integer; AState: TGridDrawState; var CellAttribs: TCellAttributes);
    procedure lvTrackersCellAttributes(Sender: TVarGrid; ACol, ARow, ADataCol: integer; AState: TGridDrawState; var CellAttribs: TCellAttributes);
    procedure lvTrackersDblClick(Sender: TObject);
    procedure lvTrackersKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
    procedure goDevelopmentSiteClick(Sender: TObject);
    procedure MainToolBarContextPopup(Sender: TObject; MousePos: TPoint; var Handled: Boolean);
    procedure MenuItem101Click(Sender: TObject);
    procedure miHomePageClick(Sender: TObject);
    procedure PageInfoResize(Sender: TObject);
    procedure panReconnectResize(Sender: TObject);
    procedure pbDownloadedPaint(Sender: TObject);
    procedure StatusBarMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
    procedure TickTimerTimer(Sender: TObject);
    procedure FilterTimerTimer(Sender: TObject);
    procedure FormClose(Sender: TObject; var CloseAction: TCloseAction);
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure FormResize(Sender: TObject);
    procedure FormShow(Sender: TObject);
    procedure lvFilterResize(Sender: TObject);
    procedure miAboutClick(Sender: TObject);
    procedure miCopyLabelClick(Sender: TObject);
    procedure PageInfoChange(Sender: TObject);
    procedure tbSearchCancelClick(Sender: TObject);
    procedure TorrentsListTimerTimer(Sender: TObject);
    procedure pmFilesPopup(Sender: TObject);
    procedure pmTorrentsPopup(Sender: TObject);
    procedure TrayIconDblClick(Sender: TObject);
    procedure VSplitterChangeBounds(Sender: TObject);
    function CorrectPath (path: string): string; // PETROV
  private
    FStarted: boolean;
    FTorrents: TVarList;
    FFiles: TVarList;
    FTrackers: TStringList;
    FResolver: TIpResolver;
    FUnZip: TUnZipper;
    FReconnectWaitStart: TDateTime;
    FReconnectTimeOut: integer;
    FTorrentProgress: TBitmap;
    FLastPieces: string;
    FLastPieceCount: integer;
    FLastDone: double;
    FCurConn: string;
    FPathMap: TStringList;
    FLastFilerIndex: integer;
    FFilterChanged: boolean;
    FCurDownSpeedLimit: integer;
    FCurUpSpeedLimit: integer;
    FFlagsPath: string;
    FAddingTorrent: integer;
    FPendingTorrents: TStringList;
    FLinksFromClipboard: boolean;
    FLastClipboardLink: string;
    FLinuxOpenDoc: integer; // no del!
    FFromNow: boolean;
    FWatchLocalFolder: string;
    FWatchDestinationFolder: string;
    FWatchDownloading: boolean;
    FRow: integer;
    FCol: integer;
{$ifdef windows}
    FFileManagerDefault: string;
    FFileManagerDefaultParam: string;
    FGlobalHotkey: string;
    fGlobalHotkeyMod: string;
    FUserDefinedMenuEx: string;
    FUserDefinedMenuParam: string;
{$endif windows}
{$ifdef LCLcarbon}
    FFormActive: boolean;
{$endif LCLcarbon}
    FSlowResponse: TProgressImage;
    FDetailsWait: TProgressImage;
    FDetailsWaitStart: TDateTime;
    FMainFormShown: boolean;
    FFilesTree: TFilesTree;
    FFilesCapt: string;
    FCalcAvg: boolean;
    FPasswords: TStringList;
    FAppProps:TApplicationProperties;

    procedure UpdateUI;
    procedure UpdateUIRpcVersion(RpcVersion: integer);
    function DoConnect: boolean;
    procedure DoCreateOutZipStream(Sender: TObject; var AStream: TStream; AItem: TFullZipFileEntry);
    procedure DoDisconnect;
    procedure DoOpenFlagsZip(Sender: TObject; var AStream: TStream);
    procedure TorrentProps(PageNo: integer);
    procedure ShowConnOptions(NewConnection: boolean);
    procedure SaveColumns(LV: TVarGrid; const AName: string; FullInfo: boolean = True);
    procedure LoadColumns(LV: TVarGrid; const AName: string; FullInfo: boolean = True);
    function GetTorrentError(t: TJSONObject; Status: integer): string;
    function SecondsToString(j: integer): string;
    function DoAddTorrent(const FileName: Utf8String): boolean;
    procedure UpdateTray;
    procedure HideApp;
    procedure ShowApp;
    procedure DownloadFinished(const TorrentName: string);
    function GetFlagImage(const CountryCode: string): integer;
    procedure BeforeCloseApp;
    function GetGeoIpDatabase: string;
    function GetFlagsArchive: string;
    function DownloadGeoIpDatabase(AUpdate: boolean): boolean;
    procedure TorrentColumnsChanged;
    function EtaToString(ETA: integer): string;
    function GetTorrentStatus(TorrentIdx: integer): string;
    function GetSeedsText(Seeds, SeedsTotal: integer): string;
    function GetPeersText(Peers, PeersTotal, Leechers: integer): string;
    function RatioToString(Ratio: double): string;
    function TorrentDateTimeToString(d: Int64; FromNow:Boolean = false): string;
    procedure DoRefresh(All: boolean = False);
    function GetFilesCommonPath(files: TJSONArray): string;
    procedure InternalRemoveTorrent(const Msg, MsgMulti: string; RemoveLocalData: boolean);
    function IncludeProperTrailingPathDelimiter(const s: string): string;
    procedure UrlLabelClick(Sender: TObject);
    procedure CenterReconnectWindow;
    procedure ProcessPieces(const Pieces: string; PieceCount: integer; const Done: double);
    function ExecRemoteFile(const FileName: string; SelectFile: boolean; Userdef: boolean= false): boolean;
    function GetSelectedTorrents: variant;
    function GetDisplayedTorrents: variant;
    procedure FillDownloadDirs(CB: TComboBox; const CurFolderParam: string);
    procedure SaveDownloadDirs(CB: TComboBox; const CurFolderParam: string);
    procedure DeleteDirs(CB: TComboBox; maxdel : Integer);
    procedure SetRefreshInterval;
    procedure AddTracker(EditMode: boolean);
    procedure UpdateConnections;
    procedure DoConnectToHost(Sender: TObject);
    procedure FillSpeedsMenu;
    procedure DoSetDownloadSpeed(Sender: TObject);
    procedure DoSetUploadSpeed(Sender: TObject);
    procedure SetSpeedLimit(const Dir: string; Speed: integer);
    function FixSeparators(const p: string): string;
    function MapRemoteToLocal(const RemotePath: string): string;
    procedure CheckAddTorrents;
    procedure CheckClipboardLink;
    procedure CenterDetailsWait;
    procedure ReadLocalFolderWatch;
    function GetPageInfoType(pg: TTabSheet): TAdvInfoType;
    procedure DetailsUpdated;
    function RenameTorrent(TorrentId: integer; const OldPath, NewName: string): boolean;
    procedure FilesTreeStateChanged(Sender: TObject);
    function SelectTorrent(TorrentId, TimeOut: integer): integer;
    procedure OpenCurrentTorrent(OpenFolderOnly: boolean; UserDef: boolean=false);
  public
    procedure FillTorrentsList(list: TJSONArray);
    procedure FillPeersList(list: TJSONArray);
    procedure FillFilesList(ATorrentId: integer; list, priorities, wanted: TJSONArray; const DownloadDir: WideString);
    procedure FillGeneralInfo(t: TJSONObject);
    procedure FillTrackersList(TrackersData: TJSONObject);
    procedure FillSessionInfo(s: TJSONObject);
    procedure FillStatistics(s: TJSONObject);
    procedure CheckStatus(Fatal: boolean = True);
    function TorrentAction(const TorrentIds: variant; const AAction: string; args: TJSONObject = nil): boolean;
    function SetFilePriority(TorrentId: integer; const Files: array of integer; const APriority: string): boolean;
    function SetCurrentFilePriority(const APriority: string): boolean;
    procedure SetTorrentPriority(APriority: integer);
    procedure ClearDetailsInfo(Skip: TAdvInfoType = aiNone);
    function SelectRemoteFolder(const CurFolder, DialogTitle: string): string;
    procedure ConnectionSettingsChanged(const ActiveConnection: string; ForceReconnect: boolean);
    procedure StatusBarSizes;
private
    procedure _onException(Sender: TObject; E: Exception);
end;

function ExcludeInvalidChar (path: string): string; // PETROV
function GetBiDi: TBiDiMode;
function CheckAppParams: boolean;
procedure LoadTranslation;
function GetHumanSize(sz: double; RoundTo: integer = 0; const EmptyStr: string = '-'): string;
function PriorityToStr(p: integer; var ImageIndex: integer): string;
procedure DrawProgressCell(Sender: TVarGrid; ACol, ARow, ADataCol: integer; AState: TGridDrawState; const ACellRect: TRect);

var
  MainForm: TMainForm;
  RpcObj: TRpc;
  FTranslationFileName: string;
  FTranslationLanguage: string;
  FAlterColor: TColor;
  IsUnity: boolean;
  Ini: TIniFileUtf8;
  FHomeDir: string;
  {$ifdef windows}
  PrevWndProc: windows.WNDPROC;
  HotKeyID: Integer;
  {$endif windows}

const
  // Torrents list
  idxName = 0;
  idxSize = 1;
  idxDone = 2;
  idxStatus = 3;
  idxSeeds = 4;
  idxPeers = 5;
  idxDownSpeed = 6;
  idxUpSpeed = 7;
  idxETA = 8;
  idxRatio = 9;
  idxDownloaded = 10;
  idxUploaded = 11;
  idxTracker = 12;
  idxTrackerStatus = 13;
  idxAddedOn = 14;
  idxCompletedOn = 15;
  idxLastActive = 16;
  idxPath = 17;
  idxPriority = 18;
  idxSizeToDowload = 19;
  idxTorrentId = 20;
  idxQueuePos = 21;
  idxSeedingTime = 22;
  idxSizeLeft = 23;
  idxPrivate = 24;

  idxTag = -1;
  idxSeedsTotal = -2;
  idxLeechersTotal = -3;
  idxStateImg = -4;
  idxDeleted = -5;
  idxDownSpeedHistory = -6;
  idxUpSpeedHistory = -7;
  TorrentsExtraColumns = 7;

  // Peers list
  idxPeerHost = 0;
  idxPeerPort = 1;
  idxPeerCountry = 2;
  idxPeerClient = 3;
  idxPeerFlags = 4;
  idxPeerDone = 5;
  idxPeerUpSpeed = 6;
  idxPeerDownSpeed = 7;
  idxPeerTag = -1;
  idxPeerIP = -2;
  idxPeerCountryImage = -3;
  PeersExtraColumns = 3;

  // Trackers list
  idxTrackersListName = 0;
  idxTrackersListStatus = 1;
  idxTrackersListUpdateIn = 2;
  idxTrackersListSeeds = 3;
  idxTrackerTag = -1;
  idxTrackerID = -2;
  TrackersExtraColumns = 2;

  // Filter idices
  fltAll      = 0;
  fltDown     = 1;
  fltDone     = 2;
  fltActive   = 3;
  fltInactive = 4;
  fltStopped  = 5;
  fltError  = 6;
  fltWaiting = 7;

  // Status images
  imgDown      = 9;
  imgSeed      = 10;
  imgDownError = 11;
  imgSeedError = 12;
  imgError     = 13;
  imgDone      = 14;
  imgStopped   = 29;
  imgDownQueue = 16;
  imgSeedQueue = 17;
  imgAll       = 19;
  imgActive    = 20;
  imgInactive  = 15;
  imgWaiting   = 42;

  StatusFiltersCount = 8;

  TorrentFieldsMap: array[idxName..idxPrivate] of string =
    ('', 'totalSize', '', 'status', 'peersSendingToUs,seeders',
    'peersGettingFromUs,leechers', '', '', 'eta', 'uploadRatio',
    'downloadedEver', 'uploadedEver', '', '', 'addedDate', 'doneDate', 'activityDate', '', 'bandwidthPriority',
    '', '', 'queuePosition', 'secondsSeeding', 'leftUntilDone', 'isPrivate');

  FinishedQueue = 1000000;

  TR_PRI_SKIP   = -1000;  // psedudo priority
  TR_PRI_LOW    = -1;
  TR_PRI_NORMAL =  0;
  TR_PRI_HIGH   =  1;

implementation

uses
{$ifdef linux}
  process, dynlibs,
{$endif linux}
{$ifdef darwin}
  urllistenerosx,
{$endif darwin}
  synacode, ConnOptions, clipbrd, DateUtils, TorrProps, DaemonOptions, About,
  ToolWin, download, ColSetup, AddLink, MoveTorrent, ssl_openssl_lib, AddTracker, lcltype,
  Options, ButtonPanel, BEncode, synautil, Math;

  {TMyHashMap}
  function TMyHashMap.DefaultHashKey(const Key: Integer): Integer;
  begin
    Result := Key;
    if Odd(Result) then
      Result := Result * 3;
    end;

  function TMyHashMap.DefaultKeysEqual(const A, B: Integer): Boolean;
  begin
    Result := A = B;
  end;

  function TMyHashMap.DefaultKeyToString(const Key: Integer): String;
  begin
    WriteStr(Result, Key);
  end;

  function TMyHashMap.DefaultItemToString(const Item: Integer): String;
  begin
    WriteStr(Result, Item);
  end;

const
  TR_STATUS_CHECK_WAIT_1   = ( 1 shl 0 ); // Waiting in queue to check files
  TR_STATUS_CHECK_1        = ( 1 shl 1 ); // Checking files
  TR_STATUS_DOWNLOAD_1     = ( 1 shl 2 ); // Downloading
  TR_STATUS_SEED_1         = ( 1 shl 3 ); // Seeding
  TR_STATUS_STOPPED_1      = ( 1 shl 4 ); // Torrent is stopped

  TR_STATUS_STOPPED_2       = 0;     // Torrent is stopped
  TR_STATUS_CHECK_WAIT_2    = 1;     // Queued to check files
  TR_STATUS_CHECK_2         = 2;     // Checking files
  TR_STATUS_DOWNLOAD_WAIT_2 = 3;     // Queued to download
  TR_STATUS_DOWNLOAD_2      = 4;     // Downloading
  TR_STATUS_SEED_WAIT_2     = 5;     // Queued to seed
  TR_STATUS_SEED_2          = 6;     // Seeding

  TR_STATUS_FINISHED        = $100; // Torrent is finished (pseudo status)

  TR_SPEEDLIMIT_GLOBAL    = 0;    // only follow the overall speed limit
  TR_SPEEDLIMIT_SINGLE    = 1;    // only follow the per-torrent limit
  TR_SPEEDLIMIT_UNLIMITED = 2;    // no limits at all

  SpeedHistorySize = 20;

const
  SizeNames: array[1..5] of string = (sByte, sKByte, sMByte, sGByte, sTByte);

var
  TR_STATUS_STOPPED, TR_STATUS_CHECK_WAIT, TR_STATUS_CHECK, TR_STATUS_DOWNLOAD_WAIT, TR_STATUS_DOWNLOAD, TR_STATUS_SEED_WAIT, TR_STATUS_SEED: integer;


  {$ifdef windows}
function WndCallback(Ahwnd: HWND; uMsg: UINT; wParam: WParam; lParam: LParam):LRESULT; stdcall;
begin
  if (uMsg=WM_HOTKEY) and (WParam=HotKeyID) then
    begin
      if (MainForm.Visible = false) or (MainForm.WindowState = wsMinimized) then
          MainForm.ShowApp
          else
          MainForm.HideApp;
    end;
  result:=CallWindowProc(PrevWndProc,Ahwnd, uMsg, WParam, LParam);
end;

  {$endif windows}

function IsHash(Hash: String): boolean;
var i: integer;
begin
      Result := false;
      if Hash = '' then exit;
      if Length(Hash) = 32 then   // possible base32 encoded hash
                      try
                        Hash:=StrToHex(Base32Decode(UpperCase(Hash)));
                      except
                        exit;
                      end;
    if Length(Hash) <> 40 then exit;
    Result := true;
    for i := 1 to 40 do if not (Hash[i] in ['a' .. 'f', 'A'..'F', '0'..'9']) then Result := false;
end;

procedure TMainForm.ReadLocalFolderWatch;
var
  sr: TSearchRec;
begin
    if FPendingTorrents.Count = 0 then
      begin
            if FindFirstUTF8(FWatchLocalFolder+'*.torrent',faAnyFile,sr)=0 then
              repeat
                    FPendingTorrents.Add(FWatchLocalFolder+sr.Name);
              until FindNextUTF8(sr)<>0;
            FindCloseUTF8(sr);
      end;
end;

function GetHumanSize(sz: double; RoundTo: integer; const EmptyStr: string): string;
var
  i: integer;
begin
  if sz < 0 then begin
    Result:=EmptyStr;
    exit;
  end;
  i:=Low(SizeNames);
  if RoundTo > 0 then begin
    Inc(i);
    sz:=sz/1024;
  end;
  while i <= High(SizeNames) do begin
    if sz < 1024 then
      break;
    sz:=sz/1024;
    Inc(i);
  end;
  if (RoundTo = 0) and (i > 3) then
    RoundTo:=i - 2;
  Result:=Format('%.' + IntToStr(RoundTo) + 'f %s', [sz, SizeNames[i]]);
end;

function AddToChannel(Clr: TColor; Value: integer; Position: byte): TColor;
var i: integer;

begin
    i:=(Clr shr (Position*8)) and $FF;
    i:=i + Value;
    if i < 0 then i:=0;
    if i > $FF then i:=$FF;
    Result:=Clr and (not (Cardinal($FF) shl (Position*8))) or (Cardinal(i) shl (Position*8));
end;

function AddToColor(Color: TColor; R, G, B: integer): TColor;
begin
    Result:=ColorToRGB(Color);
    Result:=AddToChannel(Result, R, 0);
    Result:=AddToChannel(Result, G, 1);
    Result:=AddToChannel(Result, B, 2);
end;

function GetLikeColor(Color: TColor; Delta: integer): TColor;
var i, j: integer;

begin
    Result:=ColorToRGB(Color);
    j:=Result and $FF;               //red
    i:=(Result shr 8) and $FF;       // green
    if i > j then
      j:=i;
    i:=((Result shr 16) and $FF) shr 1;      // blue
    if i > j then
      j:=i;
    if j < $80 then
      i:=(($80 - j) div $20 + 1)*Delta
    else
      i:=Delta;
    if (i + j > 255) or (i + j < 0) then
      i:=-Delta;

    Result:=AddToColor(Result, i, i, i);
end;

function LocateFile(const FileName: string; const Paths: array of string): string;
var
  i: integer;
begin
  for i:=Low(Paths) to High(Paths) do begin
    Result:=IncludeTrailingPathDelimiter(Paths[i]) + FileName;
    if FileExistsUTF8(Result) then
      exit;
  end;
  Result:='';
end;

procedure OnTranslate(Sender: TResTranslator; const ResourceName: AnsiString; var Accept: boolean);
const
  IgnoreUnits: array[0..12] of string =
      ('fpjson','jsonparser','jsonscanner','lclstrconsts','math',
      'rtlconsts','sysconst','variants','zbase','zipper','zstream',
      'xmlcfg', 'registry');

  IgnoreControls: array[0..3] of string =
    ('AboutForm.txAuthor', 'MainForm.miLn', 'ConnOptionsForm.cbUseSocks5', 'ConnOptionsForm.tabConnection');

var
  i: integer;
begin
  Accept := not AnsiMatchText(Copy2Symb(ResourceName, '.'), IgnoreUnits)
            or AnsiStartsText('lclstrconsts.rsMb', ResourceName)  //<-- dialog buttons
            or AnsiStartsText('lclstrconsts.rsMt', ResourceName); //<-- dialog message
  if Accept then
    for i:=Low(IgnoreControls) to High(IgnoreControls) do
      if AnsiStartsText(IgnoreControls[i], ResourceName) then begin
        Accept:=False;
        exit;
      end;
  if Accept and (Copy(ResourceName, Length(ResourceName) - 8, MaxInt) = '.Category') then
    Accept:=False;
end;

var
  FIPCFileName: string;
  FRunFileName: string;

function IsProtocolSupported(const url: string): boolean;
const
  Protocols: array [1..3] of string =
    ('http:', 'https:', 'magnet:');
var
  i: integer;
  s: string;
begin
  s:=AnsiLowerCase(url);
  for i:=Low(Protocols) to High(Protocols) do
    if Copy(s, 1, Length(Protocols[i])) = Protocols[i] then begin
      Result:=True;
      exit;
    end;
  Result:=False;
end;

procedure AddTorrentFile(const FileName: string);
var
  h: System.THandle;
  t: TDateTime;
  s: string;
begin
  if not IsProtocolSupported(FileName) and not FileExistsUTF8(FileName) then
    exit;
  t:=Now;
  repeat
    if FileExistsUTF8(FIPCFileName) then
      h:=FileOpenUTF8(FIPCFileName, fmOpenWrite or fmShareDenyRead or fmShareDenyWrite)
    else
      h:=FileCreateUTF8(FIPCFileName);
    if h <> System.THandle(-1) then begin
      s:=FileName + LineEnding;
      FileSeek(h, 0, soFromEnd);
      FileWrite(h, s[1], Length(s));
      FileClose(h);
      break;
    end;
    Sleep(20);
  until Now - t >= 3/SecsPerDay;
end;

procedure LoadTranslation;
begin
  if Ini.ReadBool('Translation', 'TranslateForm', True) = False then
    FTranslationLanguage := 'English'
  else begin
  FTranslationFileName := Ini.ReadString('Interface', 'TranslationFile', '');
  if FTranslationFileName <> '-' then
    if (FTranslationFileName = '') or not IsTranslationFileValid(DefaultLangDir + FTranslationFileName) then
      FTranslationLanguage := LoadDefaultTranslationFile(@OnTranslate)
    else
      FTranslationLanguage := LoadTranslationFile(DefaultLangDir + FTranslationFileName, @OnTranslate);
  if FTranslationLanguage = '' then
    FTranslationLanguage := 'English'
end;
end;

function CheckAppParams: boolean;
var
  i: integer;
  s: string;
  h: System.THandle;
  pid: SizeUInt;
{$ifdef linux}
  proc: TProcess;
  sr: TSearchRec;
  hLib: TLibHandle;
{$endif linux}
begin
  Application.Title:=AppName;
{$ifdef linux}
  IsUnity:=CompareText(GetEnvironmentVariable('XDG_CURRENT_DESKTOP'), 'unity') = 0;
  if GetEnvironmentVariable('LIBOVERLAY_SCROLLBAR') <> '0' then begin
    i:=FindFirstUTF8('/usr/lib/liboverlay-scrollbar*', faAnyFile, sr);
    FindClose(sr);
    hLib:=LoadLibrary('liboverlay-scrollbar.so');
    if hLib <> 0 then
      FreeLibrary(hLib);
    if (i = 0) or (hLib <> 0) then begin
      // Turn off overlay scrollbars, since they are not supported yet.
      // Restart the app with the LIBOVERLAY_SCROLLBAR=0 env var.
      proc:=TProcess.Create(nil);
      try
        proc.Executable:=ParamStrUTF8(0);
        for i:=1 to ParamCount do
      proc.Parameters.Add(ParamStrUTF8(i));
        for i:=0 to GetEnvironmentVariableCount - 1 do
          proc.Environment.Add(GetEnvironmentString(i));
        proc.Environment.Values['LIBOVERLAY_SCROLLBAR']:='0';
        proc.Execute;
      finally
        proc.Free;
      end;
      Result:=False;
      exit;
    end;
  end;
{$endif linux}
  FHomeDir:=GetCmdSwitchValue('home');
  if FHomeDir = '' then begin
    if FileExistsUTF8(ChangeFileExt(ParamStrUTF8(0), '.ini')) then
      FHomeDir:=ExtractFilePath(ParamStrUTF8(0)) // Portable mode
    else
      FHomeDir:=IncludeTrailingPathDelimiter(GetAppConfigDirUTF8(False));
  end
  else
    FHomeDir:=IncludeTrailingPathDelimiter(FHomeDir);
  ForceDirectoriesUTF8(FHomeDir);
  FIPCFileName:=FHomeDir + 'ipc.txt';
  FRunFileName:=FHomeDir + 'run';

  Ini:=TIniFileUtf8.Create(FHomeDir+ChangeFileExt(ExtractFileName(ParamStrUTF8(0)), '.ini'));
  Ini.CacheUpdates:=True;

  // Check for outdated IPC file
  if FileExistsUTF8(FIPCFileName) then begin
    h:=FileOpenUTF8(FIPCFileName, fmOpenRead or fmShareDenyNone);
    if h <> INVALID_HANDLE_VALUE then begin
      i:=FileGetDate(h);
      FileClose(h);
      if (i > 0) and (Abs(Now - FileDateToDateTime(i)) > 1/MinsPerDay) then
        DeleteFileUTF8(FIPCFileName);
    end;
  end;

  for i:=1 to ParamCount do begin
    s:=ParamStrUTF8(i);
    if IsProtocolSupported(s) or FileExistsUTF8(s) then
      AddTorrentFile(s);
  end;

  if FileExistsUTF8(FRunFileName) then begin
    // Another process is running
    h:=FileOpenUTF8(FRunFileName, fmOpenRead or fmShareDenyNone);
    if FileRead(h, pid, SizeOf(pid)) = SizeOf(pid) then begin
{$ifdef mswindows}
      AllowSetForegroundWindow(pid);
{$endif mswindows}
    end;
    FileClose(h);

    if not FileExistsUTF8(FIPCFileName) then
      FileClose(FileCreateUTF8(FIPCFileName));
    for i:=1 to 50 do
      if not FileExistsUTF8(FIPCFileName) then begin
        // The running process works normally. Exit application.
        Result:=False;
        exit;
      end
      else
        Sleep(200);
    // The running process is not responding
    DeleteFileUTF8(FRunFileName);
    // Delete IPC file if it is empty
    h:=FileOpenUTF8(FIPCFileName, fmOpenRead or fmShareDenyNone);
    i:=FileSeek(h, 0, soFromEnd);
    FileClose(h);
    if i = 0 then
      DeleteFileUTF8(FIPCFileName);
  end;

  // Create a new run file
  h:=FileCreateUTF8(FRunFileName);
  pid:=GetProcessID;
  FileWrite(h, pid, SizeOf(pid));
  FileClose(h);

  LoadTranslation;

  GetBiDi;

  SizeNames[1]:=sByte;
  SizeNames[2]:=sKByte;
  SizeNames[3]:=sMByte;
  SizeNames[4]:=sGByte;
  SizeNames[5]:=sTByte;

  IntfScale:=Ini.ReadInteger('Interface', 'Scaling', 100);

  Result:=True;
end;

function PriorityToStr(p: integer; var ImageIndex: integer): string;
begin
  case p of
    TR_PRI_SKIP:   begin Result:=sSkip; ImageIndex:=23; end;
    TR_PRI_LOW:    begin Result:=sLow; ImageIndex:=24; end;
    TR_PRI_NORMAL: begin Result:=sNormal; ImageIndex:=25; end;
    TR_PRI_HIGH:   begin Result:=sHigh; ImageIndex:=26; end;
    else           Result:='???';
  end;
end;

procedure DrawProgressCell(Sender: TVarGrid; ACol, ARow, ADataCol: integer; AState: TGridDrawState; const ACellRect: TRect);
var
  R, RR: TRect;
  i, j, h: integer;
  s: string;
  cl: TColor;
  Progress: double;
  sz: TSize;
  ts: TTextStyle;
begin
  Progress:=double(Sender.Items[ADataCol, ARow]);
  with Sender.Canvas do begin
    R:=ACellRect;
    FrameRect(R);
    s:=Format('%.1f%%', [Progress]);
    sz:=TextExtent(s);
    InflateRect(R, -1, -1);
    Pen.Color:=clBtnFace;
    Rectangle(R);
    InflateRect(R, -1, -1);

    i:=R.Left + Round(Progress*(R.Right - R.Left)/100.0);
    j:=(R.Top + R.Bottom) div 2;
    h:=(R.Top + R.Bottom - sz.cy) div 2;
    cl:=GetLikeColor(clHighlight, 70);
    GradientFill(Rect(R.Left, R.Top, i, j), cl, clHighlight, gdVertical);
    GradientFill(Rect(R.Left, j, i, R.Bottom), clHighlight, cl, gdVertical);

    ts:=TextStyle;
    ts.Layout:=tlTop;
    ts.Alignment:=taLeftJustify;
    ts.Wordbreak:=False;
    TextStyle:=ts;
    j:=(R.Left + R.Right - sz.cx) div 2;
    if i > R.Left then begin
      RR:=Rect(R.Left, R.Top, i, R.Bottom);
      Font.Color:=clHighlightText;
      TextRect(RR, j, h, s);
    end;
    if i < R.Right then begin
      RR:=Rect(i, R.Top, R.Right, R.Bottom);
      Brush.Color:=Sender.Color;
      FillRect(RR);
      Font.Color:=clWindowText;
      TextRect(RR, j, h, s);
    end;
  end;
end;

{ TProgressImage }

procedure TProgressImage.SetImages(const AValue: TImageList);
begin
  if FImages=AValue then exit;
  FImages:=AValue;
  Width:=FImages.Width;
  Height:=FImages.Height;
end;

procedure TProgressImage.SetStartIndex(const AValue: integer);
begin
  if FStartIndex=AValue then exit;
  FStartIndex:=AValue;
  UpdateIndex;
end;

procedure TProgressImage.UpdateIndex;
begin
  if (FImageIndex < FStartIndex) or (FImageIndex > FEndIndex) then
    FImageIndex:=FStartIndex;
  Invalidate;
end;

procedure TProgressImage.DoTimer(Sender: TObject);
begin
  ImageIndex:=ImageIndex + 1;
end;

procedure TProgressImage.SetImageIndex(const AValue: integer);
begin
  if FImageIndex=AValue then exit;
  FImageIndex:=AValue;
  UpdateIndex;
end;

procedure TProgressImage.SetEndIndex(const AValue: integer);
begin
  if FEndIndex=AValue then exit;
  FEndIndex:=AValue;
  UpdateIndex;
end;

function TProgressImage.GetFrameDelay: integer;
begin
  Result:=FTimer.Interval;
end;

procedure TProgressImage.SetBorderColor(const AValue: TColor);
begin
  if FBorderColor=AValue then exit;
  FBorderColor:=AValue;
end;

procedure TProgressImage.SetFrameDelay(const AValue: integer);
begin
  FTimer.Interval:=AValue;
end;

procedure TProgressImage.Paint;
begin
  if FBmp = nil then begin
    FBmp:=TBitmap.Create;
    FBmp.Width:=Width;
    FBmp.Height:=Height;
  end;
  with FBmp.Canvas do begin
    Brush.Color:=clBtnFace;
    if FBorderColor <> clNone then begin
      Pen.Color:=FBorderColor;
      Rectangle(0, 0, FBmp.Width, FBmp.Height);
    end
    else
      FillRect(0, 0, FBmp.Width, FBmp.Height);
    if FImages <> nil then
      FImages.Draw(FBmp.Canvas, (Self.Width - FImages.Width) div 2, (Self.Height - FImages.Height) div 2, ImageIndex);
  end;
  Canvas.Draw(0, 0, FBmp);
end;

procedure TProgressImage.VisibleChanged;
begin
  inherited VisibleChanged;
  if Visible then begin
    ImageIndex:=StartIndex;
    FTimer.Enabled:=True;
  end
  else
    FTimer.Enabled:=False;
end;

constructor TProgressImage.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  FTimer:=TTimer.Create(Self);
  FTimer.Enabled:=False;
  FTimer.Interval:=100;
  FTimer.OnTimer:=@DoTimer;
  FBorderColor:=clNone;
  Visible:=False;
end;

destructor TProgressImage.Destroy;
begin
  FBmp.Free;
  inherited Destroy;
end;

{ TMainForm }

procedure TMainForm.FormCreate(Sender: TObject);
var
  ws: TWindowState;
  i, j: integer;
  R: TRect;
  SL: TStringList;
  MI, MI2: TMenuItem;
  Ico: TIcon;
  LargeIco, SmallIco : hIcon;
  MenuCaption: String;
{$ifdef darwin}
  s: string;
  pic: TPicture;
{$endif darwin}
begin
{$ifdef darwin}
  // Load better icon if possible
  s:=ExtractFilePath(ParamStrUTF8(0)) + '..' + DirectorySeparator + 'Resources'
    + DirectorySeparator + ChangeFileExt(ExtractFileName(ParamStrUTF8(0)), '.icns');
  if FileExistsUTF8(s) then begin
    pic:=TPicture.Create;
    try
      pic.LoadFromFile(s);
      try
        Application.Icon.Assign(pic.Graphic);
      except
      end;
    finally
      pic.Free;
    end;
  end;

  RegisterURLHandler(@AddTorrentFile);
{$endif darwin}


  {$if FPC_FULlVERSION>=30101}
  AllowReuseOfLineInfoData:=false;
  {$endif}
  FAppProps := TApplicationProperties.Create(Self);
  FAppProps.OnException := @_onException;
  FAppProps.CaptureExceptions := True;


  Application.Title:=AppName + ' v' + AppVersion;
  Caption:=Application.Title;
  txTransferHeader.Font.Size:=Font.Size + 2;
  txTorrentHeader.Font.Size:=txTransferHeader.Font.Size;
  TrayIcon.Icon.Assign(Application.Icon);
  RpcObj:=TRpc.Create;
  FTorrents:=TVarList.Create(gTorrents.Columns.Count, 0);
  FTorrents.ExtraColumns:=TorrentsExtraColumns;
  gTorrents.Items.ExtraColumns:=TorrentsExtraColumns;
  lvFiles.Items.ExtraColumns:=FilesExtraColumns;
  FFiles:=lvFiles.Items;
  FFilesTree:=TFilesTree.Create(lvFiles);
  FFilesTree.Checkboxes:=True;
  FFilesTree.OnStateChange:=@FilesTreeStateChanged;
  lvPeers.Items.ExtraColumns:=PeersExtraColumns;
  lvTrackers.Items.ExtraColumns:=TrackersExtraColumns;
  FTrackers:=TStringList.Create;
  FTrackers.Sorted:=True;
  FReconnectTimeOut:=-1;
  FAlterColor:=GetLikeColor(gTorrents.Color, -$10);
  lvFilter.Items.ExtraColumns:=1;
  gTorrents.AlternateColor:=FAlterColor;
  lvPeers.AlternateColor:=FAlterColor;
  lvTrackers.AlternateColor:=FAlterColor;
  gStats.AlternateColor:=FAlterColor;
  FPendingTorrents:=TStringList.Create;
  FFilesCapt:=tabFiles.Caption;
  FPasswords:=TStringList.Create;

  FSlowResponse:=TProgressImage.Create(MainToolBar);
  with FSlowResponse do begin
    Align:=alRight;
    Images:=ImageList16;
    StartIndex:=30;
    EndIndex:=37;
    Width:=ScaleInt(24);
    Left:=MainToolBar.ClientWidth;
    Parent:=MainToolBar;
  end;

  FDetailsWait:=TProgressImage.Create(panDetailsWait);
  with FDetailsWait do begin
    Images:=ImageList16;
    StartIndex:=FSlowResponse.StartIndex;
    EndIndex:=FSlowResponse.EndIndex;
    Width:=Images.Width*2;
    Height:=Width;
    BorderColor:=clBtnShadow;
    panDetailsWait.Width:=Width;
    panDetailsWait.Height:=Height;
    Parent:=panDetailsWait;
  end;

  DoDisconnect;
  PageInfo.ActivePageIndex:=0;
  PageInfoChange(nil);
{$ifdef LCLgtk2}
  with MainToolBar do begin
    EdgeBorders:=[ebLeft, ebTop, ebRight, ebBottom];
    EdgeInner:=esNone;
    EdgeOuter:=esRaised;
    Flat:=True;
  end;
  i:=acAltSpeed.ImageIndex;
  acAltSpeed.ImageIndex:=-1;
  tbtAltSpeed.ImageIndex:=i;
{$endif}
  txTransferHeader.Color:=GetLikeColor(clBtnFace, -15);
  txTorrentHeader.Color:=txTransferHeader.Color;
  txTransferHeader.Caption:=' ' + txTransferHeader.Caption;
  txTorrentHeader.Caption:=' ' + txTorrentHeader.Caption;
  txTransferHeader.Height:=txTransferHeader.Canvas.TextHeight(txTransferHeader.Caption) + 2;
  txTorrentHeader.Height:=txTorrentHeader.Canvas.TextHeight(txTorrentHeader.Caption) + 2;

  with gStats do begin
    BeginUpdate;
    try
      Items[0, 0]:=UTF8Decode(SDownloaded);
      Items[0, 1]:=UTF8Decode(SUploaded);
      Items[0, 2]:=UTF8Decode(SFilesAdded);
      Items[0, 3]:=UTF8Decode(SActiveTime);
    finally
      EndUpdate;
    end;
  end;

  if Ini.ReadInteger('MainForm', 'State', -1) = -1 then begin
    R:=Screen.MonitorFromRect(BoundsRect).WorkareaRect;
    if R.Right - R.Left < 300 then
      R:=Rect(0, 0, Screen.Width, Screen.Height);
    j:=R.Right - R.Left;
    i:=j*3 div 4;
    j:=j*95 div 100;
    if i > Width then
      Width:=i;
    if Width > j then
      Width:=j;
    Left:=(R.Right - R.Left - Width) div 2;
    j:=R.Bottom - R.Top;
    i:=j*3 div 4;
    j:=j*8 div 10;
    if i > Height then
      Height:=i;
    if Height > j then
      Height:=j;
    Top:=(R.Bottom - R.Top - Height) div 2;
  end
  else begin
    ws:=TWindowState(Ini.ReadInteger('MainForm', 'State', integer(WindowState)));
    Left:=Ini.ReadInteger('MainForm', 'Left', Left);
    Top:=Ini.ReadInteger('MainForm', 'Top', Top);
    Width:=Ini.ReadInteger('MainForm', 'Width', Width);
    Height:=Ini.ReadInteger('MainForm', 'Height', Height);
    if ws = wsMaximized then
      WindowState:=wsMaximized;
  end;

  if Ini.ReadBool('MainForm', 'FilterPane', acFilterPane.Checked) <> acFilterPane.Checked then
    acFilterPane.Execute;
  if Ini.ReadBool('MainForm', 'InfoPane', acInfoPane.Checked) <> acInfoPane.Checked then
    acInfoPane.Execute;
  if Ini.ReadBool('MainForm', 'StatusBar', acStatusBar.Checked) <> acStatusBar.Checked then
    acStatusBar.Execute;
  if Ini.ReadBool('MainForm', 'StatusBarSizes', acStatusBarSizes.Checked) <> acStatusBarSizes.Checked then
    acStatusBarSizes.Execute;

  if Ini.ReadBool('MainForm', 'Menu', acMenuShow.Checked) <> acMenuShow.Checked then
    acMenuShow.Execute;
  if Ini.ReadBool('MainForm', 'Toolbar', acToolbarShow.Checked) <> acToolbarShow.Checked then
    acToolbarShow.Execute;
  if Ini.ReadBool('MainForm', 'BigToolbar', acBigToolBar.Checked)  <> acBigToolBar.Checked then
    acBigToolbar.Execute;

  FFromNow := Ini.ReadBool('MainForm','FromNow',false);
  FWatchLocalFolder := Ini.ReadString('Interface','WatchLocalFolder','');
  if FWatchLocalFolder  <> '' then
          if DirPathExists(FWatchLocalFolder) and DirectoryIsWritable(FWatchLocalFolder) then
            begin
                FWatchLocalFolder := AppendPathDelim(FWatchLocalFolder);
                FWatchDestinationFolder := Ini.ReadString('Interface','WatchDestinationFolder','');
                LocalWatchTimer.Interval:=trunc(Ini.ReadFloat('Interface','WatchInterval',1)*60000);
                LocalWatchTimer.Enabled := true;
            end;
  LoadColumns(gTorrents, 'TorrentsList');
  TorrentColumnsChanged;
  LoadColumns(lvFiles, 'FilesList');
  LoadColumns(lvPeers, 'PeerList');
  LoadColumns(lvTrackers, 'TrackersList');

  acResolveHost.Checked:=Ini.ReadBool('PeersList', 'ResolveHost', True);
  acResolveCountry.Checked:=Ini.ReadBool('PeersList', 'ResolveCountry', True) and (GetGeoIpDatabase <> '');
  acShowCountryFlag.Checked:=Ini.ReadBool('PeersList', 'ShowCountryFlag', True) and (GetFlagsArchive <> '');
  acShowCountryFlag.Enabled:=acResolveCountry.Checked;
  FCurConn:=Ini.ReadString('Hosts', 'CurHost', '');
  if FCurConn = '' then
    FCurConn:=Ini.ReadString('Connection', 'Host', '');
  FPathMap:=TStringList.Create;
  if Application.HasOption('hidden') then begin
    ApplicationProperties.ShowMainForm:=False;
    TickTimer.Enabled:=True;
    UpdateTray;
  end;
  UpdateConnections;

  i:=Ini.ReadInteger('Interface', 'LastRpcVersion', -1);
  if i >= 0 then
    UpdateUIRpcVersion(i);

  bidiMode := GetBiDi;

  acFolderGrouping.Checked:=Ini.ReadBool('Interface', 'FolderGrouping', True);
  acTrackerGrouping.Checked:=Ini.ReadBool('Interface', 'TrackerGrouping', True);
  FLinksFromClipboard:=Ini.ReadBool('Interface', 'LinksFromClipboard', True);
  Application.OnActivate:=@FormActivate;
  Application.OnException:=@ApplicationPropertiesException;

  {$ifdef windows}
  FFileManagerDefault:=Ini.ReadString('Interface','FileManagerDefault','explorer.exe');
  FFileManagerDefaultParam:=Ini.ReadString('Interface', 'FileManagerDefaultParam', '/select,"%s"');
  FGlobalHotkey:=Ini.ReadString('Interface','GlobalHotkey','');
  FGlobalHotkeyMod:=Ini.ReadString('Interface','GlobalHotkeyMod','0');
  HotKeyID := GlobalAddAtom('TransGUIHotkey');
  PrevWndProc:=windows.WNDPROC(SetWindowLongPtr(Self.Handle,GWL_WNDPROC,PtrInt(@WndCallback)));
  RegisterHotKey(Self.Handle,HotKeyID, VKStringToWord(FGlobalHotkeyMod), VKStringToWord(FGlobalHotkey));
  // Create UserMenus if any in [UserMenu]
      j:= 1;
      repeat
            MenuCaption := Ini.ReadString('UserMenu','Caption'+IntToStr(j),'nocaption');
            inc(J);
      until MenuCaption = 'nocaption';
      dec(j);
      if j > 0 then
        begin
          if J > 2 then
            begin
                  MI := TMenuItem.Create(Self);
                  MI.Caption:= sUserMenu;
                  MI.ImageIndex:=6;
                  pmFiles.Items.Insert(4,MI);
                  MI2 := TMenuItem.Create(Self);
                  MI2.Caption:= sUserMenu;
                  MI2.ImageIndex:=6;
                  pmTorrents.Items.Insert(2,MI2);
            end;
          for i := 1 to j-1 do
            begin
                MI := TMenuItem.Create(Self);
                MI2 := TMenuItem.Create(Self);
                MI.Caption:= Ini.ReadString('UserMenu','Caption'+IntToStr(i),'');
                if MI.Caption <> '-' then
                begin
                  MI.Tag:= 1000+i;
                  MI.OnClick:= @acOpenFileExecute;
                end;
                try
                if ExtractIconEx(PChar(Ini.ReadString('UserMenu','ExeName'+IntToStr(i),'')), 0, LargeIco, SmallIco, 1) > null then
                  begin
                        Ico := TIcon.Create;
                        try
                          Ico.Handle := SmallIco;
                          Ico.Transparent := True;
                          Ico.Masked:=True;
                        finally
                          ImageList16.AddIcon(Ico);
                          Ico.Free;
                          MI.ImageIndex := ImageList16.Count-1;
                        end;
                  end;
                except
                end;
                MI2.Caption:= MI.Caption;
                MI2.Tag:= MI.Tag;
                MI2.OnClick:=MI.OnClick;
                MI2.ImageIndex:= MI.ImageIndex;
                if j > 2 then pmFiles.Items[4].Add(MI)
                        else pmFiles.Items.Insert(4,MI);
                if j > 2 then pmTorrents.Items[2].Add(MI2)
                        else pmTorrents.Items.Insert(2,MI2);
            end;
        end;
  // end Create UserMenu
  {$else}
    {$ifdef darwin}
    FLinuxOpenDoc := Ini.ReadInteger('Interface','FileOpenDoc',0);  // macOS - OpenURL(s, p) = Original version TRGUI
    {$else}
    FLinuxOpenDoc := Ini.ReadInteger('Interface','FileOpenDoc',1);
    {$endif darwin}
    Ini.WriteInteger('Interface','FileOpenDoc',FLinuxOpenDoc);
  {$endif windows}

//Dynamic Associations of ShortCuts to Actions/Menus
  SL := TStringList.Create;
  try
    Ini.ReadSectionValues('ShortCuts', SL);
    if (SL.Text = '') or (SL.Count <> ActionList.ActionCount) then
      begin
          for i := 0 to ActionList.ActionCount-1 do
          Ini.WriteString('Shortcuts',StringReplace(ActionList.Actions[i].Name,'ac','',[]),ShortcutToText(TAction(ActionList.Actions[i]).ShortCut));
          if (i<SL.Count-1) and (SL.Text <> '') and (ActionList.ActionByName(SL.Names[i]) = nil) then Ini.WriteString('Shortcuts',StringReplace(ActionList.Actions[i].Name,'ac','',[]),ShortcutToText(TAction(ActionList.Actions[i]).ShortCut));
      end
      else
        for i := 0 to SL.Count - 1 do
              try
                  TAction(ActionList.ActionbyName('ac'+SL.Names[i])).ShortCut := TextToShortcut(SL.ValueFromIndex[i]);
              except
              end;
  finally
    SL.Free;
  end;
  // StatusBar Panels width
  i := Ini.ReadInteger('StatusBarPanels','ScreenWidth',0);
  if Screen.Width <> i then
      begin
        Ini.EraseSection('StatusBarPanels');
        Ini.WriteInteger('StatusBarPanels','ScreenWidth',Screen.Width);
      end;
  for i := 0 to StatusBar.Panels.Count-1 do
  begin
        j := Ini.ReadInteger('StatusBarPanels',IntToStr(i),0);
        if j <> 0 then StatusBar.Panels[i].Width:=j else
                  Ini.WriteInteger('StatusBarPanels',IntToStr(i),Statusbar.Panels[i].Width);
  end;
  {$IF LCL_FULLVERSION >= 1080000}
  PageInfo.Options := PageInfo.Options + [nboDoChangeOnSetIndex]
  {$ENDIF}
end;

procedure TMainForm.FormDestroy(Sender: TObject);

  procedure _CreateAllForms;
  begin
    // Create all application forms to properly update language files
    TAboutForm.Create(Self).Free;
    TAddLinkForm.Create(Self).Free;
    TAddTorrentForm.Create(Self).Free;
    TAddTrackerForm.Create(Self).Free;
    TColSetupForm.Create(Self).Free;
    TConnOptionsForm.Create(Self).Free;
    TDaemonOptionsForm.Create(Self).Free;
    TDownloadForm.Create(Self).Free;
    TMoveTorrentForm.Create(Self).Free;
    TOptionsForm.Create(Self).Free;
    TTorrPropsForm.Create(Self).Free;
  end;

begin
  if Application.HasOption('updatelang') then begin
    _CreateAllForms;
    SupplementTranslationFiles;
  end;
  if Application.HasOption('makelang') then begin
    _CreateAllForms;
    MakeTranslationFile;
  end;

  DeleteFileUTF8(FRunFileName);
  FPasswords.Free;
  FResolver.Free;
  FTrackers.Free;
  FUnZip.Free;
  RpcObj.Free;
  FTorrentProgress.Free;
  FPathMap.Free;
  FTorrents.Free;
  FPendingTorrents.Free;
  try
    Ini.UpdateFile;
  except
  end;
  {$ifdef windows}
  UnRegisterHotkey(Self.Handle,HotKeyID);
  GlobalDeleteAtom(HotKeyID);
  {$endif windows}

end;

procedure TMainForm.FormResize(Sender: TObject);
begin
  if panReconnect.Visible then
    CenterReconnectWindow;
end;

procedure TMainForm.FormShow(Sender: TObject);
begin
  if not FMainFormShown then begin
    FMainFormShown:=True;
    VSplitter.SetSplitterPosition(Ini.ReadInteger('MainForm', 'VSplitter', VSplitter.GetSplitterPosition));
    HSplitter.SetSplitterPosition(Ini.ReadInteger('MainForm', 'HSplitter', HSplitter.GetSplitterPosition));
    MakeFullyVisible;
  end;
  if not FStarted then
    TickTimer.Enabled:=True;
  UpdateTray;
end;

procedure TMainForm.lvFilterResize(Sender: TObject);
begin
  lvFilter.Columns[0].Width:=lvFilter.ClientWidth;
end;

procedure TMainForm.miAboutClick(Sender: TObject);
begin
  with TAboutForm.Create(Self) do
  try
    ShowModal;
  finally
    Free;
  end;
end;

procedure TMainForm.miCopyLabelClick(Sender: TObject);
begin
  with TLabel(pmLabels.PopupComponent) do
    if (Length(Name) > 5) and (Copy(Name, Length(Name) - 4, 5) = 'Label') then
      Clipboard.AsText:=TLabel(Parent.FindChildControl(Copy(Name, 1, Length(Name) - 5))).Caption
    else
      Clipboard.AsText:=Caption;
end;

procedure TMainForm.acConnectExecute(Sender: TObject);
begin
  if RpcObj.Connected then begin
    tbConnect.CheckMenuDropdown;
    exit;
  end;
  if FCurConn = '' then
    ShowConnOptions(True)
  else
    DoConnect;
end;

procedure TMainForm.acConnOptionsExecute(Sender: TObject);
begin
  ShowConnOptions(False);
end;

procedure TMainForm.acCopyPathExecute(Sender: TObject);
begin
  if lvFiles.Items.Count > 0 then
    Clipboard.AsText:='"' + FFilesTree.GetFullPath(lvFiles.Row) + '"';
end;

procedure TMainForm.acDelTrackerExecute(Sender: TObject);
var
  req, args: TJSONObject;
  id, torid: integer;
begin
  id:=lvTrackers.Items[idxTrackerID, lvTrackers.Row];
  torid:=RpcObj.CurTorrentId;
  if MessageDlg('', Format(SRemoveTracker, [UTF8Encode(widestring(lvTrackers.Items[idxTrackersListName, lvTrackers.Row]))]), mtConfirmation, mbYesNo, 0, mbNo) <> mrYes then exit;
  AppBusy;
  Self.Update;
  req:=TJSONObject.Create;
  try
    req.Add('method', 'torrent-set');
    args:=TJSONObject.Create;
    args.Add('ids', TJSONArray.Create([torid]));
    args.Add('trackerRemove', TJSONArray.Create([id]));
    req.Add('arguments', args);
    args:=nil;
    args:=RpcObj.SendRequest(req, False);
    if args = nil then begin
      CheckStatus(False);
      exit;
    end;
    args.Free;
  finally
    req.Free;
  end;
  DoRefresh;
  AppNormal;
end;

procedure TMainForm.acEditTrackerExecute(Sender: TObject);
begin
  AddTracker(True);
end;

procedure TMainForm.acFilterPaneExecute(Sender: TObject);
begin
  acFilterPane.Checked:=not acFilterPane.Checked;
  panFilter.Visible:=acFilterPane.Checked;
  HSplitter.Visible:=acFilterPane.Checked;
  HSplitter.Left:=panFilter.Width;
  if lvFilter.Items.Count > 0 then
    lvFilter.Row:=0;
end;

procedure TMainForm.acFolderGroupingExecute(Sender: TObject);
begin
  acFolderGrouping.Checked:=not acFolderGrouping.Checked;
  Ini.WriteBool('Interface', 'FolderGrouping', acFolderGrouping.Checked);
  RpcObj.RefreshNow:=RpcObj.RefreshNow + [rtTorrents];
end;

procedure TMainForm.acForceStartTorrentExecute(Sender: TObject);
begin
  TorrentAction(GetSelectedTorrents, 'torrent-start-now');
end;

procedure TMainForm.acHideAppExecute(Sender: TObject);
begin
  HideApp;
end;

procedure TMainForm.acInfoPaneExecute(Sender: TObject);
begin
  acInfoPane.Checked:=not acInfoPane.Checked;
  PageInfo.Visible:=acInfoPane.Checked;
  VSplitter.Top:=PageInfo.Top - VSplitter.Height;
  VSplitter.Visible:=acInfoPane.Checked;
  if VSplitter.Visible then
    PageInfoChange(nil)
  else
    RpcObj.AdvInfo:=aiNone;
end;

procedure TMainForm.acMenuShowExecute(Sender: TObject);
begin
    acMenuShow.Checked:=not acMenuShow.Checked;
    if acMenuShow.Checked = false then
      MainForm.Menu := nil
    else
      MainForm.Menu := MainMenu;
end;

procedure TMainForm.acMoveTorrentExecute(Sender: TObject);
var
  ids: variant;
  i: integer;
  s: string;
  req: TJSONObject;
  aids: TJSONArray;
  args: TJSONObject;
  ok: boolean;
  t: TDateTime;
begin
  if gTorrents.Items.Count = 0 then
    exit;
  AppBusy;
  with TMoveTorrentForm.Create(Self) do
  try
    gTorrents.Tag:=1;
    gTorrents.EnsureSelectionVisible;
    FillDownloadDirs(edTorrentDir, 'LastMoveDir');
    if gTorrents.SelCount = 0 then
      gTorrents.RowSelected[gTorrents.Row]:=True;
    ids:=GetSelectedTorrents;
    i:=gTorrents.Items.IndexOf(idxTorrentId, ids[0]);
    if VarIsEmpty(gTorrents.Items[idxPath, i]) then
      exit;

    edTorrentDir.Text:=UTF8Encode(widestring(gTorrents.Items[idxPath, i]));

    if gTorrents.SelCount > 1 then
      s:=Format(sSeveralTorrents, [gTorrents.SelCount])
    else
      s:=UTF8Encode(widestring(gTorrents.Items[idxName, i]));


    Caption:=Caption + ' - ' + s;
    AppNormal;
    if ShowModal = mrOk then begin
      Application.ProcessMessages;
      AppBusy;
      req:=TJSONObject.Create;
      try
        req.Add('method', 'torrent-set-location');
        args:=TJSONObject.Create;
        aids:=TJSONArray.Create;
        for i:=VarArrayLowBound(ids, 1) to VarArrayHighBound(ids, 1) do
          aids.Add(integer(ids[i]));
        args.Add('ids', aids);
        args.Add('location', TJSONString.Create(UTF8Decode(edTorrentDir.Text)));
        args.Add('move', TJSONIntegerNumber.Create(integer(cbMoveData.Checked) and 1));
        req.Add('arguments', args);
        args:=RpcObj.SendRequest(req, False);
        args.Free;
      finally
        req.Free;
      end;
      gTorrents.Tag:=0;
      AppNormal;
      if args = nil then
        CheckStatus(False)
      else begin
        SaveDownloadDirs(edTorrentDir, 'LastMoveDir');
        ok:=False;
        t:=Now;
        with gTorrents do
          while not ok and not Application.Terminated and (Now - t < 20/SecsPerDay) do begin
            RpcObj.RequestFullInfo:=True;
            DoRefresh(True);
            Sleep(200);
            Application.ProcessMessages;
            ok:=True;
            for i:=0 to Items.Count - 1 do
              if RowSelected[i] then begin
                if VarIsEmpty(Items[idxPath, i]) or (AnsiCompareText(UTF8Encode(widestring(Items[idxPath, i])), edTorrentDir.Text) <> 0) then begin
                  ok:=False;
                  break;
                end;
              end;
          end;
      end;
    end;
  finally
    gTorrents.Tag:=0;
    Free;
  end;
end;

procedure TMainForm.acNewConnectionExecute(Sender: TObject);
begin
  ShowConnOptions(True);
end;

procedure TMainForm.acOpenContainingFolderExecute(Sender: TObject);
begin
  if gTorrents.Items.Count = 0 then
    exit;
  Application.ProcessMessages;
  if lvFiles.Focused and (lvFiles.Items.Count > 0) then begin
    AppBusy;
    ExecRemoteFile(FFilesTree.GetFullPath(lvFiles.Row), not FFilesTree.IsFolder(lvFiles.Row));
    AppNormal;
  end
  else
    OpenCurrentTorrent(True);
end;

procedure TMainForm.acOpenFileExecute(Sender: TObject);
var UserDef: boolean;
begin
  if gTorrents.Items.Count = 0 then
    exit;
  Application.ProcessMessages;
  if (Sender is TMenuItem) and (TMenuItem(Sender).Tag > 999) then
    begin
        UserDef := true;
        {$ifdef windows}
        FUserDefinedMenuEx    := Ini.ReadString('UserMenu','ExeName'+IntToStr(TMenuItem(Sender).Tag-1000),'');
        FUserDefinedMenuParam := Ini.ReadString('UserMenu','Params'+IntToStr(TMenuItem(Sender).Tag-1000),'');
        {$endif windows}
    end
      else UserDef := false;
  if lvFiles.Focused then begin
    if lvFiles.Items.Count = 0 then exit;
    ExecRemoteFile(FFilesTree.GetFullPath(lvFiles.Row), False, Userdef)
  end
  else
    OpenCurrentTorrent(False, UserDef);
end;

procedure TMainForm.acOptionsExecute(Sender: TObject);
var
  OldCheckVer: boolean;
begin
  AppBusy;
  with TOptionsForm.Create(Self) do
  try
    ConnForm.ActiveConnection:=FCurConn;
    edRefreshInterval.Value:=Ini.ReadInteger('Interface', 'RefreshInterval', 5);
    edRefreshIntervalMin.Value:=Ini.ReadInteger('Interface', 'RefreshIntervalMin', 20);
    cbCalcAvg.Checked:=FCalcAvg;
{$ifndef darwin}
    cbTrayMinimize.Checked:=Ini.ReadBool('Interface', 'TrayMinimize', True);
{$else}
    cbTrayMinimize.Enabled:=False;
{$endif}
    cbTrayClose.Checked:=Ini.ReadBool('Interface', 'TrayClose', False);
    cbTrayIconAlways.Checked:=Ini.ReadBool('Interface', 'TrayIconAlways', True);
    cbTrayNotify.Checked:=Ini.ReadBool('Interface', 'TrayNotify', True);

    cbShowAddTorrentWindow.Checked:=Ini.ReadBool('Interface', 'ShowAddTorrentWindow', True);
    cbDeleteTorrentFile.Checked:=Ini.ReadBool('Interface', 'DeleteTorrentFile', False);
    cbLinksFromClipboard.Checked:=Ini.ReadBool('Interface', 'LinksFromClipboard', True);
    edIntfScale.Value:=Ini.ReadInteger('Interface', 'Scaling', 100);
    cbCheckNewVersion.Checked:=Ini.ReadBool('Interface', 'CheckNewVersion', False);
    edCheckVersionDays.Value:=Ini.ReadInteger('Interface', 'CheckNewVersionDays', 5);
    cbCheckNewVersionClick(nil);
    OldCheckVer:=cbCheckNewVersion.Checked;
{$ifdef linux}
    if IsUnity then begin
      cbTrayIconAlways.Enabled:=False;
      cbTrayIconAlways.Checked:=False;
      cbTrayMinimize.Enabled:=False;
      cbTrayMinimize.Checked:=False;
      cbTrayNotify.Enabled:=False;
      cbTrayNotify.Checked:=False;
    end;
{$endif linux}
    AppNormal;
    if ShowModal = mrOk then begin
      AppBusy;
      Ini.WriteInteger('Interface', 'RefreshInterval', edRefreshInterval.Value);
      Ini.WriteInteger('Interface', 'RefreshIntervalMin', edRefreshIntervalMin.Value);
      Ini.WriteBool('Interface', 'CalcAvg', cbCalcAvg.Checked);
{$ifndef darwin}
      Ini.WriteBool('Interface', 'TrayMinimize', cbTrayMinimize.Checked);
{$endif}
      Ini.WriteBool('Interface', 'TrayClose', cbTrayClose.Checked);
      Ini.WriteBool('Interface', 'TrayIconAlways', cbTrayIconAlways.Checked);
      Ini.WriteBool('Interface', 'TrayNotify', cbTrayNotify.Checked);

      Ini.WriteBool('Interface', 'ShowAddTorrentWindow', cbShowAddTorrentWindow.Checked);
      Ini.WriteBool('Interface', 'DeleteTorrentFile', cbDeleteTorrentFile.Checked);
      Ini.WriteBool('Interface', 'LinksFromClipboard', cbLinksFromClipboard.Checked);
      FLinksFromClipboard:=cbLinksFromClipboard.Checked;

      Ini.WriteInteger('Interface', 'Scaling', edIntfScale.Value);

      Ini.WriteBool('Interface', 'CheckNewVersion', cbCheckNewVersion.Checked);
      Ini.WriteInteger('Interface', 'CheckNewVersionDays', edCheckVersionDays.Value);

      if cbCheckNewVersion.Checked and not OldCheckVer then
        CheckNewVersion;
      Ini.UpdateFile;
      UpdateTray;
      AppNormal;
      with ConnForm do
        ConnectionSettingsChanged(ActiveConnection, ActiveSettingChanged);
    end;
  finally
    Free;
  end;
end;

procedure TMainForm.acAddTorrentExecute(Sender: TObject);
begin
  if not OpenTorrentDlg.Execute then exit;
  FPendingTorrents.AddStrings(OpenTorrentDlg.Files);
  TickTimerTimer(nil);
end;

procedure TMainForm.acAddTrackerExecute(Sender: TObject);
begin
  AddTracker(False);
end;

procedure TMainForm.acAdvEditTrackersExecute(Sender: TObject);
begin
  gTorrents.RemoveSelection;
  TorrentProps(1);
end;

procedure TMainForm.acAltSpeedExecute(Sender: TObject);
var
  req, args: TJSONObject;
begin
  AppBusy;
  req:=TJSONObject.Create;
  try
    req.Add('method', 'session-set');
    args:=TJSONObject.Create;
    args.Add('alt-speed-enabled', integer(not acAltSpeed.Checked) and 1);
    req.Add('arguments', args);
    args:=RpcObj.SendRequest(req, False);
    if args = nil then begin
      CheckStatus(False);
      exit;
    end;
    args.Free;
  finally
    req.Free;
  end;
  RpcObj.RefreshNow:=RpcObj.RefreshNow + [rtSession];
  AppNormal;
end;

procedure TMainForm.acBigToolbarExecute(Sender: TObject);
begin
  acBigToolbar.Checked:=not acBigToolbar.Checked;
  if acBigToolbar.Checked then begin
    MainToolBar.ButtonWidth:= Ini.ReadInteger('MainForm','BigToolBarHeight',64);
    MainToolBar.ButtonHeight:= MainToolBar.ButtonWidth;
    MainToolBar.Images:= ImageList32;
  end else begin
    MainToolBar.ButtonWidth:= 23;
    MainToolBar.ButtonHeight:=23;
    MainToolBar.Images:= ImageList16;
    end;
  Ini.WriteBool('MainForm', 'BigToolbar', acBigToolbar.Checked);
end;

procedure TMainForm.acCheckNewVersionExecute(Sender: TObject);
begin
  Application.ProcessMessages;
  AppBusy;
  CheckNewVersion(False);
  AppNormal;
end;

procedure TMainForm.acAddLinkExecute(Sender: TObject);
begin
  AppBusy;
  with TAddLinkForm.Create(Self) do
  try
    AppNormal;
    if ShowModal = mrOk then
      begin
          if isHash(edLink.Text) then edLink.Text := 'magnet:?xt=urn:btih:'+ edLink.Text;
          DoAddTorrent(edLink.Text);
      end;
  finally
    Free;
  end;
end;

function TMainForm.DoAddTorrent(const FileName: Utf8String): boolean;
var
  torrent: string;
  WaitForm: TBaseForm;
  IsAppHidden: boolean;

  Procedure _AddTrackers(TorrentId: integer);
  var
    req, args: TJSONObject;
    fs: TFileStreamUTF8;
    TorData, AnnData, LData, LLData: TBEncoded;
    t: TJSONArray;
    tt: TJSONObject;
    trackers: TJSONArray;
    i, j: Integer;
    s, tfn, TorrentHash: string;
    TrackersList: TStringList;
  begin
    RpcObj.Status:='';
    s:='';
    if TorrentId <> 0 then begin
      i:=SelectTorrent(TorrentId, 2000);
      if i >= 0 then
        s:=Format(': %s', [UTF8Encode(widestring(gTorrents.Items[idxName, i]))]);

    end;
    ForceAppNormal;
    s:=TranslateString(SDuplicateTorrent + s + '.', True);
    if RpcObj.RPCVersion < 10 then begin
      MessageDlg(s, mtError, [mbOK], 0);
      exit;
    end;
    if MessageDlg(s + LineEnding + LineEnding + SUpdateTrackers, mtConfirmation, [mbYes, mbNo], 0) <> mrYes then
      exit;
    Application.ProcessMessages;
    TrackersList:=TStringList.Create;
    try
      TorrentHash:='';
      if AnsiCompareText('magnet:?', Copy(FileName, 1, 8)) = 0 then begin
        // Get trackers from the magnet link
        TrackersList.Delimiter:='&';
        TrackersList.DelimitedText:=Copy(FileName, 9, MaxInt);
        i:=0;
        while i < TrackersList.Count do begin
          s:=TrackersList.Names[i];
          if (TorrentId = 0) and (CompareText(s, 'xt') = 0) then begin
            s:=LowerCase(TrackersList.ValueFromIndex[i]);
            if (TorrentHash = '') and (Pos('urn:btih:', s) = 1) then begin
              s:=Copy(s, 10, MaxInt);
              if Length(s) = 32 then
                // base32 encoded hash
                TorrentHash:=StrToHex(Base32Decode(UpperCase(s)))
              else
                TorrentHash:=s;
            end;
          end
          else
            if CompareText(s, 'tr') = 0 then begin
              TrackersList[i]:=DecodeURL(TrackersList.ValueFromIndex[i]);
              Inc(i);
              continue;
            end;
          TrackersList.Delete(i);
        end;
      end
      else
      try
        if IsProtocolSupported(FileName) then begin
          // Downloading torrent file
          tfn:=SysToUTF8(GetTempDir(True)) + 'remote-torrent.torrent';
          if not DownloadFile(FileName, ExtractFilePath(tfn), ExtractFileName(tfn), SDownloadingTorrent) then
            exit;
        end
        else
          tfn:=FileName;
        // Read trackers from the torrent file...
        AppBusy;
        TorData:=nil;
        fs:=TFileStreamUTF8.Create(tfn, fmOpenRead or fmShareDenyNone);
        try
          TorData:=TBEncoded.Create(fs);
          if TorrentId = 0 then begin
            // Calculate torrent hash
            LData:=(TorData.ListData.FindElement('info') as TBEncoded);
            s:='';
            LData.Encode(LData, s);
            TorrentHash:=StrToHex(SHA1(s));
          end;
          AnnData:=(TorData.ListData.FindElement('announce-list', False) as TBEncoded);
          if AnnData <> nil then
            for i:=0 to AnnData.ListData.Count - 1 do begin
              LData:=AnnData.ListData.Items[i].Data as TBEncoded;
              for j:=0 to LData.ListData.Count - 1 do begin
                LLData:=LData.ListData.Items[j].Data as TBEncoded;
                TrackersList.Add(LLData.StringData);
              end;
            end
          else begin
            AnnData:=(TorData.ListData.FindElement('announce', False) as TBEncoded);
            if AnnData <> nil then
              TrackersList.Add(AnnData.StringData);
          end;
        finally
          TorData.Free;
          fs.Free;
        end;
      finally
        // Delete temp file
        if (tfn <> '') and (tfn <> FileName) then
          DeleteFileUTF8(tfn);
      end;

      // Request trackers from the existing torrent
      req:=TJSONObject.Create;
      try
        req.Add('method', 'torrent-get');
        args:=TJSONObject.Create;
        if TorrentId = 0 then
          args.Add('ids', TJSONArray.Create([TorrentHash]))
        else
          args.Add('ids', TJSONArray.Create([TorrentId]));
        args.Add('fields', TJSONArray.Create(['id', 'trackers']));
        req.Add('arguments', args);
        args:=RpcObj.SendRequest(req);
        if args = nil then begin
          CheckStatus(False);
          exit;
        end;
        try
          t:=args.Arrays['torrents'];
          if t.Count = 0 then
            raise Exception.Create('Torrent not found.');
          tt:=t.Objects[0] as TJSONObject;
          i:=tt.Integers['id'];
          if TorrentId = 0 then
            SelectTorrent(i, 2000);
          TorrentId:=i;
          trackers:=tt.Arrays['trackers'];
          // Deleting existing trackers from the list
          for i:=0 to trackers.Count - 1 do begin
            s:=UTF8Encode((Trackers.Items[i] as TJSONObject).Strings['announce']);
            j:=TrackersList.IndexOf(s);
            if j >= 0 then
              TrackersList.Delete(j);
          end;
        finally
          args.Free;
        end;
      finally
        req.Free;
      end;

      if TrackersList.Count > 0 then begin
        trackers:=TJSONArray.Create;
        for i:=0 to TrackersList.Count - 1 do
          trackers.Add(TrackersList[i]);
        args:=TJSONObject.Create;
        args.Add('ids', TJSONArray.Create([TorrentId]));
        args.Add('trackerAdd', trackers);
        req:=TJSONObject.Create;
        try
          req.Add('method', 'torrent-set');
          req.Add('arguments', args);
          args:=RpcObj.SendRequest(req, False);
          if args = nil then begin
            CheckStatus(False);
            exit;
          end;
          args.Free;
        finally
          req.Free;
        end;
        DoRefresh;
      end;
    finally
      TrackersList.Free;
      AppNormal;
    end;
  end;

  function _AddTorrent(args: TJSONObject): integer;
  var
    req: TJSONObject;
  begin
    Result:=0;
    req:=TJSONObject.Create;
    try
      req.Add('method', 'torrent-add');
      if torrent = '-' then
        args.Add('filename', TJSONString.Create(FileName))
      else
        args.Add('metainfo', TJSONString.Create(torrent));
      req.Add('arguments', args);
      args:=RpcObj.SendRequest(req);
      if args <> nil then
      try
        if args.IndexOfName('torrent-duplicate') >= 0 then begin
          _AddTrackers(args.Objects['torrent-duplicate'].Integers['id']);
          exit;
        end;
        Result:=args.Objects['torrent-added'].Integers['id'];
      finally
        args.Free;
      end
      else
        if RpcObj.Status='duplicate torrent' then begin
          _AddTrackers(0);
          exit;
        end;
    finally
      req.Free;
    end;
    if Result = 0 then
      CheckStatus(False);
  end;

  procedure ShowWaitMsg(const AText: string);
  begin
    if WaitForm = nil then begin
      WaitForm:=TBaseForm.CreateNew(Self);
      with WaitForm do begin
{$ifndef windows}
        if IsAppHidden then
          ShowInTaskBar:=stAlways;
{$endif windows}
        Caption:=AppName;
        BorderStyle:=bsToolWindow;
        BorderIcons:=[];
        Position:=poScreenCenter;
        Constraints.MinWidth:=400;
        AutoSize:=True;
        BorderWidth:=ScaleInt(16);
        with TLabel.Create(WaitForm) do begin
          Alignment:=taCenter;
          Align:=alClient;
          Parent:=WaitForm;
        end;
      end;
    end;
    with WaitForm do begin
      TLabel(Controls[0]).Caption:=AText + '...';
      Show;
      BringToFront;
{$ifdef lclgtk2}
      Application.ProcessMessages;
{$endif lclgtk2}
      Update;
{$ifdef lclgtk2}
      sleep(100);
      Application.ProcessMessages;
{$endif lclgtk2}
    end;
  end;

  procedure HideWaitMsg;
  begin
    if WaitForm <> nil then
      FreeAndNil(WaitForm);
  end;

var
  req, args: TJSONObject;
  id: integer;
  t, files: TJSONArray;
  i: integer;
  fs: TFileStreamUTF8;
  s, ss, OldDownloadDir, IniSec, OldName: string;
  ok: boolean;
  pFD:FolderData;
begin
  Result:=False;
  if not RpcObj.Connected and not RpcObj.Connecting then
    if not DoConnect then
      exit;
  WaitForm:=nil;
  id:=0;
  Inc(FAddingTorrent);
  try
    AppBusy;
    try
      IsAppHidden:=not Self.Visible or (Self.WindowState = wsMinimized);
      with TAddTorrentForm.Create(Self) do
      try
        if IsAppHidden then begin
          ShowWaitMsg(Caption);
{$ifndef windows}
          ShowInTaskBar:=stAlways;
{$endif windows}
        end;

        Application.ProcessMessages;
        if IsProtocolSupported(FileName) then
          torrent:='-'
        else begin
          try
            fs:=TFileStreamUTF8.Create(FileName, fmOpenRead or fmShareDenyNone); // why isnt in try
          except
            AppNormal;    // if the clipboard garbage and file cant be created. just go out.
            HideWaitMsg;
            exit;
          end;

          try
            SetLength(torrent, fs.Size);
            fs.ReadBuffer(PChar(torrent)^, Length(torrent));
          finally
            fs.Free;
          end;
          torrent:=EncodeBase64(torrent);
        end;

        IniSec:='AddTorrent.' + FCurConn;
        FillDownloadDirs(cbDestFolder, 'LastDownloadDir');
        if (FWatchDownloading) and (FWatchDestinationFolder <> '') then cbDestFolder.Text:=FWatchDestinationFolder;

        req:=TJSONObject.Create;
        try
          req.Add('method', 'session-get');
          args:=RpcObj.SendRequest(req);
          if args = nil then begin
            CheckStatus(False);
            exit;
          end;
          s:=CorrectPath (UTF8Encode(args.Strings['download-dir']) );
          try
            if cbDestFolder.Items.IndexOf(s) < 0 then begin
              pFD    := FolderData.create;
              pFD.Hit:= 1;
              pFD.Ext:= '';
              pFD.Txt:= s;
              pFD.Lst:= SysUtils.Date;
              cbDestFolder.Items.Add(s);
              i := cbDestFolder.Items.IndexOf(s);
              cbDestFolder.Items.Objects[i]:= pFD;
            end;
          except
            MessageDlg('Error: LS-005. Please contact the developer', mtError, [mbOK], 0);
          end;

          if RpcObj.RPCVersion < 15 then
            if args.IndexOfName('download-dir-free-space') >= 0 then
              txDiskSpace.Caption:=txDiskSpace.Caption + ' ' + GetHumanSize(args.Floats['download-dir-free-space'])
            else begin
              txDiskSpace.Hide;
              txSize.Top:=(txSize.Top + txDiskSpace.Top) div 2;
            end;
          args.Free;
        finally
          req.Free;
        end;

        lvFilter.Row:=0;
        edSearch.Text:='';

        args:=TJSONObject.Create;
        args.Add('paused', TJSONIntegerNumber.Create(1));
        i:=Ini.ReadInteger(IniSec, 'PeerLimit', 0);
        if i <> 0 then
          args.Add('peer-limit', TJSONIntegerNumber.Create(i));

        // for larazur 1.4 and up.
        // data can be in the drop-down list, but no text is selected in the window
        try
          ss := cbDestFolder.Text;
          if (ss = '') then begin
            if (cbDestFolder.Items.Count = 0) then begin
              ss := s;
            end else begin
              cbDestFolder.ItemIndex:=0;
              ss := cbDestFolder.Text;
            end;
          end;
        except
          MessageDlg('Error: LS-006. Please contact the developer', mtError, [mbOK], 0);
        end;
        args.Add('download-dir', TJSONString.Create(UTF8Decode(ss)));
        id:=_AddTorrent(args);
        if id = 0 then
          exit;

        DoRefresh(True);

        args:=RpcObj.RequestInfo(id, ['files','maxConnectedPeers','name','metadataPercentComplete']);
        if args = nil then begin
          CheckStatus(False);
          exit;
        end;
        try
          t:=args.Arrays['torrents'];
          if t.Count = 0 then
            raise Exception.Create(sUnableGetFilesList);

          OldName:=UTF8Encode(t.Objects[0].Strings['name']);
          edSaveAs.Caption:=OldName;
          edSaveAs.Caption := ExcludeInvalidChar(edSaveAs.Caption); // petrov - Exclude prohibited characters

          if RpcObj.RPCVersion < 15 then begin
            edSaveAs.Enabled:=False;
            edSaveAs.ParentColor:=True;
          end;
          edPeerLimit.Value:=t.Objects[0].Integers['maxConnectedPeers'];
          FilesTree.FillTree(id, t.Objects[0].Arrays['files'], nil, nil);
          Width:=Ini.ReadInteger('AddTorrent', 'Width', Width);
          if (RpcObj.RPCVersion >= 7) and (lvFiles.Items.Count = 0) and (t.Objects[0].Floats['metadataPercentComplete'] <> 1.0) then begin
            // Magnet link
            gbContents.Hide;
            gbSaveAs.BorderSpacing.Bottom:=gbSaveAs.BorderSpacing.Top;
            BorderStyle:=bsDialog;
            AutoSizeForm(TCustomForm(gbContents.Parent));
            edSaveAs.Enabled:=False;
            edSaveAs.ParentColor:=True;
          end
          else
            // Torrent file
            Height:=Ini.ReadInteger('AddTorrent', 'Height', Height);
        finally
          args.Free;
        end;
        OldDownloadDir:=cbDestFolder.Text;
        AppNormal;

        ok:=not Ini.ReadBool('Interface', 'ShowAddTorrentWindow', True);
        if FWatchDownloading then ok:= true;
        if ok then
          btSelectAllClick(nil)
        else begin
          HideWaitMsg;
          ok:=ShowModal = mrOk;
          if BorderStyle = bsSizeable then begin
            Ini.WriteInteger('AddTorrent', 'Width', Width);
            Ini.WriteInteger('AddTorrent', 'Height', Height);
          end;
        end;

        if ok then begin
          if IsAppHidden then
            ShowWaitMsg(Caption);
          AppBusy;
          Self.Update;

          if OldDownloadDir <> cbDestFolder.Text then begin
            TorrentAction(id, 'torrent-remove');
            id:=0;
            args:=TJSONObject.Create;
            args.Add('paused', TJSONIntegerNumber.Create(1));
            args.Add('peer-limit', TJSONIntegerNumber.Create(edPeerLimit.Value));
            args.Add('download-dir', TJSONString.Create(UTF8Decode(cbDestFolder.Text)));
            id:=_AddTorrent(args);
            if id = 0 then
              exit;
            DoRefresh(True);
            Application.ProcessMessages;
          end;

          req:=TJSONObject.Create;
          try
            req.Add('method', 'torrent-set');
            args:=TJSONObject.Create;
            args.Add('ids', TJSONArray.Create([id]));
            args.Add('peer-limit', TJSONIntegerNumber.Create(edPeerLimit.Value));

            files:=TJSONArray.Create;
            for i:=0 to lvFiles.Items.Count - 1 do
              if not FilesTree.IsFolder(i) and (FilesTree.Checked[i] = cbChecked) then
                files.Add(integer(lvFiles.Items[idxFileId, i]));
            if files.Count > 0 then
              args.Add('files-wanted', files)
            else
              files.Free;

            files:=TJSONArray.Create;
            for i:=0 to lvFiles.Items.Count - 1 do
              if not FilesTree.IsFolder(i) and (FilesTree.Checked[i] <> cbChecked) then
                files.Add(integer(lvFiles.Items[idxFileId, i]));
            if files.Count > 0 then
              args.Add('files-unwanted', files)
            else
              files.Free;

            req.Add('arguments', args);
            args:=nil;
            args:=RpcObj.SendRequest(req, False);
            if args = nil then begin
              CheckStatus(False);
              exit;
            end;
            args.Free;

//          edSaveAs.Text := Trim(edSaveAs.Text);               // leave spaces to not rename the torrent (see below)
            edSaveAs.Text := ExcludeInvalidChar(edSaveAs.Text); // Exclude prohibited characters

            if OldName <> edSaveAs.Text then begin
              // Changing torrent name
              req.Free;
              req:=TJSONObject.Create;
              req.Add('method', 'torrent-rename-path');
              args:=TJSONObject.Create;
              args.Add('ids', TJSONArray.Create([id]));
              args.Add('path', UTF8Decode(OldName));
              args.Add('name', UTF8Decode(edSaveAs.Text));
              req.Add('arguments', args);
              args:=nil;
              args:=RpcObj.SendRequest(req, False);
              if args = nil then begin
                // CheckStatus(False); // failed to rename torrent
                // exit;               // we continue work (try)
              end;
              args.Free;
            end;
          finally
            req.Free;
          end;

          if cbStartTorrent.Checked then
            TorrentAction(id, 'torrent-start');

          SelectTorrent(id, 2000);

          id:=0;
          if (Ini.ReadBool('Interface', 'DeleteTorrentFile', False) and not IsProtocolSupported(FileName)) or (FWatchDownloading) then
            DeleteFileUTF8(FileName);

          Ini.WriteInteger(IniSec, 'PeerLimit', edPeerLimit.Value);
          SaveDownloadDirs(cbDestFolder, 'LastDownloadDir');
          Result:=True;
          AppNormal;
        end;
      finally
        Free;
      end;
    finally
      if id <> 0 then
        TorrentAction(id, 'torrent-remove');
    end;
  finally
    HideWaitMsg;
    Dec(FAddingTorrent);
  end;
end;

procedure TMainForm.UpdateTray;
begin
{$ifndef CPUARM}
  TrayIcon.Visible:=not IsUnity and
    (Ini.ReadBool('Interface', 'TrayIconAlways', True)  or
    ((WindowState = wsMinimized) and Ini.ReadBool('Interface', 'TrayMinimize', True) ) or
    (not Self.Visible and Ini.ReadBool('Interface', 'TrayClose', False) )
    );
{$endif CPUARM}

{$ifdef darwin}
  acShowApp.Visible:=False;
  acHideApp.Visible:=False;
  miTSep1.Visible:=False;
{$else}
  acHideApp.Visible:=Visible and (WindowState <> wsMinimized);
{$endif darwin}
  SetRefreshInterval;
  FCalcAvg:=Ini.ReadBool('Interface', 'CalcAvg', True);
end;

procedure TMainForm.HideApp;
begin
  if WindowState <> wsMinimized then
    Hide;
  HideTaskbarButton;
  UpdateTray;
end;

procedure TMainForm.ShowApp;
var
  i: integer;
begin
  ShowTaskbarButton;
  if WindowState = wsMinimized then
    Application.Restore;
  Application.ProcessMessages;
  Show;
  Application.BringToFront;
  BringToFront;
  for i:=0 to Screen.FormCount - 1 do
    with Screen.Forms[i] do
      if fsModal in FormState then
        BringToFront;
  UpdateTray;
end;

procedure TMainForm.DownloadFinished(const TorrentName: string);
begin
{$ifndef CPUARM}
  if not TrayIcon.Visible or not Ini.ReadBool('Interface', 'TrayNotify', True) then exit;
  TrayIcon.BalloonHint:=Format(sFinishedDownload, [TorrentName]);
  TrayIcon.BalloonTitle:=sDownloadComplete;
  TrayIcon.ShowBalloonHint;
{$endif CPUARM}
end;

Procedure TMainForm.DoOpenFlagsZip(Sender: TObject; var AStream: TStream);
begin
  AStream:=TFileStreamUTF8.Create(TUnZipper(Sender).FileName, fmOpenRead or fmShareDenyWrite);
end;

Procedure TMainForm.DoCreateOutZipStream(Sender : TObject; var AStream : TStream; AItem : TFullZipFileEntry);
begin
  ForceDirectoriesUTF8(FFlagsPath);
  AStream:=TFileStreamUTF8.Create(FFlagsPath + AItem.DiskFileName, fmCreate);
end;

function TMainForm.GetFlagImage(const CountryCode: string): integer;
var
  s, ImageName: string;
  pic: TPicture;
  fs: TFileStreamUTF8;
begin
  Result:=0;
  if CountryCode = '' then exit;
  try
    ImageName:=CountryCode + '.png';
    if FFlagsPath = '' then
      FFlagsPath:=FHomeDir + 'flags' + DirectorySeparator;
    if not FileExistsUTF8(FFlagsPath + ImageName) then begin
      // Unzipping flag image
      if FUnZip = nil then begin
        s:=GetFlagsArchive;
        if s <> '' then begin
          FUnZip:=TUnZipper.Create;
          FUnZip.FileName:=s;
          FUnZip.OnOpenInputStream:=@DoOpenFlagsZip;
          FUnZip.OnCreateStream:=@DoCreateOutZipStream;
        end
        else
          exit;
      end;

      FUnZip.Files.Clear;
      FUnZip.Files.Add(ImageName);
      try
        FUnZip.UnZipAllFiles;
      except
        FreeAndNil(FUnZip);
        DeleteFileUTF8(GetFlagsArchive);
        acShowCountryFlag.Checked:=False;
        MessageDlg(sUnableExtractFlag + LineEnding + Exception(ExceptObject).Message, mtError, [mbOK], 0);
        exit;
      end;
      if not FileExistsUTF8(FFlagsPath + ImageName) then exit;
    end;

    fs:=nil;
    pic:=TPicture.Create;
    try
      fs:=TFileStreamUTF8.Create(FFlagsPath + ImageName, fmOpenRead or fmShareDenyWrite);
      pic.LoadFromStream(fs);
      if imgFlags.Count = 1 then begin
        imgFlags.Width:=pic.Width;
        imgFlags.Height:=pic.Height;
      end;
      Result:=imgFlags.AddMasked(pic.Bitmap, clNone);
    finally
      pic.Free;
      fs.Free;
    end;
  except
  end;
end;

procedure TMainForm.BeforeCloseApp;
begin
  if WindowState = wsNormal then begin
    Ini.WriteInteger('MainForm', 'Left', Left);
    Ini.WriteInteger('MainForm', 'Top', Top);
    Ini.WriteInteger('MainForm', 'Width', Width);
    Ini.WriteInteger('MainForm', 'Height', Height);
  end;
  if WindowState <> wsMinimized then
    Ini.WriteInteger('MainForm', 'State', integer(WindowState));

  if VSplitter.Visible then
    Ini.WriteInteger('MainForm', 'VSplitter', VSplitter.GetSplitterPosition);
  if HSplitter.Visible then
    Ini.WriteInteger('MainForm', 'HSplitter', HSplitter.GetSplitterPosition);

  Ini.WriteBool('MainForm', 'FilterPane', acFilterPane.Checked);
  Ini.WriteBool('MainForm', 'InfoPane', acInfoPane.Checked);
  Ini.WriteBool('MainForm', 'StatusBar', acStatusBar.Checked);

  Ini.WriteBool('MainForm', 'Menu', acMenuShow.Checked);
  Ini.WriteBool('MainForm', 'Toolbar', acToolbarShow.Checked);


  SaveColumns(gTorrents, 'TorrentsList');
  SaveColumns(lvFiles, 'FilesList');
  SaveColumns(lvPeers, 'PeerList');
  SaveColumns(lvTrackers, 'TrackersList');

  Ini.WriteBool('PeersList', 'ResolveHost', acResolveHost.Checked);
  Ini.WriteBool('PeersList', 'ResolveCountry', acResolveCountry.Checked);
  Ini.WriteBool('PeersList', 'ShowCountryFlag', acShowCountryFlag.Checked);

  if RpcObj.Connected then
    Ini.WriteInteger('Interface', 'LastRpcVersion', RpcObj.RPCVersion);

  try
    Ini.UpdateFile;
  except
    Application.HandleException(nil);
  end;

  DoDisconnect;
  Application.ProcessMessages;
end;

function TMainForm.GetGeoIpDatabase: string;
begin
  Result:=LocateFile('GeoIP.dat', [FHomeDir, ExtractFilePath(ParamStrUTF8(0))]);
end;

function TMainForm.GetFlagsArchive: string;
begin
  Result:=LocateFile('flags.zip', [FHomeDir, ExtractFilePath(ParamStrUTF8(0))]);
end;

function TMainForm.DownloadGeoIpDatabase(AUpdate: boolean): boolean;
const
  GeoLiteURL = 'https://dl.miyuru.lk/geoip/maxmind/country/maxmind4.dat.gz';
var
  tmp: string;
  gz: TGZFileStream;
  fs: TFileStreamUTF8;
  buf: array[0..65535] of byte;
  i: integer;
begin
  Result:=False;
  tmp:=SysToUTF8(GetTempDir(True)) + 'GeoIP.dat.gz';
  if not FileExistsUTF8(tmp) or AUpdate then begin
    if MessageDlg('', sGeoIPConfirm, mtConfirmation, mbYesNo, 0, mbYes) <> mrYes then
      exit;
    if not DownloadFile(GeoLiteURL, ExtractFilePath(tmp), ExtractFileName(tmp)) then
      exit;
  end;
  try
    FreeAndNil(FResolver);
    gz:=TGZFileStream.Create(tmp, gzopenread);
    try
      fs:=TFileStreamUTF8.Create(FHomeDir + 'GeoIP.dat', fmCreate);
      try
        repeat
          i:=gz.read(buf, SizeOf(buf));
          fs.WriteBuffer(buf, i);
        until i < SizeOf(buf);
      finally
        fs.Free;
      end;
    finally
      gz.Free;
    end;
    DeleteFileUTF8(tmp);
  except
    DeleteFileUTF8(FHomeDir + 'GeoIP.dat');
    DeleteFileUTF8(tmp);
    raise;
  end;
  Result:=True;
end;

procedure TMainForm.TorrentColumnsChanged;
var
  i: integer;
  s: string;
begin
  s:='';
  for i:=0 to gTorrents.Columns.Count - 1 do
    with gTorrents.Columns[i] do
      if Visible and (Width > 0) then begin
        if TorrentFieldsMap[ID - 1] <> '' then begin
          if s <> '' then
            s:=s + ',';
          s:=s + TorrentFieldsMap[ID - 1];
        end;
      end;
  RpcObj.TorrentFields:=s;
  DoRefresh(True);
end;

function TMainForm.EtaToString(ETA: integer): string;
const
  r1 = 60;
  r2 = 5*60;
  r3 = 30*60;
  r4 = 60*60;

begin
  if (ETA < 0) or (ETA = MaxInt) then
    Result:=''
  else begin
    if ETA > 2*60*60 then  // > 5 hours - round to 1 hour
      ETA:=(ETA + r4 div 2) div r4 * r4
    else
    if ETA > 2*60*60 then  // > 2 hours - round to 30 mins
      ETA:=(ETA + r3 div 2) div r3 * r3
    else
    if ETA > 30*60 then  // > 30 mins - round to 5 mins
      ETA:=(ETA + r2 div 2) div r2 * r2
    else
    if ETA > 2*60 then   // > 2 mins - round to 1 min
    ETA:=(ETA + r1 div 2) div r1 * r1;
    Result:=SecondsToString(ETA);
  end;
end;

function TMainForm.GetTorrentStatus(TorrentIdx: integer): string;
var
  i: integer;
begin
  i:=gTorrents.Items[idxStatus, TorrentIdx];
  if i = TR_STATUS_CHECK_WAIT then
    Result:=sWaiting
  else
  if i = TR_STATUS_CHECK then
    Result:=sVerifying
  else
  if i = TR_STATUS_DOWNLOAD_WAIT then
    Result:=sWaiting
  else
  if i = TR_STATUS_DOWNLOAD then
    Result:=sDownloading
  else
  if i = TR_STATUS_SEED_WAIT then
    Result:=sWaiting
  else
  if i = TR_STATUS_SEED then
    Result:=sSeeding
  else
  if i = TR_STATUS_STOPPED then
    Result:=sStopped
  else
  if i = TR_STATUS_FINISHED then
    Result:=sFinished
  else
    Result:=sUnknown;
end;

function TMainForm.GetSeedsText(Seeds, SeedsTotal: integer): string;
begin
  if SeedsTotal <> -1 then
    Result:=Format('%d/%d', [Seeds, SeedsTotal])
  else
    Result:=Format('%d', [Seeds]);
end;

function TMainForm.GetPeersText(Peers, PeersTotal, Leechers: integer): string;
begin
  Result:=Format('%d', [Peers]);
  if Leechers <> -1 then
    Result:=Format('%s/%d', [Result, Leechers]);
  Dec(PeersTotal);
  if PeersTotal >= 0 then
    Result:=Format('%s (%d)', [Result, PeersTotal]);
end;

function TMainForm.RatioToString(Ratio: double): string;
begin
  if (Ratio = MaxInt) or (Ratio = -2) then
    Result:=Utf8Encode(WideString(WideChar($221E)))
  else
    if Ratio = -1 then
      Result:=''
    else
      Result:=Format('%.3f', [Ratio]);
end;

function HumanReadableTime(ANow,AThen: TDateTime): string;
var
  Years, Months, Days, Hours, Minutes, Seconds, Discard: Word;
begin
  Try
    PeriodBetween(ANow,AThen,Years,Months,Days);
    if Sign(Anow-Athen)*CompareTime(Anow,Athen) < 0 then Dec(Days);
    DecodeDateTime(Sign(Anow-Athen)*(Anow-AThen),discard,Discard,Discard,Hours,Minutes,Seconds,Discard);
    if Years > 0 then begin
      Result := Format(sYears,[Years]) + ' ' + Format(sMonths,[Months]);
    end else if Months > 0 then begin
      Result := Format(sMonths,[Months]) + ' ' + Format(sDays,[Days]);
    end else if Days > 0 then begin
      Result := Format(sDays,[Days]) + ' ' + Format(sHours,[Hours]);
    end else if Hours > 0 then begin
      Result := Format(sHours,[Hours]) + ' ' + Format(sMins,[Minutes]);
    end else if Minutes > 0 then begin
      Result := Format(sMins,[Minutes]) + ' ' + Format(sSecs,[Seconds]);
    end else begin
      Result := Format(sSecs,[Seconds])
    end;
  Except
    Result := 'An Eternity';
  End;
end;

function TMainForm.TorrentDateTimeToString(d: Int64; FromNow: Boolean): string;
var
  s: string;
begin
  if d = 0 then
    Result:=''
  else begin
    if FromNow then
      s := HumanReadableTime(Now,UnixToDateTime(d) + GetTimeZoneDelta)
    else
      s := DateTimeToStr(UnixToDateTime(d) + GetTimeZoneDelta);
    Result := s;
  end;
end;

procedure TMainForm.DoRefresh(All: boolean);
begin
  if All then
    RpcObj.RefreshNow:=RpcObj.RefreshNow + [rtTorrents, rtDetails]
  else
    RpcObj.RefreshNow:=RpcObj.RefreshNow + [rtDetails];
end;

procedure TMainForm.acDisconnectExecute(Sender: TObject);
begin
  DoDisconnect;
end;

procedure TMainForm.acExportExecute(Sender: TObject); // PETROV
var
  s,d : string;
  FileVar1: TextFile;
  FileVar2: TextFile;
begin
  SaveDialog1.filename := 'transgui.ini';
  if SaveDialog1.Execute then begin
    s:=SaveDialog1.filename;
    d:=Ini.getFileName();

    AssignFile(FileVar1, d );
    AssignFile(FileVar2, s );

    Reset  (FileVar1);
    Rewrite(FileVar2);

    {$I+} //use exceptions
    try

    Repeat
      Readln (FileVar1,s);
      Writeln(FileVar2,s);
    Until Eof(FileVar1);

    CloseFile(FileVar1);
    CloseFile(FileVar2);
    except
    end;
    {$I-} //!use exceptions
  end;
end;

procedure TMainForm.acImportExecute(Sender: TObject);
var
  s,d : string;
  FileVar1: TextFile;
  FileVar2: TextFile;
  P,p1,p2,p3,p4: Integer;
begin
  OpenDialog1.filename := 'transgui.ini';
  if OpenDialog1.Execute then begin
    s:=OpenDialog1.filename;
    d:=Ini.getFileName();
    p1:=0;
    p2:=0;
    p3:=0;
    p4:=0;
    // check valid Ini-file
    AssignFile(FileVar2, s);
    Reset     (FileVar2);
    {$I+} //use exceptions
    try
    Repeat
      Readln (FileVar2,s);
      P := Pos ('[Hosts]',s);
      if P>0 then p1 := P;

      P := Pos ('[MainForm]',s);
      if P>0 then p2 := P;

      P := Pos ('[TorrentsList]',s);
      if P>0 then p3 := P;

      P := Pos ('ShowCountryFlag=',s);
      if P>0 then p4 := P;
    Until Eof(FileVar2);
    CloseFile(FileVar2);
    except
    end;
    {$I-} //!use exceptions

    if (p1=0) and (p2=0) and (p3=0) and (p4=0) then begin
        MessageDlg('Invalid file!', mtError, [mbOK], 0);
        exit;
    end;

    // rewrite ini-file
    s:=OpenDialog1.filename;
    AssignFile(FileVar1, s );
    AssignFile(FileVar2, d );

    Reset  (FileVar1);
    Rewrite(FileVar2);

    {$I+} //use exceptions
    try
    Repeat
      Readln (FileVar1,s);
      Writeln(FileVar2,s);
    Until Eof(FileVar1);
    CloseFile(FileVar1);
    CloseFile(FileVar2);
    except
    end;
    {$I-} //!use exceptions

    // Read ini now!
    CheckAppParams ();
    MessageDlg(sRestartRequired, mtInformation, [mbOk], 0);
  end;
end;

procedure TMainForm.acExitExecute(Sender: TObject);
begin
  BeforeCloseApp;
  Application.Terminate;
end;

procedure TMainForm.acDaemonOptionsExecute(Sender: TObject);
var
  req, args: TJSONObject;
  s: string;
  i, j: integer;
begin
  with TDaemonOptionsForm.Create(Self) do
  try
    AppBusy;
    req:=TJSONObject.Create;
    try
      req.Add('method', 'session-get');
      args:=RpcObj.SendRequest(req);
      if args <> nil then
        try
          edDownloadDir.Text:= CorrectPath(UTF8Encode(args.Strings['download-dir']));
          if RpcObj.RPCVersion >= 5 then begin
            // RPC version 5
            edPort.Value:=args.Integers['peer-port'];
            cbPEX.Checked:=args.Integers['pex-enabled'] <> 0;
            edMaxPeers.Value:=args.Integers['peer-limit-global'];
            cbRandomPort.Checked:=args.Integers['peer-port-random-on-start'] <> 0;
            cbDHT.Checked:=args.Integers['dht-enabled'] <> 0;
            cbSeedRatio.Checked:=args.Integers['seedRatioLimited'] <> 0;
            edSeedRatio.Value:=args.Floats['seedRatioLimit'];
            cbBlocklist.Checked:=args.Integers['blocklist-enabled'] <> 0;

            cbAltEnabled.Checked:=args.Integers['alt-speed-enabled'] <> 0;
            edAltDown.Value:=args.Integers['alt-speed-down'];
            edAltUp.Value:=args.Integers['alt-speed-up'];
            cbAutoAlt.Checked:=args.Integers['alt-speed-time-enabled'] <> 0;
            edAltTimeBegin.Text:=FormatDateTime('hh:nn', args.Integers['alt-speed-time-begin']/MinsPerDay);
            edAltTimeEnd.Text:=FormatDateTime('hh:nn', args.Integers['alt-speed-time-end']/MinsPerDay);
            j:=args.Integers['alt-speed-time-day'];
            for i:=1 to 7 do begin
              TCheckBox(gbAltSpeed.FindChildControl(Format('cbDay%d', [i]))).Checked:=LongBool(j and 1);
              j:=j shr 1;
            end;
            cbAutoAltClick(nil);
          end
          else begin
            // RPC versions prior to v5
            cbPortForwarding.Top:=cbRandomPort.Top;
            edPort.Value:=args.Integers['port'];
            cbPEX.Checked:=args.Integers['pex-allowed'] <> 0;
            edMaxPeers.Value:=args.Integers['peer-limit'];
            cbRandomPort.Visible:=False;
            cbDHT.Visible:=False;
            cbSeedRatio.Visible:=False;
            edSeedRatio.Visible:=False;
            btTestPort.Visible:=False;
            cbBlocklist.Visible:=False;
            gbAltSpeed.Visible:=False;
          end;

          if RpcObj.RPCVersion >= 7 then begin
            cbIncompleteDir.Checked:=args.Integers['incomplete-dir-enabled'] <> 0;
            edIncompleteDir.Text:=UTF8Encode(args.Strings['incomplete-dir']);
            cbIncompleteDirClick(nil);
          end
          else begin
            cbIncompleteDir.Visible:=False;
            edIncompleteDir.Visible:=False;
          end;

          if RpcObj.RPCVersion >= 8 then
            cbPartExt.Checked:=args.Integers['rename-partial-files'] <> 0
          else
            cbPartExt.Visible:=False;

          if RpcObj.RPCVersion >= 9 then
            cbLPD.Checked:=args.Integers['lpd-enabled'] <> 0
          else
            cbLPD.Visible:=False;

          if RpcObj.RPCVersion >= 10 then begin
            edCacheSize.Value:=args.Integers['cache-size-mb'];
            cbIdleSeedLimit.Checked:=args.Integers['idle-seeding-limit-enabled'] <> 0;
            edIdleSeedLimit.Value:=args.Integers['idle-seeding-limit'];
            cbIdleSeedLimitClick(nil);
          end
          else begin
            edCacheSize.Visible:=False;
            txCacheSize.Visible:=False;
            txMB.Visible:=False;
            cbIdleSeedLimit.Visible:=False;
            edIdleSeedLimit.Visible:=False;
            txMinutes.Visible:=False;
          end;

          if args.IndexOfName('blocklist-url') >= 0 then
            edBlocklistURL.Text:=UTF8Encode(args.Strings['blocklist-url'])
          else begin
            edBlocklistURL.Visible:=False;
            cbBlocklist.Left:=cbPEX.Left;
            cbBlocklist.Caption:=StringReplace(cbBlocklist.Caption, ':', '', [rfReplaceAll]);
          end;
          cbBlocklistClick(nil);

          if RpcObj.RPCVersion >= 13 then
            cbUTP.Checked:=args.Integers['utp-enabled'] <> 0
          else
            cbUTP.Visible:=False;

          if RpcObj.RPCVersion >= 14 then begin
            tabQueue.TabVisible:=True;
            cbDownQueue.Checked:=args.Integers['download-queue-enabled'] <> 0;
            edDownQueue.Value:=args.Integers['download-queue-size'];
            cbUpQueue.Checked:=args.Integers['seed-queue-enabled'] <> 0;
            edUpQueue.Value:=args.Integers['seed-queue-size'];
            cbStalled.Checked:=args.Integers['queue-stalled-enabled'] <> 0;
            edStalledTime.Value:=args.Integers['queue-stalled-minutes'];
          end
          else
            tabQueue.TabVisible:=False;

          cbPortForwarding.Checked:=args.Integers['port-forwarding-enabled'] <> 0;
          s:=args.Strings['encryption'];
          if s = 'preferred' then
            cbEncryption.ItemIndex:=1
          else
          if s = 'required' then
            cbEncryption.ItemIndex:=2
          else
            cbEncryption.ItemIndex:=0;
          cbMaxDown.Checked:=args.Integers['speed-limit-down-enabled'] <> 0;
          edMaxDown.Value:=args.Integers['speed-limit-down'];
          cbMaxUp.Checked:=args.Integers['speed-limit-up-enabled'] <> 0;
          edMaxUp.Value:=args.Integers['speed-limit-up'];
        finally
          args.Free;
        end
      else begin
        CheckStatus(False);
        exit;
      end;
    finally
      req.Free;
    end;
    cbMaxDownClick(nil);
    cbMaxUpClick(nil);
    cbRandomPortClick(nil);
    cbSeedRatioClick(nil);
    AppNormal;

    if ShowModal = mrOK then begin
      AppBusy;
      Self.Update;
      req:=TJSONObject.Create;
      try
        req.Add('method', 'session-set');
        args:=TJSONObject.Create;
        args.Add('download-dir', UTF8Decode(edDownloadDir.Text));
        args.Add('port-forwarding-enabled', integer(cbPortForwarding.Checked) and 1);
        case cbEncryption.ItemIndex of
          1: s:='preferred';
          2: s:='required';
          else s:='tolerated';
        end;
        args.Add('encryption', s);
        args.Add('speed-limit-down-enabled', integer(cbMaxDown.Checked) and 1);
        if cbMaxDown.Checked then
          args.Add('speed-limit-down', edMaxDown.Value);
        args.Add('speed-limit-up-enabled', integer(cbMaxUp.Checked) and 1);
        if cbMaxUp.Checked then
          args.Add('speed-limit-up', edMaxUp.Value);
        if RpcObj.RPCVersion >= 5 then begin
          args.Add('peer-limit-global', edMaxPeers.Value);
          args.Add('peer-port', edPort.Value);
          args.Add('pex-enabled', integer(cbPEX.Checked) and 1);
          args.Add('peer-port-random-on-start', integer(cbRandomPort.Checked) and 1);
          args.Add('dht-enabled', integer(cbDHT.Checked) and 1);
          args.Add('seedRatioLimited', integer(cbSeedRatio.Checked) and 1);
          if cbSeedRatio.Checked then
            args.Add('seedRatioLimit', edSeedRatio.Value);
          args.Add('blocklist-enabled', integer(cbBlocklist.Checked) and 1);

          args.Add('alt-speed-enabled', integer(cbAltEnabled.Checked) and 1);
          args.Add('alt-speed-down', edAltDown.Value);
          args.Add('alt-speed-up', edAltUp.Value);
          args.Add('alt-speed-time-enabled', integer(cbAutoAlt.Checked) and 1);
          if cbAutoAlt.Checked then begin
            args.Add('alt-speed-time-begin', Round(Frac(StrToTime(edAltTimeBegin.Text))*MinsPerDay));
            args.Add('alt-speed-time-end', Round(Frac(StrToTime(edAltTimeEnd.Text))*MinsPerDay));
            j:=0;
            for i:=7 downto 1 do begin
              j:=j shl 1;
              j:=j or (integer(TCheckBox(gbAltSpeed.FindChildControl(Format('cbDay%d', [i]))).Checked) and 1);
            end;
            args.Add('alt-speed-time-day', j);
          end;
        end
        else begin
          args.Add('peer-limit', edMaxPeers.Value);
          args.Add('port', edPort.Value);
          args.Add('pex-allowed', integer(cbPEX.Checked) and 1);
        end;
        if RpcObj.RPCVersion >= 7 then begin
          args.Add('incomplete-dir-enabled', integer(cbIncompleteDir.Checked) and 1);
          if cbIncompleteDir.Checked then
            args.Add('incomplete-dir', UTF8Decode(edIncompleteDir.Text));
        end;
        if RpcObj.RPCVersion >= 8 then
          args.Add('rename-partial-files', integer(cbPartExt.Checked) and 1);
        if RpcObj.RPCVersion >= 9 then
          args.Add('lpd-enabled', integer(cbLPD.Checked) and 1);
        if RpcObj.RPCVersion >= 10 then begin
          args.Add('cache-size-mb', edCacheSize.Value);
          args.Add('idle-seeding-limit-enabled', integer(cbIdleSeedLimit.Checked) and 1);
          args.Add('idle-seeding-limit', edIdleSeedLimit.Value);
        end;
        if edBlocklistURL.Visible then
          if cbBlocklist.Checked then
            args.Add('blocklist-url', UTF8Decode(edBlocklistURL.Text));
        if RpcObj.RPCVersion >= 13 then
          args.Add('utp-enabled', integer(cbUTP.Checked) and 1);
        if RpcObj.RPCVersion >= 14 then begin
          args.Add('download-queue-enabled', integer(cbDownQueue.Checked) and 1);
          args.Add('download-queue-size', edDownQueue.Value);
          args.Add('seed-queue-enabled', integer(cbUpQueue.Checked) and 1);
          args.Add('seed-queue-size', edUpQueue.Value);
          args.Add('queue-stalled-enabled', integer(cbStalled.Checked) and 1);
          args.Add('queue-stalled-minutes', edStalledTime.Value);
        end;

        req.Add('arguments', args);
        args:=RpcObj.SendRequest(req, False);
        if args = nil then begin
          CheckStatus(False);
          exit;
        end;
        args.Free;
      finally
        req.Free;
      end;
      RpcObj.RefreshNow:=RpcObj.RefreshNow + [rtSession];
      AppNormal;
    end;
  finally
    Free;
  end;
end;

procedure TMainForm.acQMoveBottomExecute(Sender: TObject);
begin
  TorrentAction(GetSelectedTorrents, 'queue-move-bottom');
end;

procedure TMainForm.acQMoveDownExecute(Sender: TObject);
begin
  TorrentAction(GetSelectedTorrents, 'queue-move-down');
end;

procedure TMainForm.acQMoveTopExecute(Sender: TObject);
begin
  TorrentAction(GetSelectedTorrents, 'queue-move-top');
end;

procedure TMainForm.acQMoveUpExecute(Sender: TObject);
begin
  TorrentAction(GetSelectedTorrents, 'queue-move-up');
end;

procedure TMainForm.acReannounceTorrentExecute(Sender: TObject);
begin
  TorrentAction(GetSelectedTorrents, 'torrent-reannounce');
end;

procedure TMainForm.acRemoveTorrentAndDataExecute(Sender: TObject);
begin
  InternalRemoveTorrent(sRemoveTorrentData, sRemoveTorrentDataMulti, True);
end;

procedure TMainForm.acRemoveTorrentExecute(Sender: TObject);
begin
  InternalRemoveTorrent(sRemoveTorrent, sRemoveTorrentMulti, False);
end;

procedure TMainForm.acRenameExecute(Sender: TObject);
begin
  if lvFiles.Focused then
    lvFiles.EditCell(idxFileName, lvFiles.Row)
  else
    gTorrents.EditCell(idxName, gTorrents.Row);
end;

procedure TMainForm.acResolveCountryExecute(Sender: TObject);
begin
  if not acResolveCountry.Checked then
    if GetGeoIpDatabase = '' then
      if not DownloadGeoIpDatabase(False) then
        exit;

  acResolveCountry.Checked:=not acResolveCountry.Checked;
  FreeAndNil(FResolver);
  DoRefresh;
  acShowCountryFlag.Enabled:=acResolveCountry.Checked;
end;

procedure TMainForm.acResolveHostExecute(Sender: TObject);
begin
  acResolveHost.Checked:=not acResolveHost.Checked;
  FreeAndNil(FResolver);
  DoRefresh;
end;

procedure TMainForm.acSelectAllExecute(Sender: TObject);
begin
  Application.ProcessMessages;
  if lvFiles.Focused then
    lvFiles.SelectAll
  else
    gTorrents.SelectAll;
end;

procedure TMainForm.acSetHighPriorityExecute(Sender: TObject);
begin
  Application.ProcessMessages;
  if lvFiles.Focused then
    SetCurrentFilePriority('high')
  else
    SetTorrentPriority(TR_PRI_HIGH);
end;

procedure TMainForm.acSetLowPriorityExecute(Sender: TObject);
begin
  Application.ProcessMessages;
  if lvFiles.Focused then
    SetCurrentFilePriority('low')
  else
    SetTorrentPriority(TR_PRI_LOW);
end;

procedure TMainForm.acSetNormalPriorityExecute(Sender: TObject);
begin
  Application.ProcessMessages;
  if lvFiles.Focused then
    SetCurrentFilePriority('normal')
  else
    SetTorrentPriority(TR_PRI_NORMAL);
end;

procedure TMainForm.acSetNotDownloadExecute(Sender: TObject);
begin
  SetCurrentFilePriority('skip');
end;

procedure TMainForm.acSetupColumnsExecute(Sender: TObject);
var
  g: TVarGrid;
  s: string;
begin
  Application.ProcessMessages;
  if lvTrackers.Focused then
    g:=lvTrackers
  else
  if lvPeers.Focused then
    g:=lvPeers
  else
  if lvFiles.Focused then
    g:=lvFiles
  else
    g:=gTorrents;
  if g = gTorrents then
    s:=sTorrents
  else
    if PageInfo.ActivePage = tabFiles then
      s:=FFilesCapt
    else
      s:=PageInfo.ActivePage.Caption;
  if not SetupColumns(g, 0, s) then exit;
  if g = gTorrents then
    TorrentColumnsChanged;
end;

procedure TMainForm.acShowAppExecute(Sender: TObject);
begin
  ShowApp;
end;

procedure TMainForm.acShowCountryFlagExecute(Sender: TObject);
const
  FlagsURL = 'https://raw.githubusercontent.com/transmission-remote-gui/transgui/master/flags.zip';
begin
  if not acShowCountryFlag.Checked then
    if GetFlagsArchive = '' then begin
      if MessageDlg('', sFlagArchiveConfirm, mtConfirmation, mbYesNo, 0, mbYes) <> mrYes then
        exit;
      if not DownloadFile(FlagsURL, FHomeDir) then
        exit;
    end;
  acShowCountryFlag.Checked:=not acShowCountryFlag.Checked;
  DoRefresh;
end;

procedure TMainForm.acStartAllTorrentsExecute(Sender: TObject);
begin
  TorrentAction(NULL, 'torrent-start');
end;

procedure TMainForm.acStartTorrentExecute(Sender: TObject);
begin
  TorrentAction(GetSelectedTorrents, 'torrent-start');
end;

procedure TMainForm.acStatusBarExecute(Sender: TObject);
begin
  acStatusBar.Checked:=not acStatusBar.Checked;
  StatusBar.Visible:=acStatusBar.Checked;
  if StatusBar.Visible then
      StatusBar.Top:=ClientHeight
  else
    begin
      acStatusBarSizes.Checked := true;
      acStatusBarSizesExecute(nil);
    end;
end;

procedure TMainForm.acStatusBarSizesExecute(Sender: TObject);
begin
  acStatusBarSizes.Checked := not acStatusBarSizes.Checked;
  if acStatusBarSizes.Checked then
    begin
      acStatusBar.Checked:=false;
      acStatusBarExecute(nil);
    end
      else
        begin
          StatusBar.Panels[4].Text:= '';
          StatusBar.Panels[5].Text:= '';
          StatusBar.Panels[6].Text:= '';
          StatusBar.Panels[7].Text:= '';
        end;
        Ini.WriteBool('MainForm','StatusBarSizes',acStatusBarSizes.Checked);
end;

procedure TMainForm.acStopAllTorrentsExecute(Sender: TObject);
begin
  TorrentAction(NULL, 'torrent-stop');
end;

procedure TMainForm.acStopTorrentExecute(Sender: TObject);
begin
  TorrentAction(GetSelectedTorrents, 'torrent-stop');
end;

procedure TMainForm.gTorrentsMouseMove(Sender: TObject; Shift: TShiftState; X,
  Y: Integer);
var r, c, ADatacol: integer;
begin
  ADataCol := 0;
  gTorrents.MouseToCell(x, y, c, r);
  if c>= 0 then ADataCol := gTorrents.ColToDataCol(c);
  if r = 0 then gTorrents.Hint:='';
  if (ADataCol <> FCol) or (r <> FRow) then
    begin
      FCol := ADataCol;
      FRow := r;
      case ADataCol of
      idxAddedOn, idxCompletedOn, idxLastActive:
        begin
          Application.CancelHint;
          gTorrents.Hint := TorrentDateTimeToString(gTorrents.Items[ADataCol, FRow-1],not(FFromNow));
        end
        else gTorrents.Hint:='';
      end;
    end;
end;

procedure TMainForm.gTorrentsMouseUp(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
begin
  if Button = mbRight then pmTorrents.PopUp;
end;

procedure TMainForm.LocalWatchTimerTimer(Sender: TObject);
begin
  ReadLocalFolderWatch;
  if FPendingTorrents.Count > 0 then
    begin
      FWatchDownloading := true;
      TickTimerTimer(nil);
    end;
end;


procedure TMainForm.lvFilesMouseUp(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
begin
  if Button = mbRight then pmFiles.PopUp;
end;

procedure TMainForm.MenuShowExecute(Sender: TObject);
begin
  acMenuShow.Checked:=not acMenuShow.Checked;
  if acMenuShow.Checked = false then
    MainForm.Menu := nil
  else
    MainForm.Menu := MainMenu;
end;

procedure TMainForm.acToolbarShowExecute(Sender: TObject);
begin
  acToolbarShow.Checked:=not acToolbarShow.Checked;
  if acToolbarShow.Checked = false then
    MainToolBar.Visible:= false
  else
    MainToolBar.Visible:= true;
end;

procedure TMainForm.acTorrentPropsExecute(Sender: TObject);
begin
  TorrentProps(0);
end;

procedure TMainForm.TorrentProps(PageNo: integer);
const
  TR_RATIOLIMIT_GLOBAL    = 0; // follow the global settings
  TR_RATIOLIMIT_SINGLE    = 1; // override the global settings, seeding until a certain ratio
  TR_RATIOLIMIT_UNLIMITED = 2; // override the global settings, seeding regardless of ratio

  TR_IDLELIMIT_GLOBAL     = 0; // follow the global settings
  TR_IDLELIMIT_SINGLE     = 1; // override the global settings, seeding until a certain idle time
  TR_IDLELIMIT_UNLIMITED  = 2; // override the global settings, seeding regardless of activity

var
  req, args, t, tr: TJSONObject;
  i, j, id: integer;
  ids, Trackers, AddT, EditT, DelT: TJSONArray;
  TorrentIds: variant;
  s: string;
  trlist, sl: TStringList;
begin
  gTorrentsClick(nil);
  id:=RpcObj.CurTorrentId;
  if id = 0 then exit;
  AppBusy;
  trlist:=nil;
  with TTorrPropsForm.Create(Self) do
  try
    Page.ActivePageIndex:=PageNo;
    gTorrents.Tag:=1;
    gTorrents.EnsureSelectionVisible;
    TorrentIds:=GetSelectedTorrents;
    args:=RpcObj.RequestInfo(id, ['downloadLimit', 'downloadLimitMode', 'downloadLimited', 'uploadLimit', 'uploadLimitMode', 'uploadLimited',
                                  'name', 'maxConnectedPeers', 'seedRatioMode', 'seedRatioLimit', 'seedIdleLimit', 'seedIdleMode', 'trackers']);
    if args = nil then begin
      CheckStatus(False);
      exit;
    end;
    try
      t:=args.Arrays['torrents'].Objects[0];

      if gTorrents.SelCount > 1 then
        s:=Format(sSeveralTorrents, [gTorrents.SelCount])
      else
        s:=UTF8Encode(t.Strings['name']);

      txName.Caption:=txName.Caption + ' ' + s;
      Caption:=Caption + ' - ' + s;
      if RpcObj.RPCVersion < 5 then begin
        // RPC versions prior to v5
        j:=t.Integers['downloadLimitMode'];
        cbMaxDown.Checked:=j = TR_SPEEDLIMIT_SINGLE;
        i:=t.Integers['downloadLimit'];
        if (i < 0) or (j = TR_SPEEDLIMIT_UNLIMITED) then
          edMaxDown.ValueEmpty:=True
        else
          edMaxDown.Value:=i;

        j:=t.Integers['uploadLimitMode'];
        cbMaxUp.Checked:=j = TR_SPEEDLIMIT_SINGLE;
        i:=t.Integers['uploadLimit'];
        if (i < 0) or (j = TR_SPEEDLIMIT_UNLIMITED) then
          edMaxUp.ValueEmpty:=True
        else
          edMaxUp.Value:=i;
        cbSeedRatio.Visible:=False;
        edSeedRatio.Visible:=False;
      end else begin
        // RPC version 5
        cbMaxDown.Checked:=t.Booleans['downloadLimited'];
        i:=t.Integers['downloadLimit'];
        if i < 0 then
          edMaxDown.ValueEmpty:=True
        else
          edMaxDown.Value:=i;

        cbMaxUp.Checked:=t.Booleans['uploadLimited'];
        i:=t.Integers['uploadLimit'];
        if i < 0 then
          edMaxUp.ValueEmpty:=True
        else
          edMaxUp.Value:=i;

        case t.Integers['seedRatioMode'] of
          TR_RATIOLIMIT_SINGLE:
            cbSeedRatio.State:=cbChecked;
          TR_RATIOLIMIT_UNLIMITED:
            cbSeedRatio.State:=cbUnchecked;
          else
            cbSeedRatio.State:=cbGrayed;
        end;
        edSeedRatio.Value:=t.Floats['seedRatioLimit'];
      end;

      if RpcObj.RPCVersion >= 10 then begin
        case t.Integers['seedIdleMode'] of
          TR_IDLELIMIT_SINGLE:
            cbIdleSeedLimit.State:=cbChecked;
          TR_IDLELIMIT_UNLIMITED:
            cbIdleSeedLimit.State:=cbUnchecked;
          else
            cbIdleSeedLimit.State:=cbGrayed;
        end;
        edIdleSeedLimit.Value:=t.Integers['seedIdleLimit'];
        cbIdleSeedLimitClick(nil);

        trlist:=TStringList.Create;
        Trackers:=t.Arrays['trackers'];
        for i:=0 to Trackers.Count - 1 do begin
          tr:=Trackers[i] as TJSONObject;
            trlist.AddObject(UTF8Decode(tr.Strings['announce']), TObject(PtrUInt(tr.Integers['id'])));
        end;
        edTrackers.Lines.Assign(trlist);
      end
      else begin
        cbIdleSeedLimit.Visible:=False;
        edIdleSeedLimit.Visible:=False;
        txMinutes.Visible:=False;
        tabAdvanced.TabVisible:=False;
      end;
      edPeerLimit.Value:=t.Integers['maxConnectedPeers'];
    finally
      args.Free;
    end;
    cbMaxDownClick(nil);
    cbMaxUpClick(nil);
    cbSeedRatioClick(nil);
    AppNormal;
    if ShowModal = mrOk then begin
      AppBusy;
      Self.Update;
      req:=TJSONObject.Create;
      try
        req.Add('method', 'torrent-set');
        args:=TJSONObject.Create;
        ids:=TJSONArray.Create;
        for i:=VarArrayLowBound(TorrentIds, 1) to VarArrayHighBound(TorrentIds, 1) do
          ids.Add(integer(TorrentIds[i]));
        args.Add('ids', ids);

        if RpcObj.RPCVersion < 5 then
        begin
          // RPC versions prior to v5
          args.Add('speed-limit-down-enabled', integer(cbMaxDown.Checked) and 1);
          args.Add('speed-limit-up-enabled', integer(cbMaxUp.Checked) and 1);
          if cbMaxDown.Checked then
            args.Add('speed-limit-down', edMaxDown.Value);
          if cbMaxUp.Checked then
            args.Add('speed-limit-up', edMaxUp.Value);
        end else begin
          // RPC version 5
          args.Add('downloadLimited', integer(cbMaxDown.Checked) and 1);
          args.Add('uploadLimited', integer(cbMaxUp.Checked) and 1);
          if cbMaxDown.Checked then
            args.Add('downloadLimit', edMaxDown.Value);
          if cbMaxUp.Checked then
            args.Add('uploadLimit', edMaxUp.Value);
          case cbSeedRatio.State of
            cbChecked:
              i:=TR_RATIOLIMIT_SINGLE;
            cbUnchecked:
              i:=TR_RATIOLIMIT_UNLIMITED;
            else
              i:=TR_RATIOLIMIT_GLOBAL;
          end;
          args.Add('seedRatioMode', i);
          if cbSeedRatio.State = cbChecked then
            args.Add('seedRatioLimit', edSeedRatio.Value);
        end;

        if RpcObj.RPCVersion >= 10 then begin
          case cbIdleSeedLimit.State of
            cbChecked:
              i:=TR_IDLELIMIT_SINGLE;
            cbUnchecked:
              i:=TR_IDLELIMIT_UNLIMITED;
            else
              i:=TR_IDLELIMIT_GLOBAL;
          end;
          args.Add('seedIdleMode', i);
          if cbIdleSeedLimit.State = cbChecked then
            args.Add('seedIdleLimit', edIdleSeedLimit.Value);

          sl:=TStringList.Create;
          try
            sl.Assign(edTrackers.Lines);
            // Removing unchanged trackers
            i:=0;
            while i < sl.Count do begin
              s:=Trim(sl[i]);
              if s = '' then begin
                sl.Delete(i);
                continue;
              end;
              j:=trlist.IndexOf(s);
              if j >= 0 then begin
                trlist.Delete(j);
                sl.Delete(i);
                continue;
              end;
              Inc(i);
            end;

            AddT:=TJSONArray.Create;
            EditT:=TJSONArray.Create;
            DelT:=TJSONArray.Create;
            try
              for i:=0 to sl.Count - 1 do begin
                s:=Trim(sl[i]);
                if trlist.Count > 0 then begin
                  EditT.Add(PtrUInt(trlist.Objects[0]));
                  EditT.Add(UTF8Decode(s));
                  trlist.Delete(0);
                end
                else
                  AddT.Add(UTF8Decode(s));
              end;

              for i:=0 to trlist.Count - 1 do
                DelT.Add(PtrUInt(trlist.Objects[i]));

              if AddT.Count > 0 then begin
                args.Add('trackerAdd', AddT);
                AddT:=nil;
              end;
              if EditT.Count > 0 then begin
                args.Add('trackerReplace', EditT);
                EditT:=nil;
              end;
              if DelT.Count > 0 then begin
                args.Add('trackerRemove', DelT);
                DelT:=nil;
              end;
            finally
              DelT.Free;
              EditT.Free;
              AddT.Free;
            end;
          finally
            sl.Free;
          end;
        end;

        args.Add('peer-limit', edPeerLimit.Value);
        req.Add('arguments', args);
        args:=nil;
        args:=RpcObj.SendRequest(req, False);
        if args = nil then begin
          CheckStatus(False);
          exit;
        end;
        args.Free;
      finally
        req.Free;
      end;
      DoRefresh;
      AppNormal;
    end;
  finally
    gTorrents.Tag:=0;
    Free;
    trlist.Free;
  end;
end;

procedure TMainForm.acTrackerGroupingExecute(Sender: TObject);
begin
  acTrackerGrouping.Checked:=not acTrackerGrouping.Checked;
  Ini.WriteBool('Interface', 'TrackerGrouping', acTrackerGrouping.Checked);
  RpcObj.RefreshNow:=RpcObj.RefreshNow + [rtTorrents];
end;

procedure TMainForm.acUpdateBlocklistExecute(Sender: TObject);
var
  req: TJSONObject;
  res: TJSONObject;
begin
  Application.ProcessMessages;
  AppBusy;
  req:=TJSONObject.Create;
  try
    req.Add('method', 'blocklist-update');
    res:=RpcObj.SendRequest(req, True, 3*60000);
    AppNormal;
    if res = nil then begin
      CheckStatus(False);
      exit;
    end;
    MessageDlg(Format(sBlocklistUpdateComplete, [res.Integers[('blocklist-size')]]), mtInformation, [mbOK], 0);
    res.Free;
  finally
    req.Free;
  end;
end;

procedure TMainForm.acUpdateGeoIPExecute(Sender: TObject);
begin
  if DownloadGeoIpDatabase(True) then
    MessageDlg(sUpdateComplete, mtInformation, [mbOK], 0);
end;

procedure TMainForm.acVerifyTorrentExecute(Sender: TObject);
var
  ids: variant;
  s: string;
begin
  if gTorrents.Items.Count = 0 then exit;
  gTorrents.Tag:=1;
  try
    gTorrents.EnsureSelectionVisible;
    ids:=GetSelectedTorrents;
    if gTorrents.SelCount < 2 then
      s:=Format(sTorrentVerification, [UTF8Encode(widestring(gTorrents.Items[idxName, gTorrents.Items.IndexOf(idxTorrentId, ids[0])]))])
    else
      s:=Format(sTorrentsVerification, [gTorrents.SelCount]);
    if MessageDlg('', s, mtConfirmation, mbYesNo, 0, mbNo) <> mrYes then
      exit;
  finally
    gTorrents.Tag:=0;
  end;
  TorrentAction(ids, 'torrent-verify');
end;

procedure TMainForm.ApplicationPropertiesEndSession(Sender: TObject);
begin
  DeleteFileUTF8(FRunFileName);
  BeforeCloseApp;
end;

procedure TMainForm.ApplicationPropertiesException(Sender: TObject; E: Exception);
var
  msg: string;
{$ifdef CALLSTACK}
  sl: TStringList;
{$endif CALLSTACK}
begin
  ForceAppNormal;
  msg:=E.Message;
{$ifdef CALLSTACK}
  try
    sl:=TStringList.Create;
    try
      sl.Text:=GetLastExceptionCallStack;
      Clipboard.AsText:=msg + LineEnding + sl.Text;
      DebugLn(msg + LineEnding + sl.Text);
      if sl.Count > 20 then begin
        while sl.Count > 20 do
          sl.Delete(20);
        sl.Add('...');
      end;
      msg:=msg + LineEnding + '---' + LineEnding + 'The error details has been copied to the clipboard.' + LineEnding + '---';
      msg:=msg + LineEnding + sl.Text;
    finally
      sl.Free;
    end;
  except
    ; // suppress exception
  end;
{$endif CALLSTACK}
  MessageDlg(TranslateString(msg, True), mtError, [mbOK], 0);
end;

procedure TMainForm.ApplicationPropertiesIdle(Sender: TObject; var Done: Boolean);
begin
  UpdateUI;
{$ifdef LCLcarbon}
  CheckSynchronize;
{$endif LCLcarbon}
  Done:=True;
end;

procedure TMainForm.ApplicationPropertiesMinimize(Sender: TObject);
begin
{$ifdef CPUARM}
  exit;
{$endif  CPUARM}

{$ifndef darwin}
  if not IsUnity and Ini.ReadBool('Interface', 'TrayMinimize', True) then
    HideApp;
{$endif darwin}
  UpdateTray;
end;

procedure TMainForm.ApplicationPropertiesRestore(Sender: TObject);
begin
  UpdateTray;
  CheckClipboardLink;
end;

procedure TMainForm.edSearchChange(Sender: TObject);
begin
  DoRefresh(True);
  if edSearch.Text=  '' then tbSearchCancel.Enabled:=false
                    else tbSearchCancel.Enabled:=true;
end;

procedure TMainForm.edSearchKeyDown(Sender: TObject; var Key: Word;
  Shift: TShiftState);
begin
    if Key = VK_ESCAPE then edSearch.Text:='';
end;

procedure TMainForm.FormActivate(Sender: TObject);
begin
  CheckClipboardLink;
end;

procedure TMainForm.FormDropFiles(Sender: TObject; const FileNames: array of String);
var
  i: integer;
begin
  for i:=Low(FileNames) to High(FileNames) do
    AddTorrentFile(FileNames[i]);
end;

procedure TMainForm.FormKeyDown(Sender: TObject; var Key: Word;
  Shift: TShiftState);
var
  Keypressed: Word;
begin
    if (Shift = [ssAlt]) and not (MainForm.ActiveControl is TVarGridStringEditor) then
    begin
      Keypressed := Key;
      Key := 0;
        case Keypressed of
          VK_S: edSearch.SetFocus;
          VK_G: PageInfo.PageIndex:=0;
          VK_K: PageInfo.PageIndex:=1;
          VK_P: PageInfo.PageIndex:=2;
          VK_F: PageInfo.PageIndex:=3;
          VK_1: lvFilter.Row:=fltAll;
          VK_2: lvFilter.Row:=fltDown;
          VK_3: lvFilter.Row:=fltDone;
          VK_4: lvFilter.Row:=fltActive;
          VK_5: lvFilter.Row:=fltInactive;
          VK_6: lvFilter.Row:=fltStopped;
          VK_7: lvFilter.Row:=fltError;
          VK_8: lvFilter.Row:=fltWaiting;
        else Key := KeyPressed;
        end;
    end;

end;

procedure TMainForm.FormWindowStateChange(Sender: TObject);
begin
{$ifdef lclgtk2}
  if WindowState = wsMinimized then
    ApplicationPropertiesMinimize(nil)
  else
    ApplicationPropertiesRestore(nil);
{$endif lclgtk2}
end;

procedure TMainForm.gTorrentsCellAttributes(Sender: TVarGrid; ACol, ARow, ADataCol: integer; AState: TGridDrawState;
                                            var CellAttribs: TCellAttributes);
var
  j: integer;
begin
  if ARow < 0 then exit;
  with CellAttribs do begin
    if ACol = gTorrents.FirstVisibleColumn then
      ImageIndex:=integer(Sender.Items[idxStateImg, ARow]);
    if Text = '' then exit;
    if not VarIsEmpty(Sender.Items[idxDeleted, ARow]) then
      with Sender.Canvas.Font do
        Style:=Style + [fsStrikeOut];
    case ADataCol of
      idxStatus:
        Text:=GetTorrentStatus(ARow);
      idxSize, idxDownloaded, idxUploaded, idxSizeToDowload, idxSizeLeft:
        Text:=GetHumanSize(Sender.Items[ADataCol, ARow], 0, '?');
      idxDone:
        Text:=Format('%.1f%%', [double(Sender.Items[idxDone, ARow])]);
      idxSeeds:
        if not VarIsNull(Sender.Items[idxSeedsTotal, ARow]) then
          Text:=GetSeedsText(Sender.Items[idxSeeds, ARow], Sender.Items[idxSeedsTotal, ARow]);
      idxPeers:
        Text:=GetPeersText(Sender.Items[idxPeers, ARow], -1, Sender.Items[idxLeechersTotal, ARow]);
      idxDownSpeed, idxUpSpeed:
        begin
          j:=Sender.Items[ADataCol, ARow];
          if j > 0 then
            Text:=GetHumanSize(j, 1) + sPerSecond
          else
            Text:='';
        end;
      idxETA:
        Text:=EtaToString(Sender.Items[idxETA, ARow]);
      idxRatio:
        Text:=RatioToString(Sender.Items[idxRatio, ARow]);
      idxAddedOn, idxCompletedOn, idxLastActive:
        Text:=TorrentDateTimeToString(Sender.Items[ADataCol, ARow],FFromNow);
      idxPriority:
        Text:=PriorityToStr(Sender.Items[ADataCol, ARow], ImageIndex);
      idxQueuePos:
        begin
          j:=Sender.Items[ADataCol, ARow];
          if j >= FinishedQueue then
            Dec(j, FinishedQueue);
          Text:=IntToStr(j);
        end;
      idxSeedingTime:
        begin
          j:=Sender.Items[idxSeedingTime, ARow];
          if j > 0 then
            Text:=EtaToString(j)
          else
            Text:='';
        end;
      idxPrivate:
        begin
          j:=Sender.Items[idxPrivate, ARow];
          if j >= 1 then
            Text:=sPrivateOn
          else
            Text:=sPrivateOff;
        end;
    end;
  end;
end;

procedure TMainForm.gTorrentsClick(Sender: TObject);
var
  i: integer;
begin
  if gTorrents.Tag <> 0 then exit;
  RpcObj.Lock;
  try
    if gTorrents.Items.Count > 0 then
      i:=gTorrents.Items[idxTorrentId, gTorrents.Row]
    else
      i:=0;
    if RpcObj.CurTorrentId = i then
      exit;
    RpcObj.CurTorrentId:=i;
  finally
    RpcObj.Unlock;
    if acStatusBarSizes.Checked then StatusBarSizes;
  end;

  ClearDetailsInfo(GetPageInfoType(PageInfo.ActivePage));

  TorrentsListTimer.Enabled:=False;
  TorrentsListTimer.Enabled:=True;
end;

procedure TMainForm.gTorrentsDblClick(Sender: TObject);
var
  res: TJSONObject;
  s, n: string;
begin
  if gTorrents.Items.Count = 0 then
    exit;
  if gTorrents.Items[idxDone, gTorrents.Row] = 100.0 then begin
    // The torrent is finished. Check if it is possible to open its file/folder
    AppBusy;
    try
      res:=RpcObj.RequestInfo(gTorrents.Items[idxTorrentId, gTorrents.Row], ['downloadDir']);
      if res = nil then
        CheckStatus(False);
      with res.Arrays['torrents'].Objects[0] do
        n:=IncludeProperTrailingPathDelimiter(UTF8Encode(Strings['downloadDir'])) + UTF8Encode(widestring(gTorrents.Items[idxName, gTorrents.Row]));
      s:=MapRemoteToLocal(n);
      if s = '' then
        s:=n;
      if FileExistsUTF8(s) or DirectoryExistsUTF8(s) then begin
        // File/folder exists - open it
        OpenCurrentTorrent(False);
        exit;
      end;
    finally
      AppNormal;
    end;
  end;
  acTorrentProps.Execute;
end;

procedure TMainForm.gTorrentsDrawCell(Sender: TVarGrid; ACol, ARow, ADataCol: integer; AState: TGridDrawState; const R: TRect; var ADefaultDrawing: boolean);
begin
  if ARow < 0 then exit;
  if ADataCol = idxDone then begin
    ADefaultDrawing:=False;
    DrawProgressCell(Sender, ACol, ARow, ADataCol, AState, R);
  end;
end;

procedure TMainForm.gTorrentsEditorHide(Sender: TObject);
begin
  gTorrents.Tag:=0;
end;

procedure TMainForm.gTorrentsEditorShow(Sender: TObject);
begin
  gTorrents.Tag:=1;
  gTorrents.RemoveSelection;
end;

procedure TMainForm.gTorrentsQuickSearch(Sender: TVarGrid; var SearchText: string; var ARow: integer);
var
  i: integer;
  s: string;
  v: variant;
begin
  s:=UTF8UpperCase(SearchText);
  for i:=ARow to gTorrents.Items.Count - 1 do begin
    v:=gTorrents.Items[idxName, i];
    if VarIsEmpty(v) or VarIsNull(v) then
      continue;
    if Pos(s, Trim(UTF8UpperCase(UTF8Encode(widestring(v))))) > 0 then begin
      ARow:=i;
      break;
    end;
  end;
end;

procedure TMainForm.gTorrentsResize(Sender: TObject);
begin
  if not FStarted then begin
    VSplitter.SetSplitterPosition(Ini.ReadInteger('MainForm', 'VSplitter', VSplitter.GetSplitterPosition));
    HSplitter.SetSplitterPosition(Ini.ReadInteger('MainForm', 'HSplitter', HSplitter.GetSplitterPosition));
  end;
end;

procedure TMainForm.gTorrentsSetEditText(Sender: TObject; ACol, ARow: Integer; const Value: string);
begin
  if RenameTorrent(gTorrents.Items[idxTorrentId, ARow], UTF8Encode(widestring(gTorrents.Items[idxName, ARow])), Trim(Value)) then begin
    gTorrents.Items[idxName, ARow]:=UTF8Decode(Trim(Value));
    FFilesTree.Clear;
  end;
end;

procedure TMainForm.gTorrentsSortColumn(Sender: TVarGrid; var ASortCol: integer);
begin
  if ASortCol = idxSeeds then
    ASortCol:=idxSeedsTotal;
  if ASortCol = idxPeers then
    ASortCol:=idxLeechersTotal;
end;

procedure TMainForm.HSplitterChangeBounds(Sender: TObject);
begin
{$ifdef windows}
  Update;
{$endif windows}
end;

procedure TMainForm.lvFilesDblClick(Sender: TObject);
begin
  acOpenFile.Execute;
end;

procedure TMainForm.lvFilesEditorHide(Sender: TObject);
begin
  gTorrents.Tag:=0;
  lvFiles.Tag:=0;
  lvFiles.HideSelection:=True;
end;

procedure TMainForm.lvFilesEditorShow(Sender: TObject);
begin
  gTorrents.Tag:=1;
  lvFiles.Tag:=1;
  lvFiles.RemoveSelection;
  lvFiles.HideSelection:=False;
end;

procedure TMainForm.lvFilesSetEditText(Sender: TObject; ACol, ARow: Integer; const Value: string);
var
  p: string;
  i, lvl, len: integer;
begin
  p:=FFilesTree.GetFullPath(ARow, False);
  if RenameTorrent(gTorrents.Items[idxTorrentId, gTorrents.Row], p, Trim(Value)) then begin
    FFiles[idxFileName, ARow]:=UTF8Decode(Trim(Value));
    if FFilesTree.IsFolder(ARow) then begin
      // Updating path for child elements
      len:=Length(p);
      p:=ExtractFilePath(p) + Trim(Value);
      lvl:=FFilesTree.RowLevel[ARow];
      FFiles.BeginUpdate;
      try
        FFiles[idxFileFullPath, ARow]:=UTF8Decode(p + RemotePathDelimiter);
        for i:=ARow + 1 to FFiles.Count - 1 do
          if FFilesTree.RowLevel[i] > lvl then
            FFiles[idxFileFullPath, i]:=UTF8Decode(p + Copy(UTF8Encode(widestring(FFiles[idxFileFullPath, i])), len + 1, MaxInt))
          else
            break;
      finally
        FFiles.EndUpdate;
      end;
    end;
  end;
end;

procedure TMainForm.lvFilterCellAttributes(Sender: TVarGrid; ACol, ARow, ADataCol: integer; AState: TGridDrawState; var CellAttribs: TCellAttributes);
begin
  if ARow < 0 then exit;
  with CellAttribs do begin
    case ARow of
      0: ImageIndex:=imgAll;
      1: ImageIndex:=imgDown;
      2: ImageIndex:=imgSeed;
      3: ImageIndex:=imgActive;
      4: ImageIndex:=imgInactive;
      5: ImageIndex:=imgStopped;
      6: ImageIndex:=imgError;
      7: ImageIndex:=imgWaiting
      else
        if Text <> '' then
          if VarIsNull(Sender.Items[-1, ARow]) then
            ImageIndex:=5
          else
            ImageIndex:=22;
    end;
  end;
end;

procedure TMainForm.lvFilterClick(Sender: TObject);
begin
  if VarIsNull(lvFilter.Items[0, lvFilter.Row]) then
    if (FLastFilerIndex > lvFilter.Row) or (lvFilter.Row = lvFilter.Items.Count - 1) then
      lvFilter.Row:=lvFilter.Row - 1
    else
      lvFilter.Row:=lvFilter.Row + 1;
  FLastFilerIndex:=lvFilter.Row;
  FilterTimer.Enabled:=False;
  FilterTimer.Enabled:=True;
end;

procedure TMainForm.lvFilterDrawCell(Sender: TVarGrid; ACol, ARow, ADataCol: integer; AState: TGridDrawState; const R: TRect;
  var ADefaultDrawing: boolean);
var
  i: integer;
  RR: TRect;
begin
  ADefaultDrawing:=not VarIsNull(Sender.Items[0, ARow]);
  if ADefaultDrawing then exit;

  with lvFilter.Canvas do begin
    Brush.Color:=lvFilter.Color;
    FillRect(R);
    i:=(R.Bottom + R.Top) div 2;
    Brush.Color:=clBtnFace;
    RR:=R;
    InflateRect(RR, -4, 0);
    RR.Top:=i - 1;
    RR.Bottom:=i + 1;
    FillRect(RR);
  end;
end;

procedure TMainForm.lvPeersCellAttributes(Sender: TVarGrid; ACol, ARow, ADataCol: integer; AState: TGridDrawState; var CellAttribs: TCellAttributes);
var
  i: integer;
begin
  if ARow < 0 then exit;
  with CellAttribs do begin
    if Text = '' then exit;
    if ACol = 0 then begin
      ImageIndex:=Sender.Items[idxPeerCountryImage, ARow];
      if ImageIndex = 0 then
        ImageIndex:=-1;
    end;
    case ADataCol of
      idxPeerDone:
        Text:=Format('%.1f%%', [double(Sender.Items[ADataCol, ARow])*100.0]);
      idxPeerDownSpeed, idxPeerUpSpeed:
        begin
          i:=Sender.Items[ADataCol, ARow];
          if i > 0 then
            Text:=GetHumanSize(i, 1) + sPerSecond
          else
            Text:='';
        end;
    end;
  end;
end;

procedure TMainForm.lvTrackersCellAttributes(Sender: TVarGrid; ACol, ARow, ADataCol: integer; AState: TGridDrawState; var CellAttribs: TCellAttributes);
var
  f: double;
begin
  if ARow < 0 then exit;
  with CellAttribs do begin
    if Text = '' then exit;
    case ADataCol of
      idxTrackersListSeeds:
        if lvTrackers.Items[ADataCol, ARow] < 0 then
          Text:='';
      idxTrackersListUpdateIn:
        begin
          f:=double(lvTrackers.Items[ADataCol, ARow]);
          if f = 0 then
            Text:='-'
          else
          if f = 1 then
            Text:=sUpdating
          else
            Text:=SecondsToString(Trunc(f));
        end;
    end;
  end;
end;

procedure TMainForm.lvTrackersDblClick(Sender: TObject);
begin
  acEditTracker.Execute;
end;

procedure TMainForm.lvTrackersKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
begin
  if Key = VK_DELETE then begin
    Key:=0;
    acDelTracker.Execute;
  end;
end;

procedure TMainForm.goDevelopmentSiteClick(Sender: TObject);
begin
  goGitHub;
end;

procedure TMainForm.MainToolBarContextPopup(Sender: TObject; MousePos: TPoint;
  var Handled: Boolean);
begin
    acBigToolBar.Execute;
end;

procedure TMainForm.MenuItem101Click(Sender: TObject);
var
  req, args, tt: TJSONObject;
  ids, t: TJSONArray;
  i: Integer;
  TorrentIds: Variant;
  Magnets: TStringList;
begin
  TorrentIds:=GetSelectedTorrents;
  req:=TJSONObject.Create;
  args:=TJSONObject.Create;
  Magnets:=TStringList.Create;
  try
    req.Add('method', 'torrent-get');
    ids:=TJSONArray.Create;
    for i:=VarArrayLowBound(TorrentIds, 1) to VarArrayHighBound(TorrentIds, 1) do
      ids.Add(integer(TorrentIds[i]));
    args.Add('ids', ids);
    args.Add('fields', TJSONArray.Create(['magnetLink']));
    req.Add('arguments', args);
    args:=RpcObj.SendRequest(req);
    if args = nil then begin
      CheckStatus(False);
      exit;
    end;
    t:=TJSONArray.Create;
    t:=args.Arrays['torrents'];
    for i:= 0 to t.Count-1 do
      begin
        tt:=t.Objects[i] as TJSONObject;
        Magnets.add(tt.Strings['magnetLink']);
      end;
    FLastClipboardLink := Magnets.Text;   // To Avoid TransGUI detect again this existing links
    Clipboard.AsText := Magnets.Text;
  finally
    req.Free;
    args.Free;
    Magnets.Free;
  end;
end;

procedure TMainForm.miHomePageClick(Sender: TObject);
begin
  GoHomePage;
end;

procedure TMainForm.PageInfoResize(Sender: TObject);
begin
  if FDetailsWait.Visible then
    CenterDetailsWait;
end;

procedure TMainForm.panReconnectResize(Sender: TObject);
begin
  panReconnectFrame.BoundsRect:=panReconnect.ClientRect;
end;

procedure TMainForm.pbDownloadedPaint(Sender: TObject);
begin
  if FTorrentProgress <> nil then
    pbDownloaded.Canvas.StretchDraw(pbDownloaded.ClientRect, FTorrentProgress);
end;

procedure TMainForm.StatusBarMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
var
  pt: TPoint;
  rb: boolean;
begin
  rb:=(Button = mbRight) and RpcObj.Connected;
  pt:=StatusBar.ClientToScreen(Point(X, Y));
  case StatusBar.GetPanelIndexAt(X, Y) of
    0: if Button = mbLeft then
        acConnOptions.Execute;
    1: if rb then
        pmDownSpeeds.PopUp(pt.X, pt.Y);
    2: if rb then
        pmUpSpeeds.PopUp(pt.X, pt.Y);
  end;
end;

{$ifdef LCLcarbon}
type
  THackApplication = class(TApplication)
  end;
{$endif LCLcarbon}

procedure TMainForm.TickTimerTimer(Sender: TObject);
var
  i: integer;
begin
  TickTimer.Enabled:=False;
  try
    if not FStarted then begin
      Application.ProcessMessages;
      FStarted:=True;
      acConnect.Execute;
      Application.ProcessMessages;
      panTransfer.ChildSizing.Layout:=cclLeftToRightThenTopToBottom;
      panGeneralInfo.ChildSizing.Layout:=cclLeftToRightThenTopToBottom;
      panTransfer.ChildSizing.Layout:=cclNone;
      panGeneralInfo.ChildSizing.Layout:=cclNone;
      with panTransfer do
        ClientHeight:=Controls[ControlCount - 1].BoundsRect.Bottom + ChildSizing.TopBottomSpacing;
      with panGeneralInfo do
        ClientHeight:=Controls[ControlCount - 1].BoundsRect.Bottom + ChildSizing.TopBottomSpacing;
      panSearch.AutoSize:=False;

      if Ini.ReadBool('MainForm', 'FirstRun', True) then begin
        if not acResolveCountry.Checked then
          acResolveCountry.Execute;
        if acResolveCountry.Checked and not acShowCountryFlag.Checked then
          acShowCountryFlag.Execute;
        Ini.WriteBool('MainForm', 'FirstRun', False);
      end;

      i:=Ini.ReadInteger('Interface', 'LastNewVersionCheck', Trunc(Now));
      if i + Ini.ReadInteger('Interface', 'CheckNewVersionDays', 5) <= Trunc(Now) then begin
        if Ini.ReadBool('Interface', 'AskCheckNewVersion', True) then begin
          Ini.WriteBool('Interface', 'AskCheckNewVersion', False);
          if not Ini.ReadBool('Interface', 'CheckNewVersion', False) then
            if MessageDlg(Format(SCheckNewVersion, [AppName]), mtConfirmation, mbYesNo, 0) = mrYes then
              Ini.WriteBool('Interface', 'CheckNewVersion', True);
        end;
        if Ini.ReadBool('Interface', 'CheckNewVersion', False) then
          CheckNewVersion;
      end;
    end;

    CheckAddTorrents;

    if RpcObj.Connected then
      FReconnectTimeOut:=0
    else
      if panReconnect.Visible then
        if Now - FReconnectWaitStart >= FReconnectTimeOut/SecsPerDay then
          DoConnect
        else
          begin
          txReconnectSecs.Caption:=Format(sReconnect, [FReconnectTimeOut - Round(SecsPerDay*(Now - FReconnectWaitStart))]);
          panReconnect.Constraints.MinWidth:=450;
          end;

    if FSlowResponse.Visible then begin
      if RpcObj.RequestStartTime = 0 then
        FSlowResponse.Visible:=False;
    end
    else
      if (RpcObj.RequestStartTime <> 0) and (Now - RpcObj.RequestStartTime >= 1/SecsPerDay) then
        FSlowResponse.Visible:=True;

    if FDetailsWait.Visible then begin
      if (FDetailsWaitStart = 0) or not (rtDetails in RpcObj.RefreshNow) then begin
        FDetailsWaitStart:=0;
        FDetailsWait.Visible:=False;
        panDetailsWait.Visible:=False;
      end;
    end
    else
      if (FDetailsWaitStart <> 0) and (Now - FDetailsWaitStart >= 300/MSecsPerDay) then begin
        CenterDetailsWait;
        FDetailsWait.Visible:=True;
        panDetailsWait.Visible:=True;
        panDetailsWait.BringToFront;
      end;

{$ifdef LCLcarbon}
    THackApplication(Application).ProcessAsyncCallQueue;
    if Active and (WindowState <> wsMinimized) then begin
      if not FFormActive then begin
        FFormActive:=True;
        CheckClipboardLink;
      end;
    end
    else
      FFormActive:=False;
{$endif LCLcarbon}
  finally
    TickTimer.Enabled:=True;
  end;
end;

procedure TMainForm.FilterTimerTimer(Sender: TObject);
begin
  FilterTimer.Enabled:=False;
  FFilterChanged:=True;
  DoRefresh(True);
end;

procedure TMainForm.FormClose(Sender: TObject; var CloseAction: TCloseAction);
begin
{$ifdef CPUARM}
//  CloseAction:=caMinimize;
  BeforeCloseApp;
  exit;
{$endif CPUARM}

  if Ini.ReadBool('Interface', 'TrayClose', False) then begin
{$ifdef darwin}
    CloseAction:=caMinimize;
{$else}
{$ifdef linux}
    if IsUnity then
      CloseAction:=caMinimize
    else
{$endif linux}
    begin
      CloseAction:=caHide;
      HideApp;
      UpdateTray;
    end;
{$endif darwin}
    exit;
  end;
  BeforeCloseApp;
end;

procedure TMainForm.PageInfoChange(Sender: TObject);
begin
  if PageInfo.ActivePage.Tag <> 0 then
    FDetailsWaitStart:=Now;
  RpcObj.Lock;
  try
    RpcObj.AdvInfo:=GetPageInfoType(PageInfo.ActivePage);
    DoRefresh;
  finally
    RpcObj.Unlock;
  end;
end;

procedure TMainForm.tbSearchCancelClick(Sender: TObject);
begin
      edSearch.Text:='';
end;

procedure TMainForm.TorrentsListTimerTimer(Sender: TObject);
begin
  TorrentsListTimer.Enabled:=False;
  if RpcObj.CurTorrentId <> 0 then
    FDetailsWaitStart:=Now;
  DoRefresh;
end;

procedure TMainForm.pmFilesPopup(Sender: TObject);
begin
  UpdateUI;
end;

procedure TMainForm.pmTorrentsPopup(Sender: TObject);
begin
  UpdateUI;
end;

procedure TMainForm.TrayIconDblClick(Sender: TObject);
begin
{$ifndef darwin}
//  acShowApp.Execute;
if (MainForm.Visible = false) or (MainForm.WindowState = wsMinimized) then
    MainForm.ShowApp
    else
    MainForm.HideApp;
{$endif darwin}
end;

procedure TMainForm.VSplitterChangeBounds(Sender: TObject);
begin
{$ifdef windows}
  Update;
{$endif windows}
end;

procedure TMainForm.UrlLabelClick(Sender: TObject);
begin
  AppBusy;
  OpenURL((Sender as TLabel).Caption);
  AppNormal;
end;

procedure TMainForm.CenterReconnectWindow;
begin
  CenterOnParent(panReconnect);
end;

function TMainForm.DoConnect: boolean;
var
  Sec, pwd: string;
  i, j: integer;
begin
  Result:=True;
  panReconnect.Hide;
  DoDisconnect;
  Sec:='Connection.' + FCurConn;
  if not Ini.SectionExists(Sec) then
    Sec:='Connection';

  i:=FPasswords.IndexOfName(FCurConn);
  pwd:=Ini.ReadString(Sec, 'Password', '');
  if pwd = '-' then begin
    if i >= 0 then
      pwd:=FPasswords.ValueFromIndex[i]
    else begin
      pwd := '';
      // own dialog for entering a password (****)
      with TPasswordConnect.Create(Self) do
      try
        SetText(Format(SConnectTo, [FCurConn]), Format(SEnterPassword, [FCurConn]));
        if ShowModal <> mrOk then begin
          RpcObj.Url:='-';
          Result:=False;
          exit;
        end else begin
          pwd := gPassw
        end;
      finally
        Free;
      end;
    end;
  end
  else
    pwd:=DecodeBase64(pwd);
  if i >= 0 then
    FPasswords.Delete(i);

  RpcObj.Http.Sock.SSL.PFXfile:='';
  RpcObj.Http.Sock.SSL.KeyPassword:='';
  if Ini.ReadBool(Sec, 'UseSSL', False) then begin
    RpcObj.InitSSL;
    RpcObj.Http.Sock.SSL.PFXfile:=Ini.ReadString(Sec, 'CertFile', '');
    RpcObj.Http.Sock.SSL.KeyPassword:=DecodeBase64(Ini.ReadString(Sec, 'CertPass', ''));
    if not IsSSLloaded then begin
      MessageDlg(Format(sSSLLoadError, [DLLSSLName, DLLUtilName]), mtError, [mbOK], 0);
      exit;
    end;
    RpcObj.Url:='https';
  end
  else
    RpcObj.Url:='http';
  RpcObj.Http.UserName:=Ini.ReadString(Sec, 'UserName', '');
  RpcObj.Http.Password:=pwd;
  RpcObj.Http.ProxyHost:='';
  RpcObj.Http.ProxyPort:='';
  RpcObj.Http.ProxyUser:='';
  RpcObj.Http.ProxyPass:='';
  RpcObj.Http.Sock.SocksIP:='';
  RpcObj.Http.Sock.SocksPort:='';
  RpcObj.Http.Sock.SocksUsername:='';
  RpcObj.Http.Sock.SocksPassword:='';
  if Ini.ReadBool(Sec, 'UseProxy', False) then begin
    if Ini.ReadBool(Sec, 'UseSockProxy', False) then begin
      RpcObj.Http.Sock.SocksIP := Ini.ReadString(Sec, 'ProxyHost', '');
      RpcObj.Http.Sock.SocksPort := IntToStr(Ini.ReadInteger(Sec, 'ProxyPort', 8080));
      RpcObj.Http.Sock.SocksUsername := Ini.ReadString(Sec, 'ProxyUser', '');
      RpcObj.Http.Sock.SocksPassword := DecodeBase64(Ini.ReadString(Sec, 'ProxyPass', ''));
    end
    else begin
      RpcObj.Http.ProxyHost:=Ini.ReadString(Sec, 'ProxyHost', '');
      RpcObj.Http.ProxyPort:=IntToStr(Ini.ReadInteger(Sec, 'ProxyPort', 8080));
      RpcObj.Http.ProxyUser:=Ini.ReadString(Sec, 'ProxyUser', '');
      RpcObj.Http.ProxyPass:=DecodeBase64(Ini.ReadString(Sec, 'ProxyPass', ''));
    end;
  end;
  if (FReconnectTimeOut = -1) and Ini.ReadBool(Sec, 'Autoreconnect', False) then
        FReconnectTimeOut:=0;
  RpcObj.RpcPath:=Ini.ReadString(Sec, 'RpcPath', '');
  RpcObj.Url:=Format('%s://%s:%d', [RpcObj.Url, Ini.ReadString(Sec, 'Host', ''), Ini.ReadInteger(Sec, 'Port', 9091)]);
  SetRefreshInterval;
  RpcObj.InfoStatus:=sConnectingToDaemon;
  CheckStatus;
  TrayIcon.Hint:=RpcObj.InfoStatus;
  RpcObj.Connect;
  FPathMap.Text:=StringReplace(Ini.ReadString(Sec, 'PathMap', ''), '|', LineEnding, [rfReplaceAll]);
  i:=0;
  while i < FPathMap.Count do
    if Trim(FPathMap.ValueFromIndex[i]) = '' then
      FPathMap.Delete(i)
    else
      Inc(i);

  Ini.WriteString('Hosts', 'CurHost', FCurConn);
  if FCurConn <> Ini.ReadString('Hosts', 'Host1', '') then begin
    Ini.WriteString('Hosts', 'Host1', FCurConn);
    j:=2;
    for i:=0 to pmConnections.Items.Count - 1 do
      with pmConnections.Items[i] do
        if (Tag = 0) and (Caption <> FCurConn) then begin
          Ini.WriteString('Hosts', Format('Host%d', [j]), Caption);
          Inc(j);
        end;
    Ini.UpdateFile;
    UpdateConnections;
  end
  else
    if pmConnections.Items[0].Tag = 0 then begin
      pmConnections.Items[0].Checked:=True;
      miConnect.Items[0].Checked:=True;
    end;
  tbConnect.Caption := pmConnections.Items[0].Caption;
end;

procedure TMainForm.DoDisconnect;
var
  i: integer;
begin
  TorrentsListTimer.Enabled:=False;
  FilterTimer.Enabled:=False;
  ClearDetailsInfo;
  gTorrents.Items.Clear;
  gTorrents.Enabled:=False;
  gTorrents.Color:=clBtnFace;
  lvPeers.Enabled:=False;
  lvPeers.Color:=gTorrents.Color;
  lvFiles.Enabled:=False;
  lvFiles.Color:=gTorrents.Color;
  lvTrackers.Enabled:=False;
  lvTrackers.Color:=gTorrents.Color;

  lvFilter.Enabled:=False;
  lvFilter.Color:=gTorrents.Color;
  with lvFilter do begin
    Items[0, 0]:=UTF8Decode(SAll); // ALERT - VERIFY - PETROV
    Items[0, 1]:=UTF8Decode(SDownloading);
    Items[0, 2]:=UTF8Decode(SCompleted);
    Items[0, 3]:=UTF8Decode(SActive);
    Items[0, 4]:=UTF8Decode(SInactive);
    Items[0, 5]:=UTF8Decode(sStopped);
    Items[0, 6]:=UTF8Decode(sErrorState);
    Items[0, 7]:=UTF8Decode(sWaiting);
  end;
  edSearch.Enabled:=False;
  edSearch.Color:=gTorrents.Color;
  edSearch.Text:='';

  with gStats do begin
    BeginUpdate;
    try
      for i:=0 to Items.Count - 1 do begin
        Items[1, i]:=NULL;
        Items[2, i]:=NULL;
      end;
    finally
      EndUpdate;
    end;
    Enabled:=False;
    Color:=gTorrents.Color;
  end;

  RpcObj.Disconnect;

  RpcObj.InfoStatus:=sDisconnected;
  CheckStatus;
  UpdateUI;
  TrayIcon.Hint:=RpcObj.InfoStatus;
  gTorrents.Items.RowCnt:=0;
  FTorrents.RowCnt:=0;
  lvFilter.Row:=0;
  lvFilter.Items.RowCnt:=StatusFiltersCount;
  TorrentsListTimer.Enabled:=False;
  FilterTimer.Enabled:=False;
  pmConnections.Items[0].Checked:=False;
  miConnect.Items[0].Checked:=False;
  FCurDownSpeedLimit:=-2;
  FCurUpSpeedLimit:=-2;
  FillSpeedsMenu;
  tbConnect.Caption := Format(SConnectTo,['Transmission']);
end;

procedure TMainForm.ClearDetailsInfo(Skip: TAdvInfoType);

  procedure ClearChildren(AParent: TPanel);
  var
    i: integer;
  begin
    AParent.AutoSize:=False;
    AParent.ChildSizing.Layout:=cclNone;
    for i:=0 to AParent.ControlCount - 1 do begin
      if AParent.Controls[i] is TLabel then
        with AParent.Controls[i] as TLabel do begin
          if (Length(Name) < 5) or (Copy(Name, Length(Name) - 4, 5) <> 'Label') then
            Caption:='';
          PopupMenu:=pmLabels;
        end;
    end;
  end;

var
  i, t: integer;
begin
  if RpcObj.CurTorrentId = 0 then begin
    Skip:=aiNone;
    t:=0;
  end
  else
    t:=1;
  FDetailsWaitStart:=0;
  if Skip <> aiFiles then begin
    FFiles.Clear;
    tabFiles.Caption:=FFilesCapt;
  end;
  if Skip <> aiPeers then
    lvPeers.Items.Clear;
  if Skip <> aiTrackers then
    lvTrackers.Items.Clear;
  if Skip <> aiGeneral then begin
    ClearChildren(panGeneralInfo);
    ClearChildren(panTransfer);
    ProcessPieces('', 0, 0);
    txDownProgress.AutoSize:=False;
    txDownProgress.Caption:='';

    txMagnetLink.Text := '';
  end;
  for i:=0 to PageInfo.PageCount - 1 do
    PageInfo.Pages[i].Tag:=t;
end;

function TMainForm.SelectRemoteFolder(const CurFolder, DialogTitle: string): string;
var
  i, j: integer;
  s, ss, sss, fn: string;
  dlg: TSelectDirectoryDialog;
  d: char;
begin
  Result:='';
  if Trim(FPathMap.Text) = '' then begin
    MessageDlg(sNoPathMapping, mtInformation, [mbOK], 0);
    exit;
  end;
  s:=MapRemoteToLocal(CurFolder);
  if (s = '') or not DirectoryExistsUTF8(s) then
    s:=FPathMap.ValueFromIndex[0];

  if not DirectoryExistsUTF8(s) then begin
    MessageDlg(sNoPathMapping, mtInformation, [mbOK], 0);
    exit;
  end;

  dlg:=TSelectDirectoryDialog.Create(nil);
  try
    dlg.Title:=DialogTitle;
    dlg.InitialDir:=s;
    if not dlg.Execute then
      exit;

    fn:=dlg.FileName;
    for i:=0 to FPathMap.Count - 1 do begin
      s:=FPathMap[i];
      j:=Pos('=', s);
      if j > 0 then begin
        ss:=FixSeparators(Copy(s, j + 1, MaxInt));
        sss:=IncludeTrailingPathDelimiter(ss);
        if (CompareFilePath(ss, fn) = 0) or (CompareFilePath(sss, Copy(fn, 1, Length(sss))) = 0) then begin
          Result:=Copy(s, 1, j - 1);
          d:='/';
          for j:=1 to Length(Result) do
            if Result[j] in ['/','\'] then begin
              d:=Result[j];
              break;
            end;
          if CompareFilePath(ss, fn) <> 0 then begin
            if (Result <> '') and (Copy(Result, Length(Result), 1) <> d) then
              Result:=Result + d;
            ss:=IncludeProperTrailingPathDelimiter(ss);
            Result:=Result + Copy(fn, Length(ss) + 1, MaxInt);
          end;

          Result:=StringReplace(Result, DirectorySeparator, d, [rfReplaceAll]);
          if Copy(Result, Length(Result), 1) = d then
            SetLength(Result, Length(Result) - 1);
        end;
      end;
    end;
  finally
    dlg.Free;
  end;
  if Result = '' then
    MessageDlg(sNoPathMapping, mtError, [mbOK], 0);
end;

procedure TMainForm.ConnectionSettingsChanged(const ActiveConnection: string; ForceReconnect: boolean);
var
  Sec: string;
begin
  UpdateConnections;
  if (FCurConn <> ActiveConnection) or ForceReconnect then begin
    DoDisconnect;
    Sec:='Connection.' + ActiveConnection;
    if Ini.ReadBool(Sec, 'Autoreconnect', False) then
      FReconnectTimeOut:=0
    else
    FReconnectTimeOut:=-1;
    FCurConn:=ActiveConnection;
    if FCurConn <> '' then
      DoConnect;
  end;
end;

procedure TMainForm.UpdateUI;
var
  e: boolean;
begin
  e:=((Screen.ActiveForm = Self) or not Visible or (WindowState = wsMinimized))
    and not gTorrents.EditorMode and not lvFiles.EditorMode;

  acConnect.Enabled:=e;
  acOptions.Enabled:=e;
  acConnOptions.Enabled:=e;
  e:=RpcObj.Connected and e;
  acDisconnect.Enabled:=e;
  acSelectAll.Enabled:=e;
  acAddTorrent.Enabled:=e;
  acAddLink.Enabled:=e;
  acDaemonOptions.Enabled:=e;
  acStartAllTorrents.Enabled:=e and RpcObj.Connected;
  acStopAllTorrents.Enabled:=acStartAllTorrents.Enabled;
  acStartTorrent.Enabled:=e and (gTorrents.Items.Count > 0);
  acForceStartTorrent.Enabled:=acStartTorrent.Enabled and (RpcObj.RPCVersion >= 14);
  acStopTorrent.Enabled:=e and (gTorrents.Items.Count > 0);
  acVerifyTorrent.Enabled:=e and (gTorrents.Items.Count > 0);
  acRemoveTorrent.Enabled:=e and (gTorrents.Items.Count > 0) and not edSearch.Focused;
  acRemoveTorrentAndData.Enabled:=acRemoveTorrent.Enabled and (RpcObj.RPCVersion >= 4);
  acReannounceTorrent.Enabled:=acVerifyTorrent.Enabled and (RpcObj.RPCVersion >= 5);
  acMoveTorrent.Enabled:=acVerifyTorrent.Enabled and (RpcObj.RPCVersion >= 6);
  acTorrentProps.Enabled:=acVerifyTorrent.Enabled;
  acOpenContainingFolder.Enabled:=acTorrentProps.Enabled and (RpcObj.RPCVersion >= 4);
  pmiPriority.Enabled:=e and (gTorrents.Items.Count > 0);
  miPriority.Enabled:=pmiPriority.Enabled;
  acSetHighPriority.Enabled:=e and (gTorrents.Items.Count > 0) and
                      ( ( not lvFiles.Focused and (RpcObj.RPCVersion >= 5) ) or
                        ((lvFiles.Items.Count > 0) and (PageInfo.ActivePage = tabFiles)) );
  acSetNormalPriority.Enabled:=acSetHighPriority.Enabled;
  acSetLowPriority.Enabled:=acSetHighPriority.Enabled;
  miQueue.Enabled:=e and (gTorrents.Items.Count > 0) and (RpcObj.RPCVersion >= 14);
  pmiQueue.Enabled:=miQueue.Enabled;
  acQMoveTop.Enabled:=miQueue.Enabled;
  acQMoveUp.Enabled:=miQueue.Enabled;
  acQMoveDown.Enabled:=miQueue.Enabled;
  acQMoveBottom.Enabled:=miQueue.Enabled;
  acOpenFile.Enabled:=acSetHighPriority.Enabled and (lvFiles.SelCount < 2) and (RpcObj.RPCVersion >= 4);
  acCopyPath.Enabled:=acOpenFile.Enabled;
  acSetNotDownload.Enabled:=acSetHighPriority.Enabled;
  acRename.Enabled:=(RpcObj.RPCVersion >= 15) and acSetHighPriority.Enabled;
  acSetupColumns.Enabled:=e;
  acUpdateBlocklist.Enabled:=(acUpdateBlocklist.Tag <> 0) and e and (RpcObj.RPCVersion >= 5);
  acAddTracker.Enabled:=acTorrentProps.Enabled and (RpcObj.RPCVersion >= 10);
  acAdvEditTrackers.Enabled:=acAddTracker.Enabled;
  acEditTracker.Enabled:=acAddTracker.Enabled and (lvTrackers.Items.Count > 0);
  acDelTracker.Enabled:=acEditTracker.Enabled;
  acAltSpeed.Enabled:=e and (RpcObj.RPCVersion >= 5);
  pmiDownSpeedLimit.Enabled:=RpcObj.Connected;
  pmiUpSpeedLimit.Enabled:=RpcObj.Connected;
end;

procedure TMainForm.ShowConnOptions(NewConnection: boolean);
var
  frm: TConnOptionsForm;
begin
  AppBusy;
  frm:=TConnOptionsForm.Create(Self);
  with frm do
  try
    ActiveConnection:=FCurConn;
    if NewConnection then begin
      Caption:=SNewConnection;
      btNewClick(nil);
      if Ini.ReadInteger('Hosts', 'Count', 0) = 0 then begin
        panTop.Visible:=False;
{$ifdef LCLgtk2}
        panTop.Height:=0;
{$endif LCLgtk2}
        with Page.BorderSpacing do
          Top:=Left;
        tabPaths.TabVisible:=False;
        tabMisc.TabVisible:=False;
      end
      else begin
        btNew.Hide;
        btRename.Hide;
        btDel.Hide;
        panTop.ClientHeight:=btNew.Top;
      end;
      cbShowAdvancedClick(nil);
      AutoSizeForm(frm);
    end;
    AppNormal;
    ShowModal;
    ConnectionSettingsChanged(ActiveConnection, ActiveSettingChanged);
  finally
    Free;
  end;
end;

procedure TMainForm.SaveColumns(LV: TVarGrid; const AName: string; FullInfo: boolean);
var
  i: integer;
begin
  for i:=0 to LV.Columns.Count - 1 do
    with LV.Columns[i] do begin
      Ini.WriteInteger(AName, Format('Id%d', [i]), ID - 1);
      Ini.WriteInteger(AName, Format('Width%d', [i]), Width);
      if FullInfo then begin
        Ini.WriteInteger(AName, Format('Index%d', [i]), Index);
        Ini.WriteBool(AName, Format('Visible%d', [i]), Visible);
      end;
    end;
  if LV.SortColumn >= 0 then begin
    Ini.WriteInteger(AName, 'SortColumn', LV.SortColumn);
    Ini.WriteInteger(AName, 'SortOrder', integer(LV.SortOrder));
  end;
end;

procedure TMainForm.LoadColumns(LV: TVarGrid; const AName: string; FullInfo: boolean);
var
  i, j, ColId: integer;
begin
  LV.Columns.BeginUpdate;
  try
    for i:=0 to LV.Columns.Count - 1 do begin
      ColId:=Ini.ReadInteger(AName, Format('Id%d', [i]), -1);
      if ColId = -1 then continue;
      for j:=0 to LV.Columns.Count - 1 do
        with LV.Columns[j] do
          if ID - 1 = ColId then begin
            if FullInfo then begin
              Index:=Ini.ReadInteger(AName, Format('Index%d', [i]), Index);
              Visible:=Ini.ReadBool(AName, Format('Visible%d', [i]), Visible);
            end;
            Width:=Ini.ReadInteger(AName, Format('Width%d', [i]), Width);
            break;
          end;
    end;
  finally
    LV.Columns.EndUpdate;
  end;
  LV.SortColumn:=Ini.ReadInteger(AName, 'SortColumn', LV.SortColumn);
  LV.SortOrder:=TSortOrder(Ini.ReadInteger(AName, 'SortOrder', integer(LV.SortOrder)));
end;


//----------------------------------------------------------------
function GetBiDi: TBiDiMode;
var
  i:integer;
begin
  // PETROV - Herb off
  i:=Ini.ReadInteger ('Interface', 'IgnoreRightLeft', 0);   // 0 - by default
    Ini.WriteInteger('Interface', 'IgnoreRightLeft', i);

  if (FTranslationLanguage='English') and (i=0) then
    i := 1;

  Result := bdLeftToRight;
  case i of
    1: Result := bdLeftToRight;
    2: Result := bdRightToLeft;
    3: Result := bdRightToLeftNoAlign;
    4: Result := bdRightToLeftReadingOnly;
  end;
end;


//----------------------------------------------------------------
function ExcludeInvalidChar (path: string): string;
var
  s_old: string;
  l_old: integer;
begin
  s_old := path;
//path  := StringReplace(path, ':', '_', [rfReplaceAll, rfIgnoreCase]);
  path  := StringReplace(path, '*', '_', [rfReplaceAll, rfIgnoreCase]);
  path  := StringReplace(path, '?', '_', [rfReplaceAll, rfIgnoreCase]);
  path  := StringReplace(path, '|', '_', [rfReplaceAll, rfIgnoreCase]);
  path  := StringReplace(path, '<', '_', [rfReplaceAll, rfIgnoreCase]);
  path  := StringReplace(path, '>', '_', [rfReplaceAll, rfIgnoreCase]);
  path  := StringReplace(path, '"', '_', [rfReplaceAll, rfIgnoreCase]);
  path  := StringReplace(path, '~', '_', [rfReplaceAll, rfIgnoreCase]);
//path  := StringReplace(path, '..','_', [rfReplaceAll, rfIgnoreCase]); bag

  l_old := 0;
  if path <> s_old then begin
    l_old :=1;
  end;

  Result:= path;
end;



//----------------------------------------------------------------
function TMainForm.CorrectPath (path: string): string; // PETROV
var
  l_old: integer;
begin
  path  := StringReplace(path, '//', '/', [rfReplaceAll, rfIgnoreCase]);
  Result:= path;
  l_old := length(path);
  if l_old >= 1 then begin
    if path[l_old]='/' then
        path := MidStr(path,1,l_old-1);
    Result:= path;
  end;
end;





function TMainForm.GetTorrentError(t: TJSONObject; Status: integer): string;
var
  i: integer;
  stats: TJSONArray;
  err, gerr: widestring;
  NoTrackerError: boolean;
begin
  Result:='';
  gerr:=t.Strings['errorString'];
  if RpcObj.RPCVersion >= 7 then begin
    NoTrackerError:=False;
    stats:=t.Arrays['trackerStats'];
    for i:=0 to stats.Count - 1 do
      with stats.Objects[i] do begin
        err:='';
        if Booleans['hasAnnounced'] and not Booleans['lastAnnounceSucceeded'] then
          err:=Strings['lastAnnounceResult'];
        if err = 'Success' then
          err:='';
        if err = '' then begin
          // If at least one tracker is working, then report no error
          NoTrackerError:=True;
          Result:='';
        end
        else begin
          if not NoTrackerError and (Result = '') then
            Result:=sTrackerError + ': ' + UTF8Encode(err);
          // Workaround for transmission bug
          // If the global error string is equal to some tracker error string,
          // then igonore the global error string
          if gerr = err then
            gerr:='';
        end;
      end;
  end
  else begin
    Result:=UTF8Encode(t.Strings['announceResponse']);
    if Result = 'Success' then
      Result:=''
    else
      if Result <> '' then begin
        i:=Pos('(', Result);
        if i <> 0 then
          if Copy(Result, i, 5) = '(200)' then
            Result:=''
          else
            Result:=sTrackerError + ': ' + Copy(Result, 1, i - 1);
      end;
  end;

  if (Result = '') or (Status = TR_STATUS_STOPPED) or (Status = TR_STATUS_FINISHED) then
    Result:=UTF8Encode(gerr);
end;

function TMainForm.SecondsToString(j: integer): string;
begin
  if j < 60 then
    Result:=Format(sSecs, [j])
  else
  if j < 60*60 then begin
    Result:=Format(sMins, [j div 60]);
    j:=j mod 60;
    if j > 0 then
      Result:=Format('%s, %s', [Result, Format(sSecs, [j])]);
  end
  else begin
    j:=(j + 30) div 60;
    if j < 60*24 then begin
      Result:=Format(sHours, [j div 60]);
      j:=j mod 60;
      if j > 0 then
        Result:=Format('%s, %s', [Result, Format(sMins, [j])]);
    end
    else begin
      j:=(j + 30) div 60;
      Result:=Format(sDays, [j div 24]);
      j:=j mod 24;
      if j > 0 then
        Result:=Format('%s, %s', [Result, Format(sHours, [j])]);
    end;
  end;
end;

procedure TMainForm.FillTorrentsList(list: TJSONArray);
var
  i, j, row, crow, id, StateImg: integer;
  t: TJSONObject;
  f: double;
  ExistingRow: boolean;
  s, ss: string;

  function GetTorrentValue(AIndex: integer; const AName: string; AType: integer): boolean;
  var
    res: variant;
    i: integer;
  begin
    i:=t.IndexOfName(AName);
    Result:=i >= 0;
    if Result then
      case AType of
        vtInteger:
          res:=t.Items[i].AsInteger;
        vtExtended:
          res:=t.Items[i].AsFloat;
        else
          res:=t.Items[i].AsString;
      end
    else
      res:=NULL;

    FTorrents[AIndex, row]:=res;
  end;

  function StoreSpeed(var History: variant; Speed: integer): integer;
  var
    j, cnt: integer;
    p: PInteger;
    IsNew: boolean;
    res: Int64;
  begin
    IsNew:=VarIsEmpty(History);
    if IsNew then begin
      if Speed = 0 then begin
        Result:=0;
        exit;
      end;
      History:=VarArrayCreate([0, SpeedHistorySize], varInteger);
    end;
    p:=VarArrayLock(History);
    try
      if IsNew then begin
        for j:=1 to SpeedHistorySize do
          p[j]:=-1;
        j:=1;
      end
      else begin
        j:=Round((Now - cardinal(p[0])/SecsPerDay)/RpcObj.RefreshInterval);
        if j = 0 then
          j:=1;
      end;
      p[0]:=integer(cardinal(Round(Now*SecsPerDay)));
      // Shift speed array
      if j < SpeedHistorySize then
        Move(p[1], p[j + 1], (SpeedHistorySize - j)*SizeOf(integer))
      else
        j:=SpeedHistorySize;

      while j > 0 do begin
        p[j]:=Speed;
        Dec(j);
      end;
      // Calc average speed
      res:=Speed;
      cnt:=1;
      for j:=2 to SpeedHistorySize do
        if p[j] < 0 then
          break
        else begin
          Inc(res, p[j]);
          Inc(cnt);
        end;

      Result:=res div cnt;
    finally
      VarArrayUnlock(History);
    end;
    if Result = 0 then
      VarClear(History);
  end;

var
  FilterIdx, OldId: integer;
  TrackerFilter, PathFilter: string;
  UpSpeed, DownSpeed: double;
  DownCnt, SeedCnt, CompletedCnt, ActiveCnt, StoppedCnt, ErrorCnt, WaitingCnt: integer;
  IsActive: boolean;
  Paths: TStringList;
  v: variant;
  FieldExists: array of boolean;
begin
  if gTorrents.Tag <> 0 then exit;
  if list = nil then begin
    ClearDetailsInfo;
    exit;
  end;
{
  for i:=1 to 1000 do begin
    t:=TJSONObject.Create;
    t.Integers['id']:=i + 10000;
    t.Strings['name']:=Format('ZName %d', [i]);
    t.Integers['status']:=TR_STATUS_STOPPED;
    t.Arrays['trackerStats']:=TJSONArray.Create;
    t.Floats['sizeWhenDone']:=0;
    t.Floats['leftUntilDone']:=0;
    t.Integers['rateDownload']:=0;
    t.Integers['rateUpload']:=0;
    list.Add(t);
  end;
}
  Paths:=TStringList.Create;
  try
  Paths.Sorted:=True;
  OldId:=RpcObj.CurTorrentId;
  IsActive:=gTorrents.Enabled;
  gTorrents.Enabled:=True;
  lvFilter.Enabled:=True;
  gTorrents.Color:=clWindow;
  lvFilter.Color:=clWindow;
  edSearch.Enabled:=True;
  edSearch.Color:=clWindow;
  if not IsActive then
    ActiveControl:=gTorrents;

  for i:=0 to FTrackers.Count - 1 do
    FTrackers.Objects[i]:=nil;

  // Check fields' existence
  SetLength(FieldExists, FTorrents.ColCnt);
  if list.Count > 0 then begin
    t:=list[0] as TJSONObject;
    FieldExists[idxName]:=t.IndexOfName('name') >= 0;
    FieldExists[idxRatio]:=t.IndexOfName('uploadRatio') >= 0;
    FieldExists[idxTracker]:=t.IndexOfName('trackers') >= 0;
    FieldExists[idxPath]:=t.IndexOfName('downloadDir') >= 0;
    FieldExists[idxPriority]:=t.IndexOfName('bandwidthPriority') >= 0;
    FieldExists[idxQueuePos]:=t.IndexOfName('queuePosition') >= 0;
    FieldExists[idxSeedingTime]:=t.IndexOfName('secondsSeeding') >= 0;
    FieldExists[idxPrivate]:=t.IndexOfName('isPrivate') >= 0;
  end;

  UpSpeed:=0;
  DownSpeed:=0;
  DownCnt:=0;
  SeedCnt:=0;
  CompletedCnt:=0;
  ActiveCnt:=0;
  StoppedCnt:=0;
  ErrorCnt:=0;
  WaitingCnt:=0;

  FilterIdx:=lvFilter.Row;
  if VarIsNull(lvFilter.Items[0, FilterIdx]) then
    Dec(FilterIdx);
  if FilterIdx >= StatusFiltersCount then
    if not VarIsNull(lvFilter.Items[-1, FilterIdx]) then begin
      PathFilter:=UTF8Encode(widestring(lvFilter.Items[-1, FilterIdx]));
      FilterIdx:=fltAll;
    end
    else begin
      TrackerFilter:=UTF8Encode(widestring(lvFilter.Items[0, FilterIdx]));
      FilterIdx:=fltAll;
      i:=RPos('(', TrackerFilter);
      if i > 0 then
        TrackerFilter:=Trim(Copy(TrackerFilter, 1, i - 1));
    end;

  for i:=0 to FTorrents.Count - 1 do
    FTorrents[idxTag, i]:=0;

  for i:=0 to list.Count - 1 do begin
    StateImg:=-1;

    t:=list[i] as TJSONObject;
    id:=t.Integers['id'];
    ExistingRow:=FTorrents.Find(idxTorrentId, id, row);
    if not ExistingRow then
      FTorrents.InsertRow(row);

    FTorrents[idxTorrentId, row]:=t.Integers['id'];

    if FieldExists[idxName] then
      FTorrents[idxName, row]:=t.Strings['name'];

    j:=t.Integers['status'];
    if ExistingRow and (j = TR_STATUS_SEED) and (FTorrents[idxStatus, row] = TR_STATUS_DOWNLOAD) then
      DownloadFinished(UTF8Encode(widestring(FTorrents[idxName, row])));
    FTorrents[idxStatus, row]:=j;
    if j = TR_STATUS_CHECK_WAIT  then StateImg:=imgDownQueue else
    if j = TR_STATUS_CHECK  then StateImg:=imgDownQueue else
    if j = TR_STATUS_DOWNLOAD_WAIT  then StateImg:=imgDownQueue else
    if j = TR_STATUS_DOWNLOAD  then StateImg:=imgDown else
    if j = TR_STATUS_SEED_WAIT  then StateImg:=imgSeedQueue else
    if j = TR_STATUS_SEED  then StateImg:=imgSeed else
    if j = TR_STATUS_STOPPED  then StateImg:=imgDone;

    if GetTorrentError(t, j) <> '' then
      if t.Strings['errorString'] <> '' then
        StateImg:=imgError
      else
        if StateImg in [imgDown,imgSeed] then
          Inc(StateImg, 2);

    if j <> TR_STATUS_STOPPED then begin
      s:=GetTorrentError(t, j);
      if s <> '' then
        if t.Strings['errorString'] <> '' then
          StateImg:=imgError
        else
          if StateImg in [imgDown,imgSeed] then
            Inc(StateImg, 2);

      if RpcObj.RPCVersion >= 7 then begin
        s:='';
        if t.Arrays['trackerStats'].Count > 0 then
          with t.Arrays['trackerStats'].Objects[0] do begin
            if integer(Integers['announceState']) in [2, 3] then
              s:=sTrackerUpdating
            else
              if Booleans['hasAnnounced'] then
                if Booleans['lastAnnounceSucceeded'] then
                  s:=sTrackerWorking
                else
                  s:=TranslateString(UTF8Encode(Strings['lastAnnounceResult']), True);

            if s = 'Success' then
              s:=sTrackerWorking;
          end;
      end
      else
        s:=t.Strings['announceResponse'];
    end
    else
      s:='';
    FTorrents[idxTrackerStatus, row]:=UTF8Decode(s);

    if FTorrents[idxStatus, row] = TR_STATUS_CHECK then
      f:=t.Floats['recheckProgress']*100.0
    else begin
      f:=t.Floats['sizeWhenDone'];
      if f <> 0 then
        f:=(f - t.Floats['leftUntilDone'])*100.0/f;
      if StateImg = imgDone then
        if (t.Floats['leftUntilDone'] <> 0) or (t.Floats['sizeWhenDone'] = 0) then
          StateImg:=imgStopped
        else
          FTorrents[idxStatus, row]:=TR_STATUS_FINISHED;
    end;
    if f < 0 then
      f:=0;
    FTorrents[idxDone, row]:=Int(f*10.0)/10.0;
    FTorrents[idxStateImg, row]:=StateImg;
    GetTorrentValue(idxDownSpeed, 'rateDownload', vtInteger);
    j:=StoreSpeed(FTorrents.ItemPtrs[idxDownSpeedHistory, row]^, FTorrents[idxDownSpeed, row]);
    if FCalcAvg and (StateImg in [imgDown, imgDownError]) then
      FTorrents[idxDownSpeed, row]:=j;
    GetTorrentValue(idxUpSpeed, 'rateUpload', vtInteger);
    j:=StoreSpeed(FTorrents.ItemPtrs[idxUpSpeedHistory, row]^, FTorrents[idxUpSpeed, row]);
    if FCalcAvg and (StateImg in [imgSeed, imgSeedError]) then
      FTorrents[idxUpSpeed, row]:=j;

    GetTorrentValue(idxSize, 'totalSize', vtExtended);
    GetTorrentValue(idxSizeToDowload, 'sizeWhenDone', vtExtended);
    GetTorrentValue(idxSeeds, 'peersSendingToUs', vtInteger);
    GetTorrentValue(idxPeers, 'peersGettingFromUs', vtInteger);
    GetTorrentValue(idxETA, 'eta', vtInteger);
    v:=FTorrents[idxETA, row];
    if not VarIsNull(v) then
      if v < 0 then
        FTorrents[idxETA, row]:=MaxInt
      else begin
        f:=FTorrents[idxDownSpeed, row];
        if f > 0 then
          FTorrents[idxETA, row]:=Round(t.Floats['leftUntilDone']/f);
      end;
    GetTorrentValue(idxDownloaded, 'downloadedEver', vtExtended);
    GetTorrentValue(idxUploaded, 'uploadedEver', vtExtended);
    GetTorrentValue(idxSizeLeft, 'leftUntilDone', vtExtended);
    GetTorrentValue(idxAddedOn, 'addedDate', vtExtended);
    GetTorrentValue(idxCompletedOn, 'doneDate', vtExtended);
    GetTorrentValue(idxLastActive, 'activityDate', vtExtended);

    if RpcObj.RPCVersion >= 7 then begin
      if t.Arrays['trackerStats'].Count > 0 then
        with t.Arrays['trackerStats'].Objects[0] do begin
          FTorrents[idxSeedsTotal, row]:=Integers['seederCount'];
          FTorrents[idxLeechersTotal, row]:=Integers['leecherCount'];
        end
      else begin
        FTorrents[idxSeedsTotal, row]:=-1;
        FTorrents[idxLeechersTotal, row]:=-1;
      end;
      if t.Floats['metadataPercentComplete'] <> 1.0 then begin
        FTorrents[idxSize, row]:=-1;
        FTorrents[idxSizeToDowload, row]:=-1;
      end;
    end
    else begin
      GetTorrentValue(idxSeedsTotal, 'seeders', vtInteger);
      GetTorrentValue(idxLeechersTotal, 'leechers', vtInteger);
    end;
    if FieldExists[idxRatio] then begin
      f:=t.Floats['uploadRatio'];
      if f = -2 then
        f:=MaxInt;
      FTorrents[idxRatio, row]:=f;
    end
    else
      FTorrents[idxRatio, row]:=NULL;
    if FieldExists[idxSeedingTime] then
      FTorrents[idxSeedingTime, row]:=t.Integers['secondsSeeding']
    else
      FTorrents[idxSeedingTime, row]:=NULL;

    if RpcObj.RPCVersion >= 7 then begin
      if t.Arrays['trackerStats'].Count > 0 then
        s:=t.Arrays['trackerStats'].Objects[0].Strings['announce']
      else
        s:=sNoTracker;
    end
    else
      if FieldExists[idxTracker] then
        s:=UTF8Encode(t.Arrays['trackers'].Objects[0].Strings['announce'])
      else begin
        s:='';
        if VarIsEmpty(FTorrents[idxTracker, row]) then
          RpcObj.RequestFullInfo:=True;
      end;

    if s <> '' then begin
      j:=Pos('://', s);
      if j > 0 then
        s:=Copy(s, j + 3, MaxInt);
      j:=Pos('/', s);
      if j > 0 then
        s:=Copy(s, 1, j - 1);
      j:=Pos('.', s);
      if j > 0 then begin
        ss:=Copy(s, 1, j - 1);
        if AnsiCompareText(ss, 'bt') = 0 then
          System.Delete(s, 1, 3)
        else
          if (Length(ss) = 3) and (AnsiCompareText(Copy(ss, 1, 2), 'bt') = 0) and (ss[3] in ['1'..'9']) then
            System.Delete(s, 1, 4);
      end;
      j:=Pos(':', s);
      if j > 0 then
        System.Delete(s, j, MaxInt);
      FTorrents[idxTracker, row]:=UTF8Decode(s);
    end;

    if FieldExists[idxPath] then
      FTorrents[idxPath, row]:=UTF8Decode(ExcludeTrailingPathDelimiter(UTF8Encode(t.Strings['downloadDir'])))
    else
      if VarIsEmpty(FTorrents[idxPath, row]) then
        RpcObj.RequestFullInfo:=True;

    if not VarIsEmpty(FTorrents[idxPath, row]) then begin
      s:=UTF8Encode(widestring(FTorrents[idxPath, row]));
      j:=Paths.IndexOf(s);
      if j < 0 then
        Paths.AddObject(s, TObject(1))
      else
        Paths.Objects[j]:=TObject(PtrInt(Paths.Objects[j]) + 1);
    end;

    if FieldExists[idxPriority] then
      FTorrents[idxPriority, row]:=t.Integers['bandwidthPriority'];

    if FieldExists[idxQueuePos] then begin
      j:=t.Integers['queuePosition'];
      if FTorrents[idxStatus, row] = TR_STATUS_FINISHED then
        Inc(j, FinishedQueue);
      FTorrents[idxQueuePos, row]:=j;
    end;

    if FieldExists[idxPrivate] then
      FTorrents[idxPrivate, row]:=t.Integers['isPrivate'];

    DownSpeed:=DownSpeed + FTorrents[idxDownSpeed, row];
    UpSpeed:=UpSpeed + FTorrents[idxUpSpeed, row];

    FTorrents[idxTag, row]:=1;
  end;

  i:=0;
  while i < FTorrents.Count do
    if FTorrents[idxTag, i] = 0 then
      FTorrents.Delete(i)
    else
      Inc(i);

  gTorrents.Items.BeginUpdate;
  try
    for i:=0 to gTorrents.Items.Count - 1 do
      gTorrents.Items[idxTag, i]:=0;

    gTorrents.Items.Sort(idxTorrentId);

    for i:=0 to FTorrents.Count - 1 do begin
      IsActive:=(FTorrents[idxDownSpeed, i] <> 0) or (FTorrents[idxUpSpeed, i] <> 0);
      if IsActive then
        Inc(ActiveCnt);

      j:=FTorrents[idxStatus, i];
      if j = TR_STATUS_DOWNLOAD then
        Inc(DownCnt)
      else
      if j = TR_STATUS_SEED then begin
        Inc(SeedCnt);
        Inc(CompletedCnt);
      end
      else
      if j = TR_STATUS_FINISHED then
        Inc(CompletedCnt);

      if (j = TR_STATUS_CHECK) or (j = TR_STATUS_CHECK_WAIT) or (j = TR_STATUS_DOWNLOAD_WAIT) then
        inc(WaitingCnt);

      StateImg:=FTorrents[idxStateImg, i];
      if StateImg in [imgStopped, imgDone] then
        Inc(StoppedCnt)
      else
        if StateImg in [imgDownError, imgSeedError, imgError] then
          Inc(ErrorCnt);

      if not VarIsEmpty(FTorrents[idxTracker, i]) then begin
        s:=UTF8Encode(widestring(FTorrents[idxTracker, i]));
        j:=FTrackers.IndexOf(s);
        if j < 0 then
          j:=FTrackers.Add(s);
        FTrackers.Objects[j]:=TObject(ptruint(FTrackers.Objects[j]) + 1);
        if (TrackerFilter <> '') and (TrackerFilter <> s) then
          continue;
      end;

      if (PathFilter <> '') and not VarIsEmpty(FTorrents[idxPath, i]) and (UTF8Decode(PathFilter) <> FTorrents[idxPath, i]) then
        continue;

      case FilterIdx of
        fltActive:
          if not IsActive then
            continue;
        fltInactive:
          if (IsActive=true) or ((StateImg in [imgStopped, imgDone])=true) then // PETROV
            continue;
        fltDown:
          if FTorrents[idxStatus, i] <> TR_STATUS_DOWNLOAD then
            continue;
        fltDone:
          if (StateImg <> imgDone) and (FTorrents[idxStatus, i] <> TR_STATUS_SEED) then
            continue;
        fltStopped:
          if not (StateImg in [imgStopped, imgDone]) then
            continue;
        fltError:
          if not (StateImg in [imgDownError, imgSeedError, imgError]) then
            continue;
        fltWaiting:
            if (FTorrents[idxStatus, i] <> TR_STATUS_CHECK) and (FTorrents[idxStatus, i] <> TR_STATUS_CHECK_WAIT) and (FTorrents[idxStatus, i] <> TR_STATUS_DOWNLOAD_WAIT)then
              continue;
      end;

      if edSearch.Text <> '' then
        if UTF8Pos(UTF8UpperCase(edSearch.Text), UTF8UpperCase(UTF8Encode(widestring(FTorrents[idxName, i])))) = 0 then
          continue;

      if not gTorrents.Items.Find(idxTorrentId, FTorrents[idxTorrentId, i], row) then
        gTorrents.Items.InsertRow(row);
      for j:=-TorrentsExtraColumns to FTorrents.ColCnt - 1 do
        if (j <> idxDownSpeedHistory) and (j <> idxUpSpeedHistory) then
          gTorrents.Items[j, row]:=FTorrents[j, i];
      gTorrents.Items[idxTag, row]:=1;
    end;

    i:=0;
    while i < gTorrents.Items.Count do
      if gTorrents.Items[idxTag, i] = 0 then
        gTorrents.Items.Delete(i)
      else
        Inc(i);

    gTorrents.Sort;
    if gTorrents.Items.Count > 0 then begin
      if OldId <> 0 then begin
        i:=gTorrents.Items.IndexOf(idxTorrentId, OldId);
        if i >= 0 then
          gTorrents.Row:=i
        else
          if FFilterChanged then
            OldId:=0;
      end;
      if OldId = 0 then
        gTorrents.Row:=0;
    end;
    FFilterChanged:=False;
  finally
    gTorrents.Items.EndUpdate;
  end;
  gTorrentsClick(nil);

  crow:=-1;
  lvFilter.Items.BeginUpdate;
  try
    lvFilter.Items[0, 0]:=UTF8Decode(Format('%s (%d)', [SAll, list.Count]));
    lvFilter.Items[0, 1]:=UTF8Decode(Format('%s (%d)', [SDownloading, DownCnt]));
    lvFilter.Items[0, 2]:=UTF8Decode(Format('%s (%d)', [SCompleted, CompletedCnt]));
    lvFilter.Items[0, 3]:=UTF8Decode(Format('%s (%d)', [SActive, ActiveCnt]));
    lvFilter.Items[0, 4]:=UTF8Decode(Format('%s (%d)', [SInactive, FTorrents.Count - ActiveCnt - StoppedCnt]));
    lvFilter.Items[0, 5]:=UTF8Decode(Format('%s (%d)', [sStopped, StoppedCnt]));
    lvFilter.Items[0, 6]:=UTF8Decode(Format('%s (%d)', [sErrorState, ErrorCnt]));
    lvFilter.Items[0, 7]:=UTF8Decode(Format('%s (%d)', [sWaiting, WaitingCnt]));

    j:=StatusFiltersCount;

    if acFolderGrouping.Checked then begin
      lvFilter.Items[0, j]:=NULL;
      Inc(j);

      for i:=0 to Paths.Count - 1 do begin
        s:=ExtractFileName(Paths[i]);
        for row:=StatusFiltersCount + 1 to j - 1 do
          if ExtractFileName(UTF8Encode(widestring(lvFilter.Items[-1, row]))) = s then begin
            s:=Paths[i];
            lvFilter.Items[0, row]:=UTF8Decode(Format('%s (%d)', [UTF8Encode(widestring(lvFilter.Items[-1, row])), ptruint(Paths.Objects[row - StatusFiltersCount - 1])]));
          end;
        lvFilter.Items[ 0, j]:=UTF8Decode(Format('%s (%d)', [s, ptruint(Paths.Objects[i])]));
        lvFilter.Items[-1, j]:=UTF8Decode(Paths[i]);
        if Paths[i] = PathFilter then
          crow:=j;
        Inc(j);
      end;
    end;

    row:=j;

    if acTrackerGrouping.Checked then begin
      if not VarIsNull(lvFilter.Items[0, row - 1]) then begin
        lvFilter.Items[0, row]:=NULL;
        Inc(row);
      end;

      i:=0;
      while i < FTrackers.Count do begin
        j:=ptruint(FTrackers.Objects[i]);
        if j > 0 then begin
          lvFilter.Items[ 0, row]:=UTF8Decode(Format('%s (%d)', [FTrackers[i], j]));
          lvFilter.Items[-1, row]:=NULL;
          if FTrackers[i] = TrackerFilter then
            crow:=row;
          Inc(i);
          Inc(row);
        end
        else
          FTrackers.Delete(i);
      end;
    end;

    lvFilter.Items.RowCnt:=row;
  finally
    lvFilter.Items.EndUpdate;
  end;
  if crow >= 0 then
    lvFilter.Row:=crow
  else
    if lvFilter.Row >= StatusFiltersCount then
      lvFilterClick(nil);

  CheckStatus;

  s := GetHumanSize(FCurDownSpeedLimit*1024,2,'');
  if s = '' then s := Format(SUnlimited,[]) else s := s + '/s';
  ss := GetHumanSize(FCurUpSpeedLimit*1024,2,'');
  if ss = '' then ss := Format(SUnlimited,[]) else ss := ss + '/s';
  StatusBar.Panels[1].Text:=Format(sDownSpeed, [GetHumanSize(DownSpeed, 1)]) + ' (' + s + ')';
  StatusBar.Panels[2].Text:=Format(sUpSpeed, [GetHumanSize(UpSpeed, 1)]) + ' (' + ss + ')';

{$ifndef LCLcarbon}
  // There is memory leak in TTrayIcon implementation for Mac.
  // Disable tray icon update for Mac.
  TrayIcon.Hint:=Format(sDownloadingSeeding,
        [RpcObj.InfoStatus, LineEnding, DownCnt, SeedCnt, LineEnding, StatusBar.Panels[1].Text, StatusBar.Panels[2].Text]);
{$endif LCLcarbon}
  finally
    Paths.Free;
  end;
  DetailsUpdated;
end;

procedure TMainForm.FillPeersList(list: TJSONArray);
var
  i, j, row: integer;
  port: integer;
  d: TJSONData;
  p: TJSONObject;
  ip, s: string;
  hostinfo: PHostEntry;
  opt: TResolverOptions;
  WasEmpty: boolean;
begin
  if list = nil then begin
    ClearDetailsInfo;
    exit;
  end;
  WasEmpty:=lvPeers.Items.Count = 0;
  lvPeers.Items.BeginUpdate;
  try
    lvPeers.Enabled:=True;
    lvPeers.Color:=clWindow;
    if FResolver = nil then begin
      opt:=[];
      if acResolveHost.Checked then
        Include(opt, roResolveIP);
      if acResolveCountry.Checked then
        Include(opt, roResolveCountry);
      if opt <> [] then
        FResolver:=TIpResolver.Create(GetGeoIpDatabase, opt);
    end;

    for i:=0 to lvPeers.Items.Count - 1 do
      lvPeers.Items[idxPeerTag, i]:=0;

    lvPeers.Items.Sort(idxPeerIP);
    for i:=0 to list.Count - 1 do begin
      d:=list[i];
      if not (d is TJSONObject) then continue;
      p:=d as TJSONObject;
      ip:=p.Strings['address'];
      if p.IndexOfName('port') >= 0 then
        port:=p.Integers['port']
      else
        port:=0;

      s:=ip + ':' + IntToStr(port);
      if not lvPeers.Items.Find(idxPeerIP, s, row) then
        lvPeers.Items.InsertRow(row);
      lvPeers.Items[idxPeerIP, row]:=s;
      lvPeers.Items[idxPeerPort, row]:=port;

      if FResolver <> nil then
        hostinfo:=FResolver.Resolve(ip)
      else
        hostinfo:=nil;
      if hostinfo <> nil then
        lvPeers.Items[idxPeerHost, row]:=hostinfo^.HostName
      else
        lvPeers.Items[idxPeerHost, row]:=ip;

      if hostinfo <> nil then
        lvPeers.Items[idxPeerCountry, row]:=hostinfo^.CountryName
      else
        lvPeers.Items[idxPeerCountry, row]:='';

      if acShowCountryFlag.Checked and (hostinfo <> nil) then begin
        if hostinfo^.ImageIndex = 0 then
          hostinfo^.ImageIndex:=GetFlagImage(hostinfo^.CountryCode);
        j:=hostinfo^.ImageIndex
      end
      else
        j:=0;
      lvPeers.Items[idxPeerCountryImage, row]:=j;
      lvPeers.Items[idxPeerClient, row]:=p.Strings['clientName'];
      lvPeers.Items[idxPeerFlags, row]:=p.Strings['flagStr'];
      lvPeers.Items[idxPeerDone, row]:=p.Floats['progress'];

      if p.IndexOfName('rateToClient') >= 0 then
        lvPeers.Items[idxPeerDownSpeed, row]:=p.Integers['rateToClient'];
      if p.IndexOfName('rateToPeer') >= 0 then
        lvPeers.Items[idxPeerUpSpeed, row]:=p.Integers['rateToPeer'];

      lvPeers.Items[idxPeerTag, row]:=1;
    end;

    i:=0;
    while i < lvPeers.Items.Count do
      if lvPeers.Items[idxPeerTag, i] = 0 then
        lvPeers.Items.Delete(i)
      else
        Inc(i);
    lvPeers.Sort;
    if WasEmpty and (lvPeers.Items.Count > 0) then
      lvPeers.Row:=0;
  finally
    lvPeers.Items.EndUpdate;
  end;
  DetailsUpdated;
end;

function TMainForm.GetFilesCommonPath(files: TJSONArray): string;
var
  i: integer;
  d: TJSONData;
  f: TJSONObject;
  s: string;
begin
  Result:='';
  for i:=0 to files.Count - 1 do begin
    d:=files[i];
    if not (d is TJSONObject) then continue;
    f:=d as TJSONObject;
    s:=UTF8Encode(f.Strings['name']);
    if i = 0 then
      Result:=ExtractFilePath(s)
    else begin
      while True do begin
        if Result = '' then
          exit;
        if Copy(s, 1, Length(Result)) <> Result then begin
          SetLength(Result, Length(Result) - 1);
          Result:=ExtractFilePath(Result);
        end
        else
          break;
      end;
    end;
  end;
end;

procedure TMainForm.InternalRemoveTorrent(const Msg, MsgMulti: string; RemoveLocalData: boolean);
var
  args: TJSONObject;
  ids: variant;
  s: string;
  i, j, id: integer;
begin
  if gTorrents.Items.Count = 0 then exit;
  gTorrents.Tag:=1;
  try
    gTorrents.EnsureSelectionVisible;
    ids:=GetSelectedTorrents;
    if gTorrents.SelCount < 2 then
      s:=Format(Msg, [UTF8Encode(widestring(gTorrents.Items[idxName, gTorrents.Items.IndexOf(idxTorrentId, ids[0])]))])
    else
      s:=Format(MsgMulti, [gTorrents.SelCount]);

    s:=TranslateString(s, True);
    if MessageDlg('', s, mtConfirmation, mbYesNo, 0, mbNo) <> mrYes then exit;
  finally
    gTorrents.Tag:=0;
  end;
  args:=TJSONObject.Create;
  if RemoveLocalData then
    args.Add('delete-local-data', TJSONIntegerNumber.Create(1));

  if TorrentAction(ids, 'torrent-remove', args) then begin
    with gTorrents do begin
      BeginUpdate;
      try
        i:=0;
        while i < Items.Count do begin
          id:=Items[idxTorrentId, i];
          for j:=0 to VarArrayHighBound(ids, 1) do
            if id = ids[j] then begin
              Items.Items[idxDeleted, i]:=1;
              break;
            end;
          Inc(i);
        end;
      finally
        EndUpdate;
      end;
    end;

    if RemoveLocalData then
      RpcObj.RefreshNow:=RpcObj.RefreshNow + [rtSession];
  end;
end;

function TMainForm.IncludeProperTrailingPathDelimiter(const s: string): string;
var
  i: integer;
  d: char;
begin
  Result:=s;
  if Result = '' then exit;
  d:='/';
  for i:=1 to Length(Result) do
    if Result[i] in ['/','\'] then begin
      d:=Result[i];
      break;
    end;

  if Result[Length(Result)] <> d then
    Result:=Result + d;
end;

procedure TMainForm.FillFilesList(ATorrentId: integer; list, priorities, wanted: TJSONArray; const DownloadDir: WideString);
begin
  if lvFiles.Tag <> 0 then exit;
  if (list = nil) or (priorities = nil) or (wanted = nil) then begin
    ClearDetailsInfo;
    exit;
  end;

  lvFiles.Enabled:=True;
  lvFiles.Color  :=clWindow;
  FFilesTree.DownloadDir:=UTF8Encode(DownloadDir);
  FFilesTree.FillTree(ATorrentId, list, priorities, wanted);
  tabFiles.Caption:=Format('%s (%d)', [FFilesCapt, list.Count]);
  DetailsUpdated;
end;

procedure TMainForm.FillGeneralInfo(t: TJSONObject);
var
  i, j, idx: integer;
  s: string;
  tr: string;
  f: double;
begin
  if (gTorrents.Items.Count = 0) or (t = nil) then begin
    ClearDetailsInfo;
    exit;
  end;
  idx:=gTorrents.Items.IndexOf(idxTorrentId, t.Integers['id']);
  if idx = -1 then begin
    ClearDetailsInfo;
    exit;
  end;

  txDownProgress.Caption:=Format('%.1f%%', [double(gTorrents.Items[idxDone, idx])]);
  txDownProgress.AutoSize:=True;
  if RpcObj.RPCVersion >= 5 then
    s:=t.Strings['pieces']
  else
    s:='';
  ProcessPieces(s, t.Integers['pieceCount'], gTorrents.Items[idxDone, idx]);

  panTransfer.ChildSizing.Layout:=cclNone;
  txStatus.Caption:=GetTorrentStatus(idx);
  tr:=GetTorrentError(t, gTorrents.Items[idxStatus, idx]);
  txError.Constraints.MinWidth:=400;
  if Ini.ReadBool('Translation', 'TranslateMsg', True) then
    txError.Caption:=TranslateString(tr, True)
  else
      txError.Caption:=tr;

  i:=t.Integers['eta'];
  f:=gTorrents.Items[idxDownSpeed, idx];
  if f > 0 then
    i:=Round(t.Floats['leftUntilDone']/f);
  //txRemaining.Caption:=EtaToString(i);
  txRemaining.Caption:=EtaToString(i)+' ('+GetHumanSize(t.Floats['leftUntilDone'])+')';
  txDownloaded.Caption:=GetHumanSize(t.Floats['downloadedEver']);
  txUploaded.Caption:=GetHumanSize(t.Floats['uploadedEver']);

  f:=t.Floats['pieceSize'];
  if f > 0 then
    i:=Round(t.Floats['corruptEver']/f)
  else
    i:=0;
  txWasted.Caption:=Format(sHashfails, [GetHumanSize(t.Floats['corruptEver']), i]);
  s:=GetHumanSize(gTorrents.Items[idxDownSpeed, idx], 1)+sPerSecond;
  if t.IndexOfName('secondsDownloading') >= 0 then begin
    f:=t.Integers['secondsDownloading'];
    if f > 0 then
      s:=Format('%s (%s: %s)', [s, SAverage, GetHumanSize(t.Floats['downloadedEver']/f, 1) + sPerSecond]);
  end;
  txDownSpeed.Caption:=s;
  txUpSpeed.Caption:=GetHumanSize(gTorrents.Items[idxUpSpeed, idx], 1)+sPerSecond;
  s:=RatioToString(t.Floats['uploadRatio']);
  if t.IndexOfName('secondsSeeding') >= 0 then begin
    i:=t.Integers['secondsSeeding'];
    if i > 0 then
      s:=Format('%s (%s)', [s, EtaToString(i)]);
  end;
  txRatio.Caption:=s;

  if RpcObj.RPCVersion < 5 then
  begin
    // RPC versions prior to v5
    j:=t.Integers['downloadLimitMode'];
    if j = TR_SPEEDLIMIT_GLOBAL then
      s:='-'
    else begin
      i:=t.Integers['downloadLimit'];
      if (i < 0) or (j = TR_SPEEDLIMIT_UNLIMITED) then
        s:=Utf8Encode(WideString(WideChar($221E)))
      else
        s:=GetHumanSize(i*1024)+sPerSecond;
    end;
    txDownLimit.Caption:=s;
    j:=t.Integers['uploadLimitMode'];
    if j = TR_SPEEDLIMIT_GLOBAL then
      s:='-'
    else begin
      i:=t.Integers['uploadLimit'];
      if (i < 0) or (j = TR_SPEEDLIMIT_UNLIMITED) then
        s:=Utf8Encode(WideString(WideChar($221E)))
      else
        s:=GetHumanSize(i*1024)+sPerSecond;
    end;
    txUpLimit.Caption:=s;
  end else begin
    // RPC version 5
    if t.Booleans['downloadLimited'] then
    begin
      i:=t.Integers['downloadLimit'];
      if i < 0 then
        s:=Utf8Encode(WideString(WideChar($221E)))
      else
        s:=GetHumanSize(i*1024)+sPerSecond;
    end else s:='-';
    txDownLimit.Caption:=s;

    if t.Booleans['uploadLimited'] then
    begin
      i:=t.Integers['uploadLimit'];
      if i < 0 then
        s:=Utf8Encode(WideString(WideChar($221E)))
      else
        s:=GetHumanSize(i*1024)+sPerSecond;
    end else s:='-';
    txUpLimit.Caption:=s;
  end;

  if RpcObj.RPCVersion >= 7 then
    with t.Arrays['trackerStats'] do begin
      if Count > 0 then begin
        if integer(Objects[0].Integers['announceState']) in [2, 3] then
          f:=1
        else
          f:=Objects[0].Floats['nextAnnounceTime'];
      end
      else
        f:=0;
    end
  else
    f:=t.Floats['nextAnnounceTime'];
  if f = 0 then
    s:='-'
  else
  if f = 1 then
    s:=sUpdating
  else
    s:=TorrentDateTimeToString(Trunc(f),FFromNow);
  txTrackerUpdate.Caption:=s;
  txTrackerUpdate.Hint:=TorrentDateTimeToString(Trunc(f),not(FFromNow));
  txTracker.Caption:=UTF8Encode(widestring(gTorrents.Items[idxTracker, idx]));
  if RpcObj.RPCVersion >= 7 then
    if t.Arrays['trackerStats'].Count > 0 then
      i:=t.Arrays['trackerStats'].Objects[0].Integers['seederCount']
    else
      i:=-1
  else
    i:=t.Integers['seeders'];
  s:=GetSeedsText(t.Integers['peersSendingToUs'], i);
  txSeeds.Caption:=StringReplace(s, '/', ' ' + sOf + ' ', []) + ' '+ sConnected;
  if RpcObj.RPCVersion >= 7 then
    if t.Arrays['trackerStats'].Count > 0 then
      i:=t.Arrays['trackerStats'].Objects[0].Integers['leecherCount']
    else
      i:=-1
  else
    i:=t.Integers['leechers'];
  s:=GetPeersText(t.Integers['peersGettingFromUs'], -1, i);
  s:=StringReplace(s, ' ', ' '+ sConnected +' ', []);
  s:=StringReplace(s, '/', ' ' + sOf + ' ', []);
  txPeers.Caption:=StringReplace(s, ')', ' '+ sInSwarm+ ')', []);
  txMaxPeers.Caption:=t.Strings['maxConnectedPeers'];
  txLastActive.Caption:=TorrentDateTimeToString(Trunc(t.Floats['activityDate']),FFromNow);
  txLastActive.Hint:=TorrentDateTimeToString(Trunc(t.Floats['activityDate']),Not(FFromNow));
  panTransfer.ChildSizing.Layout:=cclLeftToRightThenTopToBottom;

  if RpcObj.RPCVersion >= 7 then
    txMagnetLink.Text := t.Strings['magnetLink'];


  panGeneralInfo.ChildSizing.Layout:=cclNone;

  s:=UTF8Encode(widestring(gTorrents.Items[idxName, idx]));
  if RpcObj.RPCVersion >= 4 then
    s:=IncludeProperTrailingPathDelimiter(UTF8Encode(t.Strings['downloadDir'])) + s;
  txTorrentName.Caption:=s;
  s:=Trim(UTF8Encode(t.Strings['creator']));
  if s <> '' then
    s:=' by ' + s;
  txCreated.Caption:=TorrentDateTimeToString(Trunc(t.Floats['dateCreated']),FFromNow) + s;
  txCreated.Hint   :=TorrentDateTimeToString(Trunc(t.Floats['dateCreated']),Not(FFromNow)) + s;
  if gTorrents.Items[idxSize, idx] >= 0 then begin
    txTotalSize.Caption:=Format(sDone, [GetHumanSize(t.Floats['totalSize']), GetHumanSize(t.Floats['sizeWhenDone'] - t.Floats['leftUntilDone'])]);
    if t.Floats['totalSize'] = t.Floats['haveValid'] then
      i:=t.Integers['pieceCount']
    else
      i:=Trunc(t.Floats['haveValid']/(t.Floats['pieceSize'] + 0.00000001)); // division by 0

    txPieces.Caption:=Format(sHave, [t.Integers['pieceCount'], GetHumanSize(t.Floats['pieceSize']), i]);
  end
  else begin
    txTotalSize.Caption:='?';
    txPieces.Caption:='?';
  end;

  txHash.Caption:=t.Strings['hashString'];
  txComment.Caption:=UTF8Encode(t.Strings['comment']);
  if (AnsiCompareText(Copy(txComment.Caption, 1, 7), 'http://') = 0)
    or (AnsiCompareText(Copy(txComment.Caption, 1, 8), 'https://') = 0)
  then begin
    if not Assigned(txComment.OnClick) then begin
      txComment.OnClick:=@UrlLabelClick;
      txComment.Cursor:=crHandPoint;
      txComment.Font.Color:=clBlue;
      txComment.Font.Style:=[fsUnderline];
    end;
  end
  else begin
    if Assigned(txComment.OnClick) then begin
      txComment.OnClick:=nil;
      txComment.Cursor:=crDefault;
      txComment.ParentFont:=True;
    end;
  end;
  txAddedOn.Caption:=TorrentDateTimeToString(Trunc(t.Floats['addedDate']),FFromNow);
  txAddedOn.Hint:=TorrentDateTimeToString(Trunc(t.Floats['addedDate']),Not(FFromNow));
  txCompletedOn.Caption:=TorrentDateTimeToString(Trunc(t.Floats['doneDate']),FFromNow);
  txCompletedOn.Hint:=TorrentDateTimeToString(Trunc(t.Floats['doneDate']),Not(FFromNow));
  panGeneralInfo.ChildSizing.Layout:=cclLeftToRightThenTopToBottom;
  DetailsUpdated;
end;

procedure TMainForm.FillTrackersList(TrackersData: TJSONObject);
var
  i, tidx, row: integer;
  id: integer;
  d: TJSONData;
  t: TJSONObject;
  f: double;
  s: string;
  Trackers, TrackerStats: TJSONArray;
  WasEmpty, NoInfo: boolean;
begin
  if TrackersData = nil then begin
    ClearDetailsInfo;
    exit;
  end;
  Trackers:=TrackersData.Arrays['trackers'];
  if RpcObj.RPCVersion >= 7 then
    TrackerStats:=TrackersData.Arrays['trackerStats']
  else
    TrackerStats:=nil;
  tidx:=gTorrents.Items.IndexOf(idxTorrentId, TrackersData.Integers['id']);
  if tidx = -1 then begin
    ClearDetailsInfo;
    exit;
  end;
  i:=gTorrents.Items[idxStatus, tidx];
  NoInfo:=(i = TR_STATUS_STOPPED) or (i = TR_STATUS_FINISHED);
  WasEmpty:=lvTrackers.Items.Count = 0;
  lvTrackers.Items.BeginUpdate;
  try
    lvTrackers.Enabled:=True;
    lvTrackers.Color:=clWindow;
    for i:=0 to lvTrackers.Items.Count - 1 do
      lvTrackers.Items[idxTrackerTag, i]:=0;

    lvTrackers.Items.Sort(idxTrackerID);
    for i:=0 to Trackers.Count - 1 do begin
      d:=Trackers[i];
      if not (d is TJSONObject) then continue;
      t:=d as TJSONObject;
      if t.IndexOfName('id') >= 0 then
        id:=t.Integers['id']
      else
        id:=i;
      if not lvTrackers.Items.Find(idxTrackerID, id, row) then
        lvTrackers.Items.InsertRow(row);
      lvTrackers.Items[idxTrackerID, row]:=id;
      lvTrackers.Items[idxTrackersListName, row]:=t.Strings['announce'];
      if NoInfo then begin
        lvTrackers.Items[idxTrackersListStatus, row]:=NULL;
        lvTrackers.Items[idxTrackersListSeeds, row]:=NULL;
        f:=0;
      end
      else
        if TrackerStats <> nil then begin
          f:=0;
          if i < TrackerStats.Count then
            with TrackerStats.Objects[i] do begin
              s:='';
              if integer(Integers['announceState']) in [2, 3] then
                s:=sTrackerUpdating
              else
                if Booleans['hasAnnounced'] then
                  if Booleans['lastAnnounceSucceeded'] then
                    s:=sTrackerWorking
                  else
                    s:=TranslateString(UTF8Encode(Strings['lastAnnounceResult']), True);

              if s = 'Success' then
                s:=sTrackerWorking;

              lvTrackers.Items[idxTrackersListStatus, row]:=UTF8Decode(s); // UTF8Decode
              lvTrackers.Items[idxTrackersListSeeds, row]:=Integers['seederCount'];

              if integer(Integers['announceState']) in [2, 3] then
                f:=1
              else
                f:=Floats['nextAnnounceTime'];
            end;
        end
        else begin
          if i = 0 then begin
            lvTrackers.Items[idxTrackersListStatus, row]:=gTorrents.Items[idxTrackerStatus, tidx];
            lvTrackers.Items[idxTrackersListSeeds, row]:=gTorrents.Items[idxSeedsTotal, tidx];
          end;
          f:=TrackersData.Floats['nextAnnounceTime'];
        end;

      if f > 1 then begin
        f:=(UnixToDateTime(Trunc(f)) + GetTimeZoneDelta - Now)*SecsPerDay;
        if f < 0 then
          f:=0;
      end;
      if (TrackerStats <> nil) or (i = 0) then
        lvTrackers.Items[idxTrackersListUpdateIn, row]:=f;

      lvTrackers.Items[idxTrackerTag, row]:=1;
    end;

    i:=0;
    while i < lvTrackers.Items.Count do
      if lvTrackers.Items[idxTrackerTag, i] = 0 then
        lvTrackers.Items.Delete(i)
      else
        Inc(i);

    lvTrackers.Sort;
    if WasEmpty and (lvTrackers.Items.Count > 0) then
      lvTrackers.Row:=0;
  finally
    lvTrackers.Items.EndUpdate;
  end;
  DetailsUpdated;
end;

procedure TMainForm.FillSessionInfo(s: TJSONObject);
var
  d, u: integer;
begin
{$ifdef LCLcarbon}
  TrayIcon.Tag:=0;
{$endif LCLcarbon}
  if RpcObj.RPCVersion < 14 then begin
    TR_STATUS_STOPPED:=TR_STATUS_STOPPED_1;
    TR_STATUS_CHECK_WAIT:=TR_STATUS_CHECK_WAIT_1;
    TR_STATUS_CHECK:=TR_STATUS_CHECK_1;
    TR_STATUS_DOWNLOAD_WAIT:=-1;
    TR_STATUS_DOWNLOAD:=TR_STATUS_DOWNLOAD_1;
    TR_STATUS_SEED_WAIT:=-1;
    TR_STATUS_SEED:=TR_STATUS_SEED_1;
  end
  else begin
    TR_STATUS_STOPPED:=TR_STATUS_STOPPED_2;
    TR_STATUS_CHECK_WAIT:=TR_STATUS_CHECK_WAIT_2;
    TR_STATUS_CHECK:=TR_STATUS_CHECK_2;
    TR_STATUS_DOWNLOAD_WAIT:=TR_STATUS_DOWNLOAD_WAIT_2;
    TR_STATUS_DOWNLOAD:=TR_STATUS_DOWNLOAD_2;
    TR_STATUS_SEED_WAIT:=TR_STATUS_SEED_WAIT_2;
    TR_STATUS_SEED:=TR_STATUS_SEED_2;
  end;

  UpdateUIRpcVersion(RpcObj.RPCVersion);

  if RpcObj.RPCVersion >= 5 then begin
{$ifdef LCLcarbon}
    if acAltSpeed.Checked <> (s.Integers['alt-speed-enabled'] <> 0) then
      TrayIcon.Tag:=1;
{$endif LCLcarbon}
    acAltSpeed.Checked:=s.Integers['alt-speed-enabled'] <> 0;
    acUpdateBlocklist.Tag:=s.Integers['blocklist-enabled'];
    acUpdateBlocklist.Enabled:=acUpdateBlocklist.Tag <> 0;
  end;
  if s.IndexOfName('download-dir-free-space') >= 0 then
    StatusBar.Panels[3].Text:=Format(SFreeSpace, [GetHumanSize(s.Floats['download-dir-free-space'])]);

  if (RpcObj.RPCVersion >= 5) and acAltSpeed.Checked then begin
    d:=s.Integers['alt-speed-down'];
    u:=s.Integers['alt-speed-up']
  end
  else begin
    if s.Integers['speed-limit-down-enabled'] <> 0 then
      d:=s.Integers['speed-limit-down']
    else
      d:=-1;
    if s.Integers['speed-limit-up-enabled'] <> 0 then
      u:=s.Integers['speed-limit-up']
    else
      u:=-1;
  end;
{$ifdef LCLcarbon}
  UpdateUI;
{$endif LCLcarbon}
  if (FCurDownSpeedLimit <> d) or (FCurUpSpeedLimit <> u) then begin
    FCurDownSpeedLimit:=d;
    FCurUpSpeedLimit:=u;
    FillSpeedsMenu;
  end;
{$ifdef LCLcarbon}
  if TrayIcon.Tag <> 0 then
    TrayIcon.InternalUpdate;
{$endif LCLcarbon}
end;

procedure TMainForm.FillStatistics(s: TJSONObject);

  procedure _Fill(idx: integer; s: TJSONObject);
  begin
    with gStats do begin
      Items[idx, 0]:=UTF8Decode(GetHumanSize(s.Floats['downloadedBytes']));
      Items[idx, 1]:=UTF8Decode(GetHumanSize(s.Floats['uploadedBytes']));
      Items[idx, 2]:=s.Integers['filesAdded'];
      Items[idx, 3]:=UTF8Decode(SecondsToString(s.Integers['secondsActive']));
    end;
  end;

begin
  if RpcObj.RPCVersion < 4 then
    exit;
  if s = nil then begin
    ClearDetailsInfo;
    exit;
  end;
  gStats.BeginUpdate;
  try
    gStats.Enabled:=True;
    gStats.Color:=clWindow;
    _Fill(1, s.Objects['current-stats']);
    _Fill(2, s.Objects['cumulative-stats']);
  finally
    gStats.EndUpdate;
  end;
  DetailsUpdated;
end;

procedure TMainForm.CheckStatus(Fatal: boolean);
var
  s: string;
  i: integer;
begin
  with MainForm do begin
    s:=TranslateString(RpcObj.Status, True);
    if s <> '' then begin
      RpcObj.Status:='';
      if Fatal then
        DoDisconnect;
      ForceAppNormal;
      if Fatal and not RpcObj.Connected and RpcObj.ReconnectAllowed and (FReconnectTimeOut <> -1) then begin
        FReconnectWaitStart:=Now;
        if FReconnectTimeOut < 60 then
          if FReconnectTimeOut < 10 then
            Inc(FReconnectTimeOut, 5)
          else
            Inc(FReconnectTimeOut, 10);
        txConnError.Caption:=s;
        panReconnectFrame.Hide;
        panReconnect.AutoSize:=True;
        CenterReconnectWindow;
        panReconnect.Show;
        panReconnect.BringToFront;
        TickTimerTimer(nil);
        panReconnect.AutoSize:=False;
        panReconnectFrame.Show;
        CenterReconnectWindow;
      end
      else
        MessageDlg(s, mtError, [mbOK], 0);
    end;

    if StatusBar.Panels[0].Text <> RpcObj.InfoStatus then begin
      StatusBar.Panels[0].Text:=RpcObj.InfoStatus;
      TrayIcon.Hint:=RpcObj.InfoStatus;
      if (RpcObj.Connected) and (RpcObj.Http.UserName <> '') then
        FPasswords.Values[FCurConn]:=RpcObj.Http.Password;  // Save password to cache
    end;
    if not RpcObj.Connected then
      for i:=1 to StatusBar.Panels.Count - 1 do
        StatusBar.Panels[i].Text:='';
  end;
end;

function TMainForm.TorrentAction(const TorrentIds: variant; const AAction: string; args: TJSONObject): boolean;
var
  req: TJSONObject;
  ids: TJSONArray;
  i: integer;
begin
  if VarIsEmpty(TorrentIds) then
    exit;
  Application.ProcessMessages;
  AppBusy;
  req:=TJSONObject.Create;
  try
    req.Add('method', AAction);
    if args = nil then
      args:=TJSONObject.Create;
    if not VarIsNull(TorrentIds) then begin
      ids:=TJSONArray.Create;
      if VarIsArray(TorrentIds) then begin
        for i:=VarArrayLowBound(TorrentIds, 1) to VarArrayHighBound(TorrentIds, 1) do
          ids.Add(integer(TorrentIds[i]));
      end
      else
        ids.Add(integer(TorrentIds));
      args.Add('ids', ids);
    end;
    req.Add('arguments', args);
    args:=RpcObj.SendRequest(req, False, 30000);
    Result:=args <> nil;
    args.Free;
  finally
    req.Free;
  end;
  if not Result then
    CheckStatus(False)
  else
    DoRefresh(True);
  AppNormal;
end;

function TMainForm.SetFilePriority(TorrentId: integer; const Files: array of integer; const APriority: string): boolean;

  function CreateFilesArray: TJSONArray;
  var
    i: integer;
  begin
    Result:=TJSONArray.Create;
    for i:=Low(Files) to High(Files) do
      Result.Add(Files[i]);
  end;

var
  req, args: TJSONObject;
begin
  AppBusy;
  req:=TJSONObject.Create;
  try
    req.Add('method', 'torrent-set');
    args:=TJSONObject.Create;
    if TorrentId <> 0 then
      args.Add('ids', TJSONArray.Create([TorrentId]));
    if APriority = 'skip' then
      args.Add('files-unwanted', CreateFilesArray)
    else begin
      args.Add('files-wanted', CreateFilesArray);
      args.Add('priority-' + APriority, CreateFilesArray);
    end;
    req.Add('arguments', args);
    args:=RpcObj.SendRequest(req, False);
    Result:=args<> nil;
    args.Free;
  finally
    req.Free;
  end;
  if not Result then
    CheckStatus(False)
  else
    DoRefresh;
  AppNormal;
end;

function TMainForm.SetCurrentFilePriority(const APriority: string): boolean;
var
  Files: array of integer;
  i, j, k, level: integer;
  pri: string;
begin
  Result:= false;
  if (gTorrents.Items.Count = 0) or (PageInfo.ActivePage <> tabFiles) then exit;
  SetLength(Files, lvFiles.Items.Count);
  pri:=APriority;
  j:=0;
  if APriority <> '' then begin
    // Priority for currently selected rows
    if lvFiles.SelCount = 0 then
      lvFiles.RowSelected[lvFiles.Row]:=True;
    level:=-1;
    for i:=0 to lvFiles.Items.Count - 1 do begin
      k:=FFilesTree.RowLevel[i];
      if k <= level then
        level:=-1;
      if lvFiles.RowSelected[i] or ( (level <> -1) and (k > level) ) then begin
        if FFilesTree.IsFolder(i) then begin
          if level = -1 then
            level:=k;
        end
        else begin
          Files[j]:=FFiles[idxFileId, i];
          Inc(j);
        end;
      end;
    end;
  end
  else begin
    // Priority based on checkbox state
    for i:=0 to FFiles.Count - 1 do
      if not FFilesTree.IsFolder(i) then begin
        k:=FFiles[idxFilePriority, i];
        if (k <> TR_PRI_SKIP) <> (FFilesTree.Checked[i] = cbChecked) then begin
          if pri = '' then
            if FFilesTree.Checked[i] = cbChecked then
              pri:='normal'
            else
              pri:='skip';
          Files[j]:=FFiles[idxFileId, i];
          Inc(j);
        end;
      end;
  end;

  if j = 0 then exit;
  SetLength(Files, j);
  Result:=SetFilePriority(RpcObj.CurTorrentId, Files, pri);
end;

procedure TMainForm.SetTorrentPriority(APriority: integer);
var
  args: TJSONObject;
begin
  if gTorrents.Items.Count = 0 then exit;
  args:=TJSONObject.Create;
  args.Add('bandwidthPriority', TJSONIntegerNumber.Create(APriority));
  TorrentAction(GetSelectedTorrents, 'torrent-set', args);
end;

procedure TMainForm.ProcessPieces(const Pieces: string; PieceCount: integer; const Done: double);
const
  MaxPieces = 4000;
var
  i, j, k, x, xx: integer;
  s: string;
  R: TRect;
  bmp: TBitmap;
  c: double;
begin
  FLastPieces:=Pieces;
  FLastPieceCount:=PieceCount;
  FLastDone:=Done;
  bmp:=nil;
  try
    if FTorrentProgress = nil then
      FTorrentProgress:=TBitmap.Create;
    if RpcObj.RPCVersion >= 5 then begin
      bmp:=TBitmap.Create;
      if PieceCount > MaxPieces then begin
        bmp.Width:=MaxPieces;
        c:=MaxPieces/PieceCount;
      end
      else begin
        bmp.Width:=PieceCount;
        c:=1;
      end;
      bmp.Height:=12;
      bmp.Canvas.Brush.Color:=clWindow;
      bmp.Canvas.FillRect(0, 0, bmp.Width, bmp.Height);
      bmp.Canvas.Brush.Color:=clHighlight;
      x:=0;
      s:=DecodeBase64(Pieces);
      for i:=1 to Length(s) do begin
        j:=byte(s[i]);
        for k:=1 to 8 do begin
          if PieceCount = 0 then
            break;
          if j and $80 <> 0 then begin
            xx:=Trunc(x*c);
            bmp.Canvas.FillRect(xx, 0, xx + 1, bmp.Height);
          end;
          Inc(x);
          j:=j shl 1;
          Dec(PieceCount);
        end;
      end;
    end;

    with FTorrentProgress.Canvas do begin
      FTorrentProgress.Width:=pbDownloaded.ClientWidth;
      if bmp <> nil then begin
        i:=bmp.Height div 3;
        FTorrentProgress.Height:=bmp.Height + 5 + i;
        Brush.Color:=clWindow;
        FillRect(0, 0, FTorrentProgress.Width, FTorrentProgress.Height);
        Brush.Color:=clBtnShadow;
        R:=Rect(0, i + 3, FTorrentProgress.Width, FTorrentProgress.Height);
        FillRect(R);
        InflateRect(R, -1, -1);
        if bmp.Width > 0 then
          StretchDraw(R, bmp)
        else begin
          Brush.Color:=clWindow;
          FillRect(R);
        end;
        R:=Rect(0, 0, FTorrentProgress.Width, i + 2);
      end
      else begin
        FTorrentProgress.Height:=14;
        R:=Rect(0, 0, FTorrentProgress.Width, FTorrentProgress.Height);
      end;
      Brush.Color:=clBtnShadow;
      FillRect(R);
      InflateRect(R, -1, -1);
      x:=R.Left + Round((R.Right - R.Left)*Done/100.0);
      Brush.Color:=clHighlight;
      FillRect(R.Left, R.Top, x, R.Bottom);
      Brush.Color:=clWindow;
      FillRect(x, R.Top, R.Right, R.Bottom);
    end;
    if pbDownloaded.Height <> FTorrentProgress.Height then begin
      pbDownloaded.Constraints.MaxHeight:=FTorrentProgress.Height;
      pbDownloaded.Height:=FTorrentProgress.Height;
      panProgress.AutoSize:=True;
      panProgress.AutoSize:=False;
    end;
    pbDownloaded.Invalidate;
  finally
    bmp.Free;
  end;
end;

function TMainForm.ExecRemoteFile(const FileName: string; SelectFile: boolean; Userdef: boolean): boolean;

  procedure _Exec(s: string);
  var
    p: string;
  begin
    AppBusy;
    if SelectFile then begin
      if FileExistsUTF8(s) then begin
{$ifdef mswindows}
if Userdef then
              begin
                    p:=Format(FUserDefinedMenuParam, [s]);
                    s:=FUserDefinedMenuEx;
              end
              else
              begin
                    p:=Format(FFileManagerDefaultParam, [s]); ; // ALERT  //      p:=Format('/select,"%s"', [s]);
                    s:=FFileManagerDefault;                               //      s:='explorer.exe';
              end;
{$else}
        p:='';
        s:=ExtractFilePath(s);
{$endif mswindows}
      end else begin
        p:='';
        s:=ExtractFilePath(s);
      end;
    end else begin

    end;

{$ifdef mswindows}
if Userdef then
              begin
                    p := Format(FUserDefinedMenuParam, [s]);
                    s := FUserDefinedMenuEx;
              end ;
                    Result:=OpenURL(s, p);
{$else}
      if FLinuxOpenDoc = 0 then
          Result := OpenURL(s, p)    // does not work in latest linux very well!!!! old.vers
      else
          Result := OpenDocument(s); // works better - new.vers
{$endif mswindows}

    AppNormal;
    if not Result then begin
      ForceAppNormal;
      MessageDlg(Format(sUnableToExecute, [s]), mtError, [mbOK], 0);
    end;
  end;

var
  s,r: string;
  i: integer;
begin
  Result:= false;
  s:=MapRemoteToLocal(FileName);
  if s <> '' then begin
    if Userdef then
      begin
      if (lvFiles.Focused) and (lvFiles.SelCount > 1) then
          begin
                r := '';
                for i := 0 to lvFiles.Items.Count-1 do
                  if lvFiles.RowSelected[i] then
                    if r = '' then r := MapRemoteToLocal(FFilesTree.GetFullPath(i)) + '"'  else
                      r := r + ' "'+ MapRemoteToLocal(FFilesTree.GetFullPath(i)) + '"';
                s := r;
          end;
        // else s := '"' + s + '"';
      end;
    _Exec(s);
    exit;
  end;
  if FileExistsUTF8(FileName) or DirectoryExistsUTF8(FileName) then begin
    _Exec(FileName);
    exit;
  end;

  ForceAppNormal;
  MessageDlg(sNoPathMapping, mtInformation, [mbOK], 0);
end;

function TMainForm.GetSelectedTorrents: variant;
var
  i, j: integer;
begin
  with gTorrents do begin
    if Items.Count = 0 then begin
      Result:=Unassigned;
      exit;
    end;
    if SelCount = 0 then
      Result:=VarArrayOf([Items[idxTorrentId, Row]])
    else begin
      Result:=VarArrayCreate([0, SelCount - 1], varinteger);
      j:=0;
      for i:=0 to gTorrents.Items.Count - 1 do
        if gTorrents.RowSelected[i] then begin
          Result[j]:=Items[idxTorrentId, i];
          Inc(j);
        end;
    end;
  end;
end;

function TMainForm.GetDisplayedTorrents: variant;
var
  i,j : integer;
begin
  with gTorrents do begin
    if Items.Count = 0 then begin
      Result:=Unassigned;
      exit;
    end;
        Result:=VarArrayCreate([0, gTorrents.Items.Count - 1], varinteger);
        j:=0;
        for i:=0 to gTorrents.Items.Count - 1 do
          if gTorrents.RowVisible[i] then begin
            Result[j]:=Items[idxTorrentId, i];
            Inc(j);
          end;
  end;
end;

procedure TMainform.StatusBarSizes;
var
  MMap: TMyHashMap;
  ids, cidx: variant;
  TotalSize, TotalDownloaded, TotalSizeToDownload, TorrentDownloaded, TorrentSizeToDownload: Int64;
  i: Integer;
  a, b, c, d: Int64;
begin
    try
    if gTorrents.Items.Count > 0 then
      begin
        if gTorrents.SelCount > 0 then
            ids := GetSelectedTorrents
        else  ids := GetDisplayedTorrents;
        TotalSize := 0;
        TotalDownloaded := 0;
        TotalSizeToDownload := 0;

        MMap := TMyHashMap.Create;
        for i:=0 to FTorrents.Count -1 do
        begin
          MMap[StrToInt(Ftorrents.Items[idxTorrentId, i])] := i;
        end;

        for i:=VarArrayLowBound(ids, 1) to VarArrayHighBound(ids, 1) do
        begin
          cidx := MMap[ids[i]];
          TotalSize             := TotalSize + FTorrents.Items[idxSize, cidx];
          TorrentSizeToDownload := FTorrents.Items[idxSizetoDowload, cidx];
          TorrentDownloaded     := TorrentSizeToDownload * (FTorrents.Items[idxDone, cidx] / 100);
          TotalSizeToDownload   := TotalSizeToDownload + TorrentSizeToDownload;
          TotalDownloaded       := TotalDownloaded + TorrentDownloaded;
        end;
        MMap.Free;

        StatusBar.Panels[4].Text:=Format(sTotalSize,[GetHumanSize(TotalSize, 0, '?')]);
        StatusBar.Panels[5].Text:=Format(sTotalSizeToDownload,[GetHumanSize(TotalSizeToDownload, 0, '?')]);
        StatusBar.Panels[6].Text:=Format(sTotalDownloaded,[GetHumanSize(TotalDownloaded, 0, '?')]);
        StatusBar.Panels[7].Text:=Format(sTotalRemain,[GetHumanSize(TotalSizeToDownload - TotalDownloaded, 0, '?')]);
    end
    else
    begin
      StatusBar.Panels[4].Text:=Format(sTotalSize,[GetHumanSize(0, 0, '?')]);
      StatusBar.Panels[5].Text:=Format(sTotalSizeToDownload,[GetHumanSize(0, 0, '?')]);
      StatusBar.Panels[6].Text:=Format(sTotalDownloaded,[GetHumanSize(0, 0, '?')]);
      StatusBar.Panels[7].Text:=Format(sTotalRemain,[GetHumanSize(0, 0, '?')]);
    end;
    except
        gTorrents.Refresh;
    end;

end;

procedure TMainForm.FillDownloadDirs(CB: TComboBox; const CurFolderParam: string);
var
  i, j, n,xx, m: integer;
  s, IniSec: string;
  lastDt:string;
  pFD : FolderData;

  dd, mm, yy : string;
  nd, nm, ny : integer;
begin
  CB.Items.Clear;

  IniSec   := 'AddTorrent.' + FCurConn;
  j        := Ini.ReadInteger(IniSec, 'FolderCount', 0);

  for i:=0 to j - 1 do begin
    s:=Ini.ReadString(IniSec, Format('Folder%d', [i]), '');
    if s <> '' then begin
      s := CorrectPath (s);

      n := 0;
      for xx:=0 to CB.Items.Count - 1 do begin
        if CB.Items[xx]=s then begin
          n := 1;
        end;
      end;

      if n=0 then begin
        m      := CB.Items.Add(s);
        pFD    := FolderData.create;
        pFD.Hit:= Ini.ReadInteger (IniSec, Format('FolHit%d', [i]), 1);
        pFD.Ext:= Ini.ReadString  (IniSec, Format('FolExt%d', [i]),'');
        lastDt := Ini.ReadString  (IniSec, Format('LastDt%d', [i]),'');
        pFD.Txt:= s; // for debug

        try
      pFD.Lst := EncodeDate(2000,1,1); // last time folder

          if (lastDt <> '') then begin
            dd := Copy (lastDt, 1, 2);
            mm := Copy (lastDt, 4, 2);
            yy := Copy (lastDt, 7, 4);
            nd := StrToInt(dd);
            nm := StrToInt(mm);
            ny := StrToInt(yy);
            if (nd < 1) or (nd > 31)   then nd := 1;
            if (nm < 1) or (nm > 12)   then nm := 1;
            if (ny < 1) or (ny > 2222) then ny := 2000;
            pFD.Lst := EncodeDate(ny,nm,nd);
          end
        except
          MessageDlg('Error: LS-007. Please contact the developer', mtError, [mbOK], 0);
            pFD.Lst := EncodeDate(2000,1,1); // last time folder
        end;

        CB.Items.Objects[m] := pFD;
      end;
    end;
  end;

  s:=CorrectPath (Ini.ReadString(IniSec, CurFolderParam, ''));
  if s <> '' then begin
    i:=CB.Items.IndexOf(s);
    if i > 0 then  // autosorting
      CB.ItemIndex:=i;
    CB.Text := s;
  end
  else begin
    if CB.Items.Count > 0 then
      CB.ItemIndex:=0;
  end;
end;

procedure TMainForm.SaveDownloadDirs(CB: TComboBox; const CurFolderParam: string);
var
  i: integer;
  IniSec: string;
  tmp,selfolder : string;
  strdate : string;
  pFD : FolderData;
begin
  IniSec   := 'AddTorrent.' + FCurConn;
  selfolder:= CorrectPath(CB.Text);
  i        := CB.Items.IndexOf(selfolder);

  try
    if CurFolderParam = 'LastMoveDir' then begin
      if i < 0 then begin
          DeleteDirs (CB, 1);
          CB.Items.Add  (selfolder);
          i := CB.Items.IndexOf(selfolder);
          if i >= 0 then begin
            pFD    := FolderData.create;
            if pFD <> nil then begin
              pFD.Hit:= 1;
              pFD.Ext:= '';
              pFD.Txt:= selfolder;
              pFD.Lst:= IncDay(Today, 7); // +7 days
              CB.Items.Objects[i]:= pFD;
            end;
          end;
      end else begin
          pFD    := CB.Items.Objects[i] as FolderData;
          if pFD <> nil then begin
            pFD.Hit:= pFD.Hit + 1;
            pFD.Lst:= Today;
            CB.Items.Objects[i]:= pFD;
          end;
          DeleteDirs (CB, 0);
      end;
    end;
  except
//    MessageDlg('Error: LS-008. Please contact the developer', mtError, [mbOK], 0);
  end;

  try
    Ini.WriteInteger(IniSec, 'FolderCount', CB.Items.Count);
    for i:=0 to CB.Items.Count - 1 do begin
      tmp := CorrectPath(CB.Items[i]);
      pFD := CB.Items.Objects[i] as FolderData;
      if pFD = nil then continue;

      Ini.WriteString (IniSec, Format('Folder%d', [i]), tmp);
      Ini.WriteInteger(IniSec, Format('FolHit%d', [i]), pFD.Hit);
      Ini.WriteString (IniSec, Format('FolExt%d', [i]), pFD.Ext);

      DateTimeToString(strdate, 'dd.mm.yyyy', pFD.Lst);
      Ini.WriteString (IniSec, Format('LastDt%d', [i]), strdate);
    end;

    // clear string
    Ini.WriteString (IniSec, Format('Folder%d', [i+1]), '' );
    Ini.WriteInteger(IniSec, Format('FolHit%d', [i+1]), -1 );
    Ini.WriteString (IniSec, Format('FolExt%d', [i+1]), '' );
    Ini.WriteString (IniSec, Format('LastDt%d', [i+1]), '' );
  except
    MessageDlg('Error: LS-009. Please contact the developer', mtError, [mbOK], 0);
  end;

  Ini.WriteString(IniSec, CurFolderParam, selfolder); // autosorting, valid from text
  Ini.UpdateFile;
end;

procedure TMainForm.DeleteDirs(CB: TComboBox; maxdel : Integer);
var
  i,min,max,indx, fldr: integer;
  pFD : FolderData;
begin
    max:=Ini.ReadInteger('Interface', 'MaxFoldersHistory',  50);
    Ini.WriteInteger    ('Interface', 'MaxFoldersHistory', max);

    try
    while (CB.Items.Count+maxdel) >= max do begin
      min := 9999999;
      indx:=-1;
      for i:=0 to CB.Items.Count - 1 do begin
        pFD := CB.Items.Objects[i] as FolderData;
        if pFD = nil then continue;

        fldr := DaysBetween(Today,pFD.Lst);
        if Today > pFD.Lst then
          fldr := 0- fldr;

        fldr := fldr + pFD.Hit;
        if fldr < min then begin
          min := fldr;
          indx:= i;
        end;
      end;

      if indx > -1 then
        CB.Items.Delete(indx);
    end;
    except
      MessageDlg('Error: LS-010. Please contact the developer', mtError, [mbOK], 0);
    end;
end;

procedure TMainForm.SetRefreshInterval;
var
  i: TDateTime;
begin
  if Visible and (WindowState <> wsMinimized) then
    i:=Ini.ReadInteger('Interface', 'RefreshInterval', 5)
  else
    i:=Ini.ReadInteger('Interface', 'RefreshIntervalMin', 20);
  if i < 1 then
    i:=1;
  RpcObj.RefreshInterval:=i/SecsPerDay;
end;

procedure TMainForm.AddTracker(EditMode: boolean);
var
  req, args: TJSONObject;
  id, torid: integer;
begin
  AppBusy;
  with TAddTrackerForm.Create(Self) do
  try
    id:=0;
    torid:=RpcObj.CurTorrentId;
    if EditMode then begin
      Caption:=STrackerProps;
      edTracker.Text:=UTF8Encode(widestring(lvTrackers.Items[idxTrackersListName, lvTrackers.Row]));
      id:=lvTrackers.Items[idxTrackerID, lvTrackers.Row];
    end;
    AppNormal;
    if ShowModal = mrOk then begin
      AppBusy;
      Self.Update;
      req:=TJSONObject.Create;
      try
        req.Add('method', 'torrent-set');
        args:=TJSONObject.Create;
        args.Add('ids', TJSONArray.Create([torid]));
        if EditMode then
          args.Add('trackerReplace', TJSONArray.Create([id, UTF8Encode(edTracker.Text)]))  //fix bag
        else
          args.Add('trackerAdd', TJSONArray.Create([UTF8Encode(edTracker.Text)])); //fix bag
        req.Add('arguments', args);
        args:=nil;
        args:=RpcObj.SendRequest(req, False);
        if args = nil then begin
          CheckStatus(False);
          exit;
        end;
        args.Free;
      finally
        req.Free;
      end;
      DoRefresh;
      AppNormal;
    end;
  finally
    Free;
  end;
end;

procedure TMainForm.UpdateConnections;
var
  i, j, cnt: integer;
  s, cur: string;
  mi: TMenuItem;
begin
  while (pmConnections.Items.Count > 0) and (pmConnections.Items[0].Tag = 0) do
    pmConnections.Items[0].Free;
  while (miConnect.Count > 0) and (miConnect.Items[0].Tag = 0) do
    miConnect.Items[0].Free;
  cur:=Ini.ReadString('Hosts', 'CurHost', '');
  cnt:=Ini.ReadInteger('Hosts', 'Count', 0);
  j:=0;
  for i:=1 to cnt do begin
    s:=Ini.ReadString('Hosts', Format('Host%d', [i]), '');
    if s <> '' then begin
      mi:=TMenuItem.Create(pmConnections);
      mi.Caption:=s;
      if s = cur then
        mi.Checked:=True;
      mi.OnClick:=@DoConnectToHost;
      pmConnections.Items.Insert(j, mi);

      mi:=TMenuItem.Create(miConnect);
      mi.Caption:=s;
      if s = cur then
        mi.Checked:=True;
      mi.OnClick:=@DoConnectToHost;
      miConnect.Insert(j, mi);
      Inc(j);
    end;
  end;
  sepCon1.Visible:=j > 0;
  sepCon2.Visible:=j > 0;
end;

procedure TMainForm.DoConnectToHost(Sender: TObject);
var
  mi: TMenuItem;
  Sec: string;
begin
  mi:=TMenuItem(Sender);
  if RpcObj.Connected and (FCurConn = mi.Caption) then
    exit;
  DoDisconnect;
  Sec:='Connection.' + FCurConn;
  if (FReconnectTimeOut = -1) and Ini.ReadBool(Sec, 'Autoreconnect', False) then
        FReconnectTimeOut:=0;
  FCurConn:=mi.Caption;
  DoConnect;
end;

procedure TMainForm.DoSetDownloadSpeed(Sender: TObject);
begin
  SetSpeedLimit('down', TMenuItem(Sender).Tag);
end;

procedure TMainForm.DoSetUploadSpeed(Sender: TObject);
begin
  SetSpeedLimit('up', TMenuItem(Sender).Tag);
end;

procedure TMainForm.SetSpeedLimit(const Dir: string; Speed: integer);
var
  req, args: TJSONObject;
begin
  AppBusy;
  req:=TJSONObject.Create;
  try
    req.Add('method', 'session-set');
    args:=TJSONObject.Create;
    args.Add(Format('speed-limit-%s-enabled', [Dir]), integer(Speed >= 0) and 1);
    if Speed >= 0 then
      args.Add(Format('speed-limit-%s', [Dir]), Speed);
    args.Add('alt-speed-enabled', 0);
    req.Add('arguments', args);
    args:=RpcObj.SendRequest(req, False);
    if args = nil then begin
      CheckStatus(False);
      exit;
    end;
    args.Free;
  finally
    req.Free;
  end;
  RpcObj.RefreshNow:=RpcObj.RefreshNow + [rtSession];
  AppNormal;
end;

function TMainForm.FixSeparators(const p: string): string;
begin
  Result:=StringReplace(p, '/', DirectorySeparator, [rfReplaceAll]);
  Result:=StringReplace(Result, '\', DirectorySeparator, [rfReplaceAll]);
end;

function TMainForm.MapRemoteToLocal(const RemotePath: string): string;
var
  i, j: integer;
  s, ss, fn: string;
begin
  Result:='';
  fn:=FixSeparators(Trim(RemotePath));
  for i:=0 to FPathMap.Count - 1 do begin
    s:=FPathMap[i];
    j:=Pos('=', s);
    if j > 0 then begin
      ss:=FixSeparators(Copy(s, 1, j - 1));
      if (ss = fn) or (Pos(IncludeProperTrailingPathDelimiter(ss), fn) = 1) then begin
        if ss = fn then
          ss:=Copy(s, j + 1, MaxInt)
        else begin
          ss:=IncludeProperTrailingPathDelimiter(ss);
          ss:=IncludeTrailingPathDelimiter(Copy(s, j + 1, MaxInt)) + Copy(fn, Length(ss) + 1, MaxInt);
        end;
        Result:=FixSeparators(ss);
        exit;
      end;
    end;
  end;
end;

procedure TMainForm.UpdateUIRpcVersion(RpcVersion: integer);
var
  vc: boolean;
begin
  acRemoveTorrentAndData.Visible:=RPCVersion >= 4;
  acReannounceTorrent.Visible:=RPCVersion >= 5;
  acUpdateBlocklist.Visible:=RPCVersion >= 5;
  acMoveTorrent.Visible:=RPCVersion >= 6;
  pmiPriority.Visible:=RPCVersion >= 5;
  miPriority.Visible:=pmiPriority.Visible;
  acOpenContainingFolder.Visible:=RPCVersion >= 4;
  acOpenFile.Visible:=acOpenContainingFolder.Visible;
  pmSepOpen1.Visible:=acOpenContainingFolder.Visible;
  pmSepOpen2.Visible:=acOpenContainingFolder.Visible;
  MenuItem101.Visible:=RPCVersion >= 7;

  vc:=not sepAltSpeed.Visible and (RPCVersion >= 5);
  sepAltSpeed.Visible:=RPCVersion >= 5;
  acAltSpeed.Visible:=RPCVersion >= 5;
  if vc then begin
    sepAltSpeed.Left:=tbStopTorrent.Left + 1;
    tbtAltSpeed.Left:=sepAltSpeed.Left + 1;
  end;

  acAddTracker.Visible:=RPCVersion >= 10;
  acEditTracker.Visible:=acAddTracker.Visible;
  acDelTracker.Visible:=acAddTracker.Visible;
  acAdvEditTrackers.Visible:=acAddTracker.Visible;
  sepTrackers.Visible:=acAddTracker.Visible;

  vc:=not sepQueue.Visible and (RPCVersion >= 14);
  sepQueue.Visible:=RPCVersion >= 14;
  acQMoveUp.Visible:=RPCVersion >= 14;
  acQMoveDown.Visible:=RPCVersion >= 14;
  miQueue.Visible:=RPCVersion >= 14;
  pmiQueue.Visible:=RPCVersion >= 14;
  if vc then begin
    sepQueue.Left:=tbStopTorrent.Left + 1;
    tbQMoveUp.Left:=sepQueue.Left + 1;
    tbQMoveDown.Left:=tbQMoveUp.Left + 1;
  end;
  acForceStartTorrent.Visible:=RPCVersion >= 14;
  tabStats.Visible:=RpcVersion >= 4;
  acRename.Visible:=RpcVersion >= 15;
end;

procedure TMainForm.CheckAddTorrents;
var
  i: integer;
  h: System.THandle;
  s: string;
  WasHidden: boolean;
begin
  h:=FileOpenUTF8(FIPCFileName, fmOpenRead or fmShareDenyWrite);
  if h <> System.THandle(-1) then begin
    i:=FileSeek(h, 0, soFromEnd);
    SetLength(s, i);
    if i > 0 then begin
      FileSeek(h, 0, soFromBeginning);
      SetLength(s, FileRead(h, s[1], i));
    end;
    FileTruncate(h, 0);
    FileClose(h);
    LazFileUtils.DeleteFileUTF8(FIPCFileName);

    if s = '' then begin
      ShowApp;
      exit;
    end;

    FPendingTorrents.Text:=FPendingTorrents.Text + s;
  end;

  if FAddingTorrent <> 0 then
    exit;

  Inc(FAddingTorrent);
  try
    if FPendingTorrents.Count > 0 then begin
      Application.ProcessMessages;
      TickTimer.Enabled:=True;
      WasHidden:=not IsTaskbarButtonVisible;
      if WasHidden then
        Application.BringToFront
      else
        ShowApp;
      try
        while FPendingTorrents.Count > 0 do begin
          s:=FPendingTorrents[0];
          FPendingTorrents.Delete(0);
          if s <> '' then
            DoAddTorrent(s);
        end;
      finally
        if WasHidden then
          HideTaskbarButton;
          FWatchDownloading := false;
      end;
    end;
  finally
    Dec(FAddingTorrent);
  end;
end;

procedure TMainForm.CheckClipboardLink;
const
  strTorrentExt = '.torrent';
var
  s: string;
begin
  try
    if not FLinksFromClipboard then
      exit;
    s:=Clipboard.AsText;
    if s = FLastClipboardLink then
      exit;
    FLastClipboardLink:=s;
    if isHash(s) then s := 'magnet:?xt=urn:btih:' + s;
    if not IsProtocolSupported(s) then
      exit;
    if (Pos('magnet:', LazUTF8.UTF8LowerCase(s)) <> 1) and (LazUTF8.UTF8LowerCase(Copy(s, Length(s) - Length(strTorrentExt) + 1, MaxInt)) <> strTorrentExt) then
      exit;

    AddTorrentFile(s);
    Clipboard.AsText:='';
  except
    // Turn off this function if an error occurs
    FLinksFromClipboard:=False;
  end;
end;

procedure TMainForm.CenterDetailsWait;
begin
  panDetailsWait.Left:=PageInfo.Left + (PageInfo.Width - panDetailsWait.Width) div 2;
  panDetailsWait.Top:=PageInfo.Top + (PageInfo.Height - panDetailsWait.Height) div 2;
end;

function TMainForm.GetPageInfoType(pg: TTabSheet): TAdvInfoType;
begin
  if pg = tabGeneral then
    Result:=aiGeneral
  else
  if pg = tabPeers then
    Result:=aiPeers
  else
  if pg = tabFiles then
    Result:=aiFiles
  else
  if pg = tabTrackers then
    Result:=aiTrackers
  else
  if pg = tabStats then
    Result:=aiStats
  else
    Result:=aiNone;
end;

procedure TMainForm.DetailsUpdated;
begin
  FDetailsWaitStart:=0;
  PageInfo.ActivePage.Tag:=0;
end;

function TMainForm.RenameTorrent(TorrentId: integer; const OldPath, NewName: string): boolean;
var
  args: TJSONObject;
begin
  Result:=False;
  if ExtractFileName(OldPath) = NewName then
    exit;
  args:=TJSONObject.Create;
  args.Add('path', UTF8Decode(OldPath));
  args.Add('name', UTF8Decode(NewName));
  Result:=TorrentAction(TorrentId, 'torrent-rename-path', args);
end;

procedure TMainForm.FilesTreeStateChanged(Sender: TObject);
begin
  SetCurrentFilePriority('');
end;

function TMainForm.SelectTorrent(TorrentId, TimeOut: integer): integer;
var
  tt: TDateTime;
  br: boolean;
begin
  Result:=-1;
  if TorrentId = 0 then
    exit;
  br:=False;
  tt:=Now;
  while True do begin
    Application.ProcessMessages;
    Result:=gTorrents.Items.IndexOf(idxTorrentId, TorrentId);
    if Result >= 0 then begin
      gTorrents.RemoveSelection;
      gTorrents.Row:=Result;
      RpcObj.CurTorrentId:=TorrentId;
      if Self.Visible and (Self.WindowState <> wsMinimized) and gTorrents.Enabled then
        Self.ActiveControl:=gTorrents;
      break;
    end;
    if br then
      break;
    Sleep(100);
    if Now - tt >= TimeOut/MSecsPerDay then
      br:=True;
  end;
end;

procedure TMainForm.OpenCurrentTorrent(OpenFolderOnly: boolean; UserDef: boolean);
var
  res: TJSONObject;
  p, s: string;
  sel: boolean;
  files: TJSONArray;
begin
  if gTorrents.Items.Count = 0 then
    exit;
  Application.ProcessMessages;
  AppBusy;
  try
    sel:=False;
    gTorrents.RemoveSelection;
    res:=RpcObj.RequestInfo(gTorrents.Items[idxTorrentId, gTorrents.Row], ['files', 'downloadDir']);
    if res = nil then
      CheckStatus(False)
    else
      try
        with res.Arrays['torrents'].Objects[0] do begin
          files:=Arrays['files'];
          if files.Count = 0 then exit;
          if files.Count = 1 then begin
            p:=UTF8Encode((files[0] as TJSONObject).Strings['name']);
            sel:=OpenFolderOnly;
          end
          else begin
            //sel:=OpenFolderOnly; // bag? missed?
            s:=GetFilesCommonPath(files);
            repeat
              p:=s;
              s:=ExtractFilePath(p);
            until (s = '') or (s = p);
          end;
          p:=IncludeTrailingPathDelimiter(UTF8Encode(Strings['downloadDir'])) + p;
        end;
      finally
        res.Free;
      end;
    ExecRemoteFile(p, sel, Userdef);
  finally
    AppNormal;
  end;
end;

procedure myDumpAddr(Addr: Pointer;var f:system.text);
begin
  try
    WriteLn(f,BackTraceStrFunc(Addr));
  except
    writeLn(f,SysBackTraceStr(Addr));
  end;
end;
procedure MyDumpExceptionBackTrace(var f:system.text);
var
  FrameCount: integer;
  Frames: PPointer;
  FrameNumber:Integer;
begin
  WriteLn(f,'Stack trace:');
  myDumpAddr(ExceptAddr,f);
  FrameCount:=ExceptFrameCount;
  Frames:=ExceptFrames;
  for FrameNumber := 0 to FrameCount-1 do
    myDumpAddr(Frames[FrameNumber],f);
end;
procedure TMainForm._onException(Sender: TObject; E: Exception);
var
  f:system.text;
  crashreportfilename:shortstring;
begin
    crashreportfilename:='crashreport.txt';
    system.Assign(f,crashreportfilename);
    if FileExists(crashreportfilename) then
        system.Append(f)
    else
        system.Rewrite(f);

    WriteLn(f,'');WriteLn(f,'v.' + AppVersion + ' crashed((');WriteLn(f,'');
    myDumpExceptionBackTrace(f);
    system.close(f);
    halt(0);
end;


procedure TMainForm.FillSpeedsMenu;

  procedure _FillMenu(Items: TMenuItem; const Speeds: string; OnClickHandler: TNotifyEvent; CurSpeed: integer);
  var
    sl: TStringList;
    i, j: integer;
    mi: TMenuItem;
  begin
    Items.Clear;
    if not RpcObj.Connected then
      exit;
    sl:=TStringList.Create;
    try
      sl.Delimiter:=',';
      sl.DelimitedText:=Speeds;
      i:=0;
      while i < sl.Count do begin
        j:=StrToIntDef(Trim(sl[i]), -1);
        if j >= 0 then begin
          sl[i]:=Format('%.08d', [j]);
          Inc(i);
        end
        else
          sl.Delete(i);
      end;
      sl.Duplicates:=dupIgnore;
      sl.Sorted:=True;
      sl.Add(Format('%.08d', [CurSpeed]));

      for i:=0 to sl.Count - 1 do begin
        j:=StrToIntDef(Trim(sl[i]), -1);
        if j >= 0 then begin
          mi:=TMenuItem.Create(Items);
          mi.Caption:=Format('%d %s%s', [j, sKByte, sPerSecond]);
          mi.Tag:=j;
          mi.OnClick:=OnClickHandler;
          if j = CurSpeed then
            mi.Checked:=True;
          Items.Insert(0, mi);
        end;
      end;
    finally
      sl.Free;
    end;
    if Items.Count > 0 then begin
      mi:=TMenuItem.Create(Items);
      mi.Caption:='-';
      Items.Insert(0, mi);
    end;
    mi:=TMenuItem.Create(Items);
    mi.Caption:=SUnlimited;
    mi.Tag:=-1;
    mi.OnClick:=OnClickHandler;
    if CurSpeed = -1 then
      mi.Checked:=True;
    Items.Insert(0, mi);
  end;

var
  s: string;
begin
  s:=Ini.ReadString('Connection.' + FCurConn, 'DownSpeeds', DefSpeeds);
  _FillMenu(pmDownSpeeds.Items, s, @DoSetDownloadSpeed, FCurDownSpeedLimit);
  _FillMenu(pmiDownSpeedLimit, s, @DoSetDownloadSpeed, FCurDownSpeedLimit);

  s:=Ini.ReadString('Connection.' + FCurConn, 'UpSpeeds', DefSpeeds);
  _FillMenu(pmUpSpeeds.Items, s, @DoSetUploadSpeed, FCurUpSpeedLimit);
  _FillMenu(pmiUpSpeedLimit, s, @DoSetUploadSpeed, FCurUpSpeedLimit);
{$ifdef LCLcarbon}
  TrayIcon.InternalUpdate;
{$endif LCLcarbon}
end;

initialization
  {$I main.lrs}

finalization
  try
    FreeAndNil(Ini);
  except
  end;
end.
