main.pas 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703
  1. unit main;
  2. {$mode objfpc}{$H+}
  3. interface
  4. uses
  5. Classes, Sysutils, Fileutil, Forms, Controls, Graphics, Dialogs, ExtCtrls, LazFileUtils,
  6. Book, BookCollection, LCLIntf, LResources, StdCtrls, Menus, LCLType, IniFiles, unitSettingsDialog,
  7. unitCoverWorker, unitStorageXML, unitMetadata, UnitBookDialog, LazUTF8;
  8. type
  9. { TForm1 }
  10. TForm1 = class(TForm)
  11. EditSearch: Tedit;
  12. ComboSort: TComboBox;
  13. PopupBook: TPopupMenu;
  14. MenuEditBook: TMenuItem;
  15. ButtonSettings: Timage;
  16. ImageToolBar: Timage;
  17. ButtonAdd: Timage;
  18. Opendialog1: Topendialog;
  19. PanelBackground: Tscrollbox;
  20. procedure FormResize({%H-}Sender: TObject);
  21. procedure ButtonAddClick({%H-}Sender: TObject);
  22. procedure ButtonAddMouseEnter({%H-}Sender: TObject);
  23. procedure ButtonAddMouseLeave({%H-}Sender: TObject);
  24. procedure ButtonSettingsClick({%H-}Sender: TObject);
  25. procedure ButtonSettingsMouseEnter({%H-}Sender: TObject);
  26. procedure ButtonSettingsMouseLeave({%H-}Sender: TObject);
  27. procedure EditSearchEnter({%H-}Sender: TObject);
  28. procedure EditSearchExit({%H-}Sender: TObject);
  29. procedure EditSearchChange({%H-}Sender: TObject);
  30. procedure EditSearchKeyPress({%H-}Sender: TObject; var Key: Char);
  31. procedure ComboSortChange({%H-}Sender: TObject);
  32. procedure FormClose({%H-}Sender: TObject; var CloseAction: TCloseAction);
  33. procedure FormCreate({%H-}Sender: TObject);
  34. procedure FormKeyDown({%H-}Sender: TObject; var Key: Word; {%H-}Shift: TShiftState);
  35. procedure PanelBackgroundClick({%H-}Sender: TObject);
  36. procedure PanelBackgroundDragDrop({%H-}Sender, Source: TObject; X, Y: Integer);
  37. procedure PanelBackgroundDragOver({%H-}Sender, {%H-}Source: TObject; {%H-}X, {%H-}Y: Integer;
  38. {%H-}State: TDragState; var Accept: Boolean);
  39. procedure PanelBackgroundPaint({%H-}Sender: TObject);
  40. procedure RearrangeBooksOnScreen();
  41. procedure PanelBackgroundResize({%H-}Sender: TObject);
  42. function GetBookIndexAtPoint(X,Y:Integer):Integer;
  43. procedure UnselectAll;
  44. function GetCoverIndex(cover:TImage):Integer;
  45. procedure PopupEditBookClick({%H-}Sender: TObject);
  46. private
  47. mAdd,mAddHover,mGear,mGearHover:TPicture;
  48. LayoutTimer: TTimer;
  49. procedure LayoutTimerTick(Sender: TObject);
  50. procedure ApplyFilterAndLayout;
  51. function AppConfigPath: String;
  52. public
  53. { public declarations }
  54. end;
  55. var
  56. Form1: TForm1;
  57. bookList: TBookCollection;
  58. xSpace, ySpace: integer;
  59. dataXmlPath: String;
  60. booksDir: String;
  61. backgroundTile, toolbar: TPicture;
  62. coverWidth, coverHeight: Integer;
  63. optCopyBooks, optRenameBooks, optExtractMeta: Boolean;
  64. isClosing: Boolean = False;
  65. implementation
  66. {$R *.lfm}
  67. { Tform1 }
  68. procedure TForm1.FormResize({%H-}Sender: TObject);
  69. begin
  70. // debounce: restart the timer, don’t layout on every pixel move
  71. LayoutTimer.Enabled := False;
  72. LayoutTimer.Enabled := True;
  73. end;
  74. procedure TForm1.LayoutTimerTick(Sender: TObject);
  75. begin
  76. LayoutTimer.Enabled := False; // one-shot
  77. RearrangeBooksOnScreen;
  78. end;
  79. function TForm1.AppConfigPath: String;
  80. begin
  81. Result := IncludeTrailingPathDelimiter(GetAppConfigDirUTF8(False)) + 'config.ini';
  82. end;
  83. procedure TForm1.PanelBackgroundClick({%H-}Sender: TObject);
  84. begin
  85. ActiveControl:=PanelBackground;
  86. UnselectAll;
  87. PanelBackground.Invalidate;
  88. end;
  89. procedure TForm1.PanelBackgroundDragDrop({%H-}Sender, Source: TObject; X, Y: Integer);
  90. var src,dest:integer;
  91. begin
  92. src:=GetCoverIndex(TImage(Source));
  93. dest:=GetBookIndexAtPoint(X,Y);
  94. if (src > -1) and (dest > -1) then bookList.SwapBooks(src,dest);
  95. UnselectAll;
  96. // After changing book order, recalculate layout so covers move immediately
  97. RearrangeBooksOnScreen();
  98. end;
  99. procedure TForm1.PanelBackgroundDragOver({%H-}Sender, {%H-}Source: TObject; {%H-}X,
  100. {%H-}Y: Integer; {%H-}State: TDragState; var Accept: Boolean);
  101. begin
  102. Accept:=True;
  103. end;
  104. procedure TForm1.PanelBackgroundPaint({%H-}Sender: TObject);
  105. var w,h:Integer;
  106. x,y:Integer;
  107. begin
  108. // Safety: if no tile or invalid size, skip custom painting
  109. if (backgroundTile = nil) or (backgroundTile.Width <= 0) or (backgroundTile.Height <= 0) then
  110. Exit;
  111. x:=0;
  112. y:=0;
  113. w:=backgroundTile.Width;
  114. h:=backgroundTile.Height;
  115. while x < PanelBackground.Canvas.Width do
  116. begin
  117. while y < PanelBackground.Canvas.Height do
  118. begin
  119. PanelBackground.Canvas.Draw(x,y,backgroundTile.Graphic);
  120. y:=y+h;
  121. end;
  122. x:=x+w;
  123. y:=0;
  124. end;
  125. end;
  126. procedure TForm1.RearrangeBooksOnScreen;
  127. var
  128. visibleCovers: array of TImage;
  129. i, j, k, countVisible: Integer;
  130. availW, minGap, rowStart, rowCount: Integer;
  131. curY: Integer;
  132. x: Double;
  133. gap: Double;
  134. cover: TImage;
  135. function PanelClientWidth: Integer;
  136. begin
  137. // Use client width (exclude borders/scrollbar)
  138. Result := PanelBackground.ClientWidth;
  139. if Result <= 0 then Result := PanelBackground.Width;
  140. end;
  141. procedure CollectVisible;
  142. var i : Integer;
  143. begin
  144. SetLength(visibleCovers, 0);
  145. for i := 0 to bookList.Count - 1 do
  146. begin
  147. cover := bookList.Books[i].Cover;
  148. if Assigned(cover) and cover.Visible then
  149. begin
  150. SetLength(visibleCovers, Length(visibleCovers) + 1);
  151. visibleCovers[High(visibleCovers)] := cover;
  152. end;
  153. end;
  154. countVisible := Length(visibleCovers);
  155. end;
  156. // Can we fit N items with at least minGap spacing including left+right margins?
  157. function FitsWithMinGaps(n: Integer; width: Integer; gapPx: Integer): Boolean;
  158. var need: Integer;
  159. begin
  160. // total = n*bookWidth + (n+1)*gap (edge gaps included)
  161. need := (n * coverWidth) + ((n + 1) * gapPx);
  162. Result := need <= width;
  163. end;
  164. begin
  165. if (bookList = nil) or isClosing then Exit;
  166. PanelBackground.DisableAlign;
  167. try
  168. availW := PanelClientWidth;
  169. if availW <= 0 then Exit;
  170. minGap := xSpace; // your existing horizontal spacing as the minimum
  171. curY := ySpace; // top margin
  172. CollectVisible;
  173. // Early exit: nothing to place
  174. if countVisible = 0 then Exit;
  175. // Ensure covers have correct size (in case they were recreated)
  176. for i := 0 to countVisible - 1 do
  177. begin
  178. visibleCovers[i].Width := coverWidth;
  179. visibleCovers[i].Height := coverHeight;
  180. visibleCovers[i].Parent := PanelBackground;
  181. end;
  182. rowStart := 0;
  183. while rowStart < countVisible do
  184. begin
  185. // Determine how many items fit in this row with at least minGap gutters.
  186. rowCount := 1;
  187. while (rowStart + rowCount < countVisible)
  188. and FitsWithMinGaps(rowCount + 1, availW, minGap) do
  189. Inc(rowCount);
  190. // Compute the gap for this row:
  191. // - For full rows, distribute leftover width evenly across (rowCount+1) gaps.
  192. // - For the last row (rowStart+rowCount = countVisible), keep it left-aligned (minGap).
  193. if (rowStart + rowCount) < countVisible then
  194. begin
  195. // Full row → justified
  196. gap := (availW - (rowCount * coverWidth)) / (rowCount + 1);
  197. if gap < minGap then gap := minGap; // safety
  198. end
  199. else
  200. begin
  201. // Last row → left align
  202. gap := minGap; // safety
  203. end;
  204. // Place row items: start at left edge gap, then [cover + gap] repeated.
  205. x := gap;
  206. for j := 0 to rowCount - 1 do
  207. begin
  208. k := rowStart + j;
  209. cover := visibleCovers[k];
  210. cover.Left := Round(x);
  211. cover.Top := curY;
  212. x := x + coverWidth + gap;
  213. end;
  214. // Next row Y
  215. curY := curY + coverHeight + ySpace + 26;
  216. Inc(rowStart, rowCount);
  217. end;
  218. // Optional: ensure panel is tall enough; comment out if not needed.
  219. // PanelBackground.AutoSize := False;
  220. // PanelBackground.Height := curY + Yspace;
  221. finally
  222. PanelBackground.EnableAlign;
  223. PanelBackground.Invalidate;
  224. end;
  225. end;
  226. procedure TForm1.PanelBackgroundResize({%H-}Sender: TObject);
  227. begin
  228. if isClosing then Exit;
  229. RearrangeBooksOnScreen();
  230. EditSearch.Left := Width - EditSearch.Width - 20;
  231. if Assigned(ComboSort) then
  232. ComboSort.Left := EditSearch.Left - ComboSort.Width - 12;
  233. End;
  234. function TForm1.GetBookIndexAtPoint(X, Y: Integer): Integer;
  235. var i:Integer;
  236. cover:TImage;
  237. begin
  238. for i:=0 to bookList.Count-1 do
  239. begin
  240. cover:=bookList.Books[i].Cover;
  241. if Assigned(cover) and cover.Visible and
  242. (X >= cover.Left) and (X <= cover.Left + cover.Width) and
  243. (Y >= cover.Top) and (Y <= cover.Top + cover.Height) then
  244. begin
  245. result :=i;
  246. exit;
  247. end;
  248. end;
  249. result:=-1;
  250. end;
  251. procedure TForm1.UnselectAll;
  252. var i:Integer;
  253. begin
  254. for i:=0 to bookList.Count-1 do
  255. begin
  256. bookList.Books[i].isSelected:=False;
  257. end;
  258. end;
  259. function TForm1.GetCoverIndex(Cover: Timage): Integer;
  260. var i:integer;
  261. begin
  262. for i:=0 to bookList.count-1 do
  263. begin
  264. if Assigned(bookList.books[i].Cover) and (bookList.books[i].Cover = Cover) then
  265. begin
  266. result:=i;
  267. exit;
  268. end;
  269. end;
  270. result:=-1;
  271. end;
  272. procedure TForm1.PopupEditBookClick({%H-}Sender: TObject);
  273. var
  274. img: TImage;
  275. idx: Integer;
  276. dlg: TBookEditDialog;
  277. begin
  278. if (PopupBook = nil) or (PopupBook.PopupComponent = nil) then Exit;
  279. if not (PopupBook.PopupComponent is TImage) then Exit;
  280. img := TImage(PopupBook.PopupComponent);
  281. idx := GetCoverIndex(img);
  282. if idx < 0 then Exit;
  283. dlg := TBookEditDialog.Create(Self);
  284. try
  285. dlg.LoadBook(bookList.Books[idx]);
  286. if dlg.ShowModal = mrOK then
  287. begin
  288. // reflect changes visually
  289. bookList.Books[idx].EnsureScaledToCoverSize;
  290. ApplyFilterAndLayout;
  291. end;
  292. finally
  293. dlg.Free;
  294. end;
  295. end;
  296. procedure TForm1.FormClose({%H-}Sender: TObject; var CloseAction: TCloseAction);
  297. begin
  298. isClosing := True;
  299. if Assigned(LayoutTimer) then LayoutTimer.Enabled := False;
  300. // Ensure background worker thread is stopped before destroying books/controls
  301. CoverWorkerStop;
  302. try
  303. if Assigned(bookList) then
  304. SaveBooksXML(dataXmlPath, bookList);
  305. except
  306. // ignore save errors on shutdown
  307. end;
  308. // Free images created at runtime
  309. FreeAndNil(mAdd);
  310. FreeAndNil(mAddHover);
  311. FreeAndNil(mGear);
  312. FreeAndNil(mGearHover);
  313. FreeAndNil(backgroundTile);
  314. FreeAndNil(bookList);
  315. CloseAction := caFree;
  316. end;
  317. procedure TForm1.ButtonAddClick({%H-}Sender: TObject);
  318. var
  319. book : TBook;
  320. i : Integer;
  321. src : String;
  322. dest : String;
  323. fname,title,authors,ext : String;
  324. files: TStringList;
  325. function CleanName(const s:String):String;
  326. const bad = '/\?*:<>|"';
  327. var c:Char;
  328. begin
  329. Result := Trim(s);
  330. for c in bad do
  331. Result := StringReplace(Result, c, '_', [rfReplaceAll]);
  332. end;
  333. procedure ProcessFile(const AFile: String);
  334. begin
  335. src := AFile;
  336. dest := src;
  337. title := '';
  338. authors := '';
  339. if optExtractMeta then
  340. ExtractBookMetadata(src, title, authors);
  341. if optCopyBooks then
  342. begin
  343. ForceDirectories(booksDir);
  344. fname := ExtractFileName(src);
  345. if optRenameBooks and (title <> '') then
  346. begin
  347. ext := ExtractFileExt(src);
  348. fname := CleanName(title);
  349. if authors <> '' then
  350. fname := fname + ' - ' + CleanName(authors);
  351. fname := fname + ext;
  352. end;
  353. dest := IncludeTrailingPathDelimiter(booksDir) + fname;
  354. // Skip copy if source already resides in booksDir and avoid exceptions on failure
  355. if CompareFilenames(src, dest) <> 0 then
  356. begin
  357. try
  358. CopyFile(src, dest);
  359. except
  360. // fall back to original path if copy fails for any reason
  361. dest := src;
  362. end;
  363. end;
  364. end;
  365. book:=TBook.Create(PanelBackground);
  366. book.FilePath:= dest;
  367. if optExtractMeta then
  368. begin
  369. if title <> '' then book.Title := title
  370. else book.Title := ChangeFileExt(ExtractFileName(dest), '');
  371. if authors <> '' then book.Authors := authors;
  372. end
  373. else
  374. book.Title := ChangeFileExt(ExtractFileName(dest), '');
  375. bookList.AddBook(book);
  376. book.Cover.Width:=coverWidth;
  377. book.Cover.Height:=coverHeight;
  378. book.Cover.Parent:=PanelBackground;
  379. book.Cover.PopupMenu := PopupBook;
  380. // Ensure the pre-scaled image matches the final cover size
  381. book.EnsureScaledToCoverSize;
  382. CoverWorkerEnqueueBookIfMissing(book);
  383. end;
  384. begin
  385. if OpenDialog1.Execute then
  386. begin
  387. files := TStringList.Create;
  388. try
  389. if OpenDialog1.Files.Count > 0 then
  390. files.Assign(OpenDialog1.Files)
  391. else if OpenDialog1.FileName <> '' then
  392. files.Add(OpenDialog1.FileName);
  393. for i := 0 to files.Count - 1 do
  394. ProcessFile(files[i]);
  395. CoverWorkerStart;
  396. RearrangeBooksOnScreen();
  397. finally
  398. files.Free;
  399. end;
  400. end;
  401. End;
  402. procedure TForm1.ButtonAddMouseEnter({%H-}Sender: TObject);
  403. begin
  404. ButtonAdd.Picture := mAddHover;
  405. end;
  406. procedure TForm1.ButtonAddMouseLeave({%H-}Sender: TObject);
  407. begin
  408. ButtonAdd.Picture := mAdd;
  409. end;
  410. procedure TForm1.ButtonSettingsClick({%H-}Sender: TObject);
  411. begin
  412. SettingsDialog := TSettingsDialog.Create(Self);
  413. try
  414. SettingsDialog.ShowModal;
  415. finally
  416. SettingsDialog.Free;
  417. end;
  418. end;
  419. procedure TForm1.ButtonSettingsMouseEnter({%H-}Sender: TObject);
  420. begin
  421. ButtonSettings.Picture := mGearHover;
  422. end;
  423. procedure TForm1.ButtonSettingsMouseLeave({%H-}Sender: TObject);
  424. begin
  425. ButtonSettings.Picture := mGear;
  426. end;
  427. procedure TForm1.EditSearchEnter({%H-}Sender: TObject);
  428. begin
  429. // Only clear placeholder, keep any typed text intact
  430. if EditSearch.Text = 'Search...' then
  431. EditSearch.Text := '';
  432. end;
  433. procedure TForm1.EditSearchExit({%H-}Sender: TObject);
  434. begin
  435. // Restore placeholder only if empty; do not wipe actual queries
  436. if Trim(EditSearch.Text) = '' then
  437. EditSearch.Text := 'Search...';
  438. end;
  439. procedure TForm1.EditSearchKeyPress({%H-}Sender: TObject; var Key: Char);
  440. begin
  441. if Key = #13 then
  442. begin
  443. //perform the search here
  444. end;
  445. end;
  446. procedure TForm1.FormCreate({%H-}Sender: TObject);
  447. var
  448. i:integer;
  449. cfgDir, cfgPath, dataDir: String;
  450. ini: TIniFile;
  451. autoPdfCover: Boolean;
  452. begin
  453. coverWidth:=130;
  454. coverHeight:=200;
  455. xSpace:=40;
  456. ySpace:=25;
  457. Form1.KeyPreview:=True;
  458. ActiveControl:=PanelBackground;
  459. backgroundTile:=TPicture.Create;
  460. backgroundTile.LoadFromLazarusResource('shelf');
  461. PanelBackground.DoubleBuffered := True; // reduce flicker
  462. Self.OnResize := @FormResize;
  463. LayoutTimer := TTimer.Create(Self);
  464. LayoutTimer.Enabled := False;
  465. LayoutTimer.Interval := 60; // ~60ms debounce feels snappy
  466. LayoutTimer.OnTimer := @LayoutTimerTick;
  467. mAdd:=TPicture.Create;
  468. mAddHover:=Tpicture.Create;
  469. mGear:=Tpicture.Create;
  470. mGearHover:=Tpicture.Create;
  471. mAdd.LoadFromLazarusResource('add');
  472. mAddHover.LoadFromLazarusResource('add_hover');
  473. mGear.LoadFromLazarusResource('gear');
  474. mGearHover.LoadFromLazarusResource('gear_hover');
  475. ButtonAdd.Picture:=mAdd;
  476. ButtonSettings.Picture:=mGear;
  477. // Load toolbar image from Lazarus resources instead of large LFM-embedded data
  478. try
  479. ImageToolBar.Picture.LoadFromLazarusResource('toolbar');
  480. except
  481. // ignore if resource missing; fallback to LFM-embedded picture
  482. end;
  483. // Load config.ini if present to resolve paths and options
  484. cfgDir := IncludeTrailingPathDelimiter(GetAppConfigDirUTF8(False));
  485. if not DirectoryExistsUTF8(cfgDir) then CreateDirUTF8(cfgDir);
  486. cfgPath := cfgDir + 'config.ini';
  487. ini := TIniFile.Create(cfgPath);
  488. try
  489. dataDir := ini.ReadString('general', 'data_dir', cfgDir);
  490. booksDir := ini.ReadString('general', 'books_dir', cfgDir);
  491. optCopyBooks := ini.ReadBool('general', 'copy_books', True);
  492. optRenameBooks := ini.ReadBool('general', 'rename_books', True);
  493. optExtractMeta := ini.ReadBool('general', 'extract_metadata', True);
  494. autoPdfCover := ini.ReadBool('general','auto_pdf_cover', True);
  495. finally
  496. ini.Free;
  497. end;
  498. if not DirectoryExistsUTF8(dataDir) then CreateDirUTF8(dataDir);
  499. if not DirectoryExistsUTF8(booksDir) then CreateDirUTF8(booksDir);
  500. dataXmlPath := IncludeTrailingPathDelimiter(dataDir) + 'books.xml';
  501. bookList:=TBookCollection.Create;
  502. // speed up startup: we skipped synchronous PDF generation during load
  503. SetPdfCoverGenerationEnabled(False);
  504. try
  505. if FileExistsUTF8(dataXmlPath) then
  506. LoadBooksXML(dataXmlPath, bookList, PanelBackground);
  507. finally
  508. SetPdfCoverGenerationEnabled(autoPdfCover); // re-enable per settings
  509. end;
  510. for i:=0 to bookList.Count-1 do
  511. begin
  512. with bookList.Books[i] do
  513. begin
  514. Cover.Width:=coverWidth;
  515. Cover.Height:=coverHeight;
  516. Cover.Parent:=PanelBackground;
  517. Cover.PopupMenu := PopupBook;
  518. EnsureScaledToCoverSize;
  519. end;
  520. end;
  521. RearrangeBooksOnScreen();
  522. // Restore sort selection and apply
  523. try
  524. ini := TIniFile.Create(AppConfigPath);
  525. try
  526. ComboSort.ItemIndex := ini.ReadInteger('ui','sort_by', 0);
  527. finally
  528. ini.Free;
  529. end;
  530. ComboSortChange(nil);
  531. except
  532. // ignore
  533. end;
  534. // Background: generate covers only where still generic
  535. CoverWorkerEnqueueMissingFromBookList(bookList);
  536. CoverWorkerStart;
  537. end;
  538. procedure TForm1.FormKeyDown({%H-}Sender: TObject; var Key: Word; {%H-}Shift: TShiftState);
  539. var i:Integer;
  540. b:TBook;
  541. begin
  542. if Key = VK_DELETE then
  543. begin
  544. for i:= bookList.Count-1 downto 0 do
  545. begin
  546. if bookList.Books[i].isSelected = True then
  547. begin
  548. // Remove the cover control first (owned by PanelBackground), then free book
  549. b := bookList.Books[i];
  550. // Ensure the background worker won't touch this book anymore
  551. CoverWorkerRemoveBook(b);
  552. if Assigned(b.Cover) then b.Cover.Free;
  553. bookList.Remove(b);
  554. b.Free;
  555. end;
  556. end;
  557. RearrangeBooksOnScreen();
  558. end;
  559. end;
  560. procedure TForm1.EditSearchChange({%H-}Sender: TObject);
  561. begin
  562. ApplyFilterAndLayout;
  563. end;
  564. procedure TForm1.ComboSortChange({%H-}Sender: TObject);
  565. begin
  566. case ComboSort.ItemIndex of
  567. 0: ; // Recently Added (keep current order)
  568. 1: bookList.SortByTitle;
  569. 2: bookList.SortByAuthor;
  570. end;
  571. // Persist selection
  572. try
  573. with TIniFile.Create(AppConfigPath) do
  574. try
  575. WriteInteger('ui', 'sort_by', ComboSort.ItemIndex);
  576. finally
  577. Free;
  578. end;
  579. except
  580. end;
  581. ApplyFilterAndLayout;
  582. end;
  583. procedure TForm1.ApplyFilterAndLayout;
  584. var
  585. q, lt, la: String;
  586. i: Integer;
  587. b: TBook;
  588. showIt: Boolean;
  589. begin
  590. if (bookList = nil) then Exit;
  591. q := UTF8LowerCase(Trim(EditSearch.Text));
  592. if (q = '') or (q = 'search...') then
  593. begin
  594. // show all
  595. for i := 0 to bookList.Count - 1 do
  596. if Assigned(bookList.Books[i].Cover) then
  597. bookList.Books[i].Cover.Visible := True;
  598. end
  599. else
  600. begin
  601. for i := 0 to bookList.Count - 1 do
  602. begin
  603. b := bookList.Books[i];
  604. lt := UTF8LowerCase(b.Title);
  605. la := UTF8LowerCase(b.Authors);
  606. showIt := (Pos(q, lt) > 0) or (Pos(q, la) > 0);
  607. if Assigned(b.Cover) then b.Cover.Visible := showIt;
  608. end;
  609. end;
  610. RearrangeBooksOnScreen();
  611. end;
  612. initialization
  613. {$i mybookshelf.lrs}
  614. end.