main.pas 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493
  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, LCLType, IniFiles, unitSettingsDialog,
  7. unitCoverWorker, unitStorageXML;
  8. type
  9. { Tform1 }
  10. Tform1 = class(Tform)
  11. EditSearch: Tedit;
  12. ButtonSettings: Timage;
  13. ImageToolBar: Timage;
  14. ButtonAdd: Timage;
  15. Opendialog1: Topendialog;
  16. PanelBackground: Tscrollbox;
  17. procedure FormResize(Sender: TObject);
  18. procedure Buttonaddclick(Sender: Tobject);
  19. procedure Buttonaddmouseenter(Sender: Tobject);
  20. procedure Buttonaddmouseleave(Sender: Tobject);
  21. procedure ButtonSettingsClick(Sender: TObject);
  22. procedure Buttonsettingsmouseenter(Sender: Tobject);
  23. procedure Buttonsettingsmouseleave(Sender: Tobject);
  24. procedure Editsearchenter(Sender: Tobject);
  25. procedure Editsearchexit(Sender: Tobject);
  26. procedure Editsearchkeypress(Sender: Tobject; var Key: Char);
  27. procedure Formclose(Sender: Tobject; var Closeaction: Tcloseaction);
  28. procedure Formcreate(Sender: Tobject);
  29. procedure Formkeydown(Sender: Tobject; var Key: Word; Shift: Tshiftstate);
  30. procedure Panelbackgroundclick(Sender: Tobject);
  31. procedure Panelbackgrounddragdrop(Sender, Source: Tobject; X, Y: Integer);
  32. procedure Panelbackgrounddragover(Sender, Source: Tobject; X, Y: Integer;
  33. State: Tdragstate; var Accept: Boolean);
  34. procedure Panelbackgroundpaint(Sender: Tobject);
  35. procedure RearrangeBooksOnScreen();
  36. procedure Panelbackgroundresize(Sender: Tobject);
  37. function getBookIndexAtPoint(X,Y:Integer):Integer;
  38. procedure UnselectAll;
  39. function getCoverIndex(cover:TImage):Integer;
  40. private
  41. mAdd,mAddHover,mGear,mGearHover:TPicture;
  42. LayoutTimer: TTimer;
  43. procedure LayoutTimerTick(Sender: TObject);
  44. public
  45. { public declarations }
  46. end;
  47. var
  48. Form1: Tform1;
  49. BookList:TBookCollection;
  50. Xspace, Yspace:integer;
  51. dataPath:String;
  52. background,toolbar:TPicture;
  53. bookWidth,bookHeight:Integer;
  54. implementation
  55. {$R *.lfm}
  56. { Tform1 }
  57. procedure TForm1.FormResize(Sender: TObject);
  58. begin
  59. // debounce: restart the timer, don’t layout on every pixel move
  60. LayoutTimer.Enabled := False;
  61. LayoutTimer.Enabled := True;
  62. end;
  63. procedure TForm1.LayoutTimerTick(Sender: TObject);
  64. begin
  65. LayoutTimer.Enabled := False; // one-shot
  66. RearrangeBooksOnScreen;
  67. end;
  68. procedure Tform1.Panelbackgroundclick(Sender: Tobject);
  69. begin
  70. ActiveControl:=PanelBackground;
  71. UnselectAll;
  72. PanelBackground.Invalidate;
  73. End;
  74. procedure Tform1.Panelbackgrounddragdrop(Sender, Source: Tobject; X, Y: Integer);
  75. var src,dest:integer;
  76. begin
  77. src:=getCoverIndex(TImage(Source));
  78. dest:=getBookIndexAtPoint(X,Y);
  79. if (src > -1) and (dest > -1) then BookList.SwapBooks(src,dest);
  80. UnselectAll;
  81. PanelBackground.Invalidate;
  82. //RearrangeBooksOnScreen();
  83. End;
  84. procedure Tform1.Panelbackgrounddragover(Sender, Source: Tobject; X,
  85. Y: Integer; State: Tdragstate; var Accept: Boolean);
  86. begin
  87. Accept:=True;
  88. End;
  89. procedure Tform1.Panelbackgroundpaint(Sender: Tobject);
  90. var w,h:Integer;
  91. x,y:Integer;
  92. begin
  93. x:=0;
  94. y:=0;
  95. w:=background.Width;
  96. h:=background.Height;
  97. while x < PanelBackground.Canvas.Width do
  98. begin
  99. while y < PanelBackground.Canvas.Height do
  100. begin
  101. PanelBackground.Canvas.Draw(x,y,background.Graphic);
  102. y:=y+h;
  103. end;
  104. x:=x+w;
  105. y:=0;
  106. end;
  107. End;
  108. procedure TForm1.RearrangeBooksOnScreen;
  109. var
  110. visibleCovers: array of TImage;
  111. i, j, k, countVisible: Integer;
  112. availW, minGap, rowStart, rowCount: Integer;
  113. curY: Integer;
  114. x: Double;
  115. gap: Double;
  116. cover: TImage;
  117. function PanelClientWidth: Integer;
  118. begin
  119. // Use client width (exclude borders/scrollbar)
  120. Result := PanelBackground.ClientWidth;
  121. if Result <= 0 then Result := PanelBackground.Width;
  122. end;
  123. procedure CollectVisible;
  124. var i : Integer;
  125. begin
  126. SetLength(visibleCovers, 0);
  127. for i := 0 to BookList.Count - 1 do
  128. begin
  129. cover := BookList.Books[i].Cover;
  130. if Assigned(cover) and cover.Visible then
  131. begin
  132. SetLength(visibleCovers, Length(visibleCovers) + 1);
  133. visibleCovers[High(visibleCovers)] := cover;
  134. end;
  135. end;
  136. countVisible := Length(visibleCovers);
  137. end;
  138. // Can we fit N items with at least minGap spacing including left+right margins?
  139. function FitsWithMinGaps(n: Integer; width: Integer; gapPx: Integer): Boolean;
  140. var need: Integer;
  141. begin
  142. // total = n*bookWidth + (n+1)*gap (edge gaps included)
  143. need := (n * bookWidth) + ((n + 1) * gapPx);
  144. Result := need <= width;
  145. end;
  146. begin
  147. PanelBackground.DisableAlign;
  148. try
  149. availW := PanelClientWidth;
  150. if availW <= 0 then Exit;
  151. minGap := Xspace; // your existing horizontal spacing as the minimum
  152. curY := Yspace; // top margin
  153. CollectVisible;
  154. // Early exit: nothing to place
  155. if countVisible = 0 then Exit;
  156. // Ensure covers have correct size (in case they were recreated)
  157. for i := 0 to countVisible - 1 do
  158. begin
  159. visibleCovers[i].Width := bookWidth;
  160. visibleCovers[i].Height := bookHeight;
  161. visibleCovers[i].Parent := PanelBackground;
  162. end;
  163. rowStart := 0;
  164. while rowStart < countVisible do
  165. begin
  166. // Determine how many items fit in this row with at least minGap gutters.
  167. rowCount := 1;
  168. while (rowStart + rowCount < countVisible)
  169. and FitsWithMinGaps(rowCount + 1, availW, minGap) do
  170. Inc(rowCount);
  171. // Compute the gap for this row:
  172. // - For full rows, distribute leftover width evenly across (rowCount+1) gaps.
  173. // - For the last row (rowStart+rowCount = countVisible), keep it left-aligned (minGap).
  174. if (rowStart + rowCount) < countVisible then
  175. begin
  176. // Full row → justified
  177. gap := (availW - (rowCount * bookWidth)) / (rowCount + 1);
  178. if gap < minGap then gap := minGap; // safety
  179. end
  180. else
  181. begin
  182. // Last row → left align
  183. gap := minGap; // safety
  184. end;
  185. // Place row items: start at left edge gap, then [cover + gap] repeated.
  186. x := gap;
  187. for j := 0 to rowCount - 1 do
  188. begin
  189. k := rowStart + j;
  190. cover := visibleCovers[k];
  191. cover.Left := Round(x);
  192. cover.Top := curY;
  193. x := x + bookWidth + gap;
  194. end;
  195. // Next row Y
  196. curY := curY + bookHeight + Yspace + 26;
  197. Inc(rowStart, rowCount);
  198. end;
  199. // Optional: ensure panel is tall enough; comment out if not needed.
  200. // PanelBackground.AutoSize := False;
  201. // PanelBackground.Height := curY + Yspace;
  202. finally
  203. PanelBackground.EnableAlign;
  204. PanelBackground.Invalidate;
  205. end;
  206. end;
  207. procedure Tform1.Panelbackgroundresize(Sender: Tobject);
  208. begin
  209. RearrangeBooksOnScreen();
  210. EditSearch.Left:=Width-EditSearch.Width-20;
  211. End;
  212. function Tform1.Getbookindexatpoint(X, Y: Integer): Integer;
  213. var i:Integer;
  214. cover:TImage;
  215. begin
  216. for i:=0 to BookList.Count-1 do
  217. begin
  218. cover:=BookList.Books[i].Cover;
  219. if (X >= cover.Left) and (X <= cover.Left + cover.Width) and
  220. (Y >= cover.Top) and (Y <= cover.Top + cover.Height) then
  221. begin
  222. result :=i;
  223. exit;
  224. end;
  225. end;
  226. result:=-1;
  227. end;
  228. procedure Tform1.Unselectall;
  229. var i:Integer;
  230. begin
  231. for i:=0 to BookList.Count-1 do
  232. begin
  233. BookList.Books[i].isSelected:=False;
  234. end;
  235. end;
  236. function Tform1.Getcoverindex(Cover: Timage): Integer;
  237. var i:integer;
  238. begin
  239. for i:=0 to Booklist.count-1 do
  240. begin
  241. if Booklist.books[i].Cover = Cover then
  242. begin
  243. result:=i;
  244. exit;
  245. end;
  246. end;
  247. result:=-1;
  248. end;
  249. procedure Tform1.Formclose(Sender: Tobject; var Closeaction: Tcloseaction);
  250. begin
  251. SaveBooksXML(dataPath, BookList);
  252. BookList.Destroy;
  253. End;
  254. procedure Tform1.Buttonaddclick(Sender: Tobject);
  255. var
  256. book:TBook;
  257. i:Integer;
  258. begin
  259. if OpenDialog1.Execute then
  260. begin
  261. for i:= 0 to Opendialog1.Files.Count-1 do
  262. begin
  263. book:=TBook.Create(PanelBackground);
  264. book.FilePath:= OpenDialog1.Files.Strings[i];
  265. BookList.AddBook(book);
  266. book.Cover.Width:=bookWidth;
  267. book.Cover.Height:=bookHeight;
  268. book.Cover.Parent:=PanelBackground;
  269. end;
  270. CoverWorkerEnqueueBookIfMissing(Book);
  271. CoverWorkerStart;
  272. RearrangeBooksOnScreen();
  273. end;
  274. End;
  275. procedure Tform1.Buttonaddmouseenter(Sender: Tobject);
  276. begin
  277. Buttonadd.Picture:=mAddHover;
  278. End;
  279. procedure Tform1.Buttonaddmouseleave(Sender: Tobject);
  280. begin
  281. Buttonadd.Picture:=mAdd;
  282. End;
  283. procedure Tform1.ButtonSettingsClick(Sender: TObject);
  284. begin
  285. SettingsDialog := TSettingsDialog.Create(Self);
  286. try
  287. SettingsDialog.ShowModal;
  288. finally
  289. SettingsDialog.Free;
  290. end;
  291. end;
  292. procedure Tform1.Buttonsettingsmouseenter(Sender: Tobject);
  293. begin
  294. ButtonSettings.Picture:=mGearHover;
  295. End;
  296. procedure Tform1.Buttonsettingsmouseleave(Sender: Tobject);
  297. begin
  298. ButtonSettings.Picture:=mGear;
  299. End;
  300. procedure Tform1.Editsearchenter(Sender: Tobject);
  301. begin
  302. EditSearch.Caption:='';
  303. End;
  304. procedure Tform1.Editsearchexit(Sender: Tobject);
  305. begin
  306. EditSearch.Caption:='Search...';
  307. End;
  308. procedure Tform1.Editsearchkeypress(Sender: Tobject; var Key: Char);
  309. begin
  310. if Key = #13 then
  311. begin
  312. //perform the search here
  313. end;
  314. End;
  315. procedure Tform1.Formcreate(Sender: Tobject);
  316. var
  317. i:integer;
  318. cfgDir, cfgPath, dataDir: String;
  319. ini: TIniFile;
  320. begin
  321. bookWidth:=130;
  322. bookHeight:=200;
  323. Xspace:=40;
  324. Yspace:=25;
  325. Form1.KeyPreview:=True;
  326. ActiveControl:=PanelBackground;
  327. background:=TPicture.Create;
  328. background.LoadFromLazarusResource('shelf');
  329. PanelBackground.DoubleBuffered := True; // reduce flicker
  330. Self.OnResize := @FormResize;
  331. LayoutTimer := TTimer.Create(Self);
  332. LayoutTimer.Enabled := False;
  333. LayoutTimer.Interval := 60; // ~60ms debounce feels snappy
  334. LayoutTimer.OnTimer := @LayoutTimerTick;
  335. mAdd:=TPicture.Create;
  336. mAddHover:=Tpicture.Create;
  337. mGear:=Tpicture.Create;
  338. mGearHover:=Tpicture.Create;
  339. mAdd.LoadFromLazarusResource('add');
  340. mAddHover.LoadFromLazarusResource('add_hover');
  341. mGear.LoadFromLazarusResource('gear');
  342. mGearHover.LoadFromLazarusResource('gear_hover');
  343. ButtonAdd.Picture:=mAdd;
  344. ButtonSettings.Picture:=mGear;
  345. // Load config.ini if present to resolve dataPath and options
  346. {$IFDEF MSWINDOWS}
  347. cfgDir := GetEnvironmentVariable('APPDATA') + DirectorySeparator + 'mybookshelf' + DirectorySeparator;
  348. {$ENDIF}
  349. {$IFDEF UNIX}
  350. cfgDir := GetEnvironmentVariable('HOME') + DirectorySeparator + '.mybookshelf' + DirectorySeparator;
  351. {$ENDIF}
  352. if not DirectoryExistsUTF8(cfgDir) then CreateDirUTF8(cfgDir);
  353. cfgPath := cfgDir + 'config.ini';
  354. ini := TIniFile.Create(cfgPath);
  355. try
  356. dataDir := ini.ReadString('general', 'data_dir', cfgDir);
  357. // You can also read autoPdfCover if you want it here:
  358. // autoPdfCover := ini.ReadBool('general', 'auto_pdf_cover', True);
  359. finally
  360. ini.Free;
  361. end;
  362. if not DirectoryExistsUTF8(dataDir) then CreateDirUTF8(dataDir);
  363. dataPath := IncludeTrailingPathDelimiter(dataDir) + 'books.xml';
  364. BookList:=TBookCollection.Create;
  365. if FileExistsUTF8(dataPath) then
  366. LoadBooksXML(dataPath, BookList, PanelBackground);
  367. for i:=0 to BookList.Count-1 do
  368. begin
  369. with BookList.Books[i] do
  370. begin
  371. Cover.Width:=bookWidth;
  372. Cover.Height:=bookHeight;
  373. Cover.Parent:=PanelBackground;
  374. EnsureScaledToCoverSize;
  375. end;
  376. end;
  377. // speed up startup: we skipped synchronous PDF generation during LoadData
  378. SetPdfCoverGenerationEnabled(False);
  379. try
  380. if FileExistsUTF8(dataPath) then
  381. LoadBooksXML(dataPath, BookList, PanelBackground);
  382. finally
  383. SetPdfCoverGenerationEnabled(True); // re-enable for user actions
  384. end;
  385. RearrangeBooksOnScreen();
  386. // Background: generate covers only where still generic
  387. CoverWorkerEnqueueMissingFromBookList(BookList);
  388. CoverWorkerStart;
  389. End;
  390. procedure Tform1.Formkeydown(Sender: Tobject; var Key: Word; Shift: Tshiftstate);
  391. var i:Integer;
  392. begin
  393. if Key = VK_DELETE then
  394. begin
  395. for i:= BookList.Count-1 downto 0 do
  396. begin
  397. if BookList.Books[i].isSelected = True then
  398. begin
  399. BookList.Books[i].Cover.Free;
  400. BookList.Remove(BookList.Books[i]);
  401. end;
  402. end;
  403. RearrangeBooksOnScreen();
  404. end;
  405. End;
  406. initialization
  407. {$i mybookshelf.lrs}
  408. end.