| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493 |
- unit main;
- {$mode objfpc}{$H+}
- interface
- uses
- Classes, Sysutils, Fileutil, Forms, Controls, Graphics, Dialogs, ExtCtrls, LazFileUtils,
- Book, BookCollection, LCLIntf, LResources, StdCtrls, LCLType, IniFiles, unitSettingsDialog,
- unitCoverWorker, unitStorageXML;
- type
- { Tform1 }
- Tform1 = class(Tform)
- EditSearch: Tedit;
- ButtonSettings: Timage;
- ImageToolBar: Timage;
- ButtonAdd: Timage;
- Opendialog1: Topendialog;
- PanelBackground: Tscrollbox;
- procedure FormResize(Sender: TObject);
- procedure Buttonaddclick(Sender: Tobject);
- procedure Buttonaddmouseenter(Sender: Tobject);
- procedure Buttonaddmouseleave(Sender: Tobject);
- procedure ButtonSettingsClick(Sender: TObject);
- procedure Buttonsettingsmouseenter(Sender: Tobject);
- procedure Buttonsettingsmouseleave(Sender: Tobject);
- procedure Editsearchenter(Sender: Tobject);
- procedure Editsearchexit(Sender: Tobject);
- procedure Editsearchkeypress(Sender: Tobject; var Key: Char);
- procedure Formclose(Sender: Tobject; var Closeaction: Tcloseaction);
- procedure Formcreate(Sender: Tobject);
- procedure Formkeydown(Sender: Tobject; var Key: Word; Shift: Tshiftstate);
- procedure Panelbackgroundclick(Sender: Tobject);
- procedure Panelbackgrounddragdrop(Sender, Source: Tobject; X, Y: Integer);
- procedure Panelbackgrounddragover(Sender, Source: Tobject; X, Y: Integer;
- State: Tdragstate; var Accept: Boolean);
- procedure Panelbackgroundpaint(Sender: Tobject);
- procedure RearrangeBooksOnScreen();
- procedure Panelbackgroundresize(Sender: Tobject);
- function getBookIndexAtPoint(X,Y:Integer):Integer;
- procedure UnselectAll;
- function getCoverIndex(cover:TImage):Integer;
- private
- mAdd,mAddHover,mGear,mGearHover:TPicture;
- LayoutTimer: TTimer;
- procedure LayoutTimerTick(Sender: TObject);
- public
- { public declarations }
- end;
- var
- Form1: Tform1;
- BookList:TBookCollection;
- Xspace, Yspace:integer;
- dataPath:String;
- background,toolbar:TPicture;
- bookWidth,bookHeight:Integer;
- implementation
- {$R *.lfm}
- { Tform1 }
- procedure TForm1.FormResize(Sender: TObject);
- begin
- // debounce: restart the timer, don’t layout on every pixel move
- LayoutTimer.Enabled := False;
- LayoutTimer.Enabled := True;
- end;
- procedure TForm1.LayoutTimerTick(Sender: TObject);
- begin
- LayoutTimer.Enabled := False; // one-shot
- RearrangeBooksOnScreen;
- end;
- procedure Tform1.Panelbackgroundclick(Sender: Tobject);
- begin
- ActiveControl:=PanelBackground;
- UnselectAll;
- PanelBackground.Invalidate;
- End;
- procedure Tform1.Panelbackgrounddragdrop(Sender, Source: Tobject; X, Y: Integer);
- var src,dest:integer;
- begin
- src:=getCoverIndex(TImage(Source));
- dest:=getBookIndexAtPoint(X,Y);
- if (src > -1) and (dest > -1) then BookList.SwapBooks(src,dest);
- UnselectAll;
- PanelBackground.Invalidate;
- //RearrangeBooksOnScreen();
- End;
- procedure Tform1.Panelbackgrounddragover(Sender, Source: Tobject; X,
- Y: Integer; State: Tdragstate; var Accept: Boolean);
- begin
- Accept:=True;
- End;
- procedure Tform1.Panelbackgroundpaint(Sender: Tobject);
- var w,h:Integer;
- x,y:Integer;
- begin
- x:=0;
- y:=0;
- w:=background.Width;
- h:=background.Height;
- while x < PanelBackground.Canvas.Width do
- begin
- while y < PanelBackground.Canvas.Height do
- begin
- PanelBackground.Canvas.Draw(x,y,background.Graphic);
- y:=y+h;
- end;
- x:=x+w;
- y:=0;
- end;
- End;
- procedure TForm1.RearrangeBooksOnScreen;
- var
- visibleCovers: array of TImage;
- i, j, k, countVisible: Integer;
- availW, minGap, rowStart, rowCount: Integer;
- curY: Integer;
- x: Double;
- gap: Double;
- cover: TImage;
- function PanelClientWidth: Integer;
- begin
- // Use client width (exclude borders/scrollbar)
- Result := PanelBackground.ClientWidth;
- if Result <= 0 then Result := PanelBackground.Width;
- end;
- procedure CollectVisible;
- var i : Integer;
- begin
- SetLength(visibleCovers, 0);
- for i := 0 to BookList.Count - 1 do
- begin
- cover := BookList.Books[i].Cover;
- if Assigned(cover) and cover.Visible then
- begin
- SetLength(visibleCovers, Length(visibleCovers) + 1);
- visibleCovers[High(visibleCovers)] := cover;
- end;
- end;
- countVisible := Length(visibleCovers);
- end;
- // Can we fit N items with at least minGap spacing including left+right margins?
- function FitsWithMinGaps(n: Integer; width: Integer; gapPx: Integer): Boolean;
- var need: Integer;
- begin
- // total = n*bookWidth + (n+1)*gap (edge gaps included)
- need := (n * bookWidth) + ((n + 1) * gapPx);
- Result := need <= width;
- end;
- begin
- PanelBackground.DisableAlign;
- try
- availW := PanelClientWidth;
- if availW <= 0 then Exit;
- minGap := Xspace; // your existing horizontal spacing as the minimum
- curY := Yspace; // top margin
- CollectVisible;
- // Early exit: nothing to place
- if countVisible = 0 then Exit;
- // Ensure covers have correct size (in case they were recreated)
- for i := 0 to countVisible - 1 do
- begin
- visibleCovers[i].Width := bookWidth;
- visibleCovers[i].Height := bookHeight;
- visibleCovers[i].Parent := PanelBackground;
- end;
- rowStart := 0;
- while rowStart < countVisible do
- begin
- // Determine how many items fit in this row with at least minGap gutters.
- rowCount := 1;
- while (rowStart + rowCount < countVisible)
- and FitsWithMinGaps(rowCount + 1, availW, minGap) do
- Inc(rowCount);
- // Compute the gap for this row:
- // - For full rows, distribute leftover width evenly across (rowCount+1) gaps.
- // - For the last row (rowStart+rowCount = countVisible), keep it left-aligned (minGap).
- if (rowStart + rowCount) < countVisible then
- begin
- // Full row → justified
- gap := (availW - (rowCount * bookWidth)) / (rowCount + 1);
- if gap < minGap then gap := minGap; // safety
- end
- else
- begin
- // Last row → left align
- gap := minGap; // safety
- end;
- // Place row items: start at left edge gap, then [cover + gap] repeated.
- x := gap;
- for j := 0 to rowCount - 1 do
- begin
- k := rowStart + j;
- cover := visibleCovers[k];
- cover.Left := Round(x);
- cover.Top := curY;
- x := x + bookWidth + gap;
- end;
- // Next row Y
- curY := curY + bookHeight + Yspace + 26;
- Inc(rowStart, rowCount);
- end;
- // Optional: ensure panel is tall enough; comment out if not needed.
- // PanelBackground.AutoSize := False;
- // PanelBackground.Height := curY + Yspace;
- finally
- PanelBackground.EnableAlign;
-
- PanelBackground.Invalidate;
- end;
- end;
- procedure Tform1.Panelbackgroundresize(Sender: Tobject);
- begin
- RearrangeBooksOnScreen();
- EditSearch.Left:=Width-EditSearch.Width-20;
- End;
- function Tform1.Getbookindexatpoint(X, Y: Integer): Integer;
- var i:Integer;
- cover:TImage;
- begin
- for i:=0 to BookList.Count-1 do
- begin
- cover:=BookList.Books[i].Cover;
- if (X >= cover.Left) and (X <= cover.Left + cover.Width) and
- (Y >= cover.Top) and (Y <= cover.Top + cover.Height) then
- begin
- result :=i;
- exit;
- end;
- end;
- result:=-1;
- end;
- procedure Tform1.Unselectall;
- var i:Integer;
- begin
- for i:=0 to BookList.Count-1 do
- begin
- BookList.Books[i].isSelected:=False;
- end;
- end;
- function Tform1.Getcoverindex(Cover: Timage): Integer;
- var i:integer;
- begin
- for i:=0 to Booklist.count-1 do
- begin
- if Booklist.books[i].Cover = Cover then
- begin
- result:=i;
- exit;
- end;
- end;
- result:=-1;
- end;
- procedure Tform1.Formclose(Sender: Tobject; var Closeaction: Tcloseaction);
- begin
- SaveBooksXML(dataPath, BookList);
- BookList.Destroy;
- End;
- procedure Tform1.Buttonaddclick(Sender: Tobject);
- var
- book:TBook;
- i:Integer;
- begin
- if OpenDialog1.Execute then
- begin
- for i:= 0 to Opendialog1.Files.Count-1 do
- begin
- book:=TBook.Create(PanelBackground);
- book.FilePath:= OpenDialog1.Files.Strings[i];
- BookList.AddBook(book);
- book.Cover.Width:=bookWidth;
- book.Cover.Height:=bookHeight;
- book.Cover.Parent:=PanelBackground;
-
- end;
- CoverWorkerEnqueueBookIfMissing(Book);
- CoverWorkerStart;
- RearrangeBooksOnScreen();
- end;
- End;
- procedure Tform1.Buttonaddmouseenter(Sender: Tobject);
- begin
- Buttonadd.Picture:=mAddHover;
- End;
- procedure Tform1.Buttonaddmouseleave(Sender: Tobject);
- begin
- Buttonadd.Picture:=mAdd;
- End;
- procedure Tform1.ButtonSettingsClick(Sender: TObject);
- begin
- SettingsDialog := TSettingsDialog.Create(Self);
- try
- SettingsDialog.ShowModal;
- finally
- SettingsDialog.Free;
- end;
- end;
- procedure Tform1.Buttonsettingsmouseenter(Sender: Tobject);
- begin
- ButtonSettings.Picture:=mGearHover;
- End;
- procedure Tform1.Buttonsettingsmouseleave(Sender: Tobject);
- begin
- ButtonSettings.Picture:=mGear;
- End;
- procedure Tform1.Editsearchenter(Sender: Tobject);
- begin
- EditSearch.Caption:='';
- End;
- procedure Tform1.Editsearchexit(Sender: Tobject);
- begin
- EditSearch.Caption:='Search...';
- End;
- procedure Tform1.Editsearchkeypress(Sender: Tobject; var Key: Char);
- begin
- if Key = #13 then
- begin
- //perform the search here
- end;
- End;
- procedure Tform1.Formcreate(Sender: Tobject);
- var
- i:integer;
- cfgDir, cfgPath, dataDir: String;
- ini: TIniFile;
- begin
- bookWidth:=130;
- bookHeight:=200;
- Xspace:=40;
- Yspace:=25;
- Form1.KeyPreview:=True;
- ActiveControl:=PanelBackground;
- background:=TPicture.Create;
- background.LoadFromLazarusResource('shelf');
- PanelBackground.DoubleBuffered := True; // reduce flicker
- Self.OnResize := @FormResize;
- LayoutTimer := TTimer.Create(Self);
- LayoutTimer.Enabled := False;
- LayoutTimer.Interval := 60; // ~60ms debounce feels snappy
- LayoutTimer.OnTimer := @LayoutTimerTick;
-
- mAdd:=TPicture.Create;
- mAddHover:=Tpicture.Create;
- mGear:=Tpicture.Create;
- mGearHover:=Tpicture.Create;
- mAdd.LoadFromLazarusResource('add');
- mAddHover.LoadFromLazarusResource('add_hover');
- mGear.LoadFromLazarusResource('gear');
- mGearHover.LoadFromLazarusResource('gear_hover');
- ButtonAdd.Picture:=mAdd;
- ButtonSettings.Picture:=mGear;
- // Load config.ini if present to resolve dataPath and options
- {$IFDEF MSWINDOWS}
- cfgDir := GetEnvironmentVariable('APPDATA') + DirectorySeparator + 'mybookshelf' + DirectorySeparator;
- {$ENDIF}
- {$IFDEF UNIX}
- cfgDir := GetEnvironmentVariable('HOME') + DirectorySeparator + '.mybookshelf' + DirectorySeparator;
- {$ENDIF}
- if not DirectoryExistsUTF8(cfgDir) then CreateDirUTF8(cfgDir);
- cfgPath := cfgDir + 'config.ini';
- ini := TIniFile.Create(cfgPath);
- try
- dataDir := ini.ReadString('general', 'data_dir', cfgDir);
- // You can also read autoPdfCover if you want it here:
- // autoPdfCover := ini.ReadBool('general', 'auto_pdf_cover', True);
- finally
- ini.Free;
- end;
- if not DirectoryExistsUTF8(dataDir) then CreateDirUTF8(dataDir);
- dataPath := IncludeTrailingPathDelimiter(dataDir) + 'books.xml';
- BookList:=TBookCollection.Create;
- if FileExistsUTF8(dataPath) then
- LoadBooksXML(dataPath, BookList, PanelBackground);
- for i:=0 to BookList.Count-1 do
- begin
- with BookList.Books[i] do
- begin
- Cover.Width:=bookWidth;
- Cover.Height:=bookHeight;
- Cover.Parent:=PanelBackground;
- EnsureScaledToCoverSize;
- end;
- end;
-
- // speed up startup: we skipped synchronous PDF generation during LoadData
- SetPdfCoverGenerationEnabled(False);
- try
- if FileExistsUTF8(dataPath) then
- LoadBooksXML(dataPath, BookList, PanelBackground);
- finally
- SetPdfCoverGenerationEnabled(True); // re-enable for user actions
- end;
- RearrangeBooksOnScreen();
- // Background: generate covers only where still generic
- CoverWorkerEnqueueMissingFromBookList(BookList);
- CoverWorkerStart;
- End;
- procedure Tform1.Formkeydown(Sender: Tobject; var Key: Word; Shift: Tshiftstate);
- var i:Integer;
- begin
- if Key = VK_DELETE then
- begin
- for i:= BookList.Count-1 downto 0 do
- begin
- if BookList.Books[i].isSelected = True then
- begin
- BookList.Books[i].Cover.Free;
- BookList.Remove(BookList.Books[i]);
- end;
- end;
- RearrangeBooksOnScreen();
- end;
- End;
- initialization
- {$i mybookshelf.lrs}
- end.
|