Compare commits

...

263 Commits

Author SHA1 Message Date
Zach H
4535a70b57 Define qtlogging.ini location (#5551) 2025-02-02 19:27:14 +00:00
BruebachL
6c1b7c83ec Debounce writing the setting to cache when adjusting card sizes. (#5550)
* Debounce writing the setting to cache when adjusting card sizes.

* Lint.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2025-02-02 18:45:26 +00:00
Zach H
f0adafb275 Reconnect Servatrice if DB Connection Dies (#5548) 2025-02-02 18:25:01 +00:00
BruebachL
9dd60d74d1 Hotfix VDS sizing scrollArea widget incorrectly when created while not visible by setting size in showEvent (#5547)
* Only resize on folder widget to make sure it doesn't squish the loading indicator.

* Clamp scrollArea widget to viewport width on showEvent to prevent widget being instantiated with wrong width when not visible.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2025-02-02 17:36:56 +00:00
RickyRister
1de09deb59 Fix size and image quality issues with new user icons (#5546) 2025-02-02 15:08:23 +00:00
BruebachL
a0b52ce450 Implement folder support for VDS. (#5545) 2025-02-02 15:08:04 +00:00
RickyRister
95cea0f191 Add custom server-side pawn colors (#5543) 2025-02-02 03:25:25 +00:00
RickyRister
0fc05e15cd pass ServerInfo_User down the chain (#5542) 2025-02-02 01:04:49 +00:00
RickyRister
26c0cdc072 Make settings window scrollable (#5539) 2025-02-01 05:03:30 +00:00
RickyRister
b1b48d50f3 move deck conversion settings into deck settings group (#5538) 2025-02-01 04:32:12 +00:00
Zach H
349c18aa6a Fix Crash with "c:" in deck editor (#5537) 2025-02-01 04:32:01 +00:00
Zach H
b956fd4bac Fix windows deck searching (#5536) 2025-02-01 04:13:46 +00:00
RickyRister
34e0130b90 fix view transform button disappearing on click (#5535) 2025-02-01 03:31:09 +00:00
BruebachL
33d8edeb9a Get file format from name. (#5532)
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2025-01-27 19:41:29 -05:00
RickyRister
5d1e905255 remove refreshTree call in remote model's constructors (#5533) 2025-01-27 19:40:59 -05:00
RickyRister
51c542aa04 Disable add to deck submenu if no deck editor tabs are open (#5530)
* refactor

* just have the deck name

* clean up submenu parenting

* disable add to deck menu if no deck editor tabs are open
2025-01-26 10:24:09 -05:00
BruebachL
4d791f4d7a Edhrec tab (#5512)
---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
Co-authored-by: Zach H <zahalpern+github@gmail.com>
2025-01-25 22:29:27 +00:00
RickyRister
aee68f8b00 add missing override and explicit specifiers in common (#5527) 2025-01-25 14:06:03 +00:00
RickyRister
b911ea6e28 add missing override and explicit specifiers in src/server (#5526) 2025-01-25 14:05:25 +00:00
RickyRister
a41e7c75c1 add missing override and explicit specifiers in src/deck and src/utility (#5525)
* add missing override and explicit specifiers in src/deck

* add missing override and explicit specifiers in src/utility
2025-01-25 14:05:00 +00:00
RickyRister
9f729bf636 add missing override and explicit specifiers in src/dialogs (#5524) 2025-01-25 14:04:26 +00:00
RickyRister
42e4c14a82 add missing override and explicit specifiers in src/client (#5523) 2025-01-25 14:03:54 +00:00
Zach H
37a0c00b3f Support right-click on game list menu (#5522) 2025-01-25 14:03:29 +00:00
Zach H
f6c31bf901 Invert "Show" games, so all games are visible by default (#5521) 2025-01-25 07:03:36 +00:00
Zach H
b48fe8b99c Better capitalization & Deck open first (#5520) 2025-01-25 05:45:44 +00:00
RickyRister
19b758591b Allow offline Replays tab (#5519) 2025-01-25 05:44:48 +00:00
Zach H
ec6a23de56 Support more indices (#5505)
* Support more indices

* Support more indices
2025-01-25 04:16:41 +00:00
BruebachL
ce416df3fb Add a dialog to prompt user to convert to .cod format if trying to apply tags to a .txt deck. (#5514)
* Add a dialog to prompt user to convert to .cod format if trying to apply tags to a .txt deck.

* Lint mocks.

* Address comments, move dialog to appropriate folder.

* Unlint.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2025-01-25 03:20:30 +00:00
BruebachL
4e96157091 Flow Layout fixes (#5515)
* Flow Layout fixes.

* Remove some comments.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2025-01-25 03:17:39 +00:00
RickyRister
f428148f64 Allow offline Deck Storage tab (#5518)
* make deck storage tab no longer close on disconnect

* add method for clearing remote decklist model

* handle connect/disconnect in deck storage tab
2025-01-25 03:16:40 +00:00
RickyRister
e8b1e3ef0c don't autoclose card view if single card gets dragged into same zone (#5517)
* rename canResize param to toNewZone

* pass toNewZone down

* don't autoclose card view if card gets dragged into same zone
2025-01-25 02:08:28 +00:00
RickyRister
085f0dd26c reduce unnecessary CardItem creation in ViewZone addCard process (#5513) 2025-01-24 05:13:08 +00:00
BruebachL
1d2ab8d3d3 Convert lastLoaded timestamp into proper QDateTime for correct comparison. (#5506)
* Convert lastLoaded timestamp into proper QDateTime for correct comparison.

* Reintroduce null check.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2025-01-22 12:58:19 +00:00
RickyRister
66e2e7a473 add missing override and explicit specifiers to all classes in src/game (#5511) 2025-01-22 12:57:56 +00:00
RickyRister
af161f00b7 Remove spacing in CardInfoWidget caused by invisible view transformation button (#5510) 2025-01-22 12:55:21 +00:00
RickyRister
420cca2402 fix after merge to use ZoneView::close (#5509) 2025-01-20 22:43:05 -05:00
RickyRister
97fdf11c8f Add setting to auto close card view when empty (#5502) 2025-01-21 03:08:01 +00:00
RickyRister
aeb1b9fb4f Fix segfault when game is closed while card view window is open (#5507) 2025-01-21 03:06:55 +00:00
RickyRister
b004e91aa4 fix segfault when bottoming card in deck view (#5508) 2025-01-21 03:06:00 +00:00
Zach H
090cc8c144 Support more indices (#5503) 2025-01-20 01:42:24 -05:00
tooomm
0467fae51b Add label to swap button (#5501) 2025-01-19 17:26:25 +00:00
BruebachL
aa24502129 Move logging from QDebug to QCDebug and introduce LoggingCategories. (#5491)
* Move logging from QDebug to QCDebug and introduce LoggingCategories.

* Lint.

* Unlint like one change.

* Remove .debug category since this is autofilled by Qt and used to differentiate between QCDebug and QCWarning and QCError.

* Uncomment defaults, include main category.

* Make PictureLoader logging a bit more useful.

* Lint...?

* Address comments.

* Clean up some unnecessary classes in logging statements.

* Add a new message format to the logging handler.

* Lint.

* Lint.

* Support Windows in Regex

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
Co-authored-by: ZeldaZach <zahalpern+github@gmail.com>
2025-01-19 13:14:23 +00:00
BruebachL
e752578d15 Add a button to easily view the transformed version of a card. (#5498)
* Add a button to easily view the transformed version of a card.

* Minor reword

* Minor fix

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
Co-authored-by: ZeldaZach <zahalpern+github@gmail.com>
2025-01-19 12:59:53 +00:00
BruebachL
724db755af Hide load from remote button in local games (#5499)
* Hide load from remote button in local games.

* Minor fix

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
Co-authored-by: ZeldaZach <zahalpern+github@gmail.com>
2025-01-19 12:59:33 +00:00
BruebachL
ec0caaf421 Give deckList a signal to emit when the tags change and hook up the display widget to that. (#5497)
* Give deckList a signal to emit when the tags change and hook up the display widget to that.

* Reload from file when loading a visual deck to ensure latest changes propagate to the decklist.

* Eliminate loadVisualDeck and use loadDeckFromFile instead.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2025-01-19 12:32:39 +00:00
Zach H
55b490ade0 Generate PDBs for Windows Builds (#5494) 2025-01-18 07:54:01 +00:00
ZeldaZach
1392bdd258 Fix Windows 7 Crash Dump 2025-01-18 00:33:30 -05:00
Zach H
648c96ac3d Allow Moderators to Grant Replay & Activate in TabAdmin (#5492) 2025-01-18 02:23:24 +00:00
Zach H
d3a1538af3 Fix Windows Crash Reporter (#5493)
* Fix Windows Crash Reporter

* Fix NSIS template
2025-01-18 02:23:05 +00:00
RickyRister
2bc71095dd get UserListProxy from TabSupervisor instead of passing it in the constructor (#5490) 2025-01-17 15:27:52 +00:00
RickyRister
92a903b035 fix chat messages not showing in the client (#5489) 2025-01-17 03:11:13 -05:00
transifex-integration[bot]
cd373edf3d Translate cockatrice_en@source.ts in pt_BR (#5487)
100% translated source file: 'cockatrice_en@source.ts'
on 'pt_BR'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-01-17 07:41:59 +00:00
RickyRister
ca2d438cda fix sideboard not being re-locked on load deck (#5486) 2025-01-17 05:25:01 +00:00
RickyRister
c148c8df7f replace foreach macro with standard for each loop (#5485) 2025-01-17 05:18:15 +00:00
Zach H
0cbad25385 General Cleanup of Unused Assets (#5484) 2025-01-17 05:08:53 +00:00
ZeldaZach
7b94d5d501 Better sanitization of pointers 2025-01-17 00:05:08 -05:00
RickyRister
ee938342f3 Change visible buttons in game lobby depending on if deck is loaded (#5480)
* rename method

* delete unused method

* refactor

* increase margins

* change visible buttons depending on if deck is loaded

* correctly send the ReadyStart command on unload

* fix force start button still being visible
2025-01-17 04:52:33 +00:00
Zach H
cb64a5eea0 Populate TabAccount if reopened (#5483) 2025-01-17 04:28:18 +00:00
BruebachL
80165c28a9 Add options to include/exclude set name and collector number during clipboard import/export. (#5482)
* Add options to include/exclude set name and collector number during clipboard import/export.

* Missing parentheses in action label.

* Revert the silliest lint in the world.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2025-01-17 02:38:01 +00:00
ZeldaZach
315c224f24 Fix crash on add/edit tags 2025-01-16 00:54:34 -05:00
ZeldaZach
55f624b634 Hide Loading Msg in VDE when not relevant 2025-01-16 00:06:25 -05:00
ZeldaZach
82b257b589 Fix index 0 tab not functioning 2025-01-15 23:55:51 -05:00
RickyRister
a51ca9f9cb fix incorrect values in deck editor tab's views menu on init (#5479) 2025-01-16 04:06:56 +00:00
RickyRister
7e19b52926 fix tab-specific menus not present when tab is opened on startup (#5478) 2025-01-16 04:04:15 +00:00
RickyRister
2d02955f8b delete overloaded signal in PendingCommand (#5477)
* remove overloaded signal since no one was using it

* remove usages of qOverload

* turns out new slot/signal syntax can ignore extra params
2025-01-15 13:16:06 +00:00
RickyRister
3a740f0bde group printings together when sorting in card reveal window (#5476) 2025-01-15 13:14:47 +00:00
RickyRister
455d68f9ea Move UserlistProxy to src/server/user and fix capitalization (#5475)
* move file

* fix capitalization
2025-01-15 13:14:16 +00:00
RickyRister
2def02e140 Remember which tabs are open between sessions (#5467) 2025-01-15 06:10:24 +00:00
RickyRister
23bd18a04c fix segfault that happens when account tab is closed (#5474) 2025-01-15 06:10:03 +00:00
RickyRister
d09b9eb533 Rename UserList class to UserListWidget (#5473) 2025-01-15 03:07:36 +00:00
github-actions[bot]
25caae6d0f Update translation files (#5472)
Co-authored-by: github-actions <github-actions@github.com>
2025-01-14 20:28:36 -05:00
BruebachL
a717e715b6 Introduce null checks, add setShortName and collectorNumber to deckList export. (#5471)
* Introduce null checks, add setShortName and collectorNumber to deckList export.

* Lint.

* Lint again.

* Lint AGAIN.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2025-01-14 10:54:15 -05:00
BruebachL
c079715c46 Properly check if a duplicate already exists in the list, no longer break loop. (#5470)
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2025-01-14 10:12:53 -05:00
BruebachL
f6c1253e84 Add a placeholder label to indicate database is still loading. (#5469)
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2025-01-14 14:19:32 +00:00
BruebachL
8462b6e906 Minor fix to sorted list reconstruction to fix duplication of cards in printing selector. (#5468)
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2025-01-14 13:59:53 +00:00
RickyRister
cca82f59eb Don't re-sort VisualDeckStorage every time it gets tabbed to (#5466)
* remove showEvent

* refresh cards on init

* fix sort order not immediately being set
2025-01-14 13:58:44 +00:00
RickyRister
81662b7fec Reduce spacing in visual deck storage (#5465)
* move thing

* reduce spacing
2025-01-14 13:52:46 +00:00
RickyRister
d2c2128e9b Rename TabUserLists to TabAccount (#5464)
* rename class

* rename variables
2025-01-14 13:50:08 +00:00
RickyRister
686645c1e4 refactor DeckViewContainer into own file (#5455)
* cut-and-paste

* remove some includes

* move refreshShortcuts

* move deck_view into src/game/deckview

* move deck_view_container to src/game/deckview

* fix build failure
2025-01-14 07:00:09 +00:00
ZeldaZach
9df71fe1e8 Update VCPkg 2025-01-14 01:47:05 -05:00
Zach H
6309e7e318 Fix Windows FlowWidget duplication (#5460)
- Delete the item widget right away, as the delay is too great with deleteLater
2025-01-14 04:33:21 +00:00
Zach H
767e83c879 Disable Force Start for host on load (#5462)
- Fix #5444
2025-01-14 04:33:04 +00:00
RickyRister
78d54b0ef2 Remove unnecessary deck_view.h includes (#5461)
* remove unnecessary deck_view imports

* remove some more imports
2025-01-14 04:32:53 +00:00
BruebachL
497e4f1be0 Add loadFromFileAsync to deckLoader and connect VisualDeckStorageWidget to it. (#5456)
* Add loadFromFileAsync to deckLoader and connect VisualDeckStorageWidget to it.

* Address comments.

* Lint.

* Unlint something.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2025-01-14 03:02:33 +00:00
BruebachL
6072df3522 .txt decks reportedly don't get saved when they're loaded. (#5459)
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2025-01-14 01:51:41 +00:00
BruebachL
ba89495dc0 Refactor Picture Loader (#5457) 2025-01-13 18:52:54 +00:00
RickyRister
a417b049da Make Visual Deck Storage tab be managed by TabSupervisor (#5453)
* remove closeRequest override

* remove visualDeckStorage from WindowMain

* manage visual deck storage in TabSupervisor

* open on startup

* refresh vds on db load finish

* open deck editor tab first on startup
2025-01-13 18:42:58 +00:00
github-actions[bot]
883f1a5c11 Update translation source strings (#5454)
Co-authored-by: github-actions <github-actions@github.com>
2025-01-13 11:08:32 -05:00
BruebachL
dd8ac14f99 Visual deck storage v2 (#5427)
* Restore some button states (ready/sideboard locked) to sensible defaults when unloading a deck.

* Update last loaded timestamp in decklist file and then restore original last modified timestamp if a user requests a deck load.

* Add some todos.

* Loading a deck from local file dialog should swap out scenes, enable unload button.

* Lint.

* Shuffle some classes and signals around.

* More sort options, sort widgets directly.

* Banner cards should respect providerIds.

* Properly updateSortOrder on load.

* Add the color identity to the Deck Preview Widget.

* Properly set sort indices.

* Change replace visualDeckStorageWidget with deckView to be in deckSelectFinished so that it also works on remote deck load.

* Include settings for unused color identities display.

* Change opacity scaling.

* Overload for Qt.

* Lint.

* Lint.

* Include QMouseEvent

* Template because MacOs.

* Include a quick filter for color identities.

* Include a quick filter for color identities.

* Save some space.

* Refactor DeckPreviewWidgets to reside in their own folder.

* Add Deck Loader logging category.

* Introduce a tagging system.

* Add some more default tags.

* Even more default tags.

* Lint.

* Lint a comma.

* Remove extra set of braces.

* Lint some stuff.

* Refresh banner cards when tags are added.

* Lint.

* Wrestle with Qt Checkboxes.

* Lint.

* Adjust some sizes, relayout.

* Address comments.

* Lint.

* Reorder kindred types.

* Add a search bar for tags.

* Remove close button (for now) and change "Add tags ..." to "Edit tags ..."

* Retranslate window title for Deck Tag Manager Dialog.

* Style tag addition widget to be consistent.

* Lint.

* Override paintEvent.

* Override sizeHint

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2025-01-12 17:46:22 -05:00
RickyRister
9bd024d39f Make all tabs closable; add tabs menu (#5451)
* make closeRequest call close by default

* make all tabs closable by default

* closeRequest instead of deleteLater on stop

* null out pointer on destroy

* no need to manually null out the tabs anymore

* comment

* pass tabsMenu into ctor

* comment

* implement tabs menu

* fix segfault on close (again)

* remove deck editor action from WindowMain
2025-01-12 16:15:19 -05:00
transifex-integration[bot]
e4611a8616 Translate cockatrice_en@source.ts in en_US (#5452)
100% translated source file: 'cockatrice_en@source.ts'
on 'en_US'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-01-12 12:41:05 -05:00
RickyRister
3f41e5dd77 don't close replay tabs and do close message tabs on disconnect (#5450) 2025-01-12 02:47:36 -05:00
RickyRister
a6fc88c79a Always set TabSupervisor as parent in Tab subclasses (#5449)
* refactor closeTab

* always set tab parent to tabSupervisor

* set tabSupervisor parent

* use close instead of deleteLater

* be more clear about overloads
2025-01-12 02:34:11 -05:00
transifex-integration[bot]
3a4ec1062b Translate cockatrice_en@source.ts in pt_BR (#5448)
100% translated source file: 'cockatrice_en@source.ts'
on 'pt_BR'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-01-11 23:41:57 -05:00
RickyRister
7347ba88ac fix segfault on disconnect (#5447)
* add new param to closeRequest

* don't emit signals in dtors

* send closeRequest

* fix build failure

* fix build failure

* see if we can get away with the overloaded triggered

* fix build failure
2025-01-11 22:19:45 -05:00
tooomm
3b544a36a8 Fix button label (#5441) 2025-01-11 21:28:23 -05:00
RickyRister
2851d0c7e6 add override specifier to tab subclasses (#5445) 2025-01-11 21:28:02 -05:00
transifex-integration[bot]
2b296badea Translate cockatrice_en@source.ts in zh-Hans (#5446)
100% translated source file: 'cockatrice_en@source.ts'
on 'zh-Hans'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-01-11 21:27:00 -05:00
ZeldaZach
a12c4ee909 Fix MacOS Builds for Non-Main Repo PRs 2025-01-11 21:21:11 -05:00
ZeldaZach
7db9c9115e Fix SoundEngine on Windows (again) 2025-01-11 00:19:00 -05:00
ZeldaZach
503985a080 Initialize audioOutput for SoundEngine 2025-01-11 00:12:32 -05:00
RickyRister
9f466162b0 disable starting life total edit in game information window (#5440) 2025-01-10 23:14:46 -05:00
Zach H
8bea3f8997 Fix sounds on Qt6 (#5439) 2025-01-10 23:10:47 -05:00
RickyRister
1a3df84f0a fix segfault on exit if any closable tabs were open (#5435) 2025-01-10 17:27:26 -05:00
Zach H
2b3c47148e GHA MacOS Only Sign/Notarize if self repo (#5437) 2025-01-10 16:46:05 -05:00
transifex-integration[bot]
59ca4397e2 Translate oracle/oracle_en@source.ts in zh-Hans (#5436)
100% translated source file: 'oracle/oracle_en@source.ts'
on 'zh-Hans'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-01-10 08:22:58 -05:00
transifex-integration[bot]
98266b0739 Translate oracle/oracle_en@source.ts in yue (#5434)
100% translated source file: 'oracle/oracle_en@source.ts'
on 'yue'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-01-10 08:22:48 -05:00
Zach H
5a82ff106d Update VCPkg (#5433) 2025-01-09 23:23:00 -05:00
Zach H
2194430019 Sign macOS Releases (#5396) 2025-01-09 22:32:53 -05:00
RickyRister
1f11015a2f Refactor files in src/utility and src/deck to new Qt Slot/Signal syntax (#5432)
* refactor in src/utility

* refactor in src/deck

* fix build failure
2025-01-09 06:33:20 -05:00
RickyRister
c3421669d5 Refactor files in src/game to new Qt Slot/Signal syntax (#5431)
* fix signals in CardDatabaseParser

* update remaining signals

* cleanup

* wait this was always just broken

* fix build failure

* fix build failure

* fix build failure
2025-01-09 06:32:25 -05:00
RickyRister
6e8adddc6d Refactor tab_supervisor to new Qt Slot/Signal syntax (#5430)
* Refactor tab_supervisor to new Qt Slot/Signal syntax

* fix build failure
2025-01-09 06:26:42 -05:00
RickyRister
22a6ded4f0 reduce vertical spacing in visual deck storage (#5422) 2025-01-09 06:25:40 -05:00
transifex-integration[bot]
0d7669db2c Translate cockatrice_en@source.ts in yue (#5428)
100% translated source file: 'cockatrice_en@source.ts'
on 'yue'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-01-08 19:23:49 -05:00
transifex-integration[bot]
9526bca168 Translate cockatrice/cockatrice_en@source.ts in de (#5429)
100% translated source file: 'cockatrice/cockatrice_en@source.ts'
on 'de'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-01-08 19:23:38 -05:00
transifex-integration[bot]
0683431f35 Translate cockatrice_en@source.ts in en_US (#5426)
100% translated source file: 'cockatrice_en@source.ts'
on 'en_US'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-01-06 17:31:14 -05:00
github-actions[bot]
70790264b8 Update translation source strings (#5425)
Co-authored-by: github-actions <github-actions@github.com>
2025-01-06 12:42:46 -05:00
transifex-integration[bot]
c8a68c83e3 Translate cockatrice_en@source.ts in yue (#5424)
100% translated source file: 'cockatrice_en@source.ts'
on 'yue'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-01-06 12:38:58 -05:00
RickyRister
23171f79d0 Refactor window_main to new Qt Slot/Signal syntax (#5423) 2025-01-06 12:38:44 -05:00
RickyRister
b7f05a12a3 get swap cards button to work with multi-selections (#5421) 2025-01-05 22:44:40 -05:00
Zach H
6078dd092a Support viewing the bottom X cards of library (#5410)
* Get cardIds to update properly in bottom view (#5414)

* Get bottom view to update properly when card is inserted into known portion (#5415)

---------

Co-authored-by: RickyRister <42636155+RickyRister@users.noreply.github.com>
2025-01-05 21:17:18 -05:00
BruebachL
81b85e97df Extend decklist parsing (#5316)
* Extend the decklist parsing from clipboard to also support SetName, CollectorNumber and Foil Status.

* Q_UNUSED foil for now but keep parsing logic for future PR's/compatibility.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2025-01-05 21:07:17 -05:00
RickyRister
cc16b8779c improve shortcut search to split by word (#5416) 2025-01-05 19:19:00 -05:00
BruebachL
62f7c7f9ce New visual deck storage (#5290)
* Add TabDeckStorageVisual

* Visual Deck Storage

* Add BannerCard to .cod format, use it in the visual deck storage widget.

* Show filename instead of deckname if deck name is empty.

* Lint.

* Don't delint cmake list through hooks.

* Add deck loading functionality.

* Open Decks on double click, not single click.

* Void event for now.

* Fix build issue with overload?

* Fix build issue with overload?

* Include QDebug.

* Turn the tab into a widget.

* Move the signals down to the widget, move the connections and slots up to the parent widgets.

* No banner card equals an empty CardInfoPtr.

* Add an option to sort by filename or last modified.

* Flip last modified comparison.

* Lint.

* Don't open decks twice in the storage tab.

* Fix unload deck not working by showing/hiding widgets instead of adding/removing to layout.

* Add a search bar.

* Add a card size slider.

* Lint.

* Lint.

* Lint.

* Fix settings mocks.

* No need to QDebug.

* No need to QDebug.

* Member variable.

* Member variable.

* Non-lambda.

* Change set to list conversion.

* Specify overload.

* Include MouseEvent

* Adjust font size dynamically.

* Add an option to show the visual deck storage on database load.

* Fix the close button not working on the tab, add an option to launch the visual deck storage tab to Cockatrice menu.

* Override virtual functions.

* Correct tab text.

* Add a setting to remember last used sorting order for visual deck storage widget.

* Update banner card combo box correctly.

* Fix mocks.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
Co-authored-by: Zach H <zahalpern+github@gmail.com>
2025-01-05 18:12:20 -05:00
BruebachL
7496e79e8c Add a button to swap the card between mainboard and sideboard to the DeckEditor (#5175)
* Add a button to swap the card between mainboard and sideboard to the deck editor.

* Add new icon to cockatrice.qrc and force update.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2025-01-05 17:57:37 -05:00
RickyRister
b8cf3e2cab add ctrl enter as shortcut for ok in load deck from clipboard (#5417) 2025-01-05 17:41:03 -05:00
RickyRister
93fab3d78f Remember last opened directory when loading decks (#5418)
* remember last directory when loading deck

* move shared code into new dlg class
2025-01-05 17:40:20 -05:00
BruebachL
9c38c9ed1b Differentiate logging in order to silence certain modules. (#5419)
* Differentiate logging in order to silence certain modules.

* Lint cmake.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2025-01-05 17:38:51 -05:00
RickyRister
38e99f2e87 implement /card command (#5413) 2025-01-04 02:07:43 +00:00
RickyRister
68226786a2 don't redraw PrintingSelector's FlowWidget unless cards actually changed (#5392) 2025-01-04 01:49:54 +00:00
RickyRister
455cd9717a add menu action to open settings folder (#5412) 2025-01-04 01:49:41 +00:00
RickyRister
fa79c5c36a populate default debug.ini with more values (#5411)
* populate default debug.ini with more values

* move the default debug.ini to a resource
2025-01-03 20:50:30 +00:00
RickyRister
0402d4b853 add debug setting to load deck and ready on join (#5409)
* new property

* refactor deck loading to new method

* another new method

* works now
2025-01-02 17:08:51 -05:00
RickyRister
8a427955e7 Add debug setting to start local game on startup (#5408)
* new properties

* refactor

* start local game on startup

* disable autoconnect
2025-01-02 09:51:59 -05:00
RickyRister
bb4214e28a Make SettingsManager params const ref (#5405)
* pass settingsPath by const ref

* pass params by const ref

* cleanup
2025-01-02 00:33:37 -05:00
RickyRister
f924b04efd add debug settings; option to show cardIds (#5404)
* add debug settings; option to show cardIds

* pass param by const ref

* change group structure again

* create debug.ini if not exists
2025-01-02 00:32:58 -05:00
transifex-integration[bot]
62f60867a9 Translate cockatrice_en@source.ts in pt_BR (#5407)
100% translated source file: 'cockatrice_en@source.ts'
on 'pt_BR'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-01-02 00:32:29 -05:00
transifex-integration[bot]
b5844f1244 Translate cockatrice/cockatrice_en@source.ts in it (#5406)
100% translated source file: 'cockatrice/cockatrice_en@source.ts'
on 'it'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-01-01 22:28:42 -05:00
ryder052
8c0093d453 Crashfix for opening Deck editor (#5403)
* CardDatabase::getCards() no longer copies the whole database

---------

Co-authored-by: Jakub Mrowinski <ryder052@outlook.com>
2025-01-01 19:25:04 -05:00
RickyRister
34df4cd060 support multi-select in deck editor (#5397)
* support multiselect in deck editor

* fix crash

* don't reset selection after each action

* maintain old reselecting behavior when changing cards from left side

* fix crash for real (probably)

* maintain reselection behavior when deleting single selection
2025-01-01 00:43:47 -05:00
Zach H
99eea3a662 Improve Database Backup Speed (#5400)
* Support better indexes for Servatrice

- Prevent searching only on msg for logs
2025-01-01 00:28:57 -05:00
Zach H
6e1047032d Keep card annotations on stack (#5399) 2024-12-31 14:08:25 -05:00
transifex-integration[bot]
b2a8748bc6 Translate cockatrice/cockatrice_en@source.ts in it (#5398)
100% translated source file: 'cockatrice/cockatrice_en@source.ts'
on 'it'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2024-12-31 13:57:14 -05:00
transifex-integration[bot]
ded6d5b8eb Translate cockatrice/cockatrice_en@source.ts in de (#5395)
100% translated source file: 'cockatrice/cockatrice_en@source.ts'
on 'de'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2024-12-30 19:18:00 +00:00
transifex-integration[bot]
832842c20c Translate cockatrice_en@source.ts in en_US (#5394)
100% translated source file: 'cockatrice_en@source.ts'
on 'en_US'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2024-12-30 17:42:32 +00:00
github-actions[bot]
b43e4ae469 Update translation source strings (#5393)
Co-authored-by: github-actions <github-actions@github.com>
2024-12-30 12:38:30 +00:00
Zach H
026afeb885 Support auto-reconnect for Servatrice (#5391)
- Fix #5022
2024-12-30 04:35:32 +00:00
RickyRister
b6793a5e01 fix cards having the wrong printing if rejoin game before card db finishes loading (#5390)
* rename cardInfoUpdated to refreshCardInfo and make it public

* refresh card infos when db finishes loading
2024-12-30 03:25:11 +00:00
moryall
d231264a16 Update Dockerfile Ubuntu version to newest LTS (#5108)
* Update Dockerfile to non-outdated Ubuntu version

1. Updated image to jammy as it is still in it's LTS window. Didn't go with 24.04/Noble as it released after latest version of cockatrice released.
2. Had to add new ARG
3. No qt5-default library, so replaced with qt5-qmake

* Update Dockerfile

Upped from Jammy -> Noble
Upped from Qt5 -> Tt6

* Update Dockerfile - new port

Added Port 4748 for new features

* Update Dockerfile

Changed Noble - > 24.04

* indentation

* remove unused dependencies

---------

Co-authored-by: tooomm <tooomm@users.noreply.github.com>
2024-12-30 01:54:18 +00:00
ryder052
6e02bdec2e Fix crash on replay list sorting, fix error popups on win debug (#5388)
* Fix annoying popups on start

* Fix replay list item parent index calculation #5311

---------

Co-authored-by: Jakub Mrowinski <ryder052@outlook.com>
2024-12-30 01:10:12 +00:00
transifex-integration[bot]
cfaadc40b1 Translate cockatrice_en@source.ts in pt_BR (#5386)
100% translated source file: 'cockatrice_en@source.ts'
on 'pt_BR'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2024-12-30 00:57:51 +00:00
transifex-integration[bot]
93475b43a5 Updates for project Cockatrice and language it (#5387)
* Translate cockatrice/cockatrice_en@source.ts in it

100% translated source file: 'cockatrice/cockatrice_en@source.ts'
on 'it'.

* Translate webclient/src/i18n-default.json in it

100% translated source file: 'webclient/src/i18n-default.json'
on 'it'.

* Translate oracle/oracle_en@source.ts in it

100% translated source file: 'oracle/oracle_en@source.ts'
on 'it'.

---------

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2024-12-30 00:57:43 +00:00
RickyRister
3348e051a1 update recently opened decks when saving a new deck (#5389) 2024-12-30 00:57:31 +00:00
Zach H
dad1aea128 Show correct art on middle mouse popup (#5385) 2024-12-29 23:24:32 +00:00
Zach H
dec001114a Clone now clones the exact printing, when possible (#5384) 2024-12-29 23:24:20 +00:00
ZeldaZach
1ce7b9f7de Update number when 'Reveal top cards of library' used 2024-12-29 16:31:13 -05:00
Zach H
2ff99f12d8 Require Admin Permissions to install Cockatrice Windows (#5383)
Fix #5382
2024-12-29 16:22:39 +00:00
tooomm
6679705254 Simpler naming (#5381) 2024-12-29 15:41:30 +00:00
transifex-integration[bot]
7eafac5b1a Translate cockatrice_en@source.ts in en_US (#5377)
100% translated source file: 'cockatrice_en@source.ts'
on 'en_US'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2024-12-29 07:11:02 +00:00
RickyRister
ac3aa949ad add "view related cards" right click menu to card info widget (#5375) 2024-12-29 06:11:12 +00:00
Zach H
b4036c8671 Disable CardMenu iff no items selected (#5376)
- Fix #4372
2024-12-29 06:10:27 +00:00
github-actions[bot]
4e0de1c066 Update translation source strings (#5373)
Co-authored-by: github-actions <github-actions@github.com>
2024-12-29 03:37:53 +00:00
RickyRister
f32890916d don't disable autoconnect on disconnect (#5372)
* don't disable autoconnect on disconnect

* update autoConnect on clicking the checkbox
2024-12-29 03:07:00 +00:00
Zach H
24a0dac420 Fix Windows Portable Crash (#5371) 2024-12-29 03:02:52 +00:00
RickyRister
716bc00533 fix "forgot password" closing connection dialogue on cancel (#5369) 2024-12-29 02:23:58 +00:00
Zach H
32dd18998d Combine card legalities in Oracle (#5370)
- Some printings have different legalities, which cause Oracle to bug out
- Fix #4783
2024-12-29 02:23:39 +00:00
ZeldaZach
5e62069444 Fix Windows Crash due to nullptr 2024-12-28 21:02:10 -05:00
Zach H
bf63dc4ab7 Add option to remove saved sever (#5368)
* Add option to remove saved sever

- Fix #4099
- Removes old method that didn't work
2024-12-29 00:37:49 +00:00
Danny Piper
7679546e30 Add Nix shell (#5362) 2024-12-28 23:52:57 +00:00
Zach H
45b11dc984 Add password reset button label (#5367) 2024-12-28 23:52:14 +00:00
RickyRister
25d21a3da6 refactor: remove ReleaseChannel keeping track of its own indexes (#5366) 2024-12-28 23:51:37 +00:00
Zach H
c8d49b5bf9 Support macOS-15 Builds (#5364) 2024-12-28 23:09:01 +00:00
RickyRister
f737d9a794 fix bug with release channel setting not being remembered (#5365) 2024-12-28 23:08:07 +00:00
RickyRister
df9c5ae53c Check for client updates on startup (#5359) 2024-12-28 21:29:59 +00:00
transifex-integration[bot]
e0829a75d2 Translate cockatrice/cockatrice_en@source.ts in it (#5363)
100% translated source file: 'cockatrice/cockatrice_en@source.ts'
on 'it'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2024-12-28 21:01:34 +00:00
Zach H
1f58f7e93d Support Mod/Admin Notes Section (#5361) 2024-12-28 18:05:49 +00:00
transifex-integration[bot]
14807ba036 Translate oracle/oracle_en@source.ts in pl (#5360)
100% translated source file: 'oracle/oracle_en@source.ts'
on 'pl'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2024-12-28 15:31:16 +00:00
transifex-integration[bot]
75fb3894a6 Translate oracle/oracle_en@source.ts in pt_BR (#5358)
100% translated source file: 'oracle/oracle_en@source.ts'
on 'pt_BR'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2024-12-28 12:08:58 +00:00
github-actions[bot]
18119bd11b Update translation source strings (#5357)
Co-authored-by: github-actions <github-actions@github.com>
2024-12-28 06:33:57 +00:00
RickyRister
4c7796537f Support folder download in deck storage tab (#5356)
* refactor

* support folder download
2024-12-28 06:31:18 +00:00
RickyRister
3452cb01d0 fix replay download not working if replay folder is empty (#5355)
* fix downloading single replays

* fix downloading replay folder
2024-12-28 06:15:25 +00:00
RickyRister
6a151ef97a Add button to open decks folder (#5354) 2024-12-28 05:09:46 +00:00
RickyRister
e3d651668c Add button to open replays folder (#5352) 2024-12-28 05:06:26 +00:00
Zach H
7a5704beaa Support Moderator/Admin force activating users (#5353) 2024-12-28 05:01:31 +00:00
RickyRister
37b78a9a4c change action's text to "unconcede" when player is conceded (#5351) 2024-12-28 00:01:36 +00:00
Zach H
8bc5a9d581 Merge pull request #5350 from Cockatrice/fix_1953
Allow Moderators/Admins to Grant Replay Access
2024-12-27 18:51:11 -05:00
ZeldaZach
57ed162b79 Fix Linter 2024-12-27 18:35:52 -05:00
ZeldaZach
3524231500 Allow Moderators/Admins to Grant Replay Access
- Only to themselves, at this time
- Automatically refreshes feed, no need to re-login
2024-12-27 18:32:39 -05:00
Zach H
5cfe2b4762 Merge pull request #5348 from Cockatrice/set_owner
Establish Card Ownership & Return on Player Leave
2024-12-27 18:26:45 -05:00
ZeldaZach
a8bac1e468 Return Tagged Cards to Owner, if possible, on concede/leave 2024-12-27 18:23:39 -05:00
ZeldaZach
4f798286af Establish Card Ownership Tag 2024-12-27 18:23:09 -05:00
transifex-integration[bot]
8a04b2d69d Translate cockatrice/cockatrice_en@source.ts in de (#5349)
100% translated source file: 'cockatrice/cockatrice_en@source.ts'
on 'de'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2024-12-27 22:26:03 +00:00
transifex-integration[bot]
17893d9747 Translate oracle/oracle_en@source.ts in it (#5347)
100% translated source file: 'oracle/oracle_en@source.ts'
on 'it'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2024-12-27 20:13:57 +00:00
ZeldaZach
8af49406cd Un-translate 'ms' 2024-12-27 14:18:44 -05:00
transifex-integration[bot]
3b068b79fe Translate webclient/src/i18n-default.json in fr (#5346)
100% translated source file: 'webclient/src/i18n-default.json'
on 'fr'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2024-12-27 08:36:06 +00:00
transifex-integration[bot]
ce14e83e78 Translate webclient/src/i18n-default.json in es (#5345)
100% translated source file: 'webclient/src/i18n-default.json'
on 'es'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2024-12-27 08:35:58 +00:00
transifex-integration[bot]
f213d6fda7 Translate cockatrice_en@source.ts in en_US (#5344)
100% translated source file: 'cockatrice_en@source.ts'
on 'en_US'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2024-12-27 08:35:48 +00:00
RickyRister
83db00d7a3 reduce vertical spacing in PrintingSelector (#5342) 2024-12-27 08:35:38 +00:00
ZeldaZach
7e9bd88eb4 Fix Shutdown Server 2024-12-27 01:38:21 -05:00
github-actions[bot]
ea716ca440 Update translation source strings (#5343)
Co-authored-by: github-actions <github-actions@github.com>
2024-12-27 06:19:12 +00:00
ZeldaZach
3cd7a04002 Change Settings from Min to Base Size 2024-12-26 23:54:24 -05:00
RickyRister
914002f846 use grid instead of FlowWidget for PrintingSelector display options (#5341)
* use grid instead of FlowWidget for PrintingSelector display options

* remove one level of nesting
2024-12-27 02:08:07 +00:00
Zach H
17b82a186f Add QSet for faster lookups in CardDatabase (#5332) 2024-12-26 19:52:18 -05:00
RickyRister
7a8e957476 allow playing cards directly transformed from hand (#5339) 2024-12-26 19:51:58 -05:00
Zach H
6dfd354973 Support starting games with fewer than max players (#5338) 2024-12-26 18:32:20 -05:00
RickyRister
956c12eb32 remove shortcut workaround; always add card menu to player (#5337)
* remove workaround

* make aCardMenu less innocuous

* make card menus active for all players
2024-12-26 16:49:17 -05:00
RickyRister
d5ae4eed26 Ctrl drag now adds/removes to selection (#5336)
* refactor: clean up to use for-each loop

* track cards in rect so far and toggle isSelected on change

* only clear selection if ctrl isn't held

* fix build errors
2024-12-26 15:08:20 -05:00
RickyRister
ca486e5ed9 Don't display unusable actions in opponent's card menus (#5335) 2024-12-26 10:23:54 -05:00
RickyRister
de63066b0b fix deck storage open deck not working at all when folder is in selection (#5333) 2024-12-26 00:25:30 -05:00
Zach H
c7ca55ceb5 Support Picking Select Art per Card Basis (#5329) 2024-12-25 23:12:06 -05:00
RickyRister
024bef7ded add local rename button to replays tab (#5331) 2024-12-25 22:34:24 -05:00
RickyRister
34d3d60f95 fix text missing from chat macro list's buttons when hidden (#5330)
* fix text missing from chat macro list's buttons when hidden

* turns out you don't need to set tooltip if you already set text
2024-12-25 22:32:53 -05:00
RickyRister
ed907d7c6f Support downloading replay folders (#5325)
* rename old get replay match method to get enclosing

* creat raw getReplayMatch method

* implement thing
2024-12-25 07:33:36 -05:00
RickyRister
9d7fd66546 fix text missing from download url list's buttons when hidden (#5326) 2024-12-25 07:29:55 -05:00
RickyRister
9934841950 make better use of space in download url settings window (#5327) 2024-12-25 07:29:27 -05:00
RickyRister
432fe1100b gitignore all cmake-build folders (#5328) 2024-12-25 07:28:41 -05:00
Zach H
d987628935 Reorder String options for Filtering (#5324) 2024-12-25 00:58:59 -05:00
RickyRister
4c3ceae0e4 open replays on double-click in replays tab (#5323) 2024-12-25 00:34:43 -05:00
RickyRister
2b9d7538bf open decks on double-click in deck storage tab (#5322) 2024-12-25 00:33:48 -05:00
RickyRister
4ca1fc083d add "open recent" menu option to deck editor tab (#5319)
* add "open recent" menu option to deck editor tab

* change texts

* also get it to work with loading from deck storage tab

* add error message when fail to open

* only update recents on successful open

* only update recents on successful open

* reword to "Clear"
2024-12-24 19:55:04 -05:00
BruebachL
e7585271fb The printingSelector should set the deckEditor modified flag on adding/removing cards. (#5321)
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2024-12-24 10:28:58 -05:00
RickyRister
6e6824117d add new folder button to local deck storage tab (#5318)
* add new folder button to local deck storage tab

* allow delete button to delete folders
2024-12-24 00:26:11 -05:00
RickyRister
3e5f2fd8b2 add new folder button to game replays tab (#5317) 2024-12-24 00:23:13 -05:00
RickyRister
6e470d788e Support multi-select for remote decks in deck storage tab (#5315)
* enable multiselection

* support multi open deck

* support multi download

* support multi delete
2024-12-24 00:05:49 -05:00
RickyRister
a40d8092ce support multi-select for local decks in deck storage tab (#5314)
* allow multi-select

* support multi upload

* support multi open deck

* support multi delete deck
2024-12-23 20:41:15 -05:00
RickyRister
0234a70bfd fix bug with uploading unnamed decks ignoring the prompt (#5313) 2024-12-23 20:39:57 -05:00
RickyRister
705b1e0c2b support multi-select for remote replays in game replays tab (#5310) 2024-12-23 20:38:47 -05:00
RickyRister
69379334f9 support multi-select for local replay tab (#5309) 2024-12-23 20:31:58 -05:00
transifex-integration[bot]
12e50a1f2f Translate cockatrice_en@source.ts in en_US (#5308)
100% translated source file: 'cockatrice_en@source.ts'
on 'en_US'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2024-12-23 20:30:17 -05:00
RickyRister
ec17a477be shortcut search now displays all rows in section (#5307) 2024-12-23 20:29:52 -05:00
github-actions[bot]
205e1c7a59 Update translation source strings (#5305)
Co-authored-by: github-actions <github-actions@github.com>
2024-12-22 18:35:34 -05:00
transifex-integration[bot]
ffb60c06cb Translate oracle_en@source.ts in en@pirate [Manual Sync] (#5295)
4% of minimum 3% translated source file: 'oracle_en@source.ts'
on 'en@pirate'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2024-12-22 18:01:02 -05:00
transifex-integration[bot]
2280f59ee6 Translate i18n-default.json in nl [Manual Sync] (#5297)
19% of minimum 3% translated source file: 'i18n-default.json'
on 'nl'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2024-12-22 18:00:44 -05:00
transifex-integration[bot]
0d4dd63edc Translate i18n-default.json in es [Manual Sync] (#5299)
99% of minimum 3% translated source file: 'i18n-default.json'
on 'es'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2024-12-22 18:00:39 -05:00
transifex-integration[bot]
69f1f4c1a5 Translate i18n-default.json in fi [Manual Sync] (#5301)
13% of minimum 3% translated source file: 'i18n-default.json'
on 'fi'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2024-12-22 18:00:35 -05:00
transifex-integration[bot]
d930d9c237 Updates for project Cockatrice and language tr (#5296)
* Translate oracle_en@source.ts in tr [Manual Sync]

36% of minimum 3% translated source file: 'oracle_en@source.ts'
on 'tr'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format

* Translate cockatrice_en@source.ts in tr [Manual Sync]

6% of minimum 3% translated source file: 'cockatrice_en@source.ts'
on 'tr'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format

---------

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2024-12-22 18:00:31 -05:00
transifex-integration[bot]
9c782d130f Translate i18n-default.json in pt_BR [Manual Sync] (#5298)
100% translated source file: 'i18n-default.json'
on 'pt_BR'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2024-12-22 18:00:13 -05:00
transifex-integration[bot]
f12053f39d Translate i18n-default.json in de [Manual Sync] (#5300)
100% translated source file: 'i18n-default.json'
on 'de'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2024-12-22 18:00:05 -05:00
transifex-integration[bot]
bcf6ca4f87 Translate i18n-default.json in fr [Manual Sync] (#5302)
99% of minimum 3% translated source file: 'i18n-default.json'
on 'fr'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2024-12-22 17:59:55 -05:00
transifex-integration[bot]
46619bb425 Translate i18n-default.json in ru [Manual Sync] (#5303)
14% of minimum 3% translated source file: 'i18n-default.json'
on 'ru'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2024-12-22 17:59:46 -05:00
transifex-integration[bot]
cdd870a129 Translate i18n-default.json in en_US [Manual Sync] (#5304)
100% translated source file: 'i18n-default.json'
on 'en_US'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2024-12-22 17:59:35 -05:00
transifex-integration[bot]
7a1b7b9438 Updates for project Cockatrice and language it (#5294)
* Translate cockatrice_en@source.ts in it [Manual Sync]

99% of minimum 3% translated source file: 'cockatrice_en@source.ts'
on 'it'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format

* Translate i18n-default.json in it [Manual Sync]

100% translated source file: 'i18n-default.json'
on 'it'.

---------

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2024-12-22 17:59:24 -05:00
transifex-integration[bot]
2183ada1f2 Translate oracle_en@source.ts in cs [Manual Sync] (#5293)
3% of minimum 3% translated source file: 'oracle_en@source.ts'
on 'cs'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2024-12-22 17:58:35 -05:00
Zach H
1d9e64ec73 Fix settings dialog tr (#5292) 2024-12-22 17:39:43 -05:00
Zach H
5339be318e Fix "ghosting" of cards sticking on invalid moves (#5289) 2024-12-22 17:35:44 -05:00
Zach H
e1ba39c437 Fix multiple "Selected Cards" in Menu on MacOS (#5288) 2024-12-22 04:33:09 +00:00
BruebachL
07ee271478 Refactor codebase to new Qt Slot/Signal syntax - Pt1 (#5202)
---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
Co-authored-by: ZeldaZach <zahalpern+github@gmail.com>
2024-12-22 02:01:17 +00:00
RickyRister
4823cce622 Show conflicting shortcut in error message (#5287) 2024-12-22 01:58:55 +00:00
Zach H
23099f7e8b Fix token name highlight on open (#5286) 2024-12-22 01:43:00 +00:00
RickyRister
5bdbd51fa8 implement search bar in shortcuts menu (#5285)
* implement search bar in shortcuts menu

* remove unneeded imports

* use expandAll
2024-12-22 00:21:53 +00:00
BruebachL
a0e5871c6e Fix the image shrinking due to repeated scaling and FP precision loss. (#5284)
* Fix the image shrinking due to repeated scaling and FP precision loss.

* Add a setting for auto-rotating sideways layout cards.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2024-12-21 22:47:43 +00:00
RickyRister
3cf0904651 add action to select all cards in table row (#5280) 2024-12-21 18:52:19 +00:00
Zach H
2bd06ff0fd Add CrashDump support for Windows OS (#5282) 2024-12-21 18:52:07 +00:00
RickyRister
6ea333d0f1 move SearchLineEdit into custom_line_edit file (#5281) 2024-12-21 05:12:14 +00:00
Zach H
91d2485940 Update PegLib, Fix Database Searching CFG (#5244)
* Support C++20 Standard

* Update peglib.h

* Fix lambdas

* Move from for loops to std::any/all_of

* Support fixed CFG

* Fix Rarity Search to be more accurate
2024-12-21 03:37:08 +00:00
RickyRister
0d99b2bcf4 make unattach shortcut always active (#5278) 2024-12-20 05:56:48 +00:00
RickyRister
a54a424f84 add action to select all cards in column (#5277)
* add action to select all cards in column

* change default shortcut to Ctrl+Shift+C
2024-12-20 03:39:17 +00:00
RickyRister
3514699f5b check that target card is in play before attaching (#5275) 2024-12-19 23:55:04 +00:00
RickyRister
d196988cab allow attached cards to be moved to other zones (#5276) 2024-12-19 23:53:48 +00:00
BruebachL
03aff83135 Add the ability to define starting life total during game creation. (#5174)
* Have the server respect gameType info when setting up zones.

* ServerPlayer::setupZones is now passed the room->getGameTypes();
* ServerPlayer::setupZones now checks if the GameType String includes "Commander" and then sets the life total to 40 instead.

* Formatting.

* Remove debug logging imports.

* Move game option value declarations to dlg_create_game.

* Lint.

* Fix mocks.

* Add a default for backwards compatibility.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2024-12-19 23:52:47 +00:00
RickyRister
17e6bfaca6 fix bug with multi-attach sometimes only attaching one card (#5272) 2024-12-19 13:38:57 +00:00
Zach H
90281262be Revert "Revert "Rotate split cards (#5264)" (#5269)" (#5273)
This reverts commit d41aa30e10.
2024-12-19 13:25:54 +00:00
RickyRister
5bbc118920 fix bug introduced in #5267 (#5270)
* fix bug introduced in #5267

* remove default args to prevent similar bugs in the future

* add newInstance overload with default properties
2024-12-19 13:17:09 +00:00
435 changed files with 79781 additions and 39496 deletions

17
.ci/macos.entitlements Normal file
View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<false/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<key>com.apple.security.cs.disable-executable-page-protection</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
</dict>
</plist>

View File

@@ -217,10 +217,18 @@ jobs:
core_count: 3
make_package: 1
- target: 14
- target: 15
soc: Apple
os: macos-14
xcode: "15.4"
os: macos-15
xcode: "16.2"
type: Release
core_count: 3
make_package: 1
- target: 15
soc: Apple
os: macos-15
xcode: "16.2"
type: Debug
core_count: 3
@@ -246,7 +254,7 @@ jobs:
brew update
brew install protobuf qt --force-bottle
- name: Build on Xcode ${{matrix.xcode}}
- name: Build & Sign on Xcode ${{matrix.xcode}}
shell: bash
id: build
env:
@@ -254,10 +262,69 @@ jobs:
MAKE_TEST: 1
MAKE_PACKAGE: '${{matrix.make_package}}'
PACKAGE_SUFFIX: '-macOS${{matrix.target}}_${{matrix.soc}}'
MACOS_CERTIFICATE: ${{ secrets.PROD_MACOS_CERTIFICATE }}
MACOS_CERTIFICATE_PWD: ${{ secrets.PROD_MACOS_CERTIFICATE_PWD }}
MACOS_CERTIFICATE_NAME: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }}
MACOS_CI_KEYCHAIN_PWD: ${{ secrets.PROD_MACOS_CI_KEYCHAIN_PWD }}
# macOS runner have 3 cores usually - only the macos-13 image has 4:
# https://docs.github.com/en/actions/using-github-hosted-runners/using-github-hosted-runners/about-github-hosted-runners#standard-github-hosted-runners-for-public-repositories
# https://github.com/actions/runner-images?tab=readme-ov-file#available-images
run: .ci/compile.sh --server --parallel ${{matrix.core_count}}
run: |
if [[ -n "$MACOS_CERTIFICATE_NAME" ]]
then
echo $MACOS_CERTIFICATE | base64 --decode > certificate.p12
security create-keychain -p "$MACOS_CI_KEYCHAIN_PWD" build.keychain
security default-keychain -s build.keychain
security set-keychain-settings -t 3600 -l build.keychain
security unlock-keychain -p "$MACOS_CI_KEYCHAIN_PWD" build.keychain
security import certificate.p12 -k build.keychain -P "$MACOS_CERTIFICATE_PWD" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$MACOS_CI_KEYCHAIN_PWD" build.keychain
fi
.ci/compile.sh --server --parallel ${{matrix.core_count}}
- name: Sign app bundle
if: matrix.make_package
env:
MACOS_CERTIFICATE_NAME: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }}
MACOS_CI_KEYCHAIN_PWD: ${{ secrets.PROD_MACOS_CI_KEYCHAIN_PWD }}
run: |
if [[ -n "$MACOS_CERTIFICATE_NAME" ]]
then
security unlock-keychain -p "$MACOS_CI_KEYCHAIN_PWD" build.keychain
/usr/bin/codesign --sign="$MACOS_CERTIFICATE_NAME" --entitlements=".ci/macos.entitlements" --options=runtime --force --deep --timestamp --verbose ${{steps.build.outputs.path}}
fi
- name: Notarize app bundle
if: matrix.make_package
env:
MACOS_NOTARIZATION_APPLE_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_APPLE_ID }}
MACOS_NOTARIZATION_TEAM_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_TEAM_ID }}
MACOS_NOTARIZATION_PWD: ${{ secrets.PROD_MACOS_NOTARIZATION_PWD }}
run: |
if [[ -n "$MACOS_NOTARIZATION_APPLE_ID" ]]
then
# Store the notarization credentials so that we can prevent a UI password dialog from blocking the CI
echo "Create keychain profile"
xcrun notarytool store-credentials "notarytool-profile" --apple-id "$MACOS_NOTARIZATION_APPLE_ID" --team-id "$MACOS_NOTARIZATION_TEAM_ID" --password "$MACOS_NOTARIZATION_PWD"
# We can't notarize an app bundle directly, but we need to compress it as an archive.
# Therefore, we create a zip file containing our app bundle, so that we can send it to the
# notarization service
echo "Creating temp notarization archive"
ditto -c -k --keepParent ${{steps.build.outputs.path}} "notarization.zip"
# Here we send the notarization request to the Apple's Notarization service, waiting for the result.
# This typically takes a few seconds inside a CI environment, but it might take more depending on the App
# characteristics. Visit the Notarization docs for more information and strategies on how to optimize it if
# you're curious
echo "Notarize app"
xcrun notarytool submit "notarization.zip" --keychain-profile "notarytool-profile" --wait
# Finally, we need to "attach the staple" to our executable, which will allow our app to be
# validated by macOS even when an internet connection is not available.
echo "Attach staple"
xcrun stapler staple ${{steps.build.outputs.path}}
fi
- name: Upload artifact
if: matrix.make_package
@@ -347,6 +414,15 @@ jobs:
path: ${{steps.build.outputs.path}}
if-no-files-found: error
- name: Upload pdb database
uses: actions/upload-artifact@v4
with:
name: Windows${{matrix.target}}-debug-pdbs
path: |
build/cockatrice/Release/*.pdb
build/servatrice/Release/*.pdb
if-no-files-found: error
- name: Upload to release
if: matrix.package != 'skip' && needs.configure.outputs.tag != null
shell: bash

3
.gitignore vendored
View File

@@ -6,10 +6,11 @@ mysql.cnf
.DS_Store
.idea/
*.aps
cmake-build-debug*
cmake-build*
preferences
compile_commands.json
.vs/
.vscode/
.cache
.gdb_history
cockatrice/resources/config/qtlogging.ini

View File

@@ -81,9 +81,9 @@ if(NOT DEFINED GIT_TAG_RELEASENAME)
set(GIT_TAG_RELEASENAME "Rings of the Wild")
endif()
# Use c++17 for all targets
# Use c++20 for all targets
set(CMAKE_CXX_STANDARD
17
20
CACHE STRING "C++ ISO Standard"
)
set(CMAKE_CXX_STANDARD_REQUIRED True)
@@ -140,12 +140,17 @@ endif()
# Define proper compilation flags
if(MSVC)
# Visual Studio: Disable Warning C4251, C++17 compatibility, Multi-threaded Builds, Warn Detection, Unwind Semantics
set(CMAKE_CXX_FLAGS "/wd4251 /Zc:__cplusplus /std:c++17 /permissive- /W4 /MP /EHsc")
# Disable Warning C4251, C++20 compatibility, Multi-threaded Builds, Warn Detection, Unwind Semantics, Debug Symbols
set(CMAKE_CXX_FLAGS "/wd4251 /Zc:__cplusplus /std:c++20 /permissive- /W4 /MP /EHsc /Zi")
# Visual Studio: Maximum Optimization, Multi-threaded DLL
set(CMAKE_CXX_FLAGS_RELEASE "/Ox /MD")
# Visual Studio: No Optimization, Multi-threaded Debug DLL, Debug Symbols
set(CMAKE_CXX_FLAGS_DEBUG "/Od /MDd /Zi")
# Visual Studio: No Optimization, Multi-threaded Debug DLL
set(CMAKE_CXX_FLAGS_DEBUG "/Od /MDd")
# Generate PDB, even when in release (So developers can better analyze crash logs)
set(CMAKE_EXE_LINKER_FLAGS_RELEASE "${CMAKE_EXE_LINKER_FLAGS_RELEASE} /DEBUG /OPT:REF /OPT:ICF")
add_compile_definitions(_SILENCE_STDEXT_ARR_ITERS_DEPRECATION_WARNING)
elseif(CMAKE_COMPILER_IS_GNUCXX)
# linux/gcc, bsd/gcc, windows/mingw
include(CheckCXXCompilerFlag)
@@ -158,7 +163,7 @@ elseif(CMAKE_COMPILER_IS_GNUCXX)
endif()
if(APPLE)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=gnu++17")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=gnu++20")
endif()
set(ADDITIONAL_DEBUG_FLAGS
@@ -266,6 +271,7 @@ if(UNIX)
set(CPACK_PACKAGE_ICON "${CMAKE_CURRENT_SOURCE_DIR}/cockatrice/resources/appicon.icns")
set(CPACK_DMG_DS_STORE_SETUP_SCRIPT "${CMAKE_CURRENT_SOURCE_DIR}/cmake/CMakeDMGSetup.script")
set(CPACK_DMG_BACKGROUND_IMAGE "${CMAKE_CURRENT_SOURCE_DIR}/cmake/dmgBackground.tif")
set(CPACK_PRE_BUILD_SCRIPTS "${CMAKE_CURRENT_SOURCE_DIR}/cmake/SignMacApplications.cmake")
else()
# linux
if(CPACK_GENERATOR STREQUAL "RPM")

View File

@@ -1,19 +1,20 @@
FROM ubuntu:bionic
MAINTAINER Zach Halpern <zahalpern+github@gmail.com>
FROM ubuntu:24.04
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y\
build-essential\
cmake\
git\
libprotobuf-dev\
libqt5sql5-mysql\
libmysqlclient-dev\
libqt5websockets5-dev\
protobuf-compiler\
qt5-default\
qtbase5-dev\
qttools5-dev-tools\
qttools5-dev
build-essential \
cmake \
file \
g++ \
git \
libmariadb-dev-compat \
libprotobuf-dev \
libqt6sql6-mysql \
qt6-websockets-dev \
protobuf-compiler \
qt6-tools-dev \
qt6-tools-dev-tools
COPY . /home/servatrice/code/
WORKDIR /home/servatrice/code
@@ -25,7 +26,6 @@ RUN cmake .. -DWITH_SERVER=1 -DWITH_CLIENT=0 -DWITH_ORACLE=0 -DWITH_DBCONVERTER=
WORKDIR /home/servatrice
EXPOSE 4747
EXPOSE 4747 4748
ENTRYPOINT [ "servatrice", "--log-to-console" ]

View File

@@ -23,6 +23,7 @@ if(WITH_CLIENT)
Svg
WebSockets
Widgets
Xml
)
endif()
if(WITH_ORACLE)

View File

@@ -5,7 +5,7 @@ OutFile "@CPACK_TOPLEVEL_DIRECTORY@/@CPACK_OUTPUT_FILE_NAME@"
!define INST_DIR "@CPACK_TEMPORARY_DIRECTORY@"
RequestExecutionlevel highest
RequestExecutionlevel admin
SetCompressor LZMA
Var NormalDestDir
@@ -235,6 +235,13 @@ ${If} $PortableMode = 0
WriteUninstaller "$INSTDIR\uninstall.exe"
${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2
IntFmt $0 "0x%08X" $0
; Enable Windows User-Mode Dumps
; https://learn.microsoft.com/en-us/windows/win32/wer/collecting-user-mode-dumps
WriteRegExpandStr HKLM "Software\Microsoft\Windows\Windows Error Reporting\LocalDumps\cockatrice.exe" "DumpFolder" "%LOCALAPPDATA%\CrashDumps\Cockatrice"
WriteRegDWORD HKLM "Software\Microsoft\Windows\Windows Error Reporting\LocalDumps\cockatrice.exe" "DumpCount" "5"
WriteRegDWORD HKLM "Software\Microsoft\Windows\Windows Error Reporting\LocalDumps\cockatrice.exe" "DumpType" "2"
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "DisplayIcon" "$INSTDIR\cockatrice.exe"
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "DisplayName" "Cockatrice"
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" "DisplayVersion" "@CPACK_PACKAGE_VERSION_MAJOR@.@CPACK_PACKAGE_VERSION_MINOR@.@CPACK_PACKAGE_VERSION_PATCH@"

View File

@@ -0,0 +1,27 @@
# This script re-signs all apps after CPack packages them. This is necessary because CPack modifies
# the library references used by Cockatrice to App relative paths, invalidating the code signature.
string(LENGTH "$ENV{MACOS_CERTIFICATE_NAME}" MACOS_CERTIFICATE_NAME_LEN)
if(APPLE AND MACOS_CERTIFICATE_NAME_LEN GREATER 0)
set(APPLICATIONS "cockatrice" "servatrice" "oracle" "dbconverter")
foreach(app_name IN LISTS APPLICATIONS)
set(FULL_APP_PATH "${CPACK_TEMPORARY_INSTALL_DIRECTORY}/${app_name}.app")
message(STATUS "Signing Interior Dynamically Loaded Libraries for ${app_name}.app")
execute_process(COMMAND "find" "${FULL_APP_PATH}" "-name" "*.dylib" OUTPUT_VARIABLE INTERIOR_DLLS)
string(REPLACE "\n" ";" INTERIOR_DLLS_LIST ${INTERIOR_DLLS})
foreach(INTERIOR_DLL IN LISTS INTERIOR_DLLS_LIST)
execute_process(
COMMAND "codesign" "--sign" "$ENV{MACOS_CERTIFICATE_NAME}" "--entitlements" "../.ci/macos.entitlements"
"--options" "runtime" "--force" "--deep" "--timestamp" "--verbose" "${INTERIOR_DLL}"
)
endforeach()
message(STATUS "Signing Exterior Applications ${app_name}.app")
execute_process(
COMMAND "codesign" "--sign" "$ENV{MACOS_CERTIFICATE_NAME}" "--entitlements" "../.ci/macos.entitlements"
"--options" "runtime" "--force" "--deep" "--timestamp" "--verbose" "${FULL_APP_PATH}"
)
endforeach()
endif()

View File

@@ -34,8 +34,8 @@ set(cockatrice_SOURCES
src/deck/deck_loader.cpp
src/deck/deck_list_model.cpp
src/deck/deck_stats_interface.cpp
src/deck/deck_view.cpp
src/dialogs/dlg_connect.cpp
src/dialogs/dlg_convert_deck_to_cod_format.cpp
src/dialogs/dlg_create_token.cpp
src/dialogs/dlg_create_game.cpp
src/dialogs/dlg_edit_avatar.cpp
@@ -56,13 +56,14 @@ set(cockatrice_SOURCES
src/dialogs/dlg_tip_of_the_day.cpp
src/dialogs/dlg_update.cpp
src/dialogs/dlg_view_log.cpp
src/dialogs/dlg_load_deck.cpp
src/game/deckview/deck_view.cpp
src/game/deckview/deck_view_container.cpp
src/game/filters/filter_string.cpp
src/game/filters/filter_builder.cpp
src/game/filters/filter_tree.cpp
src/game/filters/filter_tree_model.cpp
src/client/ui/layouts/flow_layout.cpp
src/client/ui/layouts/horizontal_flow_layout.cpp
src/client/ui/layouts/vertical_flow_layout.cpp
src/client/ui/widgets/general/layout_containers/flow_widget.cpp
src/game/game_scene.cpp
src/game/game_selector.cpp
@@ -80,6 +81,7 @@ set(cockatrice_SOURCES
src/utility/logger.cpp
src/client/ui/widgets/cards/card_info_picture_enlarged_widget.cpp
src/client/ui/widgets/cards/card_info_picture_with_text_overlay_widget.cpp
src/client/ui/widgets/general/display/banner_widget.cpp
src/client/ui/widgets/general/display/labeled_input.cpp
src/client/ui/widgets/general/display/dynamic_font_size_label.cpp
src/client/ui/widgets/general/display/dynamic_font_size_push_button.cpp
@@ -92,7 +94,9 @@ set(cockatrice_SOURCES
src/server/pending_command.cpp
src/game/phase.cpp
src/client/ui/phases_toolbar.cpp
src/client/ui/picture_loader.cpp
src/client/ui/picture_loader/picture_loader.cpp
src/client/ui/picture_loader/picture_loader_worker.cpp
src/client/ui/picture_loader/picture_to_load.cpp
src/game/zones/pile_zone.cpp
src/client/ui/pixel_map_generator.cpp
src/game/player/player.cpp
@@ -110,6 +114,7 @@ set(cockatrice_SOURCES
src/client/ui/widgets/printing_selector/printing_selector_view_options_widget.cpp
src/client/ui/widgets/printing_selector/set_name_and_collectors_number_display_widget.cpp
src/client/network/release_channel.cpp
src/client/network/client_update_checker.cpp
src/server/remote/remote_client.cpp
src/server/remote/remote_decklist_tree_widget.cpp
src/server/remote/remote_replay_list_tree_widget.cpp
@@ -122,10 +127,14 @@ set(cockatrice_SOURCES
src/settings/game_filters_settings.cpp
src/settings/layouts_settings.cpp
src/settings/message_settings.cpp
src/settings/recents_settings.cpp
src/settings/servers_settings.cpp
src/settings/settings_manager.cpp
src/settings/cache_settings.cpp
src/settings/shortcuts_settings.cpp
src/settings/shortcut_treeview.cpp
src/settings/card_override_settings.cpp
src/settings/debug_settings.cpp
src/client/sound_engine.cpp
src/client/network/spoiler_background_updater.cpp
src/game/zones/stack_zone.cpp
@@ -141,6 +150,19 @@ set(cockatrice_SOURCES
src/client/tabs/tab_room.cpp
src/client/tabs/tab_server.cpp
src/client/tabs/tab_supervisor.cpp
src/client/tabs/api/edhrec/tab_edhrec.cpp
src/client/tabs/api/edhrec/edhrec_commander_api_response_display_widget.cpp
src/client/tabs/api/edhrec/edhrec_commander_api_response_card_details_display_widget.cpp
src/client/tabs/api/edhrec/edhrec_commander_api_response_card_list_display_widget.cpp
src/client/tabs/api/edhrec/edhrec_commander_api_response_commander_details_display_widget.cpp
src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response_archidekt_links.cpp
src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response_average_deck_statistics.cpp
src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response_card_details.cpp
src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response_card_list.cpp
src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response_card_container.cpp
src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response_card_prices.cpp
src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response_commander_details.cpp
src/client/tabs/api/edhrec/api_response/edhrec_commander_api_response.cpp
src/game/zones/table_zone.cpp
src/client/tapped_out_interface.cpp
src/client/ui/theme_manager.cpp
@@ -150,15 +172,34 @@ set(cockatrice_SOURCES
src/server/user/user_context_menu.cpp
src/server/user/user_info_connection.cpp
src/server/user/user_info_box.cpp
src/server/user/user_list.cpp
src/server/user/user_list_manager.cpp
src/server/user/user_list_widget.cpp
src/client/ui/window_main.cpp
src/game/zones/view_zone_widget.cpp
src/game/zones/view_zone.cpp
src/client/tabs/visual_deck_storage/tab_deck_storage_visual.cpp
src/client/ui/widgets/cards/deck_preview_card_picture_widget.cpp
src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_color_identity_widget.cpp
src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_color_identity_filter_widget.cpp
src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_tag_addition_widget.cpp
src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_tag_display_widget.cpp
src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_tag_dialog.cpp
src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_tag_item_widget.cpp
src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_deck_tags_display_widget.cpp
src/client/ui/widgets/visual_deck_storage/deck_preview/deck_preview_widget.cpp
src/client/ui/widgets/visual_deck_storage/visual_deck_storage_widget.cpp
src/client/ui/widgets/visual_deck_storage/visual_deck_storage_folder_display_widget.cpp
src/client/ui/widgets/visual_deck_storage/visual_deck_storage_search_widget.cpp
src/client/ui/widgets/visual_deck_storage/visual_deck_storage_sort_widget.cpp
src/client/ui/widgets/visual_deck_storage/visual_deck_storage_tag_filter_widget.cpp
${VERSION_STRING_CPP}
)
add_subdirectory(sounds)
add_subdirectory(themes)
configure_file(
${CMAKE_SOURCE_DIR}/cockatrice/resources/config/qtlogging.ini ${CMAKE_BINARY_DIR}/cockatrice/qtlogging.ini COPYONLY
)
set(cockatrice_RESOURCES cockatrice.qrc)
@@ -279,7 +320,7 @@ if(APPLE)
set(plugin_dest_dir cockatrice.app/Contents/Plugins)
set(qtconf_dest_dir cockatrice.app/Contents/Resources)
# Qt plugins: audio (Qt5), iconengines, imageformats, platforms, printsupport (Qt5), styles, tls (Qt6)
# Qt plugins: audio (Qt5), iconengines, imageformats, multimedia (Qt6), platforms, printsupport (Qt5), styles, tls (Qt6)
install(
DIRECTORY "${QT_PLUGINS_DIR}/"
DESTINATION ${plugin_dest_dir}
@@ -290,6 +331,7 @@ if(APPLE)
PATTERN "audio/*.dylib"
PATTERN "iconengines/*.dylib"
PATTERN "imageformats/*.dylib"
PATTERN "multimedia/*.dylib"
PATTERN "platforms/*.dylib"
PATTERN "printsupport/*.dylib"
PATTERN "styles/*.dylib"
@@ -330,7 +372,7 @@ if(WIN32)
PATTERN "*.dll"
)
# Qt plugins: audio (Qt5), iconengines, imageformats, platforms, printsupport (Qt5), styles, tls (Qt6)
# Qt plugins: audio (Qt5), iconengines, imageformats, multimedia (Qt6) platforms, printsupport (Qt5), styles, tls (Qt6)
install(
DIRECTORY "${QT_PLUGINS_DIR}/"
DESTINATION ${plugin_dest_dir}
@@ -342,6 +384,7 @@ if(WIN32)
PATTERN "imageformats/*.dll"
PATTERN "mediaservice/dsengine.dll"
PATTERN "mediaservice/wmfengine.dll"
PATTERN "multimedia/*.dll"
PATTERN "platforms/qdirect2d.dll"
PATTERN "platforms/qminimal.dll"
PATTERN "platforms/qoffscreen.dll"

View File

@@ -29,6 +29,7 @@
<file>resources/icons/search.svg</file>
<file>resources/icons/settings.svg</file>
<file>resources/icons/spectator.svg</file>
<file>resources/icons/swap.svg</file>
<file>resources/icons/sync.svg</file>
<file>resources/icons/tab_changed.svg</file>
<file>resources/icons/update.png</file>
@@ -41,6 +42,8 @@
<file>resources/config/deckeditor.svg</file>
<file>resources/config/shorcuts.svg</file>
<file>resources/config/sound.svg</file>
<file>resources/config/debug.ini</file>
<file>resources/config/qtlogging.ini</file>
<file>resources/counters/w.svg</file>
<file>resources/counters/w_highlight.svg</file>
@@ -326,6 +329,13 @@
<file>resources/replay/fastforward.svg</file>
<file>resources/replay/pause.svg</file>
<file>resources/usericons/pawn_single.svg</file>
<file>resources/usericons/pawn_double.svg</file>
<file>resources/usericons/pawn_vip_single.svg</file>
<file>resources/usericons/pawn_vip_double.svg</file>
<file>resources/usericons/star_single.svg</file>
<file>resources/usericons/star_double.svg</file>
<file>resources/userlevels/normal.svg</file>
<file>resources/userlevels/registered.svg</file>
<file>resources/userlevels/registered_buddy.svg</file>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,11 @@
[debug]
showCardId=false
[localgame]
onStartup=false
playerCount=1
;deck\Player 1=path/to/deck
;deck\Player 2=path/to/deck
; Fun Fact: You can assign a deck to your username and it will auto load and ready when you join a server game
;deck\Your Username Here=path/to/deck

View File

@@ -0,0 +1,49 @@
[Rules]
# Uncomment a rule to disable logging for that category
# main = false
# qt_translator = false
# window_main.* = false
# release_channel = false
# spoiler_background_updater = false
# theme_manager = false
# sound_engine = false
# tapped_out_interface = false
# tab_game = false
# tab_message = false
# tab_supervisor = false
# dlg_edit_avatar = false
# dlg_settings = false
# dlg_tip_of_the_day = false
# dlg_update = false
# settings_cache = false
# servers_settings = false
# shortcuts_settings = false
# player = false
# game_scene = false
# game_scene.player_addition_removal = false
# card_zone = false
# view_zone = false
# user_info_connection = false
# picture_loader = false
# picture_loader.worker = false
# picture_loader.card_back_cache_fail = false
# picture_loader.picture_to_load = false
# deck_loader = false
# card_database = false
# card_database.loading = false
# card_database.loading.success_or_failure = false
# cockatrice_xml.* = false
# cockatrice_xml.xml_3_parser = false
# cockatrice_xml.xml_4_parser = false
# card_list = false
# pixel_map_generator = false
# filter_string = false

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000" version="1.1" id="Capa_1"
xmlns="http://www.w3.org/2000/svg"
width="800px" height="800px" viewBox="0 0 71.753 71.753"
xml:space="preserve">
<g>
<path d="M39.798,20.736H28.172v20.738L11.625,41.47V20.736H0L19.899,0.839L39.798,20.736z M51.855,70.914l19.897-19.896H60.129
V30.282l-16.547-0.004v20.74H31.957L51.855,70.914z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 648 B

View File

@@ -0,0 +1,268 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="100"
height="100"
id="svg5322"
version="1.1"
inkscape:version="1.4 (86a8ad7, 2024-10-11)"
sodipodi:docname="pawn_double.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs3">
<inkscape:perspective
sodipodi:type="inkscape:persp3d"
inkscape:vp_x="0 : 526.18109 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_z="744.09448 : 526.18109 : 1"
inkscape:persp3d-origin="372.04724 : 350.78739 : 1"
id="perspective5328" />
<inkscape:perspective
id="perspective5305"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<linearGradient
id="linearGradient5181">
<stop
style="stop-color:#0fbb00;stop-opacity:1;"
offset="0"
id="stop5183" />
<stop
style="stop-color:#064400;stop-opacity:1;"
offset="1"
id="stop5185" />
</linearGradient>
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3600-2"
id="radialGradient3606-7"
cx="324.32715"
cy="131.40274"
fx="324.32715"
fy="131.40274"
r="25.501276"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)" />
<linearGradient
id="linearGradient3600-2">
<stop
style="stop-color:#ffc33d;stop-opacity:1;"
offset="0"
id="stop3602-4" />
<stop
style="stop-color:#e09900;stop-opacity:1;"
offset="1"
id="stop3604-9" />
</linearGradient>
<inkscape:perspective
id="perspective5478"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<linearGradient
id="linearGradient5189">
<stop
style="stop-color:#000ec9;stop-opacity:1;"
offset="0"
id="stop5191" />
<stop
style="stop-color:#000657;stop-opacity:1;"
offset="1"
id="stop5193" />
</linearGradient>
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3600-4"
id="radialGradient3606-1"
cx="324.32715"
cy="131.40274"
fx="324.32715"
fy="131.40274"
r="25.501276"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)" />
<linearGradient
id="linearGradient3600-4">
<stop
style="stop-color:#ffc33d;stop-opacity:1;"
offset="0"
id="stop3602-3" />
<stop
style="stop-color:#e09900;stop-opacity:1;"
offset="1"
id="stop3604-5" />
</linearGradient>
<inkscape:perspective
id="perspective5559"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient5173"
id="linearGradient5179"
x1="167.33386"
y1="178.83276"
x2="244.78181"
y2="178.83276"
gradientUnits="userSpaceOnUse" />
<linearGradient
id="linearGradient5173">
<stop
style="stop-color:#f50000;stop-opacity:1;"
offset="0"
id="stop5175" />
<stop
style="stop-color:#950000;stop-opacity:1;"
offset="1"
id="stop5177" />
</linearGradient>
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3600"
id="radialGradient5169"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)"
cx="324.32715"
cy="131.40274"
fx="324.32715"
fy="131.40274"
r="25.501276" />
<linearGradient
id="linearGradient3600">
<stop
style="stop-color:#ffc13d;stop-opacity:1;"
offset="0"
id="stop3602" />
<stop
style="stop-color:#e09900;stop-opacity:1;"
offset="1"
id="stop3604" />
</linearGradient>
<radialGradient
r="25.501276"
fy="131.40274"
fx="324.32715"
cy="131.40274"
cx="324.32715"
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)"
gradientUnits="userSpaceOnUse"
id="radialGradient5574"
xlink:href="#linearGradient3600"
inkscape:collect="always" />
<inkscape:perspective
id="perspective5663"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3600-7"
id="radialGradient3606-8"
cx="324.32715"
cy="131.40274"
fx="324.32715"
fy="131.40274"
r="25.501276"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)" />
<linearGradient
id="linearGradient3600-7">
<stop
style="stop-color:#ffc13d;stop-opacity:1;"
offset="0"
id="stop3602-7" />
<stop
style="stop-color:#e09900;stop-opacity:1;"
offset="1"
id="stop3604-6" />
</linearGradient>
<radialGradient
r="25.501276"
fy="131.40274"
fx="324.32715"
cy="131.40274"
cx="324.32715"
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)"
gradientUnits="userSpaceOnUse"
id="radialGradient5676"
xlink:href="#linearGradient3600-7"
inkscape:collect="always" />
</defs>
<sodipodi:namedview
inkscape:document-units="mm"
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="5.6"
inkscape:cx="63.214286"
inkscape:cy="46.160714"
inkscape:current-layer="g5249"
showgrid="false"
inkscape:window-width="1147"
inkscape:window-height="1211"
inkscape:window-x="2842"
inkscape:window-y="58"
inkscape:window-maximized="0"
inkscape:showpageshadow="2"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1" />
<metadata
id="metadata4">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-952.36218)">
<g
id="g5249"
transform="translate(0.53874115,0.90502985)">
<path
style="stroke:#000000;stroke-width:4.45809746000000030;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;fill-opacity:1"
d="m 49.582319,954.34642 c -12.850034,0 -23.284789,10.43476 -23.284789,23.28479 0,7.32135 3.403263,13.81811 8.689724,18.08319 -10.278401,5.8502 -16.663073,17.8469 -20.19443,31.0259 -5.1178053,19.1 15.207096,22.0401 34.269334,22.0915 l 0,0.031 c 0.290839,0 0.566498,0.031 0.856734,0.031 19.210152,0 39.855802,-3.1837 34.789494,-22.0914 -3.636192,-13.5705 -10.027831,-25.4711 -20.378015,-31.17899 5.208701,-4.26694 8.506139,-10.73278 8.506139,-17.9914 0,-12.85003 -10.404159,-23.28479 -23.254191,-23.28479 z"
id="left"
inkscape:connector-curvature="0" />
<path
fill="none"
style="stroke:#000000;stroke-width:1.97203517px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 50.522358,952.70715 0,95.71425"
id="center"
inkscape:connector-curvature="0" />
<path
style="opacity:1;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.71966;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 51.054254,1001.4773 v -45.77683 l 1.097241,0.005 c 3.642211,0.0172 9.170661,2.46935 12.395732,5.49816 4.897489,4.59945 7.421654,10.97001 6.981907,17.62114 -0.389167,5.88609 -2.631878,10.66609 -6.951818,14.81672 l -2.05562,1.97506 2.959813,2.0746 c 3.467097,2.43015 7.403677,6.55065 9.666109,10.11765 3.325898,5.2437 6.79289,13.8355 8.153827,20.2065 2.584451,12.0989 -5.997953,18.2384 -26.592174,19.0232 l -5.655017,0.2154 v -45.7768 z"
id="right"
inkscape:connector-curvature="0" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

@@ -0,0 +1,253 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="100"
height="100"
id="svg5322"
version="1.1"
inkscape:version="1.4 (86a8ad7, 2024-10-11)"
sodipodi:docname="pawn_single.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs3">
<inkscape:perspective
sodipodi:type="inkscape:persp3d"
inkscape:vp_x="0 : 526.18109 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_z="744.09448 : 526.18109 : 1"
inkscape:persp3d-origin="372.04724 : 350.78739 : 1"
id="perspective5328" />
<inkscape:perspective
id="perspective5305"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<linearGradient
id="linearGradient5181">
<stop
style="stop-color:#0fbb00;stop-opacity:1;"
offset="0"
id="stop5183" />
<stop
style="stop-color:#064400;stop-opacity:1;"
offset="1"
id="stop5185" />
</linearGradient>
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3600-2"
id="radialGradient3606-7"
cx="324.32715"
cy="131.40274"
fx="324.32715"
fy="131.40274"
r="25.501276"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)" />
<linearGradient
id="linearGradient3600-2">
<stop
style="stop-color:#ffc33d;stop-opacity:1;"
offset="0"
id="stop3602-4" />
<stop
style="stop-color:#e09900;stop-opacity:1;"
offset="1"
id="stop3604-9" />
</linearGradient>
<inkscape:perspective
id="perspective5478"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<linearGradient
id="linearGradient5189">
<stop
style="stop-color:#000ec9;stop-opacity:1;"
offset="0"
id="stop5191" />
<stop
style="stop-color:#000657;stop-opacity:1;"
offset="1"
id="stop5193" />
</linearGradient>
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3600-4"
id="radialGradient3606-1"
cx="324.32715"
cy="131.40274"
fx="324.32715"
fy="131.40274"
r="25.501276"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)" />
<linearGradient
id="linearGradient3600-4">
<stop
style="stop-color:#ffc33d;stop-opacity:1;"
offset="0"
id="stop3602-3" />
<stop
style="stop-color:#e09900;stop-opacity:1;"
offset="1"
id="stop3604-5" />
</linearGradient>
<inkscape:perspective
id="perspective5559"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient5173"
id="linearGradient5179"
x1="167.33386"
y1="178.83276"
x2="244.78181"
y2="178.83276"
gradientUnits="userSpaceOnUse" />
<linearGradient
id="linearGradient5173">
<stop
style="stop-color:#f50000;stop-opacity:1;"
offset="0"
id="stop5175" />
<stop
style="stop-color:#950000;stop-opacity:1;"
offset="1"
id="stop5177" />
</linearGradient>
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3600"
id="radialGradient5169"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)"
cx="324.32715"
cy="131.40274"
fx="324.32715"
fy="131.40274"
r="25.501276" />
<linearGradient
id="linearGradient3600">
<stop
style="stop-color:#ffc13d;stop-opacity:1;"
offset="0"
id="stop3602" />
<stop
style="stop-color:#e09900;stop-opacity:1;"
offset="1"
id="stop3604" />
</linearGradient>
<radialGradient
r="25.501276"
fy="131.40274"
fx="324.32715"
cy="131.40274"
cx="324.32715"
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)"
gradientUnits="userSpaceOnUse"
id="radialGradient5574"
xlink:href="#linearGradient3600"
inkscape:collect="always" />
<inkscape:perspective
id="perspective5663"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3600-7"
id="radialGradient3606-8"
cx="324.32715"
cy="131.40274"
fx="324.32715"
fy="131.40274"
r="25.501276"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)" />
<linearGradient
id="linearGradient3600-7">
<stop
style="stop-color:#ffc13d;stop-opacity:1;"
offset="0"
id="stop3602-7" />
<stop
style="stop-color:#e09900;stop-opacity:1;"
offset="1"
id="stop3604-6" />
</linearGradient>
<radialGradient
r="25.501276"
fy="131.40274"
fx="324.32715"
cy="131.40274"
cx="324.32715"
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)"
gradientUnits="userSpaceOnUse"
id="radialGradient5676"
xlink:href="#linearGradient3600-7"
inkscape:collect="always" />
</defs>
<sodipodi:namedview
inkscape:document-units="mm"
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="5.6"
inkscape:cx="63.214286"
inkscape:cy="46.160714"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1147"
inkscape:window-height="1211"
inkscape:window-x="3185"
inkscape:window-y="44"
inkscape:window-maximized="0"
inkscape:showpageshadow="2"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1" />
<metadata
id="metadata4">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-952.36218)">
<path
style="fill-opacity:1;stroke:black;stroke-width:2.78220296;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;opacity:1"
d="M 49.84375 1.71875 C 36.719738 1.71875 26.0625 12.375988 26.0625 25.5 C 26.0625 32.977454 29.538325 39.612734 34.9375 43.96875 C 24.439951 49.943698 17.919149 62.196126 14.3125 75.65625 C 9.0380874 95.34065 30.224013 98.21875 49.84375 98.21875 C 69.463486 98.21875 90.549327 94.96715 85.375 75.65625 C 81.693381 61.916246 75.224585 49.827177 64.8125 43.9375 C 70.181573 39.580662 73.59375 32.953205 73.59375 25.5 C 73.59375 12.375988 62.967762 1.71875 49.84375 1.71875 z "
id="left"
transform="translate(0,952.36218)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

@@ -0,0 +1,368 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="100"
height="100"
id="svg5322"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="moderator_vip.svg">
<defs
id="defs3">
<inkscape:perspective
sodipodi:type="inkscape:persp3d"
inkscape:vp_x="0 : 526.18109 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_z="744.09448 : 526.18109 : 1"
inkscape:persp3d-origin="372.04724 : 350.78739 : 1"
id="perspective5328" />
<inkscape:perspective
id="perspective5305"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<linearGradient
id="linearGradient5181">
<stop
style="stop-color:#0fbb00;stop-opacity:1;"
offset="0"
id="stop5183" />
<stop
style="stop-color:#064400;stop-opacity:1;"
offset="1"
id="stop5185" />
</linearGradient>
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3600-2"
id="radialGradient3606-7"
cx="324.32715"
cy="131.40274"
fx="324.32715"
fy="131.40274"
r="25.501276"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)" />
<linearGradient
id="linearGradient3600-2">
<stop
style="stop-color:#ffc33d;stop-opacity:1;"
offset="0"
id="stop3602-4" />
<stop
style="stop-color:#e09900;stop-opacity:1;"
offset="1"
id="stop3604-9" />
</linearGradient>
<inkscape:perspective
id="perspective5478"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<linearGradient
id="linearGradient5189">
<stop
style="stop-color:#000ec9;stop-opacity:1;"
offset="0"
id="stop5191" />
<stop
style="stop-color:#000657;stop-opacity:1;"
offset="1"
id="stop5193" />
</linearGradient>
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3600-4"
id="radialGradient3606-1"
cx="324.32715"
cy="131.40274"
fx="324.32715"
fy="131.40274"
r="25.501276"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)" />
<linearGradient
id="linearGradient3600-4">
<stop
style="stop-color:#ffc33d;stop-opacity:1;"
offset="0"
id="stop3602-3" />
<stop
style="stop-color:#e09900;stop-opacity:1;"
offset="1"
id="stop3604-5" />
</linearGradient>
<inkscape:perspective
id="perspective5559"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient5173"
id="linearGradient5179"
x1="167.33386"
y1="178.83276"
x2="244.78181"
y2="178.83276"
gradientUnits="userSpaceOnUse" />
<linearGradient
id="linearGradient5173">
<stop
style="stop-color:#f50000;stop-opacity:1;"
offset="0"
id="stop5175" />
<stop
style="stop-color:#950000;stop-opacity:1;"
offset="1"
id="stop5177" />
</linearGradient>
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3600"
id="radialGradient5169"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)"
cx="324.32715"
cy="131.40274"
fx="324.32715"
fy="131.40274"
r="25.501276" />
<linearGradient
id="linearGradient3600">
<stop
style="stop-color:#ffc13d;stop-opacity:1;"
offset="0"
id="stop3602" />
<stop
style="stop-color:#e09900;stop-opacity:1;"
offset="1"
id="stop3604" />
</linearGradient>
<radialGradient
r="25.501276"
fy="131.40274"
fx="324.32715"
cy="131.40274"
cx="324.32715"
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)"
gradientUnits="userSpaceOnUse"
id="radialGradient5574"
xlink:href="#linearGradient3600"
inkscape:collect="always" />
<inkscape:perspective
id="perspective5663"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3600-7"
id="radialGradient3606-8"
cx="324.32715"
cy="131.40274"
fx="324.32715"
fy="131.40274"
r="25.501276"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)" />
<linearGradient
id="linearGradient3600-7">
<stop
style="stop-color:#ffc13d;stop-opacity:1;"
offset="0"
id="stop3602-7" />
<stop
style="stop-color:#e09900;stop-opacity:1;"
offset="1"
id="stop3604-6" />
</linearGradient>
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3600-7"
id="radialGradient5254"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)"
cx="324.32715"
cy="131.40274"
fx="324.32715"
fy="131.40274"
r="25.501276" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient5189-1"
id="linearGradient5394"
gradientUnits="userSpaceOnUse"
x1="385.03503"
y1="180.09546"
x2="462.48297"
y2="180.09546"
gradientTransform="matrix(0.96839241,0,0,0.96839241,-360.365,847.52359)" />
<linearGradient
id="linearGradient5189-1">
<stop
style="stop-color:#000ec9;stop-opacity:1;"
offset="0"
id="stop5191-0" />
<stop
style="stop-color:#000657;stop-opacity:1;"
offset="1"
id="stop5193-4" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient5173-1"
id="linearGradient5581"
gradientUnits="userSpaceOnUse"
x1="167.33386"
y1="178.83276"
x2="244.78181"
y2="178.83276"
gradientTransform="matrix(0.96839241,0,0,0.96839241,-149.54484,848.74636)" />
<linearGradient
id="linearGradient5173-1">
<stop
style="stop-color:#f50000;stop-opacity:1;"
offset="0"
id="stop5175-5" />
<stop
style="stop-color:#950000;stop-opacity:1;"
offset="1"
id="stop5177-3" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient5181-9"
id="linearGradient5782"
gradientUnits="userSpaceOnUse"
x1="282.50455"
y1="181.61069"
x2="359.95248"
y2="181.61069"
gradientTransform="matrix(0.96839241,0,0,0.96839241,-261.07526,846.05625)" />
<linearGradient
id="linearGradient5181-9">
<stop
style="stop-color:#80d600;stop-opacity:1;"
offset="0"
id="stop5183-3" />
<stop
style="stop-color:#80d600;stop-opacity:1;"
offset="1"
id="stop5185-0" />
</linearGradient>
<linearGradient
y2="181.61069"
x2="359.95248"
y1="181.61069"
x1="282.50455"
gradientTransform="matrix(0.96839241,0,0,0.96839241,-175.71812,893.2775)"
gradientUnits="userSpaceOnUse"
id="linearGradient5799"
xlink:href="#linearGradient5181-9"
inkscape:collect="always" />
<linearGradient
inkscape:collect="always"
id="linearGradient3419">
<stop
style="stop-color:#ffec79;stop-opacity:1"
offset="0"
id="stop3421" />
<stop
style="stop-color:#f2c15b;stop-opacity:1"
offset="1"
id="stop3423" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3419"
id="linearGradient3425-5"
x1="-126.90256"
y1="941.52618"
x2="-125.73831"
y2="984.13751"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(154.2532,0.90556908)" />
</defs>
<sodipodi:namedview
inkscape:document-units="mm"
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="4.2946338"
inkscape:cx="34.155294"
inkscape:cy="65.175571"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1440"
inkscape:window-height="792"
inkscape:window-x="11"
inkscape:window-y="0"
inkscape:window-maximized="0" />
<metadata
id="metadata4">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-952.36218)">
<path
style="fill-opacity:1;stroke:black;stroke-width:2.78220296;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
d="M 49.84375 1.71875 C 36.719738 1.71875 26.0625 12.375988 26.0625 25.5 C 26.0625 32.977454 29.538325 39.612734 34.9375 43.96875 C 24.439951 49.943698 17.919149 62.196126 14.3125 75.65625 C 9.0380874 95.34065 30.224013 98.21875 49.84375 98.21875 C 69.463486 98.21875 90.549327 94.96715 85.375 75.65625 C 81.693381 61.916246 75.224585 49.827177 64.8125 43.9375 C 70.181573 39.580662 73.59375 32.953205 73.59375 25.5 C 73.59375 12.375988 62.967762 1.71875 49.84375 1.71875 z "
transform="translate(0,952.36218)"
id="right" />
<path
style="opacity:1;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.73577702;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 51.28696,1001.834 0,-46.98372 1.434151,0.16768 c 5.155008,0.60274 9.462857,2.72154 12.938257,6.36366 4.74393,4.9715 6.87913,11.35611 6.16464,18.43328 -0.53702,5.31935 -3.09008,10.59498 -6.83833,14.13074 l -1.94072,1.83069 3.04083,2.20427 c 3.58084,2.5957 7.18975,6.4912 9.55296,10.3116 4.89572,7.9144 9.23593,21.4918 8.50487,26.6055 -0.81312,5.6877 -5.43872,9.6977 -13.62216,11.8093 -3.80822,0.9826 -7.68056,1.4713 -14.763321,1.8633 l -4.471177,0.2474 0,-46.9837 z"
id="left"
inkscape:connector-curvature="0" />
<path
sodipodi:type="star"
style="fill:url(#linearGradient3425-5);fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
id="star"
sodipodi:sides="5"
sodipodi:cx="27.80283"
sodipodi:cy="970.9433"
sodipodi:r1="17.189852"
sodipodi:r2="8.5949249"
sodipodi:arg1="0.9349579"
sodipodi:arg2="1.5632764"
inkscape:flatsided="false"
inkscape:rounded="0"
inkscape:randomized="0"
d="m 38.011063,984.77381 -10.143601,-5.23583 -10.063711,5.38779 1.845023,-11.2651 -8.233948,-7.90624 11.283888,-1.72639 4.974851,-10.27411 5.128803,10.19813 11.308575,1.55649 -8.114112,8.02918 z"
inkscape:transform-center-x="0.094945927"
inkscape:transform-center-y="-3.9764964"
transform="matrix(2.3768784,0,0,2.4799382,-15.920285,-1400.1716)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,363 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="100"
height="100"
id="svg5322"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="admin_vip.svg">
<defs
id="defs3">
<linearGradient
inkscape:collect="always"
id="linearGradient3419">
<stop
style="stop-color:#ffec79;stop-opacity:1"
offset="0"
id="stop3421" />
<stop
style="stop-color:#f2c15b;stop-opacity:1"
offset="1"
id="stop3423" />
</linearGradient>
<inkscape:perspective
sodipodi:type="inkscape:persp3d"
inkscape:vp_x="0 : 526.18109 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_z="744.09448 : 526.18109 : 1"
inkscape:persp3d-origin="372.04724 : 350.78739 : 1"
id="perspective5328" />
<inkscape:perspective
id="perspective5305"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<linearGradient
id="linearGradient5181">
<stop
style="stop-color:#0fbb00;stop-opacity:1;"
offset="0"
id="stop5183" />
<stop
style="stop-color:#064400;stop-opacity:1;"
offset="1"
id="stop5185" />
</linearGradient>
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3600-2"
id="radialGradient3606-7"
cx="324.32715"
cy="131.40274"
fx="324.32715"
fy="131.40274"
r="25.501276"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)" />
<linearGradient
id="linearGradient3600-2">
<stop
style="stop-color:#ffc33d;stop-opacity:1;"
offset="0"
id="stop3602-4" />
<stop
style="stop-color:#e09900;stop-opacity:1;"
offset="1"
id="stop3604-9" />
</linearGradient>
<inkscape:perspective
id="perspective5478"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<linearGradient
id="linearGradient5189">
<stop
style="stop-color:#000ec9;stop-opacity:1;"
offset="0"
id="stop5191" />
<stop
style="stop-color:#000657;stop-opacity:1;"
offset="1"
id="stop5193" />
</linearGradient>
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3600-4"
id="radialGradient3606-1"
cx="324.32715"
cy="131.40274"
fx="324.32715"
fy="131.40274"
r="25.501276"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)" />
<linearGradient
id="linearGradient3600-4">
<stop
style="stop-color:#ffc33d;stop-opacity:1;"
offset="0"
id="stop3602-3" />
<stop
style="stop-color:#e09900;stop-opacity:1;"
offset="1"
id="stop3604-5" />
</linearGradient>
<inkscape:perspective
id="perspective5559"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient5173"
id="linearGradient5179"
x1="167.33386"
y1="178.83276"
x2="244.78181"
y2="178.83276"
gradientUnits="userSpaceOnUse" />
<linearGradient
id="linearGradient5173">
<stop
style="stop-color:#f50000;stop-opacity:1;"
offset="0"
id="stop5175" />
<stop
style="stop-color:#950000;stop-opacity:1;"
offset="1"
id="stop5177" />
</linearGradient>
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3600"
id="radialGradient5169"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)"
cx="324.32715"
cy="131.40274"
fx="324.32715"
fy="131.40274"
r="25.501276" />
<linearGradient
id="linearGradient3600">
<stop
style="stop-color:#ffc13d;stop-opacity:1;"
offset="0"
id="stop3602" />
<stop
style="stop-color:#e09900;stop-opacity:1;"
offset="1"
id="stop3604" />
</linearGradient>
<radialGradient
r="25.501276"
fy="131.40274"
fx="324.32715"
cy="131.40274"
cx="324.32715"
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)"
gradientUnits="userSpaceOnUse"
id="radialGradient5574"
xlink:href="#linearGradient3600"
inkscape:collect="always" />
<inkscape:perspective
id="perspective5663"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3600-7"
id="radialGradient3606-8"
cx="324.32715"
cy="131.40274"
fx="324.32715"
fy="131.40274"
r="25.501276"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)" />
<linearGradient
id="linearGradient3600-7">
<stop
style="stop-color:#ffc13d;stop-opacity:1;"
offset="0"
id="stop3602-7" />
<stop
style="stop-color:#e09900;stop-opacity:1;"
offset="1"
id="stop3604-6" />
</linearGradient>
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3600-7"
id="radialGradient5254"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.92332021,0.38403097,-0.41592401,1.0000002,78.192026,-120.05314)"
cx="324.32715"
cy="131.40274"
fx="324.32715"
fy="131.40274"
r="25.501276" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient5189-1"
id="linearGradient5394"
gradientUnits="userSpaceOnUse"
x1="385.03503"
y1="180.09546"
x2="462.48297"
y2="180.09546"
gradientTransform="matrix(0.96839241,0,0,0.96839241,-360.365,847.52359)" />
<linearGradient
id="linearGradient5189-1">
<stop
style="stop-color:#000ec9;stop-opacity:1;"
offset="0"
id="stop5191-0" />
<stop
style="stop-color:#000657;stop-opacity:1;"
offset="1"
id="stop5193-4" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient5173-1"
id="linearGradient5581"
gradientUnits="userSpaceOnUse"
x1="167.33386"
y1="178.83276"
x2="244.78181"
y2="178.83276"
gradientTransform="matrix(0.96839241,0,0,0.96839241,-149.54484,848.74636)" />
<linearGradient
id="linearGradient5173-1">
<stop
style="stop-color:#f50000;stop-opacity:1;"
offset="0"
id="stop5175-5" />
<stop
style="stop-color:#950000;stop-opacity:1;"
offset="1"
id="stop5177-3" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient5181-9"
id="linearGradient5782"
gradientUnits="userSpaceOnUse"
x1="282.50455"
y1="181.61069"
x2="359.95248"
y2="181.61069"
gradientTransform="matrix(0.96839241,0,0,0.96839241,-261.07526,846.05625)" />
<linearGradient
id="linearGradient5181-9">
<stop
style="stop-color:#80d600;stop-opacity:1;"
offset="0"
id="stop5183-3" />
<stop
style="stop-color:#80d600;stop-opacity:1;"
offset="1"
id="stop5185-0" />
</linearGradient>
<linearGradient
y2="181.61069"
x2="359.95248"
y1="181.61069"
x1="282.50455"
gradientTransform="matrix(0.96839241,0,0,0.96839241,-175.71812,893.2775)"
gradientUnits="userSpaceOnUse"
id="linearGradient5799"
xlink:href="#linearGradient5181-9"
inkscape:collect="always" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3419"
id="linearGradient3425"
x1="-126.90256"
y1="941.52618"
x2="-125.73831"
y2="984.13751"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(154.2532,0.90556908)" />
</defs>
<sodipodi:namedview
inkscape:document-units="mm"
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="4.2946338"
inkscape:cx="0.39222903"
inkscape:cy="65.175571"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1440"
inkscape:window-height="792"
inkscape:window-x="11"
inkscape:window-y="0"
inkscape:window-maximized="0" />
<metadata
id="metadata4">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-952.36218)">
<path
style="fill-opacity:1;stroke:black;stroke-width:2.78220296;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
d="M 49.84375 1.71875 C 36.719738 1.71875 26.0625 12.375988 26.0625 25.5 C 26.0625 32.977454 29.538325 39.612734 34.9375 43.96875 C 24.439951 49.943698 17.919149 62.196126 14.3125 75.65625 C 9.0380874 95.34065 30.224013 98.21875 49.84375 98.21875 C 69.463486 98.21875 90.549327 94.96715 85.375 75.65625 C 81.693381 61.916246 75.224585 49.827177 64.8125 43.9375 C 70.181573 39.580662 73.59375 32.953205 73.59375 25.5 C 73.59375 12.375988 62.967762 1.71875 49.84375 1.71875 z "
transform="translate(0,952.36218)"
id="left" />
<path
sodipodi:type="star"
style="fill:url(#linearGradient3425);fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
id="star"
sodipodi:sides="5"
sodipodi:cx="27.80283"
sodipodi:cy="970.9433"
sodipodi:r1="17.189852"
sodipodi:r2="8.5949249"
sodipodi:arg1="0.9349579"
sodipodi:arg2="1.5632764"
inkscape:flatsided="false"
inkscape:rounded="0"
inkscape:randomized="0"
d="m 38.011063,984.77381 -10.143601,-5.23583 -10.063711,5.38779 1.845023,-11.2651 -8.233948,-7.90624 11.283888,-1.72639 4.974851,-10.27411 5.128803,10.19813 11.308575,1.55649 -8.114112,8.02918 z"
inkscape:transform-center-x="0.094945927"
inkscape:transform-center-y="-3.9764964"
transform="matrix(2.3768784,0,0,2.4799382,-16.393468,-1400.3733)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,131 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:osb="http://www.openswatchbook.org/uri/2009/osb"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
viewBox="0 0 64 64"
enable-background="new 0 0 64 64"
id="svg2"
version="1.1"
inkscape:version="0.48.5 r10040"
width="100%"
height="100%"
sodipodi:docname="moderator_buddy.svg">
<metadata
id="metadata12">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs10">
<linearGradient
id="linearGradient5225">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop5227" />
<stop
style="stop-color:#ffffff;stop-opacity:0;"
offset="1"
id="stop5229" />
</linearGradient>
<linearGradient
id="linearGradient5219"
osb:paint="solid">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop5221" />
</linearGradient>
<linearGradient
id="linearGradient3758">
<stop
id="stop3760"
offset="0"
style="stop-color:#0fbb00;stop-opacity:1;" />
<stop
id="stop3762"
offset="1"
style="stop-color:#064400;stop-opacity:1;" />
</linearGradient>
<linearGradient
id="linearGradient3750">
<stop
style="stop-color:#ece400;stop-opacity:1;"
offset="0"
id="stop3752" />
<stop
style="stop-color:#ece400;stop-opacity:0;"
offset="1"
id="stop3754" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3758-7"
id="linearGradient3756-1"
x1="1.960216"
y1="31.261461"
x2="60.456024"
y2="31.261461"
gradientUnits="userSpaceOnUse" />
<linearGradient
id="linearGradient3758-7">
<stop
id="stop3760-4"
offset="0"
style="stop-color:#ece400;stop-opacity:1;" />
<stop
id="stop3762-0"
offset="1"
style="stop-color:#ec8b00;stop-opacity:1;" />
</linearGradient>
</defs>
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1028"
id="namedview8"
showgrid="false"
inkscape:zoom="5.2149125"
inkscape:cx="-26.445493"
inkscape:cy="31.598459"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<path
d="M 61.442826,23.621762 38.532402,23.515555 31.556101,0.331013 24.578789,23.515555 1.6673502,23.621765 20.267785,38.422001 10.863888,63.668987 31.556101,47.626631 52.258426,63.668987 42.843404,38.422001 z"
id="outline"
style="fill:#000000;fill-opacity:1"
inkscape:connector-curvature="0" />
<path
d="M 55.041981,25.814432 36.921945,25.730432 31.404334,7.3935963 25.885923,25.730432 7.7650846,25.814434 22.476316,37.520057 15.0387,57.488097 31.404334,44.800071 47.777965,57.488097 40.331551,37.520057 z"
id="left"
style="fill-opacity:1;fill-rule:nonzero"
inkscape:connector-curvature="0" />
<path
d="M 56.276895,25.211993 37.3433,24.856806 31.486705,5.7742084 c 0.04705,37.4359336 -0.01851,2.6744908 -0.0678,40.1841446 L 48.19932,58.580578 40.956295,37.527792 z"
id="right"
style="fill-opacity:1;fill-rule:nonzero"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccc" />
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@@ -0,0 +1,115 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
viewBox="0 0 64 64"
enable-background="new 0 0 64 64"
id="svg2"
version="1.1"
inkscape:version="0.48.5 r10040"
width="100%"
height="100%"
sodipodi:docname="registered_buddy.svg">
<metadata
id="metadata12">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs10">
<linearGradient
id="linearGradient3758">
<stop
id="stop3760"
offset="0"
style="stop-color:#80d600;stop-opacity:1;" />
<stop
id="stop3762"
offset="1"
style="stop-color:#80d600;stop-opacity:1;" />
</linearGradient>
<linearGradient
id="linearGradient3750">
<stop
style="stop-color:#ece400;stop-opacity:1;"
offset="0"
id="stop3752" />
<stop
style="stop-color:#ece400;stop-opacity:0;"
offset="1"
id="stop3754" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3758"
id="linearGradient3756"
x1="1.960216"
y1="31.261461"
x2="60.456024"
y2="31.261461"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.87626222,0,0,0.87626222,4.174756,4.8555263)" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3758-7"
id="linearGradient3756-1"
x1="1.960216"
y1="31.261461"
x2="60.456024"
y2="31.261461"
gradientUnits="userSpaceOnUse" />
<linearGradient
id="linearGradient3758-7">
<stop
id="stop3760-4"
offset="0"
style="stop-color:#ece400;stop-opacity:1;" />
<stop
id="stop3762-0"
offset="1"
style="stop-color:#ec8b00;stop-opacity:1;" />
</linearGradient>
</defs>
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1600"
inkscape:window-height="1178"
id="namedview8"
showgrid="false"
inkscape:zoom="7.375"
inkscape:cx="-11.596718"
inkscape:cy="34.975297"
inkscape:window-x="1912"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<path
d="M 61.442826,23.621762 38.532402,23.515555 31.556101,0.331013 24.578789,23.515555 1.6673502,23.621765 20.267785,38.422001 10.863888,63.668987 31.556101,47.626631 52.258426,63.668987 42.843404,38.422001 z"
id="path4-9"
style="fill:#000000;fill-opacity:1"
inkscape:connector-curvature="0" />
<path
d="M 57.150089,25.064396 37.504323,24.973324 31.522122,5.092503 25.539054,24.973324 5.8924192,25.064399 21.842354,37.75565 13.778482,59.405024 31.522122,45.64865 49.274434,59.405024 41.201022,37.75565 z"
id="left"
style="fill-opacity:1"
inkscape:connector-curvature="0" />
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@@ -52,7 +52,7 @@ AbstractClient::AbstractClient(QObject *parent)
FeatureSet features;
features.initalizeFeatureList(clientFeatures);
connect(this, SIGNAL(sigQueuePendingCommand(PendingCommand *)), this, SLOT(queuePendingCommand(PendingCommand *)));
connect(this, &AbstractClient::sigQueuePendingCommand, this, &AbstractClient::queuePendingCommand);
}
AbstractClient::~AbstractClient()

View File

@@ -48,6 +48,7 @@ class AbstractClient : public QObject
Q_OBJECT
signals:
void statusChanged(ClientStatus _status);
void maxPingTime(int seconds, int maxSeconds);
// Room events
void roomEventReceived(const RoomEvent &event);
@@ -97,8 +98,8 @@ protected:
virtual void sendCommandContainer(const CommandContainer &cont) = 0;
public:
AbstractClient(QObject *parent = nullptr);
~AbstractClient();
explicit AbstractClient(QObject *parent = nullptr);
~AbstractClient() override;
ClientStatus getStatus() const
{

View File

@@ -23,7 +23,7 @@ signals:
void onCtrlC();
protected:
virtual bool eventFilter(QObject *, QEvent *event);
bool eventFilter(QObject *, QEvent *event) override;
};
#endif

View File

@@ -0,0 +1,35 @@
#include "client_update_checker.h"
#include "../../settings/cache_settings.h"
#include "release_channel.h"
ClientUpdateChecker::ClientUpdateChecker(QObject *parent) : QObject(parent)
{
}
void ClientUpdateChecker::check()
{
auto releaseChannel = SettingsCache::instance().getUpdateReleaseChannel();
finishedCheckConnection =
connect(releaseChannel, &ReleaseChannel::finishedCheck, this, &ClientUpdateChecker::actFinishedCheck);
errorConnection = connect(releaseChannel, &ReleaseChannel::error, this, &ClientUpdateChecker::actError);
releaseChannel->checkForUpdates();
}
void ClientUpdateChecker::actFinishedCheck(bool needToUpdate, bool isCompatible, Release *release)
{
disconnect(finishedCheckConnection);
disconnect(errorConnection);
emit finishedCheck(needToUpdate, isCompatible, release);
}
void ClientUpdateChecker::actError(const QString &errorString)
{
disconnect(finishedCheckConnection);
disconnect(errorConnection);
emit error(errorString);
}

View File

@@ -0,0 +1,45 @@
#ifndef CLIENT_UPDATE_CHECKER_H
#define CLIENT_UPDATE_CHECKER_H
#include <QObject>
class Release;
/**
* We use a singleton instance of UpdateChannel, which can cause interference and feedback loops when multiple objects
* connect to it.
*
* This class encapsulates the usage of that UpdateChannel to ensure that the check only happens once per connection and
* the connection is destroyed after it's been used.
*/
class ClientUpdateChecker : public QObject
{
Q_OBJECT
QMetaObject::Connection finishedCheckConnection;
QMetaObject::Connection errorConnection;
void actFinishedCheck(bool needToUpdate, bool isCompatible, Release *release);
void actError(const QString &errorString);
public:
explicit ClientUpdateChecker(QObject *parent = nullptr);
/**
* Actually performs the check, using the currently selected update channel in the settings.
* Any resulting signals will only be sent once.
* This method should only be called ONCE per instance.
*/
void check();
signals:
/**
* Forwarded from UpdateChannel::finishedCheck
*/
void finishedCheck(bool needToUpdate, bool isCompatible, Release *release);
/**
* Forwarded from UpdateChannel::error
*/
void error(const QString &errorString);
};
#endif // CLIENT_UPDATE_CHECKER_H

View File

@@ -21,11 +21,8 @@
#define GIT_SHORT_HASH_LEN 7
int ReleaseChannel::sharedIndex = 0;
ReleaseChannel::ReleaseChannel() : netMan(new QNetworkAccessManager(this)), response(nullptr), lastRelease(nullptr)
{
index = sharedIndex++;
}
ReleaseChannel::~ReleaseChannel()
@@ -36,9 +33,9 @@ ReleaseChannel::~ReleaseChannel()
void ReleaseChannel::checkForUpdates()
{
QString releaseChannelUrl = getReleaseChannelUrl();
qDebug() << "Searching for updates on the channel: " << releaseChannelUrl;
qCDebug(ReleaseChannelLog) << "Searching for updates on the channel: " << releaseChannelUrl;
response = netMan->get(QNetworkRequest(releaseChannelUrl));
connect(response, SIGNAL(finished()), this, SLOT(releaseListFinished()));
connect(response, &QNetworkReply::finished, this, &ReleaseChannel::releaseListFinished);
}
// Different release channel checking functions for different operating systems
@@ -89,7 +86,7 @@ QString StableReleaseChannel::getManualDownloadUrl() const
QString StableReleaseChannel::getName() const
{
return tr("Stable Releases");
return tr("Default");
}
QString StableReleaseChannel::getReleaseChannelUrl() const
@@ -112,7 +109,7 @@ void StableReleaseChannel::releaseListFinished()
QVariantMap resultMap = jsonResponse.toVariant().toMap();
if (!(resultMap.contains("name") && resultMap.contains("html_url") && resultMap.contains("tag_name") &&
resultMap.contains("published_at"))) {
qWarning() << "Invalid received from the release update server.";
qWarning() << "Invalid received from the release update server:" << resultMap;
emit error(tr("Invalid reply received from the release update server."));
return;
}
@@ -148,17 +145,17 @@ void StableReleaseChannel::releaseListFinished()
QString shortHash = lastRelease->getCommitHash().left(GIT_SHORT_HASH_LEN);
QString myHash = QString(VERSION_COMMIT);
qDebug() << "Current hash=" << myHash << "update hash=" << shortHash;
qCDebug(ReleaseChannelLog) << "Current hash=" << myHash << "update hash=" << shortHash;
qDebug() << "Got reply from release server, name=" << lastRelease->getName()
<< "desc=" << lastRelease->getDescriptionUrl() << "date=" << lastRelease->getPublishDate()
<< "url=" << lastRelease->getDownloadUrl();
qCDebug(ReleaseChannelLog) << "Got reply from release server, name=" << lastRelease->getName()
<< "desc=" << lastRelease->getDescriptionUrl()
<< "date=" << lastRelease->getPublishDate() << "url=" << lastRelease->getDownloadUrl();
const QString &tagName = resultMap["tag_name"].toString();
QString url = QString(STABLETAG_URL) + tagName;
qDebug() << "Searching for commit hash corresponding to stable channel tag: " << tagName;
qCDebug(ReleaseChannelLog) << "Searching for commit hash corresponding to stable channel tag: " << tagName;
response = netMan->get(QNetworkRequest(url));
connect(response, SIGNAL(finished()), this, SLOT(tagListFinished()));
connect(response, &QNetworkReply::finished, this, &StableReleaseChannel::tagListFinished);
}
void StableReleaseChannel::tagListFinished()
@@ -181,11 +178,11 @@ void StableReleaseChannel::tagListFinished()
}
lastRelease->setCommitHash(resultMap["object"].toMap()["sha"].toString());
qDebug() << "Got reply from tag server, commit=" << lastRelease->getCommitHash();
qCDebug(ReleaseChannelLog) << "Got reply from tag server, commit=" << lastRelease->getCommitHash();
QString shortHash = lastRelease->getCommitHash().left(GIT_SHORT_HASH_LEN);
QString myHash = QString(VERSION_COMMIT);
qDebug() << "Current hash=" << myHash << "update hash=" << shortHash;
qCDebug(ReleaseChannelLog) << "Current hash=" << myHash << "update hash=" << shortHash;
const bool needToUpdate = (QString::compare(shortHash, myHash, Qt::CaseInsensitive) != 0);
emit finishedCheck(needToUpdate, lastRelease->isCompatibleVersionFound(), lastRelease);
@@ -203,7 +200,7 @@ QString BetaReleaseChannel::getManualDownloadUrl() const
QString BetaReleaseChannel::getName() const
{
return tr("Beta Releases");
return tr("Beta");
}
QString BetaReleaseChannel::getReleaseChannelUrl() const
@@ -252,15 +249,15 @@ void BetaReleaseChannel::releaseListFinished()
lastRelease->setName(QString("%1 (%2)").arg(resultMap["tag_name"].toString()).arg(shortHash));
lastRelease->setDescriptionUrl(QString(BETARELEASE_CHANGESURL).arg(VERSION_COMMIT, shortHash));
qDebug() << "Got reply from release server, size=" << resultMap.size() << "name=" << lastRelease->getName()
<< "desc=" << lastRelease->getDescriptionUrl() << "commit=" << lastRelease->getCommitHash()
<< "date=" << lastRelease->getPublishDate();
qCDebug(ReleaseChannelLog) << "Got reply from release server, size=" << resultMap.size()
<< "name=" << lastRelease->getName() << "desc=" << lastRelease->getDescriptionUrl()
<< "commit=" << lastRelease->getCommitHash() << "date=" << lastRelease->getPublishDate();
QString betaBuildDownloadUrl = resultMap["assets_url"].toString();
qDebug() << "Searching for a corresponding file on the beta channel: " << betaBuildDownloadUrl;
qCDebug(ReleaseChannelLog) << "Searching for a corresponding file on the beta channel: " << betaBuildDownloadUrl;
response = netMan->get(QNetworkRequest(betaBuildDownloadUrl));
connect(response, SIGNAL(finished()), this, SLOT(fileListFinished()));
connect(response, &QNetworkReply::finished, this, &BetaReleaseChannel::fileListFinished);
}
void BetaReleaseChannel::fileListFinished()
@@ -278,7 +275,7 @@ void BetaReleaseChannel::fileListFinished()
QVariantList resultList = jsonResponse.toVariant().toList();
QString shortHash = lastRelease->getCommitHash().left(GIT_SHORT_HASH_LEN);
QString myHash = QString(VERSION_COMMIT);
qDebug() << "Current hash=" << myHash << "update hash=" << shortHash;
qCDebug(ReleaseChannelLog) << "Current hash=" << myHash << "update hash=" << shortHash;
bool needToUpdate = (QString::compare(shortHash, myHash, Qt::CaseInsensitive) != 0);
bool compatibleVersion = false;
@@ -295,7 +292,7 @@ void BetaReleaseChannel::fileListFinished()
if (downloadMatchesCurrentOS(*url)) {
compatibleVersion = true;
lastRelease->setDownloadUrl(*url);
qDebug() << "Found compatible version url=" << *url;
qCDebug(ReleaseChannelLog) << "Found compatible version url=" << *url;
break;
}
}

View File

@@ -2,11 +2,14 @@
#define RELEASECHANNEL_H
#include <QDate>
#include <QLoggingCategory>
#include <QObject>
#include <QString>
#include <QVariantMap>
#include <utility>
inline Q_LOGGING_CATEGORY(ReleaseChannelLog, "release_channel");
class QNetworkReply;
class QNetworkAccessManager;
@@ -82,9 +85,6 @@ public:
~ReleaseChannel() override;
protected:
// shared by all instances
static int sharedIndex;
int index;
QNetworkAccessManager *netMan;
QNetworkReply *response;
Release *lastRelease;
@@ -94,10 +94,6 @@ protected:
virtual QString getReleaseChannelUrl() const = 0;
public:
int getIndex() const
{
return index;
}
Release *getLastRelease()
{
return lastRelease;

View File

@@ -10,7 +10,7 @@ ReplayTimelineWidget::ReplayTimelineWidget(QWidget *parent)
currentEvent(0)
{
replayTimer = new QTimer(this);
connect(replayTimer, SIGNAL(timeout()), this, SLOT(replayTimerTimeout()));
connect(replayTimer, &QTimer::timeout, this, &ReplayTimelineWidget::replayTimerTimeout);
rewindBufferingTimer = new QTimer(this);
rewindBufferingTimer->setSingleShot(true);

View File

@@ -57,28 +57,29 @@ public:
SortRole = Qt::UserRole
};
SetsModel(CardDatabase *_db, QObject *parent = nullptr);
~SetsModel();
int rowCount(const QModelIndex &parent = QModelIndex()) const;
int columnCount(const QModelIndex &parent = QModelIndex()) const
explicit SetsModel(CardDatabase *_db, QObject *parent = nullptr);
~SetsModel() override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
int columnCount(const QModelIndex &parent = QModelIndex()) const override
{
Q_UNUSED(parent);
return NUM_COLS;
}
QVariant data(const QModelIndex &index, int role) const;
bool setData(const QModelIndex &index, const QVariant &value, int role);
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const;
Qt::ItemFlags flags(const QModelIndex &index) const;
Qt::DropActions supportedDropActions() const;
QVariant data(const QModelIndex &index, int role) const override;
bool setData(const QModelIndex &index, const QVariant &value, int role) override;
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
Qt::ItemFlags flags(const QModelIndex &index) const override;
Qt::DropActions supportedDropActions() const override;
QMimeData *mimeData(const QModelIndexList &indexes) const;
bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent);
QStringList mimeTypes() const;
QMimeData *mimeData(const QModelIndexList &indexes) const override;
bool
dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) override;
QStringList mimeTypes() const override;
void swapRows(int oldRow, int newRow);
void toggleRow(int row, bool enable);
void toggleRow(int row);
void toggleAll(bool);
void sort(int column, Qt::SortOrder order = Qt::AscendingOrder);
void sort(int column, Qt::SortOrder order = Qt::AscendingOrder) override;
void save(CardDatabase *db);
void restore(CardDatabase *db);
void restoreOriginalOrder();
@@ -88,7 +89,7 @@ class SetsDisplayModel : public QSortFilterProxyModel
{
Q_OBJECT
public:
SetsDisplayModel(QObject *parent = NULL);
explicit SetsDisplayModel(QObject *parent = nullptr);
protected:
bool lessThan(const QModelIndex &left, const QModelIndex &right) const override;

View File

@@ -28,7 +28,7 @@ SpoilerBackgroundUpdater::SpoilerBackgroundUpdater(QObject *apParent) : QObject(
// File exists means we're in spoiler season
startSpoilerDownloadProcess(SPOILERS_STATUS_URL, false);
} else {
qDebug() << "Spoilers Disabled";
qCDebug(SpoilerBackgroundUpdaterLog) << "Spoilers Disabled";
}
}
@@ -45,10 +45,10 @@ void SpoilerBackgroundUpdater::downloadFromURL(QUrl url, bool saveResults)
if (saveResults) {
// This will write out to the file (used for spoiler.xml)
connect(reply, SIGNAL(finished()), this, SLOT(actDownloadFinishedSpoilersFile()));
connect(reply, &QNetworkReply::finished, this, &SpoilerBackgroundUpdater::actDownloadFinishedSpoilersFile);
} else {
// This will check the status (used to see if we're in spoiler season or not)
connect(reply, SIGNAL(finished()), this, SLOT(actCheckIfSpoilerSeasonEnabled()));
connect(reply, &QNetworkReply::finished, this, &SpoilerBackgroundUpdater::actCheckIfSpoilerSeasonEnabled);
}
}
@@ -67,7 +67,7 @@ void SpoilerBackgroundUpdater::actDownloadFinishedSpoilersFile()
reply->deleteLater();
emit spoilerCheckerDone();
} else {
qDebug() << "Error downloading spoilers file" << errorCode;
qCDebug(SpoilerBackgroundUpdaterLog) << "Error downloading spoilers file" << errorCode;
emit spoilerCheckerDone();
}
}
@@ -81,11 +81,11 @@ bool SpoilerBackgroundUpdater::deleteSpoilerFile()
// Delete the spoiler.xml file
if (file.exists() && file.remove()) {
qDebug() << "Deleting spoiler.xml";
qCDebug(SpoilerBackgroundUpdaterLog) << "Deleting spoiler.xml";
return true;
}
qDebug() << "Error: Spoiler.xml not found or not deleted";
qCDebug(SpoilerBackgroundUpdaterLog) << "Error: Spoiler.xml not found or not deleted";
return false;
}
@@ -101,24 +101,24 @@ void SpoilerBackgroundUpdater::actCheckIfSpoilerSeasonEnabled()
trayIcon->showMessage(tr("Spoilers season has ended"), tr("Deleting spoiler.xml. Please run Oracle"));
}
qDebug() << "Spoiler Season Offline";
qCDebug(SpoilerBackgroundUpdaterLog) << "Spoiler Season Offline";
emit spoilerCheckerDone();
} else if (errorCode == QNetworkReply::NoError) {
qDebug() << "Spoiler Service Online";
qCDebug(SpoilerBackgroundUpdaterLog) << "Spoiler Service Online";
startSpoilerDownloadProcess(SPOILERS_URL, true);
} else if (errorCode == QNetworkReply::HostNotFoundError) {
if (trayIcon) {
trayIcon->showMessage(tr("Spoilers download failed"), tr("No internet connection"));
}
qDebug() << "Spoiler download failed due to no internet connection";
qCDebug(SpoilerBackgroundUpdaterLog) << "Spoiler download failed due to no internet connection";
emit spoilerCheckerDone();
} else {
if (trayIcon) {
trayIcon->showMessage(tr("Spoilers download failed"), tr("Error") + " " + (short)errorCode);
}
qDebug() << "Spoiler download failed with reason" << errorCode;
qCDebug(SpoilerBackgroundUpdaterLog) << "Spoiler download failed with reason" << errorCode;
emit spoilerCheckerDone();
}
}
@@ -139,19 +139,19 @@ bool SpoilerBackgroundUpdater::saveDownloadedFile(QByteArray data)
trayIcon->showMessage(tr("Spoilers already up to date"), tr("No new spoilers added"));
}
qDebug() << "Spoilers Up to Date";
qCDebug(SpoilerBackgroundUpdaterLog) << "Spoilers Up to Date";
return false;
}
QFile file(fileName);
if (!file.open(QIODevice::WriteOnly)) {
qDebug() << "Spoiler Service Error: File open (w) failed for" << fileName;
qCDebug(SpoilerBackgroundUpdaterLog) << "Spoiler Service Error: File open (w) failed for" << fileName;
file.close();
return false;
}
if (file.write(data) == -1) {
qDebug() << "Spoiler Service Error: File write (w) failed for" << fileName;
qCDebug(SpoilerBackgroundUpdaterLog) << "Spoiler Service Error: File write (w) failed for" << fileName;
file.close();
return false;
}
@@ -159,7 +159,7 @@ bool SpoilerBackgroundUpdater::saveDownloadedFile(QByteArray data)
file.close();
// Data written, so reload the card database
qDebug() << "Spoiler Service Data Written";
qCDebug(SpoilerBackgroundUpdaterLog) << "Spoiler Service Data Written";
const auto reloadOk = QtConcurrent::run([] { CardDatabaseManager::getInstance()->loadCardDatabases(); });
// If the user has notifications enabled, let them know
@@ -167,7 +167,7 @@ bool SpoilerBackgroundUpdater::saveDownloadedFile(QByteArray data)
if (trayIcon) {
QList<QByteArray> lines = data.split('\n');
foreach (QByteArray line, lines) {
for (const QByteArray &line : lines) {
if (line.contains("Created At:")) {
QString timeStamp = QString(line).replace("Created At:", "").trimmed();
timeStamp.chop(6); // Remove " (UTC)"
@@ -202,12 +202,12 @@ QByteArray SpoilerBackgroundUpdater::getHash(const QString fileName)
QCryptographicHash hash(QCryptographicHash::Algorithm::Md5);
hash.addData(bytes);
qDebug() << "File Hash =" << hash.result();
qCDebug(SpoilerBackgroundUpdaterLog) << "File Hash =" << hash.result();
file.close();
return hash.result();
} else {
qDebug() << "getHash ReadOnly failed!";
qCDebug(SpoilerBackgroundUpdaterLog) << "getHash ReadOnly failed!";
file.close();
return QByteArray();
}
@@ -221,7 +221,7 @@ QByteArray SpoilerBackgroundUpdater::getHash(QByteArray data)
QCryptographicHash hash(QCryptographicHash::Algorithm::Md5);
hash.addData(bytes);
qDebug() << "Data Hash =" << hash.result();
qCDebug(SpoilerBackgroundUpdaterLog) << "Data Hash =" << hash.result();
return hash.result();
}

View File

@@ -2,9 +2,12 @@
#define COCKATRICE_SPOILER_DOWNLOADER_H
#include <QByteArray>
#include <QLoggingCategory>
#include <QObject>
#include <QProcess>
inline Q_LOGGING_CATEGORY(SpoilerBackgroundUpdaterLog, "spoiler_background_updater");
class SpoilerBackgroundUpdater : public QObject
{
Q_OBJECT

View File

@@ -12,11 +12,11 @@
#define DEFAULT_THEME_NAME "Default"
#define TEST_SOUND_FILENAME "player_join"
SoundEngine::SoundEngine(QObject *parent) : QObject(parent), player(nullptr)
SoundEngine::SoundEngine(QObject *parent) : QObject(parent), audioOutput(nullptr), player(nullptr)
{
ensureThemeDirectoryExists();
connect(&SettingsCache::instance(), SIGNAL(soundThemeChanged()), this, SLOT(themeChangedSlot()));
connect(&SettingsCache::instance(), SIGNAL(soundEnabledChanged()), this, SLOT(soundEnabledChanged()));
connect(&SettingsCache::instance(), &SettingsCache::soundThemeChanged, this, &SoundEngine::themeChangedSlot);
connect(&SettingsCache::instance(), &SettingsCache::soundEnabledChanged, this, &SoundEngine::soundEnabledChanged);
soundEnabledChanged();
themeChangedSlot();
@@ -28,26 +28,34 @@ SoundEngine::~SoundEngine()
player->deleteLater();
player = nullptr;
}
if (audioOutput) {
audioOutput->deleteLater();
audioOutput = nullptr;
}
}
void SoundEngine::soundEnabledChanged()
{
if (SettingsCache::instance().getSoundEnabled()) {
qDebug() << "SoundEngine: enabling sound with" << audioData.size() << "sounds";
qCDebug(SoundEngineLog) << "SoundEngine: enabling sound with" << audioData.size() << "sounds";
if (!player) {
player = new QMediaPlayer;
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
auto qAudioOutput = new QAudioOutput;
player->setAudioOutput(qAudioOutput);
audioOutput = new QAudioOutput(player);
player->setAudioOutput(audioOutput);
#endif
}
} else {
qDebug() << "SoundEngine: disabling sound";
qCDebug(SoundEngineLog) << "SoundEngine: disabling sound";
if (player) {
player->stop();
player->deleteLater();
player = nullptr;
}
if (audioOutput) {
audioOutput->deleteLater();
audioOutput = nullptr;
}
}
}
@@ -82,7 +90,7 @@ void SoundEngine::ensureThemeDirectoryExists()
{
if (SettingsCache::instance().getSoundThemeName().isEmpty() ||
!getAvailableThemes().contains(SettingsCache::instance().getSoundThemeName())) {
qDebug() << "Sounds theme name not set, setting default value";
qCDebug(SoundEngineLog) << "Sounds theme name not set, setting default value";
SettingsCache::instance().setSoundThemeName(DEFAULT_THEME_NAME);
}
}
@@ -123,7 +131,7 @@ QStringMap &SoundEngine::getAvailableThemes()
void SoundEngine::themeChangedSlot()
{
QString themeName = SettingsCache::instance().getSoundThemeName();
qDebug() << "Sound theme changed:" << themeName;
qCDebug(SoundEngineLog) << "Sound theme changed:" << themeName;
QDir dir = getAvailableThemes().value(themeName);

View File

@@ -1,12 +1,15 @@
#ifndef SOUNDENGINE_H
#define SOUNDENGINE_H
#include <QAudioOutput>
#include <QLoggingCategory>
#include <QMap>
#include <QMediaPlayer>
#include <QObject>
#include <QString>
class QAudioOutput;
inline Q_LOGGING_CATEGORY(SoundEngineLog, "sound_engine");
class QBuffer;
typedef QMap<QString, QString> QStringMap;
@@ -23,6 +26,7 @@ public:
private:
QStringMap availableThemes;
QMap<QString, QString> audioData;
QAudioOutput *audioOutput;
QMediaPlayer *player;
protected:

View File

@@ -0,0 +1,45 @@
#include "edhrec_commander_api_response.h"
#include <QDebug>
#include <QJsonArray>
void EdhrecCommanderApiResponse::fromJson(const QJsonObject &json)
{
// Parse the collapsed DeckStatistics
deckStats.fromJson(json);
// Parse Archidekt section
QJsonArray archidektJson = json.value("archidekt").toArray();
archidekt.fromJson(archidektJson);
// Parse other fields
similar = json.value("similar").toObject();
header = json.value("header").toString();
panels = json.value("panels").toObject();
description = json.value("description").toString();
QJsonObject containerJson = json.value("container").toObject();
container.fromJson(containerJson);
}
void EdhrecCommanderApiResponse::debugPrint() const
{
qDebug() << "Deck Statistics:";
qDebug() << " Creature:" << deckStats.creature;
qDebug() << " Instant:" << deckStats.instant;
qDebug() << " Sorcery:" << deckStats.sorcery;
qDebug() << " Artifact:" << deckStats.artifact;
qDebug() << " Enchantment:" << deckStats.enchantment;
qDebug() << " Battle:" << deckStats.battle;
qDebug() << " Planeswalker:" << deckStats.planeswalker;
qDebug() << " Land:" << deckStats.land;
qDebug() << " Basic:" << deckStats.basic;
qDebug() << " Nonbasic:" << deckStats.nonbasic;
archidekt.debugPrint();
qDebug() << "Similar:" << similar;
qDebug() << "Header:" << header;
qDebug() << "Panels:" << panels;
qDebug() << "Description:" << description;
container.debugPrint();
}

View File

@@ -0,0 +1,28 @@
#ifndef DECKDATA_H
#define DECKDATA_H
#include "edhrec_commander_api_response_archidekt_links.h"
#include "edhrec_commander_api_response_average_deck_statistics.h"
#include "edhrec_commander_api_response_card_container.h"
#include <QDebug>
#include <QJsonObject>
#include <QString>
// Represents the main structure of the JSON
class EdhrecCommanderApiResponse
{
public:
EdhrecCommanderApiResponseAverageDeckStatistics deckStats;
EdhrecCommanderApiResponseArchidektLinks archidekt;
QJsonObject similar;
QString header;
QJsonObject panels;
QString description;
EdhrecCommanderApiResponseCardContainer container;
void fromJson(const QJsonObject &json);
void debugPrint() const;
};
#endif // DECKDATA_H

View File

@@ -0,0 +1,43 @@
#include "edhrec_commander_api_response_archidekt_links.h"
#include <QDebug>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
void EdhrecCommanderApiResponseArchidektLink::fromJson(const QJsonObject &json)
{
c = json.value("c").toString();
f = json.value("f").toInt(0);
q = json.value("q").toInt(0);
u = json.value("u").toString();
}
void EdhrecCommanderApiResponseArchidektLink::debugPrint() const
{
qDebug() << " C:" << c;
qDebug() << " F:" << f;
qDebug() << " Q:" << q;
qDebug() << " U:" << u;
}
void EdhrecCommanderApiResponseArchidektLinks::fromJson(const QJsonArray &json)
{
entries.clear();
for (const QJsonValue &value : json) {
if (value.isObject()) {
QJsonObject entryJson = value.toObject();
EdhrecCommanderApiResponseArchidektLink entry;
entry.fromJson(entryJson);
entries.append(entry);
}
}
}
void EdhrecCommanderApiResponseArchidektLinks::debugPrint() const
{
qDebug() << "Archidekt Entries:";
for (const auto &entry : entries) {
entry.debugPrint();
}
}

View File

@@ -0,0 +1,32 @@
#ifndef ARCHIDEKTENTRY_H
#define ARCHIDEKTENTRY_H
#include <QDebug>
#include <QJsonObject>
#include <QString>
#include <QVector>
// Represents a single Archidekt entry
class EdhrecCommanderApiResponseArchidektLink
{
public:
QString c;
int f = 0;
int q = 0;
QString u;
void fromJson(const QJsonObject &json);
void debugPrint() const;
};
// Represents the Archidekt section as a list of entries
class EdhrecCommanderApiResponseArchidektLinks
{
public:
QVector<EdhrecCommanderApiResponseArchidektLink> entries;
void fromJson(const QJsonArray &json);
void debugPrint() const;
};
#endif // ARCHIDEKTENTRY_H

View File

@@ -0,0 +1,15 @@
#include "edhrec_commander_api_response_average_deck_statistics.h"
void EdhrecCommanderApiResponseAverageDeckStatistics::fromJson(const QJsonObject &json)
{
creature = json.value("creature").toInt(0);
instant = json.value("instant").toInt(0);
sorcery = json.value("sorcery").toInt(0);
artifact = json.value("artifact").toInt(0);
enchantment = json.value("enchantment").toInt(0);
battle = json.value("battle").toInt(0);
planeswalker = json.value("planeswalker").toInt(0);
land = json.value("land").toInt(0);
basic = json.value("basic").toInt(0);
nonbasic = json.value("nonbasic").toInt(0);
}

View File

@@ -0,0 +1,22 @@
#ifndef AVERAGE_DECK_STATISTICS_H
#define AVERAGE_DECK_STATISTICS_H
#include <QJsonObject>
// Represents the typical deck statistics (collapsed section)
struct EdhrecCommanderApiResponseAverageDeckStatistics
{
int creature = 0;
int instant = 0;
int sorcery = 0;
int artifact = 0;
int enchantment = 0;
int battle = 0;
int planeswalker = 0;
int land = 0;
int basic = 0;
int nonbasic = 0;
void fromJson(const QJsonObject &json);
};
#endif // AVERAGE_DECK_STATISTICS_H

View File

@@ -0,0 +1,49 @@
#include "edhrec_commander_api_response_card_container.h"
#include <QDebug>
#include <QJsonArray>
#include <QJsonObject>
void EdhrecCommanderApiResponseCardContainer::fromJson(const QJsonObject &json)
{
// Parse breadcrumb
QJsonArray breadcrumbArray = json.value("breadcrumb").toArray();
for (const QJsonValue &breadcrumbValue : breadcrumbArray) {
breadcrumb.push_back(breadcrumbValue.toObject());
}
description = json.value("description").toString();
QJsonObject jsonDict = json.value("json_dict").toObject();
card.fromJson(jsonDict.value("card").toObject());
QJsonArray cardlistsArray = jsonDict.value("cardlists").toArray();
for (const QJsonValue &cardlistValue : cardlistsArray) {
QJsonObject cardlistObj = cardlistValue.toObject();
QJsonArray cardviewsArray = cardlistObj.value("cardviews").toArray();
EdhrecCommanderApiResponseCardList cardView;
cardView.fromJson(cardlistValue.toObject());
cardlists.push_back(cardView);
}
keywords = json.value("keywords").toString();
title = json.value("title").toString();
}
void EdhrecCommanderApiResponseCardContainer::debugPrint() const
{
qDebug() << "Breadcrumb:";
for (const auto &breadcrumbEntry : breadcrumb) {
qDebug() << breadcrumbEntry;
}
qDebug() << "Description:" << description;
card.debugPrint();
qDebug() << "Cardlists:";
for (const auto &cardlist : cardlists) {
cardlist.debugPrint();
}
qDebug() << "Keywords:" << keywords;
qDebug() << "Title:" << title;
}

View File

@@ -0,0 +1,60 @@
#ifndef CONTAINER_ENTRY_H
#define CONTAINER_ENTRY_H
#include "edhrec_commander_api_response_card_list.h"
#include "edhrec_commander_api_response_commander_details.h"
#include <QDebug>
#include <QJsonArray>
#include <QJsonObject>
#include <QString>
#include <QVector>
class EdhrecCommanderApiResponseCardContainer
{
public:
// Constructor
EdhrecCommanderApiResponseCardContainer() = default;
// Parse deck-related data from JSON
void fromJson(const QJsonObject &json);
// Debug method for logging
void debugPrint() const;
// Getter methods for deck container
const QString &getDescription() const
{
return description;
}
const QVector<QJsonObject> &getBreadcrumb() const
{
return breadcrumb;
}
const EdhrecCommanderApiResponseCommanderDetails &getCommanderDetails() const
{
return card;
}
const QVector<EdhrecCommanderApiResponseCardList> &getCardlists() const
{
return cardlists;
}
const QString &getKeywords() const
{
return keywords;
}
const QString &getTitle() const
{
return title;
}
private:
QString description;
QVector<QJsonObject> breadcrumb;
EdhrecCommanderApiResponseCommanderDetails card;
QVector<EdhrecCommanderApiResponseCardList> cardlists;
QString keywords;
QString title;
};
#endif // CONTAINER_ENTRY_H

View File

@@ -0,0 +1,36 @@
#include "edhrec_commander_api_response_card_details.h"
#include <QDebug>
EdhrecCommanderApiResponseCardDetails::EdhrecCommanderApiResponseCardDetails()
: synergy(0.0), inclusion(0), numDecks(0), potentialDecks(0)
{
}
void EdhrecCommanderApiResponseCardDetails::fromJson(const QJsonObject &json)
{
// Parse the fields from the JSON object
name = json.value("name").toString();
sanitized = json.value("sanitized").toString();
sanitizedWo = json.value("sanitized_wo").toString();
url = json.value("url").toString();
synergy = json.value("synergy").toDouble(0.0);
inclusion = json.value("inclusion").toInt(0);
label = json.value("label").toString();
numDecks = json.value("num_decks").toInt(0);
potentialDecks = json.value("potential_decks").toInt(0);
}
void EdhrecCommanderApiResponseCardDetails::debugPrint() const
{
// Print out all the fields for debugging
qDebug() << "Name:" << name;
qDebug() << "Sanitized:" << sanitized;
qDebug() << "Sanitized Wo:" << sanitizedWo;
qDebug() << "URL:" << url;
qDebug() << "Synergy:" << synergy;
qDebug() << "Inclusion:" << inclusion;
qDebug() << "Label:" << label;
qDebug() << "Num Decks:" << numDecks;
qDebug() << "Potential Decks:" << potentialDecks;
}

View File

@@ -0,0 +1,29 @@
#ifndef CARD_VIEW_H
#define CARD_VIEW_H
#include <QJsonObject>
#include <QString>
class EdhrecCommanderApiResponseCardDetails
{
public:
QString name;
QString sanitized;
QString sanitizedWo;
QString url;
double synergy;
int inclusion;
QString label;
int numDecks;
int potentialDecks;
EdhrecCommanderApiResponseCardDetails();
// Method to populate the object from a JSON object
void fromJson(const QJsonObject &json);
// Debug method to print out the data
void debugPrint() const;
};
#endif // CARD_VIEW_H

View File

@@ -0,0 +1,33 @@
#include "edhrec_commander_api_response_card_list.h"
#include <QDebug>
EdhrecCommanderApiResponseCardList::EdhrecCommanderApiResponseCardList()
{
}
void EdhrecCommanderApiResponseCardList::fromJson(const QJsonObject &json)
{
// Parse the header from the JSON object
header = json.value("header").toString();
// Parse the cardviews array and populate cardViews
QJsonArray cardviewsArray = json.value("cardviews").toArray();
for (const QJsonValue &value : cardviewsArray) {
QJsonObject cardviewObj = value.toObject();
EdhrecCommanderApiResponseCardDetails cardView;
cardView.fromJson(cardviewObj);
cardViews.append(cardView);
}
}
void EdhrecCommanderApiResponseCardList::debugPrint() const
{
// Print out the header
qDebug() << "Header:" << header;
// Print out all the CardView objects
for (const EdhrecCommanderApiResponseCardDetails &cardView : cardViews) {
cardView.debugPrint();
}
}

View File

@@ -0,0 +1,27 @@
#ifndef CARD_LIST_H
#define CARD_LIST_H
#include "edhrec_commander_api_response_card_details.h"
#include <QJsonArray>
#include <QJsonObject>
#include <QList>
#include <QString>
class EdhrecCommanderApiResponseCardList
{
public:
QString header;
QList<EdhrecCommanderApiResponseCardDetails> cardViews;
// Default constructor
EdhrecCommanderApiResponseCardList();
// Method to populate the object from a JSON object
void fromJson(const QJsonObject &json);
// Debug method to print out the data
void debugPrint() const;
};
#endif // CARD_LIST_H

View File

@@ -0,0 +1,31 @@
#include "edhrec_commander_api_response_card_prices.h"
#include <QDebug>
void CardPrices::fromJson(const QJsonObject &json)
{
// Parse prices from various sources
cardhoarder = json.value("cardhoarder").toObject();
cardkingdom = json.value("cardkingdom").toObject();
cardmarket = json.value("cardmarket").toObject();
face2face = json.value("face2face").toObject();
manapool = json.value("manapool").toObject();
mtgstocks = json.value("mtgstocks").toObject();
scg = json.value("scg").toObject();
tcgl = json.value("tcgl").toObject();
tcgplayer = json.value("tcgplayer").toObject();
}
void CardPrices::debugPrint() const
{
qDebug() << "Card Prices:";
qDebug() << "Cardhoarder:" << cardhoarder;
qDebug() << "Cardkingdom:" << cardkingdom;
qDebug() << "Cardmarket:" << cardmarket;
qDebug() << "Face2Face:" << face2face;
qDebug() << "Manapool:" << manapool;
qDebug() << "Mtgstocks:" << mtgstocks;
qDebug() << "SCG:" << scg;
qDebug() << "TCGL:" << tcgl;
qDebug() << "Tcgplayer:" << tcgplayer;
}

View File

@@ -0,0 +1,66 @@
#ifndef EDHREC_COMMANDER_API_RESPONSE_CARD_PRICES_H
#define EDHREC_COMMANDER_API_RESPONSE_CARD_PRICES_H
#include <QJsonObject>
class CardPrices
{
public:
// Constructor
CardPrices() = default;
// Parse prices from JSON
void fromJson(const QJsonObject &json);
void debugPrint() const;
// Getter methods for card prices
const QJsonObject &getCardhoarder() const
{
return cardhoarder;
}
const QJsonObject &getCardkingdom() const
{
return cardkingdom;
}
const QJsonObject &getCardmarket() const
{
return cardmarket;
}
const QJsonObject &getFace2face() const
{
return face2face;
}
const QJsonObject &getManapool() const
{
return manapool;
}
const QJsonObject &getMtgstocks() const
{
return mtgstocks;
}
const QJsonObject &getScg() const
{
return scg;
}
const QJsonObject &getTcgl() const
{
return tcgl;
}
const QJsonObject &getTcgplayer() const
{
return tcgplayer;
}
private:
QJsonObject cardhoarder;
QJsonObject cardkingdom;
QJsonObject cardmarket;
QJsonObject face2face;
QJsonObject manapool;
QJsonObject mtgstocks;
QJsonObject scg;
QJsonObject tcgl;
QJsonObject tcgplayer;
};
#endif // EDHREC_COMMANDER_API_RESPONSE_CARD_PRICES_H

View File

@@ -0,0 +1,90 @@
#include "edhrec_commander_api_response_commander_details.h"
#include <QDebug>
void EdhrecCommanderApiResponseCommanderDetails::fromJson(const QJsonObject &json)
{
// Parse card-related data
aetherhubUri = json.value("aetherhub_uri").toString();
archidektUri = json.value("archidekt_uri").toString();
cmc = json.value("cmc").toInt(0);
colorIdentity = json.value("color_identity").toArray();
combos = json.value("combos").toBool(false);
deckstatsUri = json.value("deckstats_uri").toString();
// Parse image URIs
QJsonArray imageUrisArray = json.value("image_uris").toArray();
for (const QJsonValue &imageValue : imageUrisArray) {
QJsonObject imageObject = imageValue.toObject();
imageUris.push_back(imageObject.value("normal").toString());
imageUris.push_back(imageObject.value("art_crop").toString());
}
inclusion = json.value("inclusion").toInt(0);
isCommander = json.value("is_commander").toBool(false);
label = json.value("label").toString();
layout = json.value("layout").toString();
legalCommander = json.value("legal_commander").toBool(false);
moxfieldUri = json.value("moxfield_uri").toString();
mtggoldfishUri = json.value("mtggoldfish_uri").toString();
name = json.value("name").toString();
names = json.value("names").toArray();
numDecks = json.value("num_decks").toInt(0);
potentialDecks = json.value("potential_decks").toInt(0);
precon = json.value("precon").toString();
// Parse prices
prices.fromJson(json.value("prices").toObject());
primaryType = json.value("primary_type").toString();
rarity = json.value("rarity").toString();
salt = json.value("salt").toDouble(0.0);
sanitized = json.value("sanitized").toString();
sanitizedWo = json.value("sanitized_wo").toString();
scryfallUri = json.value("scryfall_uri").toString();
spellbookUri = json.value("spellbook_uri").toString();
type = json.value("type").toString();
url = json.value("url").toString();
}
void EdhrecCommanderApiResponseCommanderDetails::debugPrint() const
{
qDebug() << "Card Data:";
qDebug() << "Aetherhub URI:" << aetherhubUri;
qDebug() << "Archidekt URI:" << archidektUri;
qDebug() << "CMC:" << cmc;
qDebug() << "Color Identity:" << colorIdentity;
qDebug() << "Combos:" << combos;
qDebug() << "Deckstats URI:" << deckstatsUri;
qDebug() << "Image URIs:";
for (const auto &uri : imageUris) {
qDebug() << uri;
}
qDebug() << "Inclusion:" << inclusion;
qDebug() << "Is Commander:" << isCommander;
qDebug() << "Label:" << label;
qDebug() << "Layout:" << layout;
qDebug() << "Legal Commander:" << legalCommander;
qDebug() << "Moxfield URI:" << moxfieldUri;
qDebug() << "MTGGoldfish URI:" << mtggoldfishUri;
qDebug() << "Name:" << name;
qDebug() << "Names:" << names;
qDebug() << "Number of Decks:" << numDecks;
qDebug() << "Potential Decks:" << potentialDecks;
qDebug() << "Precon:" << precon;
// Print the prices using the debugPrint method from CardPrices
prices.debugPrint();
qDebug() << "Primary Type:" << primaryType;
qDebug() << "Rarity:" << rarity;
qDebug() << "Salt:" << salt;
qDebug() << "Sanitized:" << sanitized;
qDebug() << "Sanitized WO:" << sanitizedWo;
qDebug() << "Scryfall URI:" << scryfallUri;
qDebug() << "Spellbook URI:" << spellbookUri;
qDebug() << "Type:" << type;
qDebug() << "URL:" << url;
}

View File

@@ -0,0 +1,173 @@
#ifndef EDHREC_COMMANDER_API_RESPONSE_COMMANDER_DETAILS_H
#define EDHREC_COMMANDER_API_RESPONSE_COMMANDER_DETAILS_H
#include "edhrec_commander_api_response_card_prices.h"
#include <QJsonArray>
#include <QJsonObject>
#include <QString>
#include <QVector>
class EdhrecCommanderApiResponseCommanderDetails
{
public:
// Constructor
EdhrecCommanderApiResponseCommanderDetails() = default;
// Parse card-related data from JSON
void fromJson(const QJsonObject &json);
// Debug method for logging
void debugPrint() const;
// Getters for the card data
const QString &getAetherhubUri() const
{
return aetherhubUri;
}
const QString &getArchidektUri() const
{
return archidektUri;
}
int getCmc() const
{
return cmc;
}
const QJsonArray &getColorIdentity() const
{
return colorIdentity;
}
bool isCombos() const
{
return combos;
}
const QString &getDeckstatsUri() const
{
return deckstatsUri;
}
const QVector<QString> &getImageUris() const
{
return imageUris;
}
int getInclusion() const
{
return inclusion;
}
bool getIsCommander() const
{
return isCommander;
}
const QString &getLabel() const
{
return label;
}
const QString &getLayout() const
{
return layout;
}
bool getLegalCommander() const
{
return legalCommander;
}
const QString &getMoxfieldUri() const
{
return moxfieldUri;
}
const QString &getMtggoldfishUri() const
{
return mtggoldfishUri;
}
const QString &getName() const
{
return name;
}
const QJsonArray &getNames() const
{
return names;
}
int getNumDecks() const
{
return numDecks;
}
int getPotentialDecks() const
{
return potentialDecks;
}
const QString &getPrecon() const
{
return precon;
}
const CardPrices &getPrices() const
{
return prices;
}
const QString &getPrimaryType() const
{
return primaryType;
}
const QString &getRarity() const
{
return rarity;
}
double getSalt() const
{
return salt;
}
const QString &getSanitized() const
{
return sanitized;
}
const QString &getSanitizedWo() const
{
return sanitizedWo;
}
const QString &getScryfallUri() const
{
return scryfallUri;
}
const QString &getSpellbookUri() const
{
return spellbookUri;
}
const QString &getType() const
{
return type;
}
const QString &getUrl() const
{
return url;
}
private:
QString aetherhubUri;
QString archidektUri;
int cmc = 0;
QJsonArray colorIdentity;
bool combos = false;
QString deckstatsUri;
QVector<QString> imageUris;
int inclusion = 0;
bool isCommander = false;
QString label;
QString layout;
bool legalCommander = false;
QString moxfieldUri;
QString mtggoldfishUri;
QString name;
QJsonArray names;
int numDecks = 0;
int potentialDecks = 0;
QString precon;
CardPrices prices;
QString primaryType;
QString rarity;
double salt = 0.0;
QString sanitized;
QString sanitizedWo;
QString scryfallUri;
QString spellbookUri;
QString type;
QString url;
};
#endif // EDHREC_COMMANDER_API_RESPONSE_COMMANDER_DETAILS_H

View File

@@ -0,0 +1,41 @@
#include "edhrec_commander_api_response_card_details_display_widget.h"
#include "../../../../game/cards/card_database_manager.h"
EdhrecCommanderApiResponseCardDetailsDisplayWidget::EdhrecCommanderApiResponseCardDetailsDisplayWidget(
QWidget *parent,
const EdhrecCommanderApiResponseCardDetails &_toDisplay)
: QWidget(parent), toDisplay(_toDisplay)
{
layout = new QVBoxLayout(this);
setLayout(layout);
cardPictureWidget = new CardInfoPictureWidget(this);
cardPictureWidget->setCard(CardDatabaseManager::getInstance()->getCard(toDisplay.name));
label = new QLabel(this);
label->setText(toDisplay.name + "\n" + toDisplay.label);
label->setAlignment(Qt::AlignHCenter);
// Set label color based on inclusion rate
int inclusionRate = (toDisplay.numDecks * 100) / toDisplay.potentialDecks;
QColor labelColor;
if (inclusionRate <= 30) {
labelColor = QColor(255, 0, 0); // Red
} else if (inclusionRate <= 60) {
int red = 255 - ((inclusionRate - 30) * 2);
int green = (inclusionRate - 30) * 4; // Adjust green to make the transition smoother
labelColor = QColor(red, green, 0); // purple-ish
} else if (inclusionRate <= 90) {
int green = (inclusionRate - 60) * 5; // Increase green
labelColor = QColor(100, green, 100); // Green shades
} else {
labelColor = QColor(100, 200, 100); // Dark Green
}
label->setStyleSheet(QString("color: %1").arg(labelColor.name()));
layout->addWidget(cardPictureWidget);
layout->addWidget(label);
}

View File

@@ -0,0 +1,26 @@
#ifndef EDHREC_COMMANDER_API_RESPONSE_CARD_DETAILS_DISPLAY_WIDGET_H
#define EDHREC_COMMANDER_API_RESPONSE_CARD_DETAILS_DISPLAY_WIDGET_H
#include "../../../ui/widgets/cards/card_info_picture_widget.h"
#include "api_response/edhrec_commander_api_response_card_details.h"
#include <QHBoxLayout>
#include <QLabel>
#include <QWidget>
class EdhrecCommanderApiResponseCardDetailsDisplayWidget : public QWidget
{
Q_OBJECT
public:
explicit EdhrecCommanderApiResponseCardDetailsDisplayWidget(
QWidget *parent,
const EdhrecCommanderApiResponseCardDetails &_toDisplay);
private:
EdhrecCommanderApiResponseCardDetails toDisplay;
QVBoxLayout *layout;
CardInfoPictureWidget *cardPictureWidget;
QLabel *label;
};
#endif // EDHREC_COMMANDER_API_RESPONSE_CARD_DETAILS_DISPLAY_WIDGET_H

View File

@@ -0,0 +1,34 @@
#include "edhrec_commander_api_response_card_list_display_widget.h"
#include "../../../ui/widgets/general/display/banner_widget.h"
#include "edhrec_commander_api_response_card_details_display_widget.h"
#include <QLabel>
EdhrecCommanderApiResponseCardListDisplayWidget::EdhrecCommanderApiResponseCardListDisplayWidget(
QWidget *parent,
EdhrecCommanderApiResponseCardList toDisplay)
: QWidget(parent)
{
layout = new QVBoxLayout(this);
setLayout(layout);
header = new BannerWidget(this, toDisplay.header);
flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAlwaysOff);
header->setBuddy(flowWidget);
foreach (EdhrecCommanderApiResponseCardDetails card_detail, toDisplay.cardViews) {
auto widget = new EdhrecCommanderApiResponseCardDetailsDisplayWidget(flowWidget, card_detail);
flowWidget->addWidget(widget);
}
layout->addWidget(header);
layout->addWidget(flowWidget);
}
void EdhrecCommanderApiResponseCardListDisplayWidget::resizeEvent(QResizeEvent *event)
{
QWidget::resizeEvent(event);
qDebug() << event->size();
}

View File

@@ -0,0 +1,30 @@
#ifndef EDHREC_COMMANDER_API_RESPONSE_CARD_LIST_DISPLAY_WIDGET_H
#define EDHREC_COMMANDER_API_RESPONSE_CARD_LIST_DISPLAY_WIDGET_H
#include "../../../ui/widgets/general/display/banner_widget.h"
#include "../../../ui/widgets/general/layout_containers/flow_widget.h"
#include "api_response/edhrec_commander_api_response_card_list.h"
#include <QResizeEvent>
#include <QVBoxLayout>
#include <QWidget>
class EdhrecCommanderApiResponseCardListDisplayWidget : public QWidget
{
Q_OBJECT
public:
explicit EdhrecCommanderApiResponseCardListDisplayWidget(QWidget *parent,
EdhrecCommanderApiResponseCardList toDisplay);
void resizeEvent(QResizeEvent *event) override;
[[nodiscard]] QString getBannerText() const
{
return header->getText();
};
private:
QVBoxLayout *layout;
BannerWidget *header;
FlowWidget *flowWidget;
};
#endif // EDHREC_COMMANDER_API_RESPONSE_CARD_LIST_DISPLAY_WIDGET_H

View File

@@ -0,0 +1,36 @@
#include "edhrec_commander_api_response_commander_details_display_widget.h"
#include "../../../../game/cards/card_database_manager.h"
#include "../../../ui/widgets/cards/card_info_picture_widget.h"
#include <QLabel>
EdhrecCommanderResponseCommanderDetailsDisplayWidget::EdhrecCommanderResponseCommanderDetailsDisplayWidget(
QWidget *parent,
const EdhrecCommanderApiResponseCommanderDetails &_commanderDetails)
: QWidget(parent), commanderDetails(_commanderDetails)
{
layout = new QVBoxLayout(this);
setLayout(layout);
commanderPicture = new CardInfoPictureWidget(this);
commanderPicture->setCard(CardDatabaseManager::getInstance()->getCard(commanderDetails.getName()));
commanderDetails.debugPrint();
label = new QLabel(this);
label->setAlignment(Qt::AlignCenter);
salt = new QLabel(this);
salt->setAlignment(Qt::AlignCenter);
layout->addWidget(commanderPicture);
layout->addWidget(label);
layout->addWidget(salt);
retranslateUi();
}
void EdhrecCommanderResponseCommanderDetailsDisplayWidget::retranslateUi()
{
label->setText(commanderDetails.getLabel());
salt->setText(tr("Salt: ") + QString::number(commanderDetails.getSalt()));
}

View File

@@ -0,0 +1,28 @@
#ifndef EDHREC_COMMANDER_API_RESPONSE_COMMANDER_DETAILS_DISPLAY_WIDGET_H
#define EDHREC_COMMANDER_API_RESPONSE_COMMANDER_DETAILS_DISPLAY_WIDGET_H
#include "../../../ui/widgets/cards/card_info_picture_widget.h"
#include "api_response/edhrec_commander_api_response_commander_details.h"
#include <QLabel>
#include <QVBoxLayout>
#include <QWidget>
class EdhrecCommanderResponseCommanderDetailsDisplayWidget : public QWidget
{
Q_OBJECT
public:
explicit EdhrecCommanderResponseCommanderDetailsDisplayWidget(
QWidget *parent,
const EdhrecCommanderApiResponseCommanderDetails &_commanderDetails);
void retranslateUi();
private:
QLabel *label;
QLabel *salt;
QVBoxLayout *layout;
CardInfoPictureWidget *commanderPicture;
EdhrecCommanderApiResponseCommanderDetails commanderDetails;
};
#endif // EDHREC_COMMANDER_API_RESPONSE_COMMANDER_DETAILS_DISPLAY_WIDGET_H

View File

@@ -0,0 +1,102 @@
#include "edhrec_commander_api_response_display_widget.h"
#include "../../../ui/widgets/cards/card_info_picture_widget.h"
#include "api_response/edhrec_commander_api_response.h"
#include "edhrec_commander_api_response_card_list_display_widget.h"
#include "edhrec_commander_api_response_commander_details_display_widget.h"
#include <QListView>
#include <QResizeEvent>
#include <QScrollArea>
#include <QSplitter>
#include <QStringListModel>
EdhrecCommanderApiResponseDisplayWidget::EdhrecCommanderApiResponseDisplayWidget(QWidget *parent,
EdhrecCommanderApiResponse response)
: QWidget(parent)
{
layout = new QHBoxLayout(this);
setLayout(layout);
cardDisplayLayout = new QVBoxLayout(this);
// Create a QSplitter to hold the ListView and ScrollArea holding CardListdisplayWidgets side by side
auto splitter = new QSplitter(this);
splitter->setOrientation(Qt::Horizontal);
auto listView = new QListView(splitter);
listView->setMinimumWidth(50);
listView->setMaximumWidth(150);
auto listModel = new QStringListModel(this);
QStringList widgetNames;
// Add commander details
auto commanderPicture =
new EdhrecCommanderResponseCommanderDetailsDisplayWidget(this, response.container.getCommanderDetails());
cardDisplayLayout->addWidget(commanderPicture);
widgetNames.append("Commander Details");
// Add card list widgets
auto edhrec_commander_api_response_card_lists = response.container.getCardlists();
for (const EdhrecCommanderApiResponseCardList &card_list : edhrec_commander_api_response_card_lists) {
auto cardListDisplayWidget = new EdhrecCommanderApiResponseCardListDisplayWidget(this, card_list);
cardDisplayLayout->addWidget(cardListDisplayWidget);
widgetNames.append(cardListDisplayWidget->getBannerText());
}
// Create a QScrollArea to hold the card display widgets
scrollArea = new QScrollArea(splitter);
scrollArea->setWidgetResizable(true);
scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
// Set the cardDisplayLayout inside the scroll area
auto scrollWidget = new QWidget(scrollArea);
scrollWidget->setLayout(cardDisplayLayout);
connect(splitter, &QSplitter::splitterMoved, this, &EdhrecCommanderApiResponseDisplayWidget::onSplitterChange);
scrollArea->setWidget(scrollWidget);
// Configure the list view
listModel->setStringList(widgetNames);
listView->setModel(listModel);
listView->setEditTriggers(QAbstractItemView::NoEditTriggers);
// Connect the list view to ensure the corresponding widget is visible
connect(listView, &QListView::clicked, this, [this](const QModelIndex &index) {
int widgetIndex = index.row();
qDebug() << "clicked: " << widgetIndex;
auto targetWidget = cardDisplayLayout->itemAt(widgetIndex)->widget();
if (targetWidget) {
qDebug() << "Found targetWidget" << targetWidget;
// Attempt to cast the parent to QScrollArea
auto scrollArea = qobject_cast<QScrollArea *>(this->scrollArea); // Use the scroll area instance
if (scrollArea) {
qDebug() << "ScrollArea" << scrollArea;
scrollArea->ensureWidgetVisible(targetWidget);
}
}
});
// Add splitter to the main layout
splitter->addWidget(listView);
splitter->addWidget(scrollArea);
layout->addWidget(splitter);
}
void EdhrecCommanderApiResponseDisplayWidget::onSplitterChange()
{
scrollArea->widget()->resize(scrollArea->size());
}
void EdhrecCommanderApiResponseDisplayWidget::resizeEvent(QResizeEvent *event)
{
QWidget::resizeEvent(event);
qDebug() << event->size();
layout->invalidate();
layout->activate();
layout->update();
if (scrollArea && scrollArea->widget()) {
scrollArea->widget()->resize(event->size());
}
}

View File

@@ -0,0 +1,27 @@
#ifndef EDHREC_COMMANDER_API_RESPONSE_DISPLAY_WIDGET_H
#define EDHREC_COMMANDER_API_RESPONSE_DISPLAY_WIDGET_H
#include "api_response/edhrec_commander_api_response.h"
#include <QScrollArea>
#include <QVBoxLayout>
#include <QWidget>
class EdhrecCommanderApiResponseDisplayWidget : public QWidget
{
Q_OBJECT
public:
explicit EdhrecCommanderApiResponseDisplayWidget(QWidget *parent, EdhrecCommanderApiResponse response);
void resizeEvent(QResizeEvent *event) override;
public slots:
void onSplitterChange();
private:
QHBoxLayout *layout;
QVBoxLayout *cardDisplayLayout;
QScrollArea *scrollArea;
};
#endif // EDHREC_COMMANDER_API_RESPONSE_DISPLAY_WIDGET_H

View File

@@ -0,0 +1,112 @@
#include "tab_edhrec.h"
#include "api_response/edhrec_commander_api_response.h"
#include "edhrec_commander_api_response_display_widget.h"
#include <QDebug>
#include <QHBoxLayout>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QRegularExpression>
#include <QResizeEvent>
TabEdhRec::TabEdhRec(TabSupervisor *_tabSupervisor) : Tab(_tabSupervisor)
{
networkManager = new QNetworkAccessManager(this);
#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0))
networkManager->setTransferTimeout(); // Use Qt's default timeout
#endif
networkManager->setRedirectPolicy(QNetworkRequest::ManualRedirectPolicy);
connect(networkManager, SIGNAL(finished(QNetworkReply *)), this, SLOT(processApiJson(QNetworkReply *)));
}
void TabEdhRec::retranslateUi()
{
}
void TabEdhRec::setCard(CardInfoPtr _cardToQuery, bool isCommander)
{
cardToQuery = _cardToQuery;
if (!cardToQuery) {
qDebug() << "Invalid card information provided.";
return;
}
QString cardName = cardToQuery->getName();
QString formattedName = cardName.toLower().replace(" ", "-").remove(QRegularExpression("[^a-z0-9\\-]"));
QString url;
if (isCommander) {
url = QString("https://json.edhrec.com/pages/commanders/%1.json").arg(formattedName);
} else {
url = QString("https://json.edhrec.com/pages/cards/%1.json").arg(formattedName);
}
QNetworkRequest request{QUrl(url)};
networkManager->get(request);
}
void TabEdhRec::processApiJson(QNetworkReply *reply)
{
if (reply->error() != QNetworkReply::NoError) {
qDebug() << "Network error occurred:" << reply->errorString();
reply->deleteLater();
return;
}
QByteArray responseData = reply->readAll();
QJsonDocument jsonDoc = QJsonDocument::fromJson(responseData);
if (!jsonDoc.isObject()) {
qDebug() << "Invalid JSON response received.";
reply->deleteLater();
return;
}
QJsonObject jsonObj = jsonDoc.object();
// qDebug() << jsonObj;
EdhrecCommanderApiResponse deckData;
deckData.fromJson(jsonObj);
displayWidget = new EdhrecCommanderApiResponseDisplayWidget(this, deckData);
// flowWidget->addWidget(displayWidget);
setCentralWidget(displayWidget);
reply->deleteLater();
update();
}
void TabEdhRec::prettyPrintJson(const QJsonValue &value, int indentLevel)
{
const QString indent(indentLevel * 2, ' '); // Adjust spacing as needed for pretty printing
if (value.isObject()) {
QJsonObject obj = value.toObject();
for (auto it = obj.begin(); it != obj.end(); ++it) {
qDebug().noquote() << indent + it.key() + ":";
prettyPrintJson(it.value(), indentLevel + 1);
}
} else if (value.isArray()) {
QJsonArray array = value.toArray();
for (int i = 0; i < array.size(); ++i) {
qDebug().noquote() << indent + QString("[%1]:").arg(i);
prettyPrintJson(array[i], indentLevel + 1);
}
} else if (value.isString()) {
qDebug().noquote() << indent + "\"" + value.toString() + "\"";
} else if (value.isDouble()) {
qDebug().noquote() << indent + QString::number(value.toDouble());
} else if (value.isBool()) {
qDebug().noquote() << indent + (value.toBool() ? "true" : "false");
} else if (value.isNull()) {
qDebug().noquote() << indent + "null";
}
}

View File

@@ -0,0 +1,37 @@
#ifndef TAB_EDHREC_H
#define TAB_EDHREC_H
#include "../../../../game/cards/card_database.h"
#include "../../../ui/widgets/general/layout_containers/flow_widget.h"
#include "../../tab.h"
#include "edhrec_commander_api_response_display_widget.h"
#include <QHBoxLayout>
#include <QNetworkAccessManager>
class TabEdhRec : public Tab
{
Q_OBJECT
public:
explicit TabEdhRec(TabSupervisor *_tabSupervisor);
void retranslateUi() override;
QString getTabText() const override
{
auto cardName = cardToQuery.isNull() ? QString() : cardToQuery->getName();
return tr("EDHREC: ") + cardName;
}
QNetworkAccessManager *networkManager;
public slots:
void processApiJson(QNetworkReply *reply);
void prettyPrintJson(const QJsonValue &value, int indentLevel);
void setCard(CardInfoPtr _cardToQuery, bool isCommander = false);
private:
CardInfoPtr cardToQuery;
EdhrecCommanderApiResponseDisplayWidget *displayWidget;
};
#endif // TAB_EDHREC_H

View File

@@ -1,25 +1,29 @@
#include "tab.h"
#include "../ui/widgets/cards/card_info_display_widget.h"
#include "./tab_supervisor.h"
#include <QApplication>
#include <QCloseEvent>
#include <QDebug>
#include <QScreen>
Tab::Tab(TabSupervisor *_tabSupervisor, QWidget *parent)
: QMainWindow(parent), tabSupervisor(_tabSupervisor), contentsChanged(false), infoPopup(0)
Tab::Tab(TabSupervisor *_tabSupervisor)
: QMainWindow(_tabSupervisor), tabSupervisor(_tabSupervisor), contentsChanged(false), infoPopup(0)
{
setAttribute(Qt::WA_DeleteOnClose);
}
void Tab::showCardInfoPopup(const QPoint &pos, const QString &cardName)
void Tab::showCardInfoPopup(const QPoint &pos, const QString &cardName, const QString &providerId)
{
if (infoPopup) {
infoPopup->deleteLater();
}
currentCardName = cardName;
infoPopup = new CardInfoDisplayWidget(
cardName, 0, Qt::Widget | Qt::FramelessWindowHint | Qt::X11BypassWindowManagerHint | Qt::WindowStaysOnTopHint);
currentProviderId = providerId;
infoPopup = new CardInfoDisplayWidget(cardName, providerId, nullptr,
Qt::Widget | Qt::FramelessWindowHint | Qt::X11BypassWindowManagerHint |
Qt::WindowStaysOnTopHint);
infoPopup->setAttribute(Qt::WA_TransparentForMouseEvents);
auto screenRect = qApp->primaryScreen()->geometry();
@@ -39,3 +43,17 @@ void Tab::deleteCardInfoPopup(const QString &cardName)
}
}
}
/**
* Overrides the closeEvent in order to emit a close signal
*/
void Tab::closeEvent(QCloseEvent *event)
{
emit closed();
event->accept();
}
void Tab::closeRequest(bool /*forced*/)
{
close();
}

View File

@@ -13,6 +13,12 @@ class Tab : public QMainWindow
signals:
void userEvent(bool globalEvent = true);
void tabTextChanged(Tab *tab, const QString &newTabText);
/**
* Emitted when the tab is closed (because Qt doesn't provide a built-in close signal)
* This signal is emitted from this class's overridden Tab::closeEvent method.
* Make sure any subclasses that override closeEvent still emit this signal from there.
*/
void closed();
protected:
TabSupervisor *tabSupervisor;
@@ -21,17 +27,18 @@ protected:
tabMenus.append(menu);
}
protected slots:
void showCardInfoPopup(const QPoint &pos, const QString &cardName);
void showCardInfoPopup(const QPoint &pos, const QString &cardName, const QString &providerId);
void deleteCardInfoPopup(const QString &cardName);
void closeEvent(QCloseEvent *event) override;
private:
QString currentCardName;
QString currentCardName, currentProviderId;
bool contentsChanged;
CardInfoDisplayWidget *infoPopup;
QList<QMenu *> tabMenus;
public:
Tab(TabSupervisor *_tabSupervisor, QWidget *parent = nullptr);
explicit Tab(TabSupervisor *_tabSupervisor);
const QList<QMenu *> &getTabMenus() const
{
return tabMenus;
@@ -50,9 +57,13 @@ public:
}
virtual QString getTabText() const = 0;
virtual void retranslateUi() = 0;
virtual void closeRequest()
{
}
/**
* Sends a request to close the tab.
* Signals for cleanup should be emitted from this method instead of the destructor.
*
* @param forced whether this close request was initiated by the user or forced by the server.
*/
virtual void closeRequest(bool forced = false);
virtual void tabActivated()
{
}

View File

@@ -3,7 +3,8 @@
#include "../../deck/custom_line_edit.h"
#include "../../server/pending_command.h"
#include "../../server/user/user_info_box.h"
#include "../../server/user/user_list.h"
#include "../../server/user/user_list_manager.h"
#include "../../server/user/user_list_widget.h"
#include "../game_logic/abstract_client.h"
#include "../sound_engine.h"
#include "pb/event_add_to_list.pb.h"
@@ -12,125 +13,118 @@
#include "pb/event_user_left.pb.h"
#include "pb/response_list_users.pb.h"
#include "pb/session_commands.pb.h"
#include "tab_supervisor.h"
#include "trice_limits.h"
#include <QHBoxLayout>
#include <QPushButton>
#include <QVBoxLayout>
TabUserLists::TabUserLists(TabSupervisor *_tabSupervisor,
AbstractClient *_client,
const ServerInfo_User &userInfo,
QWidget *parent)
: Tab(_tabSupervisor, parent), client(_client)
TabAccount::TabAccount(TabSupervisor *_tabSupervisor, AbstractClient *_client, const ServerInfo_User &userInfo)
: Tab(_tabSupervisor), client(_client)
{
allUsersList = new UserList(_tabSupervisor, client, UserList::AllUsersList);
buddyList = new UserList(_tabSupervisor, client, UserList::BuddyList);
ignoreList = new UserList(_tabSupervisor, client, UserList::IgnoreList);
allUsersList = new UserListWidget(_tabSupervisor, client, UserListWidget::AllUsersList);
buddyList = new UserListWidget(_tabSupervisor, client, UserListWidget::BuddyList);
ignoreList = new UserListWidget(_tabSupervisor, client, UserListWidget::IgnoreList);
userInfoBox = new UserInfoBox(client, true);
userInfoBox->updateInfo(userInfo);
connect(allUsersList, SIGNAL(openMessageDialog(const QString &, bool)), this,
SIGNAL(openMessageDialog(const QString &, bool)));
connect(buddyList, SIGNAL(openMessageDialog(const QString &, bool)), this,
SIGNAL(openMessageDialog(const QString &, bool)));
connect(ignoreList, SIGNAL(openMessageDialog(const QString &, bool)), this,
SIGNAL(openMessageDialog(const QString &, bool)));
connect(allUsersList, &UserListWidget::openMessageDialog, this, &TabAccount::openMessageDialog);
connect(buddyList, &UserListWidget::openMessageDialog, this, &TabAccount::openMessageDialog);
connect(ignoreList, &UserListWidget::openMessageDialog, this, &TabAccount::openMessageDialog);
connect(client, SIGNAL(userJoinedEventReceived(const Event_UserJoined &)), this,
SLOT(processUserJoinedEvent(const Event_UserJoined &)));
connect(client, SIGNAL(userLeftEventReceived(const Event_UserLeft &)), this,
SLOT(processUserLeftEvent(const Event_UserLeft &)));
connect(client, SIGNAL(buddyListReceived(const QList<ServerInfo_User> &)), this,
SLOT(buddyListReceived(const QList<ServerInfo_User> &)));
connect(client, SIGNAL(ignoreListReceived(const QList<ServerInfo_User> &)), this,
SLOT(ignoreListReceived(const QList<ServerInfo_User> &)));
connect(client, SIGNAL(addToListEventReceived(const Event_AddToList &)), this,
SLOT(processAddToListEvent(const Event_AddToList &)));
connect(client, SIGNAL(removeFromListEventReceived(const Event_RemoveFromList &)), this,
SLOT(processRemoveFromListEvent(const Event_RemoveFromList &)));
connect(client, &AbstractClient::userJoinedEventReceived, this, &TabAccount::processUserJoinedEvent);
connect(client, &AbstractClient::userLeftEventReceived, this, &TabAccount::processUserLeftEvent);
connect(client, &AbstractClient::buddyListReceived, this, &TabAccount::buddyListReceived);
connect(client, &AbstractClient::ignoreListReceived, this, &TabAccount::ignoreListReceived);
connect(client, &AbstractClient::addToListEventReceived, this, &TabAccount::processAddToListEvent);
connect(client, &AbstractClient::removeFromListEventReceived, this, &TabAccount::processRemoveFromListEvent);
PendingCommand *pend = client->prepareSessionCommand(Command_ListUsers());
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
SLOT(processListUsersResponse(const Response &)));
// Attempt to populate the tab with the cache
buddyListReceived(tabSupervisor->getUserListManager()->getBuddyList().values());
ignoreListReceived(tabSupervisor->getUserListManager()->getIgnoreList().values());
PendingCommand *pend = AbstractClient::prepareSessionCommand(Command_ListUsers());
connect(pend, &PendingCommand::finished, this, &TabAccount::processListUsersResponse);
client->sendCommand(pend);
QVBoxLayout *vbox = new QVBoxLayout;
auto *vbox = new QVBoxLayout;
vbox->addWidget(userInfoBox);
vbox->addWidget(allUsersList);
QHBoxLayout *addToBuddyList = new QHBoxLayout;
auto *addToBuddyList = new QHBoxLayout;
addBuddyEdit = new LineEditUnfocusable;
addBuddyEdit->setMaxLength(MAX_NAME_LENGTH);
addBuddyEdit->setPlaceholderText(tr("Add to Buddy List"));
connect(addBuddyEdit, SIGNAL(returnPressed()), this, SLOT(addToBuddyList()));
QPushButton *addBuddyButton = new QPushButton("Add");
connect(addBuddyButton, SIGNAL(clicked()), this, SLOT(addToBuddyList()));
connect(addBuddyEdit, &LineEditUnfocusable::returnPressed, this, &TabAccount::addToBuddyList);
auto *addBuddyButton = new QPushButton("Add");
connect(addBuddyButton, &QPushButton::clicked, this, &TabAccount::addToBuddyList);
addToBuddyList->addWidget(addBuddyEdit);
addToBuddyList->addWidget(addBuddyButton);
QHBoxLayout *addToIgnoreList = new QHBoxLayout;
auto *addToIgnoreList = new QHBoxLayout;
addIgnoreEdit = new LineEditUnfocusable;
addIgnoreEdit->setMaxLength(MAX_NAME_LENGTH);
addIgnoreEdit->setPlaceholderText(tr("Add to Ignore List"));
connect(addIgnoreEdit, SIGNAL(returnPressed()), this, SLOT(addToIgnoreList()));
QPushButton *addIgnoreButton = new QPushButton("Add");
connect(addIgnoreButton, SIGNAL(clicked()), this, SLOT(addToIgnoreList()));
connect(addIgnoreEdit, &LineEditUnfocusable::returnPressed, this, &TabAccount::addToIgnoreList);
auto *addIgnoreButton = new QPushButton("Add");
connect(addIgnoreButton, &QPushButton::clicked, this, &TabAccount::addToIgnoreList);
addToIgnoreList->addWidget(addIgnoreEdit);
addToIgnoreList->addWidget(addIgnoreButton);
QVBoxLayout *buddyPanel = new QVBoxLayout;
auto *buddyPanel = new QVBoxLayout;
buddyPanel->addWidget(buddyList);
buddyPanel->addLayout(addToBuddyList);
QVBoxLayout *ignorePanel = new QVBoxLayout;
auto *ignorePanel = new QVBoxLayout;
ignorePanel->addWidget(ignoreList);
ignorePanel->addLayout(addToIgnoreList);
QHBoxLayout *mainLayout = new QHBoxLayout;
auto *mainLayout = new QHBoxLayout;
mainLayout->addLayout(buddyPanel);
mainLayout->addLayout(ignorePanel);
mainLayout->addLayout(vbox);
retranslateUi();
QWidget *mainWidget = new QWidget(this);
auto *mainWidget = new QWidget(this);
mainWidget->setLayout(mainLayout);
setCentralWidget(mainWidget);
}
void TabUserLists::addToBuddyList()
void TabAccount::addToBuddyList()
{
QString userName = addBuddyEdit->text();
if (userName.length() < 1)
const QString &userName = addBuddyEdit->text();
if (userName.isEmpty()) {
return;
}
std::string listName = "buddy";
const std::string listName = "buddy";
addToList(listName, userName);
addBuddyEdit->clear();
}
void TabUserLists::addToIgnoreList()
void TabAccount::addToIgnoreList()
{
QString userName = addIgnoreEdit->text();
if (userName.length() < 1)
const QString &userName = addIgnoreEdit->text();
if (userName.isEmpty()) {
return;
}
std::string listName = "ignore";
const std::string listName = "ignore";
addToList(listName, userName);
addIgnoreEdit->clear();
}
void TabUserLists::addToList(const std::string &listName, const QString &userName)
void TabAccount::addToList(const std::string &listName, const QString &userName)
{
Command_AddToList cmd;
cmd.set_list(listName);
cmd.set_user_name(userName.toStdString());
client->sendCommand(client->prepareSessionCommand(cmd));
client->sendCommand(AbstractClient::prepareSessionCommand(cmd));
}
void TabUserLists::retranslateUi()
void TabAccount::retranslateUi()
{
allUsersList->retranslateUi();
buddyList->retranslateUi();
@@ -138,14 +132,12 @@ void TabUserLists::retranslateUi()
userInfoBox->retranslateUi();
}
void TabUserLists::processListUsersResponse(const Response &response)
void TabAccount::processListUsersResponse(const Response &response)
{
const Response_ListUsers &resp = response.GetExtension(Response_ListUsers::ext);
const int userListSize = resp.user_list_size();
for (int i = 0; i < userListSize; ++i) {
for (int i = 0; i < resp.user_list_size(); ++i) {
const ServerInfo_User &info = resp.user_list(i);
const QString userName = QString::fromStdString(info.name());
const QString &userName = QString::fromStdString(info.name());
allUsersList->processUserInfo(info, true);
ignoreList->setUserOnline(userName, true);
buddyList->setUserOnline(userName, true);
@@ -156,10 +148,10 @@ void TabUserLists::processListUsersResponse(const Response &response)
buddyList->sortItems();
}
void TabUserLists::processUserJoinedEvent(const Event_UserJoined &event)
void TabAccount::processUserJoinedEvent(const Event_UserJoined &event)
{
const ServerInfo_User &info = event.user_info();
const QString userName = QString::fromStdString(info.name());
const QString &userName = QString::fromStdString(info.name());
allUsersList->processUserInfo(info, true);
ignoreList->setUserOnline(userName, true);
@@ -169,18 +161,20 @@ void TabUserLists::processUserJoinedEvent(const Event_UserJoined &event)
ignoreList->sortItems();
buddyList->sortItems();
if (buddyList->getUsers().keys().contains(userName))
if (buddyList->getUsers().keys().contains(userName)) {
soundEngine->playSound("buddy_join");
}
emit userJoined(info);
}
void TabUserLists::processUserLeftEvent(const Event_UserLeft &event)
void TabAccount::processUserLeftEvent(const Event_UserLeft &event)
{
QString userName = QString::fromStdString(event.name());
const QString &userName = QString::fromStdString(event.name());
if (buddyList->getUsers().keys().contains(userName))
if (buddyList->getUsers().keys().contains(userName)) {
soundEngine->playSound("buddy_leave");
}
if (allUsersList->deleteUser(userName)) {
ignoreList->setUserOnline(userName, false);
@@ -192,47 +186,54 @@ void TabUserLists::processUserLeftEvent(const Event_UserLeft &event)
}
}
void TabUserLists::buddyListReceived(const QList<ServerInfo_User> &_buddyList)
void TabAccount::buddyListReceived(const QList<ServerInfo_User> &_buddyList)
{
for (int i = 0; i < _buddyList.size(); ++i)
buddyList->processUserInfo(_buddyList[i], false);
for (const auto &user : _buddyList) {
buddyList->processUserInfo(user, false);
}
buddyList->sortItems();
}
void TabUserLists::ignoreListReceived(const QList<ServerInfo_User> &_ignoreList)
void TabAccount::ignoreListReceived(const QList<ServerInfo_User> &_ignoreList)
{
for (int i = 0; i < _ignoreList.size(); ++i)
ignoreList->processUserInfo(_ignoreList[i], false);
for (const auto &user : _ignoreList) {
ignoreList->processUserInfo(user, false);
}
ignoreList->sortItems();
}
void TabUserLists::processAddToListEvent(const Event_AddToList &event)
void TabAccount::processAddToListEvent(const Event_AddToList &event)
{
const ServerInfo_User &info = event.user_info();
bool online = allUsersList->getUsers().contains(QString::fromStdString(info.name()));
QString list = QString::fromStdString(event.list_name());
UserList *userList = 0;
if (list == "buddy")
const bool online = allUsersList->getUsers().contains(QString::fromStdString(info.name()));
const QString &list = QString::fromStdString(event.list_name());
UserListWidget *userList;
if (list == "buddy") {
userList = buddyList;
else if (list == "ignore")
} else if (list == "ignore") {
userList = ignoreList;
if (!userList)
} else {
return;
}
userList->processUserInfo(info, online);
userList->sortItems();
}
void TabUserLists::processRemoveFromListEvent(const Event_RemoveFromList &event)
void TabAccount::processRemoveFromListEvent(const Event_RemoveFromList &event)
{
QString list = QString::fromStdString(event.list_name());
QString user = QString::fromStdString(event.user_name());
UserList *userList = 0;
if (list == "buddy")
const auto &list = QString::fromStdString(event.list_name());
const auto &user = QString::fromStdString(event.user_name());
UserListWidget *userList;
if (list == "buddy") {
userList = buddyList;
else if (list == "ignore")
} else if (list == "ignore") {
userList = ignoreList;
if (!userList)
} else {
return;
}
userList->deleteUser(user);
}

View File

@@ -5,25 +5,25 @@
#include "tab.h"
class AbstractClient;
class UserList;
class UserInfoBox;
class LineEditUnfocusable;
class Event_AddToList;
class Event_ListRooms;
class Event_RemoveFromList;
class Event_UserJoined;
class Event_UserLeft;
class LineEditUnfocusable;
class Response;
class ServerInfo_User;
class Event_AddToList;
class Event_RemoveFromList;
class UserInfoBox;
class UserListWidget;
class TabUserLists : public Tab
class TabAccount : public Tab
{
Q_OBJECT
signals:
void openMessageDialog(const QString &userName, bool focus);
void userLeft(const QString &userName);
void userJoined(const ServerInfo_User &userInfo);
private slots:
void processListUsersResponse(const Response &response);
void processUserJoinedEvent(const Event_UserJoined &event);
@@ -37,36 +37,21 @@ private slots:
private:
AbstractClient *client;
UserList *allUsersList;
UserList *buddyList;
UserList *ignoreList;
UserListWidget *allUsersList;
UserListWidget *buddyList;
UserListWidget *ignoreList;
UserInfoBox *userInfoBox;
LineEditUnfocusable *addBuddyEdit;
LineEditUnfocusable *addIgnoreEdit;
void addToList(const std::string &listName, const QString &userName);
public:
TabUserLists(TabSupervisor *_tabSupervisor,
AbstractClient *_client,
const ServerInfo_User &userInfo,
QWidget *parent = nullptr);
void retranslateUi();
QString getTabText() const
explicit TabAccount(TabSupervisor *_tabSupervisor, AbstractClient *_client, const ServerInfo_User &userInfo);
void retranslateUi() override;
[[nodiscard]] QString getTabText() const override
{
return tr("Account");
}
const UserList *getAllUsersList() const
{
return allUsersList;
}
const UserList *getBuddyList() const
{
return buddyList;
}
const UserList *getIgnoreList() const
{
return ignoreList;
}
};
#endif

View File

@@ -1,19 +1,20 @@
#include "tab_admin.h"
#include "../../server/pending_command.h"
#include "../game_logic/abstract_client.h"
#include "pb/admin_commands.pb.h"
#include "pb/event_replay_added.pb.h"
#include "pb/moderator_commands.pb.h"
#include "trice_limits.h"
#include <QDialogButtonBox>
#include <QGridLayout>
#include <QGroupBox>
#include <QHBoxLayout>
#include <QLabel>
#include <QLineEdit>
#include <QMessageBox>
#include <QPushButton>
#include <QSpinBox>
#include <QVBoxLayout>
ShutdownDialog::ShutdownDialog(QWidget *parent) : QDialog(parent)
{
@@ -29,8 +30,8 @@ ShutdownDialog::ShutdownDialog(QWidget *parent) : QDialog(parent)
minutesEdit->setMaximum(999);
QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
connect(buttonBox, SIGNAL(accepted()), this, SLOT(accept()));
connect(buttonBox, SIGNAL(rejected()), this, SLOT(reject()));
connect(buttonBox, &QDialogButtonBox::accepted, this, &ShutdownDialog::accept);
connect(buttonBox, &QDialogButtonBox::rejected, this, &ShutdownDialog::reject);
QGridLayout *mainLayout = new QGridLayout;
mainLayout->addWidget(reasonLabel, 0, 0);
@@ -53,34 +54,66 @@ int ShutdownDialog::getMinutes() const
return minutesEdit->value();
}
TabAdmin::TabAdmin(TabSupervisor *_tabSupervisor, AbstractClient *_client, bool _fullAdmin, QWidget *parent)
: Tab(_tabSupervisor, parent), locked(true), client(_client), fullAdmin(_fullAdmin)
TabAdmin::TabAdmin(TabSupervisor *_tabSupervisor, AbstractClient *_client, bool _fullAdmin)
: Tab(_tabSupervisor), locked(true), client(_client), fullAdmin(_fullAdmin)
{
updateServerMessageButton = new QPushButton;
connect(updateServerMessageButton, SIGNAL(clicked()), this, SLOT(actUpdateServerMessage()));
connect(updateServerMessageButton, &QPushButton::clicked, this, &TabAdmin::actUpdateServerMessage);
shutdownServerButton = new QPushButton;
connect(shutdownServerButton, SIGNAL(clicked()), this, SLOT(actShutdownServer()));
connect(shutdownServerButton, &QPushButton::clicked, this, &TabAdmin::actShutdownServer);
reloadConfigButton = new QPushButton;
connect(reloadConfigButton, SIGNAL(clicked()), this, SLOT(actReloadConfig()));
connect(reloadConfigButton, &QPushButton::clicked, this, &TabAdmin::actReloadConfig);
QVBoxLayout *vbox = new QVBoxLayout;
vbox->addWidget(updateServerMessageButton);
vbox->addWidget(shutdownServerButton);
vbox->addWidget(reloadConfigButton);
vbox->addStretch();
grantReplayAccessButton = new QPushButton;
grantReplayAccessButton->setEnabled(false);
connect(grantReplayAccessButton, &QPushButton::clicked, this, &TabAdmin::actGrantReplayAccess);
replayIdToGrant = new QLineEdit;
replayIdToGrant->setMaximumWidth(500);
replayIdToGrant->setValidator(new QIntValidator(0, INT_MAX, this));
connect(replayIdToGrant, &QLineEdit::textChanged, this,
[=, this]() { grantReplayAccessButton->setEnabled(!replayIdToGrant->text().isEmpty()); });
auto *grandReplayAccessLayout = new QGridLayout(this);
grandReplayAccessLayout->addWidget(replayIdToGrant, 0, 0);
grandReplayAccessLayout->addWidget(grantReplayAccessButton, 0, 1);
activateUserButton = new QPushButton;
activateUserButton->setEnabled(false);
connect(activateUserButton, &QPushButton::clicked, this, &TabAdmin::actForceActivateUser);
userToActivate = new QLineEdit;
userToActivate->setMaximumWidth(500);
connect(userToActivate, &QLineEdit::textChanged, this,
[=, this]() { activateUserButton->setEnabled(!userToActivate->text().isEmpty()); });
auto *activateUserLayout = new QGridLayout(this);
activateUserLayout->addWidget(userToActivate, 0, 0);
activateUserLayout->addWidget(activateUserButton, 0, 1);
auto *adminVBox = new QVBoxLayout;
adminVBox->addWidget(updateServerMessageButton);
adminVBox->addWidget(shutdownServerButton);
adminVBox->addWidget(reloadConfigButton);
adminGroupBox = new QGroupBox;
adminGroupBox->setLayout(vbox);
adminGroupBox->setLayout(adminVBox);
adminGroupBox->setEnabled(false);
auto *moderatorVBox = new QVBoxLayout;
moderatorVBox->addLayout(grandReplayAccessLayout);
moderatorVBox->addLayout(activateUserLayout);
moderatorGroupBox = new QGroupBox;
moderatorGroupBox->setLayout(moderatorVBox);
moderatorGroupBox->setEnabled(false);
unlockButton = new QPushButton;
connect(unlockButton, SIGNAL(clicked()), this, SLOT(actUnlock()));
connect(unlockButton, &QPushButton::clicked, this, &TabAdmin::actUnlock);
lockButton = new QPushButton;
lockButton->setEnabled(false);
connect(lockButton, SIGNAL(clicked()), this, SLOT(actLock()));
connect(lockButton, &QPushButton::clicked, this, &TabAdmin::actLock);
QVBoxLayout *mainLayout = new QVBoxLayout;
mainLayout->addWidget(adminGroupBox);
mainLayout->addWidget(moderatorGroupBox);
mainLayout->addStretch();
mainLayout->addWidget(unlockButton);
mainLayout->addWidget(lockButton);
@@ -99,6 +132,13 @@ void TabAdmin::retranslateUi()
shutdownServerButton->setText(tr("&Shut down server"));
reloadConfigButton->setText(tr("&Reload configuration"));
adminGroupBox->setTitle(tr("Server administration functions"));
moderatorGroupBox->setTitle(tr("Server moderator functions"));
replayIdToGrant->setPlaceholderText(tr("Replay ID"));
grantReplayAccessButton->setText(tr("Grant Replay Access"));
userToActivate->setPlaceholderText(tr("Username to Activate"));
activateUserButton->setText(tr("Force Activate User"));
unlockButton->setText(tr("&Unlock functions"));
lockButton->setText(tr("&Lock functions"));
@@ -117,7 +157,7 @@ void TabAdmin::actShutdownServer()
cmd.set_reason(dlg.getReason().toStdString());
cmd.set_minutes(dlg.getMinutes());
client->sendCommand(client->prepareAdminCommand(cmd));
client->sendCommand(AbstractClient::prepareAdminCommand(cmd));
}
}
@@ -127,22 +167,98 @@ void TabAdmin::actReloadConfig()
client->sendCommand(client->prepareAdminCommand(cmd));
}
void TabAdmin::actGrantReplayAccess()
{
if (!replayIdToGrant) {
return;
}
Command_GrantReplayAccess cmd;
cmd.set_replay_id(replayIdToGrant->text().toUInt());
cmd.set_moderator_name(client->getUserName().toStdString());
auto *pend = client->prepareModeratorCommand(cmd);
connect(pend, &PendingCommand::finished, this, &TabAdmin::grantReplayAccessProcessResponse);
client->sendCommand(pend);
}
void TabAdmin::actForceActivateUser()
{
if (!userToActivate) {
return;
}
Command_ForceActivateUser cmd;
cmd.set_username_to_activate(userToActivate->text().trimmed().toStdString());
cmd.set_moderator_name(client->getUserName().toStdString());
auto *pend = client->prepareModeratorCommand(cmd);
connect(pend,
QOverload<const Response &, const CommandContainer &, const QVariant &>::of(&PendingCommand::finished),
this, &TabAdmin::activateUserProcessResponse);
client->sendCommand(pend);
}
void TabAdmin::grantReplayAccessProcessResponse(const Response &response)
{
auto *event = new Event_ReplayAdded();
switch (response.response_code()) {
case Response::RespOk:
client->replayAddedEventReceived(*event);
QMessageBox::information(this, tr("Success"), tr("Replay access granted"));
break;
case Response::RespContextError:
QMessageBox::critical(this, tr("Error"), tr("Unable to grant replay access. Replay ID invalid"));
break;
default:
QMessageBox::critical(this, tr("Error"), tr("Unable to grant replay access. Internal error"));
break;
}
}
void TabAdmin::activateUserProcessResponse(const Response &response)
{
switch (response.response_code()) {
case Response::RespActivationAccepted:
QMessageBox::information(this, tr("Success"), tr("User successfully activated"));
break;
case Response::RespNameNotFound:
QMessageBox::critical(this, tr("Error"), tr("Unable to activate user. Username invalid"));
break;
case Response::RespActivationFailed:
QMessageBox::critical(this, tr("Error"), tr("Unable to activate user. User already active"));
break;
default:
QMessageBox::critical(this, tr("Error"), tr("Unable to activate user. Internal error"));
break;
}
}
void TabAdmin::actUnlock()
{
if (fullAdmin)
if (fullAdmin) {
adminGroupBox->setEnabled(true);
}
moderatorGroupBox->setEnabled(true);
lockButton->setEnabled(true);
unlockButton->setEnabled(false);
locked = false;
emit adminLockChanged(false);
}
void TabAdmin::actLock()
{
if (fullAdmin)
if (fullAdmin) {
adminGroupBox->setEnabled(false);
}
moderatorGroupBox->setEnabled(false);
lockButton->setEnabled(false);
unlockButton->setEnabled(true);
locked = true;
emit adminLockChanged(true);
}

View File

@@ -1,6 +1,8 @@
#ifndef TAB_ADMIN_H
#define TAB_ADMIN_H
#include "pb/commands.pb.h"
#include "pb/response.pb.h"
#include "tab.h"
#include <QDialog>
@@ -20,7 +22,7 @@ private:
QSpinBox *minutesEdit;
public:
ShutdownDialog(QWidget *parent = nullptr);
explicit ShutdownDialog(QWidget *parent = nullptr);
QString getReason() const;
int getMinutes() const;
};
@@ -32,23 +34,29 @@ private:
bool locked;
AbstractClient *client;
bool fullAdmin;
QPushButton *updateServerMessageButton, *shutdownServerButton, *reloadConfigButton;
QGroupBox *adminGroupBox;
QPushButton *updateServerMessageButton, *shutdownServerButton, *reloadConfigButton, *grantReplayAccessButton,
*activateUserButton;
QGroupBox *adminGroupBox, *moderatorGroupBox;
QPushButton *unlockButton, *lockButton;
QLineEdit *replayIdToGrant, *userToActivate;
signals:
void adminLockChanged(bool lock);
private slots:
void actUpdateServerMessage();
void actShutdownServer();
void actReloadConfig();
void actGrantReplayAccess();
void actForceActivateUser();
void grantReplayAccessProcessResponse(const Response &response);
void activateUserProcessResponse(const Response &response);
void actUnlock();
void actLock();
public:
TabAdmin(TabSupervisor *_tabSupervisor, AbstractClient *_client, bool _fullAdmin, QWidget *parent = nullptr);
void retranslateUi();
QString getTabText() const
TabAdmin(TabSupervisor *_tabSupervisor, AbstractClient *_client, bool _fullAdmin);
void retranslateUi() override;
QString getTabText() const override
{
return tr("Administration");
}

View File

@@ -3,8 +3,8 @@
#include "../../client/game_logic/abstract_client.h"
#include "../../client/tapped_out_interface.h"
#include "../../client/ui/widgets/cards/card_info_frame_widget.h"
#include "../../deck/deck_list_model.h"
#include "../../deck/deck_stats_interface.h"
#include "../../dialogs/dlg_load_deck.h"
#include "../../dialogs/dlg_load_deck_from_clipboard.h"
#include "../../game/cards/card_database_manager.h"
#include "../../game/cards/card_database_model.h"
@@ -13,9 +13,8 @@
#include "../../main.h"
#include "../../server/pending_command.h"
#include "../../settings/cache_settings.h"
#include "../ui/picture_loader.h"
#include "../ui/picture_loader/picture_loader.h"
#include "../ui/pixel_map_generator.h"
#include "../ui/widgets/printing_selector/printing_selector.h"
#include "pb/command_deck_upload.pb.h"
#include "pb/response.pb.h"
#include "tab_supervisor.h"
@@ -25,6 +24,8 @@
#include <QApplication>
#include <QClipboard>
#include <QCloseEvent>
#include <QComboBox>
#include <QDebug>
#include <QDesktopServices>
#include <QDir>
#include <QDockWidget>
@@ -50,20 +51,11 @@
#include <QUrl>
#include <QVBoxLayout>
void SearchLineEdit::keyPressEvent(QKeyEvent *event)
static bool canBeCommander(const CardInfoPtr &cardInfo)
{
// List of key events that must be handled by the card list instead of the search box
static const QVector<Qt::Key> forwardToTreeView = {Qt::Key_Up, Qt::Key_Down, Qt::Key_PageDown, Qt::Key_PageUp};
// forward only if the search text is empty
static const QVector<Qt::Key> forwardWhenEmpty = {Qt::Key_Home, Qt::Key_End};
Qt::Key key = static_cast<Qt::Key>(event->key());
if (treeView) {
if (forwardToTreeView.contains(key))
QCoreApplication::sendEvent(treeView, event);
if (text().isEmpty() && forwardWhenEmpty.contains(key))
QCoreApplication::sendEvent(treeView, event);
}
LineEditUnfocusable::keyPressEvent(event);
return ((cardInfo->getCardType().contains("Legendary", Qt::CaseInsensitive) &&
cardInfo->getCardType().contains("Creature", Qt::CaseInsensitive))) ||
cardInfo->getText().contains("can be your commander", Qt::CaseInsensitive);
}
void TabDeckEditor::createDeckDock()
@@ -80,6 +72,7 @@ void TabDeckEditor::createDeckDock()
deckView->header()->setSectionResizeMode(QHeaderView::ResizeToContents);
deckView->installEventFilter(&deckViewKeySignals);
deckView->setContextMenuPolicy(Qt::CustomContextMenu);
deckView->setSelectionMode(QAbstractItemView::ExtendedSelection);
connect(deckView->selectionModel(), SIGNAL(currentRowChanged(const QModelIndex &, const QModelIndex &)), this,
SLOT(updateCardInfoRight(const QModelIndex &, const QModelIndex &)));
connect(deckView->selectionModel(), SIGNAL(currentRowChanged(const QModelIndex &, const QModelIndex &)), this,
@@ -109,6 +102,16 @@ void TabDeckEditor::createDeckDock()
commentsEdit->setObjectName("commentsEdit");
commentsLabel->setBuddy(commentsEdit);
connect(commentsEdit, SIGNAL(textChanged()), this, SLOT(updateComments()));
bannerCardLabel = new QLabel();
bannerCardLabel->setObjectName("bannerCardLabel");
bannerCardLabel->setText(tr("Banner Card"));
bannerCardComboBox = new QComboBox(this);
connect(deckModel, &DeckListModel::dataChanged, this, [this]() {
// Delay the update to avoid race conditions
QTimer::singleShot(100, this, &TabDeckEditor::updateBannerCardComboBox);
});
connect(bannerCardComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
&TabDeckEditor::setBannerCard);
aIncrement = new QAction(QString(), this);
aIncrement->setIcon(QPixmap("theme:icons/increment"));
@@ -128,6 +131,12 @@ void TabDeckEditor::createDeckDock()
auto *tbRemoveCard = new QToolButton(this);
tbRemoveCard->setDefaultAction(aRemoveCard);
aSwapCard = new QAction(QString(), this);
aSwapCard->setIcon(QPixmap("theme:icons/swap"));
connect(aSwapCard, SIGNAL(triggered()), this, SLOT(actSwapCard()));
auto *tbSwapCard = new QToolButton(this);
tbSwapCard->setDefaultAction(aSwapCard);
auto *upperLayout = new QGridLayout;
upperLayout->setObjectName("upperLayout");
upperLayout->addWidget(nameLabel, 0, 0);
@@ -136,6 +145,9 @@ void TabDeckEditor::createDeckDock()
upperLayout->addWidget(commentsLabel, 1, 0);
upperLayout->addWidget(commentsEdit, 1, 1);
upperLayout->addWidget(bannerCardLabel, 2, 0);
upperLayout->addWidget(bannerCardComboBox, 2, 1);
hashLabel1 = new QLabel();
hashLabel1->setObjectName("hashLabel1");
auto *hashSizePolicy = new QSizePolicy();
@@ -153,7 +165,8 @@ void TabDeckEditor::createDeckDock()
lowerLayout->addWidget(tbIncrement, 0, 2);
lowerLayout->addWidget(tbDecrement, 0, 3);
lowerLayout->addWidget(tbRemoveCard, 0, 4);
lowerLayout->addWidget(deckView, 1, 0, 1, 5);
lowerLayout->addWidget(tbSwapCard, 0, 5);
lowerLayout->addWidget(deckView, 1, 0, 1, 6);
// Create widgets for both layouts to make splitter work correctly
auto *topWidget = new QWidget;
@@ -308,6 +321,15 @@ void TabDeckEditor::createMenus()
aLoadDeck = new QAction(QString(), this);
connect(aLoadDeck, SIGNAL(triggered()), this, SLOT(actLoadDeck()));
loadRecentDeckMenu = new QMenu(this);
connect(&SettingsCache::instance().recents(), &RecentsSettings::recentlyOpenedDeckPathsChanged, this,
&TabDeckEditor::updateRecentlyOpened);
aClearRecents = new QAction(QString(), this);
connect(aClearRecents, &QAction::triggered, this, &TabDeckEditor::actClearRecents);
updateRecentlyOpened();
aSaveDeck = new QAction(QString(), this);
connect(aSaveDeck, SIGNAL(triggered()), this, SLOT(actSaveDeck()));
@@ -320,9 +342,17 @@ void TabDeckEditor::createMenus()
aSaveDeckToClipboard = new QAction(QString(), this);
connect(aSaveDeckToClipboard, SIGNAL(triggered()), this, SLOT(actSaveDeckToClipboard()));
aSaveDeckToClipboardNoSetNameAndNumber = new QAction(QString(), this);
connect(aSaveDeckToClipboardNoSetNameAndNumber, SIGNAL(triggered()), this,
SLOT(actSaveDeckToClipboardNoSetNameAndNumber()));
aSaveDeckToClipboardRaw = new QAction(QString(), this);
connect(aSaveDeckToClipboardRaw, SIGNAL(triggered()), this, SLOT(actSaveDeckToClipboardRaw()));
aSaveDeckToClipboardRawNoSetNameAndNumber = new QAction(QString(), this);
connect(aSaveDeckToClipboardRawNoSetNameAndNumber, SIGNAL(triggered()), this,
SLOT(actSaveDeckToClipboardRawNoSetNameAndNumber()));
aPrintDeck = new QAction(QString(), this);
connect(aPrintDeck, SIGNAL(triggered()), this, SLOT(actPrintDeck()));
@@ -341,7 +371,7 @@ void TabDeckEditor::createMenus()
analyzeDeckMenu->addAction(aAnalyzeDeckTappedout);
aClose = new QAction(QString(), this);
connect(aClose, SIGNAL(triggered()), this, SLOT(closeRequest()));
connect(aClose, &QAction::triggered, this, [this] { closeRequest(); });
aClearFilterAll = new QAction(QString(), this);
aClearFilterAll->setIcon(QPixmap("theme:icons/clearsearch"));
@@ -353,11 +383,14 @@ void TabDeckEditor::createMenus()
saveDeckToClipboardMenu = new QMenu(this);
saveDeckToClipboardMenu->addAction(aSaveDeckToClipboard);
saveDeckToClipboardMenu->addAction(aSaveDeckToClipboardNoSetNameAndNumber);
saveDeckToClipboardMenu->addAction(aSaveDeckToClipboardRaw);
saveDeckToClipboardMenu->addAction(aSaveDeckToClipboardRawNoSetNameAndNumber);
deckMenu = new QMenu(this);
deckMenu->addAction(aNewDeck);
deckMenu->addAction(aLoadDeck);
deckMenu->addMenu(loadRecentDeckMenu);
deckMenu->addAction(aSaveDeck);
deckMenu->addAction(aSaveDeckAs);
deckMenu->addSeparator();
@@ -514,31 +547,40 @@ void TabDeckEditor::databaseCustomMenu(QPoint point)
QMenu menu;
const CardInfoPtr info = currentCardInfo();
// add to deck and sideboard options
QAction *addToDeck, *addToSideboard, *selectPrinting;
addToDeck = menu.addAction(tr("Add to Deck"));
addToSideboard = menu.addAction(tr("Add to Sideboard"));
selectPrinting = menu.addAction(tr("Select Printing"));
connect(addToDeck, SIGNAL(triggered()), this, SLOT(actAddCard()));
connect(addToSideboard, SIGNAL(triggered()), this, SLOT(actAddCardToSideboard()));
connect(selectPrinting, &QAction::triggered, this, [this, info] { this->showPrintingSelector(); });
// filling out the related cards submenu
auto *relatedMenu = new QMenu(tr("Show Related cards"));
menu.addMenu(relatedMenu);
auto relatedCards = info->getAllRelatedCards();
if (relatedCards.isEmpty()) {
relatedMenu->setDisabled(true);
} else {
for (const CardRelation *rel : relatedCards) {
const QString &relatedCardName = rel->getName();
QAction *relatedCard = relatedMenu->addAction(relatedCardName);
connect(relatedCard, &QAction::triggered, cardInfo,
[this, relatedCardName] { cardInfo->setCard(relatedCardName); });
if (info) {
// add to deck and sideboard options
QAction *addToDeck, *addToSideboard, *selectPrinting, *edhRecCommander, *edhRecCard;
addToDeck = menu.addAction(tr("Add to Deck"));
addToSideboard = menu.addAction(tr("Add to Sideboard"));
selectPrinting = menu.addAction(tr("Select Printing"));
if (canBeCommander(info)) {
edhRecCommander = menu.addAction(tr("Show on EDHREC (Commander)"));
connect(edhRecCommander, &QAction::triggered, this,
[this, info] { this->tabSupervisor->addEdhrecTab(info, true); });
}
edhRecCard = menu.addAction(tr("Show on EDHREC (Card)"));
connect(addToDeck, SIGNAL(triggered()), this, SLOT(actAddCard()));
connect(addToSideboard, SIGNAL(triggered()), this, SLOT(actAddCardToSideboard()));
connect(selectPrinting, &QAction::triggered, this, [this, info] { this->showPrintingSelector(); });
connect(edhRecCard, &QAction::triggered, this, [this, info] { this->tabSupervisor->addEdhrecTab(info); });
// filling out the related cards submenu
auto *relatedMenu = new QMenu(tr("Show Related cards"));
menu.addMenu(relatedMenu);
auto relatedCards = info->getAllRelatedCards();
if (relatedCards.isEmpty()) {
relatedMenu->setDisabled(true);
} else {
for (const CardRelation *rel : relatedCards) {
const QString &relatedCardName = rel->getName();
QAction *relatedCard = relatedMenu->addAction(relatedCardName);
connect(relatedCard, &QAction::triggered, cardInfo,
[this, relatedCardName] { cardInfo->setCard(relatedCardName); });
}
}
menu.exec(databaseView->mapToGlobal(point));
}
menu.exec(databaseView->mapToGlobal(point));
}
void TabDeckEditor::decklistCustomMenu(QPoint point)
@@ -650,10 +692,10 @@ void TabDeckEditor::loadLayout()
restoreGeometry(layouts.getDeckEditorGeometry());
}
aCardInfoDockVisible->setChecked(cardInfoDock->isVisible());
aFilterDockVisible->setChecked(filterDock->isVisible());
aDeckDockVisible->setChecked(deckDock->isVisible());
aPrintingSelectorDockVisible->setChecked(printingSelectorDock->isVisible());
aCardInfoDockVisible->setChecked(!cardInfoDock->isHidden());
aFilterDockVisible->setChecked(!filterDock->isHidden());
aDeckDockVisible->setChecked(!deckDock->isHidden());
aPrintingSelectorDockVisible->setChecked(!printingSelectorDock->isHidden());
aCardInfoDockFloating->setEnabled(aCardInfoDockVisible->isChecked());
aDeckDockFloating->setEnabled(aDeckDockVisible->isChecked());
@@ -680,8 +722,7 @@ void TabDeckEditor::loadLayout()
QTimer::singleShot(100, this, SLOT(freeDocksSize()));
}
TabDeckEditor::TabDeckEditor(TabSupervisor *_tabSupervisor, QWidget *parent)
: Tab(_tabSupervisor, parent), modified(false)
TabDeckEditor::TabDeckEditor(TabSupervisor *_tabSupervisor) : Tab(_tabSupervisor), modified(false)
{
setObjectName("TabDeckEditor");
@@ -703,11 +744,6 @@ TabDeckEditor::TabDeckEditor(TabSupervisor *_tabSupervisor, QWidget *parent)
loadLayout();
}
TabDeckEditor::~TabDeckEditor()
{
emit deckEditorClosing(this);
}
void TabDeckEditor::retranslateUi()
{
cardInfo->retranslateUi();
@@ -721,13 +757,17 @@ void TabDeckEditor::retranslateUi()
aNewDeck->setText(tr("&New deck"));
aLoadDeck->setText(tr("&Load deck..."));
loadRecentDeckMenu->setTitle(tr("Load recent deck..."));
aClearRecents->setText(tr("Clear"));
aSaveDeck->setText(tr("&Save deck"));
aSaveDeckAs->setText(tr("Save deck &as..."));
aLoadDeckFromClipboard->setText(tr("Load deck from cl&ipboard..."));
saveDeckToClipboardMenu->setTitle(tr("Save deck to clipboard"));
aSaveDeckToClipboard->setText(tr("Annotated"));
aSaveDeckToClipboardNoSetNameAndNumber->setText(tr("Annotated (No set name or number)"));
aSaveDeckToClipboardRaw->setText(tr("Not Annotated"));
aSaveDeckToClipboardRawNoSetNameAndNumber->setText(tr("Not Annotated (No set name or number)"));
aPrintDeck->setText(tr("&Print deck..."));
@@ -741,11 +781,10 @@ void TabDeckEditor::retranslateUi()
aAddCard->setText(tr("Add card to &maindeck"));
aAddCardToSideboard->setText(tr("Add card to &sideboard"));
aRemoveCard->setText(tr("&Remove row"));
aIncrement->setText(tr("&Increment number"));
aDecrement->setText(tr("&Decrement number"));
aRemoveCard->setText(tr("&Remove row"));
aSwapCard->setText(tr("Swap card to/from sideboard"));
deckMenu->setTitle(tr("&Deck Editor"));
@@ -797,6 +836,79 @@ void TabDeckEditor::updateComments()
setSaveStatus(true);
}
void TabDeckEditor::updateBannerCardComboBox()
{
// Store the current text of the combo box
QString currentText = bannerCardComboBox->currentText();
// Block signals temporarily
bool wasBlocked = bannerCardComboBox->blockSignals(true);
// Clear the existing items in the combo box
bannerCardComboBox->clear();
// Prepare the new items with deduplication
QSet<QPair<QString, QString>> bannerCardSet;
InnerDecklistNode *listRoot = deckModel->getDeckList()->getRoot();
for (int i = 0; i < listRoot->size(); i++) {
InnerDecklistNode *currentZone = dynamic_cast<InnerDecklistNode *>(listRoot->at(i));
for (int j = 0; j < currentZone->size(); j++) {
DecklistCardNode *currentCard = dynamic_cast<DecklistCardNode *>(currentZone->at(j));
if (!currentCard)
continue;
for (int k = 0; k < currentCard->getNumber(); ++k) {
CardInfoPtr info = CardDatabaseManager::getInstance()->getCardByNameAndProviderId(
currentCard->getName(), currentCard->getCardProviderId());
if (info) {
bannerCardSet.insert(
QPair<QString, QString>(currentCard->getName(), currentCard->getCardProviderId()));
}
}
}
}
QList<QPair<QString, QString>> pairList = bannerCardSet.values();
// Sort QList by the first() element of the QPair
std::sort(pairList.begin(), pairList.end(), [](const QPair<QString, QString> &a, const QPair<QString, QString> &b) {
return a.first.toLower() < b.first.toLower();
});
for (const auto &pair : pairList) {
QVariantMap dataMap;
dataMap["name"] = pair.first;
dataMap["uuid"] = pair.second;
bannerCardComboBox->addItem(pair.first, dataMap);
}
// Try to restore the previous selection by finding the currentText
int restoredIndex = bannerCardComboBox->findText(currentText);
if (restoredIndex != -1) {
bannerCardComboBox->setCurrentIndex(restoredIndex);
} else {
// Add a placeholder "-" and set it as the current selection
int bannerIndex = bannerCardComboBox->findText(deckModel->getDeckList()->getBannerCard().first);
if (bannerIndex != -1) {
bannerCardComboBox->setCurrentIndex(bannerIndex);
} else {
bannerCardComboBox->insertItem(0, "-");
bannerCardComboBox->setCurrentIndex(0);
}
}
// Restore the previous signal blocking state
bannerCardComboBox->blockSignals(wasBlocked);
}
void TabDeckEditor::setBannerCard(int /* changedIndex */)
{
QVariantMap itemData = bannerCardComboBox->itemData(bannerCardComboBox->currentIndex()).toMap();
deckModel->getDeckList()->setBannerCard(
QPair<QString, QString>(itemData["name"].toString(), itemData["uuid"].toString()));
}
void TabDeckEditor::updateCardInfo(CardInfoPtr _card)
{
cardInfo->setCard(_card);
@@ -868,6 +980,20 @@ void TabDeckEditor::updateHash()
hashLabel->setText(deckModel->getDeckList()->getDeckHash());
}
void TabDeckEditor::updateRecentlyOpened()
{
loadRecentDeckMenu->clear();
for (const auto &deckPath : SettingsCache::instance().recents().getRecentlyOpenedDeckPaths()) {
QAction *aRecentlyOpenedDeck = new QAction(deckPath, this);
loadRecentDeckMenu->addAction(aRecentlyOpenedDeck);
connect(aRecentlyOpenedDeck, &QAction::triggered, this,
[=, this] { actOpenRecent(aRecentlyOpenedDeck->text()); });
}
loadRecentDeckMenu->addSeparator();
loadRecentDeckMenu->addAction(aClearRecents);
aClearRecents->setEnabled(SettingsCache::instance().recents().getRecentlyOpenedDeckPaths().length() > 0);
}
bool TabDeckEditor::confirmClose()
{
if (modified) {
@@ -881,10 +1007,14 @@ bool TabDeckEditor::confirmClose()
return true;
}
void TabDeckEditor::closeRequest()
void TabDeckEditor::closeRequest(bool forced)
{
if (confirmClose())
deleteLater();
if (!forced && !confirmClose()) {
return;
}
emit deckEditorClosing(this);
close();
}
void TabDeckEditor::actNewDeck()
@@ -916,28 +1046,60 @@ void TabDeckEditor::actLoadDeck()
return;
}
QFileDialog dialog(this, tr("Load deck"));
dialog.setDirectory(SettingsCache::instance().getDeckPath());
dialog.setNameFilters(DeckLoader::fileNameFilters);
DlgLoadDeck dialog(this);
if (!dialog.exec())
return;
QString fileName = dialog.selectedFiles().at(0);
openDeckFromFile(fileName, deckOpenLocation);
updateBannerCardComboBox();
}
void TabDeckEditor::actOpenRecent(const QString &fileName)
{
auto deckOpenLocation = confirmOpen();
if (deckOpenLocation == CANCELLED) {
return;
}
openDeckFromFile(fileName, deckOpenLocation);
}
/**
* Actually opens the deck from file
* @param fileName The path of the deck to open
* @param deckOpenLocation Which tab to open the deck
*/
void TabDeckEditor::openDeckFromFile(const QString &fileName, DeckOpenLocation deckOpenLocation)
{
DeckLoader::FileFormat fmt = DeckLoader::getFormatFromName(fileName);
auto *l = new DeckLoader;
if (l->loadFromFile(fileName, fmt)) {
if (l->loadFromFile(fileName, fmt, true)) {
SettingsCache::instance().recents().updateRecentlyOpenedDeckPaths(fileName);
updateBannerCardComboBox();
if (!l->getBannerCard().first.isEmpty()) {
bannerCardComboBox->setCurrentIndex(bannerCardComboBox->findText(l->getBannerCard().first));
}
if (deckOpenLocation == NEW_TAB) {
emit openDeckEditor(l);
} else {
setSaveStatus(false);
setDeck(l);
}
} else
} else {
delete l;
QMessageBox::critical(this, tr("Error"), tr("Could not open deck at %1").arg(fileName));
}
setSaveStatus(true);
}
void TabDeckEditor::actClearRecents()
{
SettingsCache::instance().recents().clearRecentlyOpenedDeckPaths();
}
void TabDeckEditor::saveDeckRemoteFinished(const Response &response)
{
if (response.response_code() != Response::RespOk)
@@ -999,6 +1161,9 @@ bool TabDeckEditor::actSaveDeckAs()
return false;
}
setModified(false);
SettingsCache::instance().recents().updateRecentlyOpenedDeckPaths(fileName);
return true;
}
@@ -1033,6 +1198,15 @@ void TabDeckEditor::actSaveDeckToClipboard()
QApplication::clipboard()->setText(buffer, QClipboard::Selection);
}
void TabDeckEditor::actSaveDeckToClipboardNoSetNameAndNumber()
{
QString buffer;
QTextStream stream(&buffer);
deckModel->getDeckList()->saveToStream_Plain(stream, true, false);
QApplication::clipboard()->setText(buffer, QClipboard::Clipboard);
QApplication::clipboard()->setText(buffer, QClipboard::Selection);
}
void TabDeckEditor::actSaveDeckToClipboardRaw()
{
QString buffer;
@@ -1042,6 +1216,15 @@ void TabDeckEditor::actSaveDeckToClipboardRaw()
QApplication::clipboard()->setText(buffer, QClipboard::Selection);
}
void TabDeckEditor::actSaveDeckToClipboardRawNoSetNameAndNumber()
{
QString buffer;
QTextStream stream(&buffer);
deckModel->getDeckList()->saveToStream_Plain(stream, false, false);
QApplication::clipboard()->setText(buffer, QClipboard::Clipboard);
QApplication::clipboard()->setText(buffer, QClipboard::Selection);
}
void TabDeckEditor::actPrintDeck()
{
auto *dlg = new QPrintPreviewDialog(this);
@@ -1199,9 +1382,25 @@ CardInfoPtr TabDeckEditor::currentCardInfo() const
return CardDatabaseManager::getInstance()->getCard(cardName);
}
void TabDeckEditor::addCardHelper(QString zoneName)
/**
* Gets the index of all the currently selected card nodes in the decklist table.
* The list is in reverse order of the visual selection, so that rows can be deleted while iterating over them.
*
* @return A model index list containing all selected card nodes
*/
QModelIndexList TabDeckEditor::getSelectedCardNodes() const
{
auto selectedRows = deckView->selectionModel()->selectedRows();
const auto notLeafNode = [this](const auto &index) { return deckModel->hasChildren(index); };
selectedRows.erase(std::remove_if(selectedRows.begin(), selectedRows.end(), notLeafNode), selectedRows.end());
std::reverse(selectedRows.begin(), selectedRows.end());
return selectedRows;
}
void TabDeckEditor::addCardHelper(const CardInfoPtr info, QString zoneName)
{
const CardInfoPtr info = currentCardInfo();
if (!info)
return;
if (info->getIsToken())
@@ -1209,6 +1408,7 @@ void TabDeckEditor::addCardHelper(QString zoneName)
QModelIndex newCardIndex = deckModel->addPreferredPrintingCard(info->getName(), zoneName, false);
recursiveExpand(newCardIndex);
deckView->clearSelection();
deckView->setCurrentIndex(newCardIndex);
setModified(true);
searchEdit->setSelection(0, searchEdit->text().length());
@@ -1216,18 +1416,50 @@ void TabDeckEditor::addCardHelper(QString zoneName)
void TabDeckEditor::actSwapCard()
{
const QModelIndex currentIndex = deckView->selectionModel()->currentIndex();
auto selectedRows = getSelectedCardNodes();
// hack to maintain the old reselection behavior when currently selected row of a single-selection gets deleted
// TODO: remove the hack and also handle reselection when all rows of a multi-selection gets deleted
if (selectedRows.length() == 1) {
deckView->setSelectionMode(QAbstractItemView::SingleSelection);
}
bool isModified = false;
for (const auto &currentIndex : selectedRows) {
if (swapCard(currentIndex)) {
isModified = true;
}
}
deckView->setSelectionMode(QAbstractItemView::ExtendedSelection);
if (isModified) {
setModified(true);
setSaveStatus(true);
}
update();
}
/**
* Swaps the card at the index between the maindeck and sideboard
*
* @param currentIndex The index to swap.
* @return True if the swap was successful
*/
bool TabDeckEditor::swapCard(const QModelIndex &currentIndex)
{
if (!currentIndex.isValid())
return;
return false;
const QString cardName = currentIndex.sibling(currentIndex.row(), 1).data().toString();
const QString cardProviderID = currentIndex.sibling(currentIndex.row(), 4).data().toString();
const QModelIndex gparent = currentIndex.parent().parent();
if (!gparent.isValid())
return;
return false;
const QString zoneName = gparent.sibling(gparent.row(), 1).data(Qt::EditRole).toString();
actDecrement();
offsetCountAtIndex(currentIndex, -1);
const QString otherZoneName = zoneName == DECK_ZONE_MAIN ? DECK_ZONE_SIDE : DECK_ZONE_MAIN;
// Third argument (true) says create the card no matter what, even if not in DB
@@ -1236,8 +1468,7 @@ void TabDeckEditor::actSwapCard()
true);
recursiveExpand(newCardIndex);
setModified(true);
setSaveStatus(true);
return true;
}
void TabDeckEditor::actAddCard()
@@ -1245,37 +1476,53 @@ void TabDeckEditor::actAddCard()
if (QApplication::keyboardModifiers() & Qt::ControlModifier)
actAddCardToSideboard();
else
addCardHelper(DECK_ZONE_MAIN);
addCardHelper(currentCardInfo(), DECK_ZONE_MAIN);
setSaveStatus(true);
}
void TabDeckEditor::actAddCardToSideboard()
{
addCardHelper(DECK_ZONE_SIDE);
addCardHelper(currentCardInfo(), DECK_ZONE_SIDE);
setSaveStatus(true);
}
void TabDeckEditor::actRemoveCard()
{
const QModelIndex &currentIndex = deckView->selectionModel()->currentIndex();
if (!currentIndex.isValid() || deckModel->hasChildren(currentIndex))
return;
deckModel->removeRow(currentIndex.row(), currentIndex.parent());
auto selectedRows = getSelectedCardNodes();
DeckLoader *const deck = deckModel->getDeckList();
setSaveStatus(!deck->isEmpty());
setModified(true);
// hack to maintain the old reselection behavior when currently selected row of a single-selection gets deleted
// TODO: remove the hack and also handle reselection when all rows of a multi-selection gets deleted
if (selectedRows.length() == 1) {
deckView->setSelectionMode(QAbstractItemView::SingleSelection);
}
bool isModified = false;
for (const auto &index : selectedRows) {
if (!index.isValid() || deckModel->hasChildren(index)) {
continue;
}
deckModel->removeRow(index.row(), index.parent());
isModified = true;
}
deckView->setSelectionMode(QAbstractItemView::ExtendedSelection);
if (isModified) {
DeckLoader *const deck = deckModel->getDeckList();
setSaveStatus(!deck->isEmpty());
setModified(true);
}
}
void TabDeckEditor::offsetCountAtIndex(const QModelIndex &idx, int offset)
{
if (!idx.isValid() || offset == 0)
if (!idx.isValid() || deckModel->hasChildren(idx)) {
return;
}
const QModelIndex numberIndex = idx.sibling(idx.row(), 0);
const int count = deckModel->data(numberIndex, Qt::EditRole).toInt();
const int new_count = count + offset;
deckView->setCurrentIndex(numberIndex);
if (new_count <= 0)
deckModel->removeRow(idx.row(), idx.parent());
else
@@ -1286,14 +1533,18 @@ void TabDeckEditor::offsetCountAtIndex(const QModelIndex &idx, int offset)
void TabDeckEditor::decrementCardHelper(QString zoneName)
{
const CardInfoPtr info = currentCardInfo();
QModelIndex idx;
if (!info)
return;
if (info->getIsToken())
zoneName = DECK_ZONE_TOKENS;
idx = deckModel->findCard(info->getName(), zoneName);
QModelIndex idx = deckModel->findCard(info->getName(), zoneName);
if (!idx.isValid()) {
return;
}
deckView->clearSelection();
deckView->setCurrentIndex(idx);
offsetCountAtIndex(idx, -1);
}
@@ -1315,14 +1566,28 @@ void TabDeckEditor::copyDatabaseCellContents()
void TabDeckEditor::actIncrement()
{
const QModelIndex &currentIndex = deckView->selectionModel()->currentIndex();
offsetCountAtIndex(currentIndex, 1);
auto selectedRows = getSelectedCardNodes();
for (const auto &index : selectedRows) {
offsetCountAtIndex(index, 1);
}
}
void TabDeckEditor::actDecrement()
{
const QModelIndex &currentIndex = deckView->selectionModel()->currentIndex();
offsetCountAtIndex(currentIndex, -1);
auto selectedRows = getSelectedCardNodes();
// hack to maintain the old reselection behavior when currently selected row of a single-selection gets deleted
// TODO: remove the hack and also handle reselection when all rows of a multi-selection gets deleted
if (selectedRows.length() == 1) {
deckView->setSelectionMode(QAbstractItemView::SingleSelection);
}
for (const auto &index : selectedRows) {
offsetCountAtIndex(index, -1);
}
deckView->setSelectionMode(QAbstractItemView::ExtendedSelection);
}
void TabDeckEditor::setDeck(DeckLoader *_deck)
@@ -1331,6 +1596,8 @@ void TabDeckEditor::setDeck(DeckLoader *_deck)
nameEdit->setText(deckModel->getDeckList()->getName());
commentsEdit->setText(deckModel->getDeckList()->getComments());
bannerCardComboBox->setCurrentText(deckModel->getDeckList()->getBannerCard().first);
updateBannerCardComboBox();
updateHash();
deckModel->sort(deckView->header()->sortIndicatorSection(), deckView->header()->sortIndicatorOrder());
deckView->expandAll();
@@ -1415,25 +1682,25 @@ void TabDeckEditor::dockVisibleTriggered()
{
QObject *o = sender();
if (o == aCardInfoDockVisible) {
cardInfoDock->setVisible(aCardInfoDockVisible->isChecked());
cardInfoDock->setHidden(!aCardInfoDockVisible->isChecked());
aCardInfoDockFloating->setEnabled(aCardInfoDockVisible->isChecked());
return;
}
if (o == aDeckDockVisible) {
deckDock->setVisible(aDeckDockVisible->isChecked());
deckDock->setHidden(!aDeckDockVisible->isChecked());
aDeckDockFloating->setEnabled(aDeckDockVisible->isChecked());
return;
}
if (o == aFilterDockVisible) {
filterDock->setVisible(aFilterDockVisible->isChecked());
filterDock->setHidden(!aFilterDockVisible->isChecked());
aFilterDockFloating->setEnabled(aFilterDockVisible->isChecked());
return;
}
if (o == aPrintingSelectorDockVisible) {
printingSelectorDock->setVisible(aPrintingSelectorDockVisible->isChecked());
printingSelectorDock->setHidden(!aPrintingSelectorDockVisible->isChecked());
aPrintingSelectorDockFloating->setEnabled(aPrintingSelectorDockVisible->isChecked());
return;
}
@@ -1497,7 +1764,9 @@ void TabDeckEditor::setSaveStatus(bool newStatus)
aSaveDeck->setEnabled(newStatus);
aSaveDeckAs->setEnabled(newStatus);
aSaveDeckToClipboard->setEnabled(newStatus);
aSaveDeckToClipboardNoSetNameAndNumber->setEnabled(newStatus);
aSaveDeckToClipboardRaw->setEnabled(newStatus);
aSaveDeckToClipboardRawNoSetNameAndNumber->setEnabled(newStatus);
saveDeckToClipboardMenu->setEnabled(newStatus);
aPrintDeck->setEnabled(newStatus);
analyzeDeckMenu->setEnabled(newStatus);
@@ -1535,6 +1804,6 @@ void TabDeckEditor::showSearchSyntaxHelp()
browser->document()->setDefaultStyleSheet(sheet);
browser->setHtml(text);
connect(browser, &QTextBrowser::anchorClicked, [=](const QUrl &link) { searchEdit->setText(link.fragment()); });
connect(browser, &QTextBrowser::anchorClicked, [this](const QUrl &link) { searchEdit->setText(link.fragment()); });
browser->show();
}

View File

@@ -22,6 +22,7 @@ class DeckLoader;
class Response;
class FilterTreeModel;
class FilterBuilder;
class QComboBox;
class QGroupBox;
class QMessageBox;
class QHBoxLayout;
@@ -29,30 +30,14 @@ class QVBoxLayout;
class QPushButton;
class QDockWidget;
class SearchLineEdit : public LineEditUnfocusable
{
private:
QTreeView *treeView;
protected:
void keyPressEvent(QKeyEvent *event) override;
public:
SearchLineEdit() : LineEditUnfocusable(), treeView(nullptr)
{
}
void setTreeView(QTreeView *_treeView)
{
treeView = _treeView;
}
};
class TabDeckEditor : public Tab
{
Q_OBJECT
private slots:
void updateName(const QString &name);
void updateComments();
void updateBannerCardComboBox();
void setBannerCard(int);
void updateHash();
void updateCardInfoLeft(const QModelIndex &current, const QModelIndex &previous);
void updateCardInfoRight(const QModelIndex &current, const QModelIndex &previous);
@@ -61,14 +46,19 @@ private slots:
void updateSearch(const QString &search);
void databaseCustomMenu(QPoint point);
void decklistCustomMenu(QPoint point);
void updateRecentlyOpened();
void actNewDeck();
void actLoadDeck();
void actOpenRecent(const QString &fileName);
void actClearRecents();
bool actSaveDeck();
bool actSaveDeckAs();
void actLoadDeckFromClipboard();
void actSaveDeckToClipboard();
void actSaveDeckToClipboardNoSetNameAndNumber();
void actSaveDeckToClipboardRaw();
void actSaveDeckToClipboardRawNoSetNameAndNumber();
void actPrintDeck();
void actExportDeckDecklist();
void actAnalyzeDeckDeckstats();
@@ -120,10 +110,13 @@ private:
bool isBlankNewDeck() const;
CardInfoPtr currentCardInfo() const;
void addCardHelper(QString zoneName);
void offsetCountAtIndex(const QModelIndex &idx, int offset);
void decrementCardHelper(QString zoneName);
bool swapCard(const QModelIndex &idx);
void recursiveExpand(const QModelIndex &index);
void openDeckFromFile(const QString &fileName, DeckOpenLocation deckOpenLocation);
QModelIndexList getSelectedCardNodes() const;
CardDatabaseModel *databaseModel;
CardDatabaseDisplayModel *databaseDisplayModel;
@@ -141,6 +134,8 @@ private:
LineEditUnfocusable *nameEdit;
QLabel *commentsLabel;
QTextEdit *commentsEdit;
QLabel *bannerCardLabel;
QComboBox *bannerCardComboBox;
QLabel *hashLabel1;
LineEditUnfocusable *hashLabel;
FilterTreeModel *filterModel;
@@ -149,12 +144,13 @@ private:
QWidget *filterBox;
QMenu *deckMenu, *viewMenu, *cardInfoDockMenu, *deckDockMenu, *filterDockMenu, *printingSelectorDockMenu,
*analyzeDeckMenu, *saveDeckToClipboardMenu;
QAction *aNewDeck, *aLoadDeck, *aSaveDeck, *aSaveDeckAs, *aLoadDeckFromClipboard, *aSaveDeckToClipboard,
*aSaveDeckToClipboardRaw, *aPrintDeck, *aExportDeckDecklist, *aAnalyzeDeckDeckstats, *aAnalyzeDeckTappedout,
*aClose;
*analyzeDeckMenu, *saveDeckToClipboardMenu, *loadRecentDeckMenu;
QAction *aNewDeck, *aLoadDeck, *aClearRecents, *aSaveDeck, *aSaveDeckAs, *aLoadDeckFromClipboard,
*aSaveDeckToClipboard, *aSaveDeckToClipboardNoSetNameAndNumber, *aSaveDeckToClipboardRaw,
*aSaveDeckToClipboardRawNoSetNameAndNumber, *aPrintDeck, *aExportDeckDecklist, *aAnalyzeDeckDeckstats,
*aAnalyzeDeckTappedout, *aClose;
QAction *aClearFilterAll, *aClearFilterOne;
QAction *aAddCard, *aAddCardToSideboard, *aRemoveCard, *aIncrement, *aDecrement;
QAction *aAddCard, *aAddCardToSideboard, *aRemoveCard, *aIncrement, *aDecrement, *aSwapCard;
QAction *aResetLayout;
QAction *aCardInfoDockVisible, *aCardInfoDockFloating, *aDeckDockVisible, *aDeckDockFloating, *aFilterDockVisible,
*aFilterDockFloating, *aPrintingSelectorDockVisible, *aPrintingSelectorDockFloating;
@@ -169,8 +165,7 @@ private:
QWidget *centralWidget;
public:
explicit TabDeckEditor(TabSupervisor *_tabSupervisor, QWidget *parent = nullptr);
~TabDeckEditor() override;
explicit TabDeckEditor(TabSupervisor *_tabSupervisor);
void retranslateUi() override;
QString getTabText() const override;
void setDeck(DeckLoader *_deckLoader);
@@ -183,9 +178,10 @@ public:
void createMenus();
void createCentralFrame();
void updateCardInfo(CardInfoPtr _card);
void addCardHelper(CardInfoPtr info, QString zoneName);
public slots:
void closeRequest() override;
void closeRequest(bool forced = false) override;
void showPrintingSelector();
signals:
void openDeckEditor(const DeckLoader *deckLoader);

View File

@@ -4,7 +4,6 @@
#include "../../server/pending_command.h"
#include "../../server/remote/remote_decklist_tree_widget.h"
#include "../../settings/cache_settings.h"
#include "../game_logic/abstract_client.h"
#include "../get_text_with_max.h"
#include "decklist.h"
#include "pb/command_deck_del.pb.h"
@@ -19,6 +18,7 @@
#include <QAction>
#include <QApplication>
#include <QDebug>
#include <QDesktopServices>
#include <QFileSystemModel>
#include <QGroupBox>
#include <QHBoxLayout>
@@ -27,9 +27,12 @@
#include <QMessageBox>
#include <QToolBar>
#include <QTreeView>
#include <QUrl>
#include <QVBoxLayout>
TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor, AbstractClient *_client)
TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor,
AbstractClient *_client,
const ServerInfo_User *currentUserInfo)
: Tab(_tabSupervisor), client(_client)
{
localDirModel = new QFileSystemModel(this);
@@ -41,16 +44,33 @@ TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor, AbstractClient *_c
localDirView->setColumnHidden(1, true);
localDirView->setRootIndex(localDirModel->index(localDirModel->rootPath(), 0));
localDirView->setSortingEnabled(true);
localDirView->setSelectionMode(QAbstractItemView::ExtendedSelection);
localDirView->header()->setSectionResizeMode(QHeaderView::ResizeToContents);
localDirView->header()->setSortIndicator(0, Qt::AscendingOrder);
leftToolBar = new QToolBar;
connect(localDirView, &QTreeView::doubleClicked, this, &TabDeckStorage::actLocalDoubleClick);
// Left side layout
/* put an invisible dummy QToolBar in the leftmost column so that the main toolbar is centered.
* Really ugly workaround, but I couldn't figure out the proper way to make it centered */
QToolBar *dummyToolBar = new QToolBar(this);
QSizePolicy sizePolicy = dummyToolBar->sizePolicy();
sizePolicy.setRetainSizeWhenHidden(true);
dummyToolBar->setSizePolicy(sizePolicy);
dummyToolBar->setVisible(false);
leftToolBar = new QToolBar(this);
leftToolBar->setOrientation(Qt::Horizontal);
leftToolBar->setIconSize(QSize(32, 32));
QHBoxLayout *leftToolBarLayout = new QHBoxLayout;
leftToolBarLayout->addStretch();
leftToolBarLayout->addWidget(leftToolBar);
leftToolBarLayout->addStretch();
QToolBar *leftRightmostToolBar = new QToolBar(this);
leftRightmostToolBar->setOrientation(Qt::Horizontal);
leftRightmostToolBar->setIconSize(QSize(32, 32));
QGridLayout *leftToolBarLayout = new QGridLayout;
leftToolBarLayout->addWidget(dummyToolBar, 0, 0, Qt::AlignLeft);
leftToolBarLayout->addWidget(leftToolBar, 0, 1, Qt::AlignHCenter);
leftToolBarLayout->addWidget(leftRightmostToolBar, 0, 2, Qt::AlignRight);
QVBoxLayout *leftVbox = new QVBoxLayout;
leftVbox->addWidget(localDirView);
@@ -58,6 +78,7 @@ TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor, AbstractClient *_c
leftGroupBox = new QGroupBox;
leftGroupBox->setLayout(leftVbox);
// Right side layout
rightToolBar = new QToolBar;
rightToolBar->setOrientation(Qt::Horizontal);
rightToolBar->setIconSize(QSize(32, 32));
@@ -68,25 +89,38 @@ TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor, AbstractClient *_c
serverDirView = new RemoteDeckList_TreeWidget(client);
connect(serverDirView, &QTreeView::doubleClicked, this, &TabDeckStorage::actRemoteDoubleClick);
QVBoxLayout *rightVbox = new QVBoxLayout;
rightVbox->addWidget(serverDirView);
rightVbox->addLayout(rightToolBarLayout);
rightGroupBox = new QGroupBox;
rightGroupBox->setLayout(rightVbox);
// combine layouts
QHBoxLayout *hbox = new QHBoxLayout;
hbox->addWidget(leftGroupBox);
hbox->addWidget(rightGroupBox);
// Left side actions
aOpenLocalDeck = new QAction(this);
aOpenLocalDeck->setIcon(QPixmap("theme:icons/pencil"));
connect(aOpenLocalDeck, SIGNAL(triggered()), this, SLOT(actOpenLocalDeck()));
aUpload = new QAction(this);
aUpload->setIcon(QPixmap("theme:icons/arrow_right_green"));
connect(aUpload, SIGNAL(triggered()), this, SLOT(actUpload()));
aNewLocalFolder = new QAction(this);
aNewLocalFolder->setIcon(qApp->style()->standardIcon(QStyle::SP_FileDialogNewFolder));
connect(aNewLocalFolder, &QAction::triggered, this, &TabDeckStorage::actNewLocalFolder);
aDeleteLocalDeck = new QAction(this);
aDeleteLocalDeck->setIcon(QPixmap("theme:icons/remove_row"));
connect(aDeleteLocalDeck, SIGNAL(triggered()), this, SLOT(actDeleteLocalDeck()));
aOpenDecksFolder = new QAction(this);
aOpenDecksFolder->setIcon(qApp->style()->standardIcon(QStyle::SP_DirOpenIcon));
connect(aOpenDecksFolder, &QAction::triggered, this, &TabDeckStorage::actOpenDecksFolder);
// Right side actions
aOpenRemoteDeck = new QAction(this);
aOpenRemoteDeck->setIcon(QPixmap("theme:icons/pencil"));
connect(aOpenRemoteDeck, SIGNAL(triggered()), this, SLOT(actOpenRemoteDeck()));
@@ -100,9 +134,14 @@ TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor, AbstractClient *_c
aDeleteRemoteDeck->setIcon(QPixmap("theme:icons/remove_row"));
connect(aDeleteRemoteDeck, SIGNAL(triggered()), this, SLOT(actDeleteRemoteDeck()));
// Add actions to toolbars
leftToolBar->addAction(aOpenLocalDeck);
leftToolBar->addAction(aUpload);
leftToolBar->addAction(aNewLocalFolder);
leftToolBar->addAction(aDeleteLocalDeck);
leftRightmostToolBar->addAction(aOpenDecksFolder);
rightToolBar->addAction(aOpenRemoteDeck);
rightToolBar->addAction(aDownload);
rightToolBar->addAction(aNewFolder);
@@ -113,6 +152,10 @@ TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor, AbstractClient *_c
QWidget *mainWidget = new QWidget(this);
mainWidget->setLayout(hbox);
setCentralWidget(mainWidget);
connect(client, &AbstractClient::userInfoChanged, this, &TabDeckStorage::handleConnected);
connect(client, &AbstractClient::statusChanged, this, &TabDeckStorage::handleConnectionChanged);
setRemoteEnabled(currentUserInfo && currentUserInfo->user_level() & ServerInfo_User::IsRegistered);
}
void TabDeckStorage::retranslateUi()
@@ -124,9 +167,11 @@ void TabDeckStorage::retranslateUi()
aUpload->setText(tr("Upload deck"));
aOpenRemoteDeck->setText(tr("Open in deck editor"));
aDownload->setText(tr("Download deck"));
aNewLocalFolder->setText(tr("New folder"));
aNewFolder->setText(tr("New folder"));
aDeleteLocalDeck->setText(tr("Delete"));
aDeleteRemoteDeck->setText(tr("Delete"));
aOpenDecksFolder->setText(tr("Open decks folder"));
}
QString TabDeckStorage::getTargetPath() const
@@ -147,43 +192,91 @@ QString TabDeckStorage::getTargetPath() const
}
}
void TabDeckStorage::handleConnected(const ServerInfo_User &userInfo)
{
setRemoteEnabled(userInfo.user_level() & ServerInfo_User::IsRegistered);
}
/**
* This is only responsible for handling the disconnect. The connect is already handled elsewhere
*/
void TabDeckStorage::handleConnectionChanged(ClientStatus status)
{
if (status == StatusDisconnected) {
setRemoteEnabled(false);
}
}
void TabDeckStorage::setRemoteEnabled(bool enabled)
{
aUpload->setEnabled(enabled);
aOpenRemoteDeck->setEnabled(enabled);
aDownload->setEnabled(enabled);
aNewFolder->setEnabled(enabled);
aDeleteRemoteDeck->setEnabled(enabled);
if (enabled) {
serverDirView->refreshTree();
} else {
serverDirView->clearTree();
}
}
void TabDeckStorage::actLocalDoubleClick(const QModelIndex &curLeft)
{
if (!localDirModel->isDir(curLeft)) {
actOpenLocalDeck();
}
}
void TabDeckStorage::actOpenLocalDeck()
{
QModelIndex curLeft = localDirView->selectionModel()->currentIndex();
if (localDirModel->isDir(curLeft))
return;
QString filePath = localDirModel->filePath(curLeft);
QModelIndexList curLefts = localDirView->selectionModel()->selectedRows();
for (const auto &curLeft : curLefts) {
if (localDirModel->isDir(curLeft))
continue;
QString filePath = localDirModel->filePath(curLeft);
DeckLoader deckLoader;
if (!deckLoader.loadFromFile(filePath, DeckLoader::CockatriceFormat))
return;
DeckLoader deckLoader;
if (!deckLoader.loadFromFile(filePath, DeckLoader::CockatriceFormat, true))
continue;
emit openDeckEditor(&deckLoader);
SettingsCache::instance().recents().updateRecentlyOpenedDeckPaths(filePath);
emit openDeckEditor(&deckLoader);
}
}
void TabDeckStorage::actUpload()
{
QModelIndex curLeft = localDirView->selectionModel()->currentIndex();
if (localDirModel->isDir(curLeft))
QModelIndexList curLefts = localDirView->selectionModel()->selectedRows();
if (curLefts.isEmpty()) {
return;
}
QString targetPath = getTargetPath();
if (targetPath.length() > MAX_NAME_LENGTH) {
qCritical() << "target path to upload to is too long" << targetPath;
return;
}
QString filePath = localDirModel->filePath(curLeft);
for (const auto &curLeft : curLefts) {
if (localDirModel->isDir(curLeft)) {
continue;
}
QString filePath = localDirModel->filePath(curLeft);
uploadDeck(filePath, targetPath);
}
}
void TabDeckStorage::uploadDeck(const QString &filePath, const QString &targetPath)
{
QFile deckFile(filePath);
QFileInfo deckFileInfo(deckFile);
QString deckString;
DeckLoader deck;
bool error = !deck.loadFromFile(filePath, DeckLoader::CockatriceFormat);
if (!error) {
deckString = deck.writeToString_Native();
error = deckString.length() > MAX_FILE_LENGTH;
}
if (error) {
if (!deck.loadFromFile(filePath, DeckLoader::CockatriceFormat)) {
QMessageBox::critical(this, tr("Error"), tr("Invalid deck file"));
return;
}
@@ -202,6 +295,12 @@ void TabDeckStorage::actUpload()
deck.setName(deck.getName().left(MAX_NAME_LENGTH));
}
QString deckString = deck.writeToString_Native();
if (deckString.length() > MAX_FILE_LENGTH) {
QMessageBox::critical(this, tr("Error"), tr("Invalid deck file"));
return;
}
Command_DeckUpload cmd;
cmd.set_path(targetPath.toStdString());
cmd.set_deck_list(deckString.toStdString());
@@ -226,34 +325,73 @@ void TabDeckStorage::uploadFinished(const Response &r, const CommandContainer &c
serverDirView->addFileToTree(resp.new_file(), serverDirView->getNodeByPath(QString::fromStdString(cmd.path())));
}
void TabDeckStorage::actDeleteLocalDeck()
void TabDeckStorage::actNewLocalFolder()
{
QModelIndex curLeft = localDirView->selectionModel()->currentIndex();
if (localDirModel->isDir(curLeft))
QModelIndex dirIndex;
if (curLeft.isValid() && !localDirModel->isDir(curLeft)) {
dirIndex = curLeft.parent();
} else {
dirIndex = curLeft;
}
bool ok;
QString folderName =
QInputDialog::getText(this, tr("New folder"), tr("Name of new folder:"), QLineEdit::Normal, "", &ok);
if (!ok || folderName.isEmpty())
return;
if (QMessageBox::warning(this, tr("Delete local file"),
tr("Are you sure you want to delete \"%1\"?").arg(localDirModel->fileName(curLeft)),
localDirModel->mkdir(dirIndex, folderName);
}
void TabDeckStorage::actDeleteLocalDeck()
{
const QModelIndexList curLefts = localDirView->selectionModel()->selectedRows();
if (curLefts.isEmpty()) {
return;
}
if (QMessageBox::warning(this, tr("Delete local file"), tr("Are you sure you want to delete the selected files?"),
QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes)
return;
localDirModel->remove(curLeft);
for (const auto &curLeft : curLefts) {
if (curLeft.isValid()) {
localDirModel->remove(curLeft);
}
}
}
void TabDeckStorage::actOpenDecksFolder()
{
QString dir = localDirModel->rootPath();
QDesktopServices::openUrl(QUrl::fromLocalFile(dir));
}
void TabDeckStorage::actRemoteDoubleClick(const QModelIndex &curRight)
{
if (dynamic_cast<RemoteDeckList_TreeModel::FileNode *>(serverDirView->getNode(curRight))) {
actOpenRemoteDeck();
}
}
void TabDeckStorage::actOpenRemoteDeck()
{
RemoteDeckList_TreeModel::FileNode *curRight =
dynamic_cast<RemoteDeckList_TreeModel::FileNode *>(serverDirView->getCurrentItem());
if (!curRight)
return;
for (const auto &curRight : serverDirView->getCurrentSelection()) {
RemoteDeckList_TreeModel::FileNode *node = dynamic_cast<RemoteDeckList_TreeModel::FileNode *>(curRight);
if (!node)
continue;
Command_DeckDownload cmd;
cmd.set_deck_id(curRight->getId());
Command_DeckDownload cmd;
cmd.set_deck_id(node->getId());
PendingCommand *pend = client->prepareSessionCommand(cmd);
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
SLOT(openRemoteDeckFinished(Response, CommandContainer)));
client->sendCommand(pend);
PendingCommand *pend = client->prepareSessionCommand(cmd);
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
SLOT(openRemoteDeckFinished(Response, CommandContainer)));
client->sendCommand(pend);
}
}
void TabDeckStorage::openRemoteDeckFinished(const Response &r, const CommandContainer &commandContainer)
@@ -273,30 +411,46 @@ void TabDeckStorage::openRemoteDeckFinished(const Response &r, const CommandCont
void TabDeckStorage::actDownload()
{
QString filePath;
QModelIndex curLeft = localDirView->selectionModel()->currentIndex();
if (!curLeft.isValid())
filePath = localDirModel->rootPath();
else {
while (!localDirModel->isDir(curLeft))
curLeft = curLeft.parent();
filePath = localDirModel->filePath(curLeft);
while (!localDirModel->isDir(curLeft)) {
curLeft = curLeft.parent();
}
RemoteDeckList_TreeModel::FileNode *curRight =
dynamic_cast<RemoteDeckList_TreeModel::FileNode *>(serverDirView->getCurrentItem());
if (!curRight)
return;
filePath += QString("/deck_%1.cod").arg(curRight->getId());
for (const auto curRight : serverDirView->selectionModel()->selectedRows()) {
downloadNodeAtIndex(curLeft, curRight);
}
}
Command_DeckDownload cmd;
cmd.set_deck_id(curRight->getId());
void TabDeckStorage::downloadNodeAtIndex(const QModelIndex &curLeft, const QModelIndex &curRight)
{
auto node = serverDirView->getNode(curRight);
if (const auto dirNode = dynamic_cast<RemoteDeckList_TreeModel::DirectoryNode *>(node)) {
// node at index is a folder
const QString name = dirNode->getName();
PendingCommand *pend = client->prepareSessionCommand(cmd);
pend->setExtraData(filePath);
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
SLOT(downloadFinished(Response, CommandContainer, QVariant)));
client->sendCommand(pend);
const auto dirIndex = curLeft.isValid() ? curLeft : localDirModel->index(localDirModel->rootPath());
const auto newDirIndex = localDirModel->mkdir(dirIndex, name);
int rows = serverDirView->model()->rowCount(curRight);
for (int i = 0; i < rows; i++) {
const auto childIndex = serverDirView->model()->index(i, 0, curRight);
downloadNodeAtIndex(newDirIndex, childIndex);
}
} else if (const auto fileNode = dynamic_cast<RemoteDeckList_TreeModel::FileNode *>(node)) {
// node at index is a deck
const QString dirPath = curLeft.isValid() ? localDirModel->filePath(curLeft) : localDirModel->rootPath();
const QString filePath = dirPath + QString("/deck_%1.cod").arg(fileNode->getId());
Command_DeckDownload cmd;
cmd.set_deck_id(fileNode->getId());
PendingCommand *pend = client->prepareSessionCommand(cmd);
pend->setExtraData(filePath);
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
SLOT(downloadFinished(Response, CommandContainer, QVariant)));
client->sendCommand(pend);
}
// node at index is invalid
}
void TabDeckStorage::downloadFinished(const Response &r,
@@ -352,12 +506,30 @@ void TabDeckStorage::newFolderFinished(const Response &response, const CommandCo
void TabDeckStorage::actDeleteRemoteDeck()
{
PendingCommand *pend;
RemoteDeckList_TreeModel::Node *curRight = serverDirView->getCurrentItem();
if (!curRight)
auto curRights = serverDirView->getCurrentSelection();
if (curRights.isEmpty()) {
return;
RemoteDeckList_TreeModel::DirectoryNode *dir = dynamic_cast<RemoteDeckList_TreeModel::DirectoryNode *>(curRight);
if (dir) {
}
if (QMessageBox::warning(this, tr("Delete remote decks"), tr("Are you sure you want to delete the selected decks?"),
QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) {
return;
}
for (const auto &curRight : curRights) {
deleteRemoteDeck(curRight);
}
}
void TabDeckStorage::deleteRemoteDeck(const RemoteDeckList_TreeModel::Node *curRight)
{
if (!curRight) {
return;
}
PendingCommand *pend;
if (const auto *dir = dynamic_cast<const RemoteDeckList_TreeModel::DirectoryNode *>(curRight)) {
QString targetPath = dir->getPath();
if (targetPath.isEmpty())
return;
@@ -365,22 +537,13 @@ void TabDeckStorage::actDeleteRemoteDeck()
qCritical() << "target path to delete is too long" << targetPath;
return;
}
if (QMessageBox::warning(this, tr("Delete remote folder"),
tr("Are you sure you want to delete \"%1\"?").arg(targetPath),
QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes)
return;
Command_DeckDelDir cmd;
cmd.set_path(targetPath.toStdString());
pend = client->prepareSessionCommand(cmd);
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
SLOT(deleteFolderFinished(Response, CommandContainer)));
} else {
RemoteDeckList_TreeModel::FileNode *deckNode = dynamic_cast<RemoteDeckList_TreeModel::FileNode *>(curRight);
if (QMessageBox::warning(this, tr("Delete remote deck"),
tr("Are you sure you want to delete \"%1\"?").arg(deckNode->getName()),
QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes)
return;
const auto *deckNode = dynamic_cast<const RemoteDeckList_TreeModel::FileNode *>(curRight);
Command_DeckDel cmd;
cmd.set_deck_id(deckNode->getId());
pend = client->prepareSessionCommand(cmd);

View File

@@ -1,8 +1,11 @@
#ifndef TAB_DECK_STORAGE_H
#define TAB_DECK_STORAGE_H
#include "../../server/remote/remote_decklist_tree_widget.h"
#include "../game_logic/abstract_client.h"
#include "tab.h"
class ServerInfo_User;
class AbstractClient;
class QTreeView;
class QFileSystemModel;
@@ -10,7 +13,6 @@ class QToolBar;
class QTreeWidget;
class QTreeWidgetItem;
class QGroupBox;
class RemoteDeckList_TreeWidget;
class CommandContainer;
class Response;
class DeckLoader;
@@ -26,16 +28,34 @@ private:
RemoteDeckList_TreeWidget *serverDirView;
QGroupBox *leftGroupBox, *rightGroupBox;
QAction *aOpenLocalDeck, *aUpload, *aDeleteLocalDeck, *aOpenRemoteDeck, *aDownload, *aNewFolder, *aDeleteRemoteDeck;
QAction *aOpenLocalDeck, *aUpload, *aNewLocalFolder, *aDeleteLocalDeck;
QAction *aOpenDecksFolder;
QAction *aOpenRemoteDeck, *aDownload, *aNewFolder, *aDeleteRemoteDeck;
QString getTargetPath() const;
void setRemoteEnabled(bool enabled);
void uploadDeck(const QString &filePath, const QString &targetPath);
void deleteRemoteDeck(const RemoteDeckList_TreeModel::Node *node);
void downloadNodeAtIndex(const QModelIndex &curLeft, const QModelIndex &curRight);
private slots:
void handleConnected(const ServerInfo_User &userInfo);
void handleConnectionChanged(ClientStatus status);
void actLocalDoubleClick(const QModelIndex &curLeft);
void actOpenLocalDeck();
void actUpload();
void uploadFinished(const Response &r, const CommandContainer &commandContainer);
void actNewLocalFolder();
void actDeleteLocalDeck();
void actOpenDecksFolder();
void actRemoteDoubleClick(const QModelIndex &curRight);
void actOpenRemoteDeck();
void openRemoteDeckFinished(const Response &r, const CommandContainer &commandContainer);
@@ -50,11 +70,11 @@ private slots:
void deleteDeckFinished(const Response &response, const CommandContainer &commandContainer);
public:
TabDeckStorage(TabSupervisor *_tabSupervisor, AbstractClient *_client);
void retranslateUi();
QString getTabText() const
TabDeckStorage(TabSupervisor *_tabSupervisor, AbstractClient *_client, const ServerInfo_User *currentUserInfo);
void retranslateUi() override;
QString getTabText() const override
{
return tr("Deck storage");
return tr("Deck Storage");
}
signals:
void openDeckEditor(const DeckLoader *deckLoader);

View File

@@ -1,43 +1,36 @@
#include "tab_game.h"
#include "../../client/ui/widgets/cards/card_info_frame_widget.h"
#include "../../deck/deck_loader.h"
#include "../../deck/deck_view.h"
#include "../../dialogs/dlg_create_game.h"
#include "../../dialogs/dlg_load_remote_deck.h"
#include "../../dialogs/dlg_manage_sets.h"
#include "../../game/board/arrow_item.h"
#include "../../game/cards/card_database.h"
#include "../../game/cards/card_database_manager.h"
#include "../../game/cards/card_item.h"
#include "../../game/deckview/deck_view_container.h"
#include "../../game/game_scene.h"
#include "../../game/game_view.h"
#include "../../game/player/player.h"
#include "../../game/player/player_list_widget.h"
#include "../../game/zones/view_zone.h"
#include "../../game/zones/view_zone_widget.h"
#include "../../game/zones/card_zone.h"
#include "../../main.h"
#include "../../server/message_log_widget.h"
#include "../../server/pending_command.h"
#include "../../server/user/user_list_manager.h"
#include "../../settings/cache_settings.h"
#include "../game_logic/abstract_client.h"
#include "../network/replay_timeline_widget.h"
#include "../ui/line_edit_completer.h"
#include "../ui/phases_toolbar.h"
#include "../ui/picture_loader.h"
#include "../ui/picture_loader/picture_loader.h"
#include "../ui/window_main.h"
#include "get_pb_extension.h"
#include "pb/command_concede.pb.h"
#include "pb/command_deck_select.pb.h"
#include "pb/command_delete_arrow.pb.h"
#include "pb/command_game_say.pb.h"
#include "pb/command_leave_game.pb.h"
#include "pb/command_next_turn.pb.h"
#include "pb/command_ready_start.pb.h"
#include "pb/command_reverse_turn.pb.h"
#include "pb/command_set_active_phase.pb.h"
#include "pb/command_set_sideboard_lock.pb.h"
#include "pb/command_set_sideboard_plan.pb.h"
#include "pb/context_connection_state_changed.pb.h"
#include "pb/context_deck_select.pb.h"
#include "pb/context_ping_changed.pb.h"
@@ -55,7 +48,6 @@
#include "pb/event_set_active_player.pb.h"
#include "pb/game_event_container.pb.h"
#include "pb/game_replay.pb.h"
#include "pb/response_deck_download.pb.h"
#include "tab_supervisor.h"
#include "trice_limits.h"
@@ -73,110 +65,239 @@
#include <QTimer>
#include <QToolButton>
#include <QWidget>
#include <google/protobuf/descriptor.h>
ToggleButton::ToggleButton(QWidget *parent) : QPushButton(parent), state(false)
TabGame::TabGame(TabSupervisor *_tabSupervisor, GameReplay *_replay)
: Tab(_tabSupervisor), secondsElapsed(0), hostId(-1), localPlayerId(-1),
isLocalGame(_tabSupervisor->getIsLocalGame()), spectator(true), judge(false), gameStateKnown(false),
resuming(false), currentPhase(-1), activeCard(nullptr), gameClosed(false), replay(_replay), currentReplayStep(0),
sayLabel(nullptr), sayEdit(nullptr)
{
}
// THIS CTOR IS USED ON REPLAY
gameInfo.CopyFrom(replay->game_info());
gameInfo.set_spectators_omniscient(true);
void ToggleButton::paintEvent(QPaintEvent *event)
{
QPushButton::paintEvent(event);
// Create list: event number -> time [ms]
// Distribute simultaneous events evenly across 1 second.
unsigned int lastEventTimestamp = 0;
const int eventCount = replay->event_list_size();
for (int i = 0; i < eventCount; ++i) {
int j = i + 1;
while ((j < eventCount) && (replay->event_list(j).seconds_elapsed() == lastEventTimestamp))
++j;
QPainter painter(this);
QPen pen;
pen.setWidth(3);
pen.setJoinStyle(Qt::MiterJoin);
pen.setColor(state ? Qt::green : Qt::red);
painter.setPen(pen);
painter.drawRect(QRect(1, 1, width() - 3, height() - 3));
}
const int numberEventsThisSecond = j - i;
for (int k = 0; k < numberEventsThisSecond; ++k)
replayTimeline.append(replay->event_list(i + k).seconds_elapsed() * 1000 +
(int)((qreal)k / (qreal)numberEventsThisSecond * 1000));
void ToggleButton::setState(bool _state)
{
state = _state;
emit stateChanged();
update();
}
DeckViewContainer::DeckViewContainer(int _playerId, TabGame *parent)
: QWidget(nullptr), parentGame(parent), playerId(_playerId)
{
loadLocalButton = new QPushButton;
loadRemoteButton = new QPushButton;
readyStartButton = new ToggleButton;
readyStartButton->setEnabled(false);
sideboardLockButton = new ToggleButton;
sideboardLockButton->setEnabled(false);
connect(loadLocalButton, SIGNAL(clicked()), this, SLOT(loadLocalDeck()));
connect(readyStartButton, SIGNAL(clicked()), this, SLOT(readyStart()));
connect(sideboardLockButton, SIGNAL(clicked()), this, SLOT(sideboardLockButtonClicked()));
connect(sideboardLockButton, SIGNAL(stateChanged()), this, SLOT(updateSideboardLockButtonText()));
if (parentGame->getIsLocalGame()) {
loadRemoteButton->setEnabled(false);
} else {
connect(loadRemoteButton, SIGNAL(clicked()), this, SLOT(loadRemoteDeck()));
if (j < eventCount)
lastEventTimestamp = replay->event_list(j).seconds_elapsed();
i += numberEventsThisSecond - 1;
}
auto *buttonHBox = new QHBoxLayout;
buttonHBox->addWidget(loadLocalButton);
buttonHBox->addWidget(loadRemoteButton);
buttonHBox->addWidget(readyStartButton);
buttonHBox->addWidget(sideboardLockButton);
buttonHBox->setContentsMargins(0, 0, 0, 0);
buttonHBox->addStretch();
deckView = new DeckView;
connect(deckView, SIGNAL(newCardAdded(AbstractCardItem *)), this, SIGNAL(newCardAdded(AbstractCardItem *)));
connect(deckView, SIGNAL(sideboardPlanChanged()), this, SLOT(sideboardPlanChanged()));
createCardInfoDock(true);
createPlayerListDock(true);
createMessageDock(true);
createPlayAreaWidget(true);
createDeckViewContainerWidget(true);
createReplayDock();
auto *deckViewLayout = new QVBoxLayout;
deckViewLayout->addLayout(buttonHBox);
deckViewLayout->addWidget(deckView);
deckViewLayout->setContentsMargins(0, 0, 0, 0);
setLayout(deckViewLayout);
addDockWidget(Qt::RightDockWidgetArea, cardInfoDock);
addDockWidget(Qt::RightDockWidgetArea, playerListDock);
addDockWidget(Qt::RightDockWidgetArea, messageLayoutDock);
addDockWidget(Qt::BottomDockWidgetArea, replayDock);
mainWidget = new QStackedWidget(this);
mainWidget->addWidget(deckViewContainerWidget);
mainWidget->addWidget(gamePlayAreaWidget);
setCentralWidget(mainWidget);
createReplayMenuItems();
createViewMenuItems();
retranslateUi();
connect(&SettingsCache::instance().shortcuts(), SIGNAL(shortCutChanged()), this, SLOT(refreshShortcuts()));
refreshShortcuts();
messageLog->logReplayStarted(gameInfo.game_id());
this->installEventFilter(this);
QTimer::singleShot(0, this, SLOT(loadLayout()));
}
void DeckViewContainer::retranslateUi()
TabGame::TabGame(TabSupervisor *_tabSupervisor,
QList<AbstractClient *> &_clients,
const Event_GameJoined &event,
const QMap<int, QString> &_roomGameTypes)
: Tab(_tabSupervisor), userListProxy(_tabSupervisor->getUserListManager()), clients(_clients),
gameInfo(event.game_info()), roomGameTypes(_roomGameTypes), hostId(event.host_id()),
localPlayerId(event.player_id()), isLocalGame(_tabSupervisor->getIsLocalGame()), spectator(event.spectator()),
judge(event.judge()), gameStateKnown(false), resuming(event.resuming()), currentPhase(-1), activeCard(nullptr),
gameClosed(false), replay(nullptr), replayPlayButton(nullptr), replayFastForwardButton(nullptr),
aReplaySkipForward(nullptr), aReplaySkipBackward(nullptr), aReplaySkipForwardBig(nullptr),
aReplaySkipBackwardBig(nullptr), replayDock(nullptr)
{
loadLocalButton->setText(tr("Load deck..."));
loadRemoteButton->setText(tr("Load remote deck..."));
readyStartButton->setText(tr("Ready to start"));
updateSideboardLockButtonText();
// THIS CTOR IS USED ON GAMES
gameInfo.set_started(false);
createCardInfoDock();
createPlayerListDock();
createMessageDock();
createPlayAreaWidget();
createDeckViewContainerWidget();
addDockWidget(Qt::RightDockWidgetArea, cardInfoDock);
addDockWidget(Qt::RightDockWidgetArea, playerListDock);
addDockWidget(Qt::RightDockWidgetArea, messageLayoutDock);
mainWidget = new QStackedWidget(this);
mainWidget->addWidget(deckViewContainerWidget);
mainWidget->addWidget(gamePlayAreaWidget);
mainWidget->setContentsMargins(0, 0, 0, 0);
setCentralWidget(mainWidget);
createMenuItems();
createViewMenuItems();
retranslateUi();
connect(&SettingsCache::instance().shortcuts(), SIGNAL(shortCutChanged()), this, SLOT(refreshShortcuts()));
refreshShortcuts();
// append game to rooms game list for others to see
for (int i = gameInfo.game_types_size() - 1; i >= 0; i--)
gameTypes.append(roomGameTypes.find(gameInfo.game_types(i)).value());
this->installEventFilter(this);
QTimer::singleShot(0, this, SLOT(loadLayout()));
}
void DeckViewContainer::setButtonsVisible(bool _visible)
void TabGame::addMentionTag(const QString &value)
{
loadLocalButton->setVisible(_visible);
loadRemoteButton->setVisible(_visible);
readyStartButton->setVisible(_visible);
sideboardLockButton->setVisible(_visible);
sayEdit->insert(value + " ");
sayEdit->setFocus();
}
void DeckViewContainer::updateSideboardLockButtonText()
void TabGame::linkCardToChat(const QString &cardName)
{
if (sideboardLockButton->getState()) {
sideboardLockButton->setText(tr("Sideboard unlocked"));
} else {
sideboardLockButton->setText(tr("Sideboard locked"));
sayEdit->insert("[[" + cardName + "]] ");
sayEdit->setFocus();
}
void TabGame::emitUserEvent()
{
bool globalEvent = !spectator || SettingsCache::instance().getSpectatorNotificationsEnabled();
emit userEvent(globalEvent);
updatePlayerListDockTitle();
}
TabGame::~TabGame()
{
delete replay;
}
void TabGame::updatePlayerListDockTitle()
{
QString tabText = " | " + (replay ? tr("Replay") : tr("Game")) + " #" + QString::number(gameInfo.game_id());
QString userCountInfo = QString(" %1/%2").arg(players.size()).arg(gameInfo.max_players());
playerListDock->setWindowTitle(tr("Player List") + userCountInfo +
(playerListDock->isWindow() ? tabText : QString()));
}
bool TabGame::isMainPlayerConceded() const
{
Player *player = players.value(localPlayerId, nullptr);
return player && player->getConceded();
}
void TabGame::retranslateUi()
{
QString tabText = " | " + (replay ? tr("Replay") : tr("Game")) + " #" + QString::number(gameInfo.game_id());
updatePlayerListDockTitle();
cardInfoDock->setWindowTitle(tr("Card Info") + (cardInfoDock->isWindow() ? tabText : QString()));
messageLayoutDock->setWindowTitle(tr("Messages") + (messageLayoutDock->isWindow() ? tabText : QString()));
if (replayDock)
replayDock->setWindowTitle(tr("Replay Timeline") + (replayDock->isWindow() ? tabText : QString()));
if (phasesMenu) {
for (int i = 0; i < phaseActions.size(); ++i)
phaseActions[i]->setText(phasesToolbar->getLongPhaseName(i));
phasesMenu->setTitle(tr("&Phases"));
}
// setting text on a button removes its shortcut
ShortcutsSettings &shortcuts = SettingsCache::instance().shortcuts();
sideboardLockButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/sideboardLockButton"));
}
void DeckViewContainer::refreshShortcuts()
{
ShortcutsSettings &shortcuts = SettingsCache::instance().shortcuts();
loadLocalButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/loadLocalButton"));
loadRemoteButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/loadRemoteButton"));
readyStartButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/readyStartButton"));
sideboardLockButton->setShortcut(shortcuts.getSingleShortcut("DeckViewContainer/sideboardLockButton"));
gameMenu->setTitle(tr("&Game"));
if (aNextPhase) {
aNextPhase->setText(tr("Next &phase"));
}
if (aNextPhaseAction) {
aNextPhaseAction->setText(tr("Next phase with &action"));
}
if (aNextTurn) {
aNextTurn->setText(tr("Next &turn"));
}
if (aReverseTurn) {
aReverseTurn->setText(tr("Reverse turn order"));
}
if (aRemoveLocalArrows) {
aRemoveLocalArrows->setText(tr("&Remove all local arrows"));
}
if (aRotateViewCW) {
aRotateViewCW->setText(tr("Rotate View Cl&ockwise"));
}
if (aRotateViewCCW) {
aRotateViewCCW->setText(tr("Rotate View Co&unterclockwise"));
}
if (aGameInfo)
aGameInfo->setText(tr("Game &information"));
if (aConcede) {
if (isMainPlayerConceded()) {
aConcede->setText(tr("Un&concede"));
} else {
aConcede->setText(tr("&Concede"));
}
}
if (aLeaveGame) {
aLeaveGame->setText(tr("&Leave game"));
}
if (aCloseReplay) {
aCloseReplay->setText(tr("C&lose replay"));
}
if (aFocusChat) {
aFocusChat->setText(tr("&Focus Chat"));
}
if (sayLabel) {
sayLabel->setText(tr("&Say:"));
}
viewMenu->setTitle(tr("&View"));
cardInfoDockMenu->setTitle(tr("Card Info"));
messageLayoutDockMenu->setTitle(tr("Messages"));
playerListDockMenu->setTitle(tr("Player List"));
aCardInfoDockVisible->setText(tr("Visible"));
aCardInfoDockFloating->setText(tr("Floating"));
aMessageLayoutDockVisible->setText(tr("Visible"));
aMessageLayoutDockFloating->setText(tr("Floating"));
aPlayerListDockVisible->setText(tr("Visible"));
aPlayerListDockFloating->setText(tr("Floating"));
if (replayDock) {
replayDockMenu->setTitle(tr("Replay Timeline"));
aReplayDockVisible->setText(tr("Visible"));
aReplayDockFloating->setText(tr("Floating"));
}
aResetLayout->setText(tr("Reset layout"));
cardInfoFrameWidget->retranslateUi();
QMapIterator<int, Player *> i(players);
while (i.hasNext())
i.next().value()->retranslateUi();
QMapIterator<int, DeckViewContainer *> j(deckViewContainers);
while (j.hasNext())
j.next().value()->retranslateUi();
scene->retranslateUi();
}
void TabGame::refreshShortcuts()
@@ -278,341 +399,15 @@ void TabGame::refreshShortcuts()
}
}
void DeckViewContainer::loadLocalDeck()
void TabGame::closeRequest(bool forced)
{
QFileDialog dialog(this, tr("Load deck"));
dialog.setDirectory(SettingsCache::instance().getDeckPath());
dialog.setNameFilters(DeckLoader::fileNameFilters);
if (!dialog.exec())
return;
QString fileName = dialog.selectedFiles().at(0);
DeckLoader::FileFormat fmt = DeckLoader::getFormatFromName(fileName);
QString deckString;
DeckLoader deck;
bool error = !deck.loadFromFile(fileName, fmt);
if (!error) {
deckString = deck.writeToString_Native();
error = deckString.length() > MAX_FILE_LENGTH;
}
if (error) {
QMessageBox::critical(this, tr("Error"), tr("The selected file could not be loaded."));
if (!forced && !leaveGame()) {
return;
}
Command_DeckSelect cmd;
cmd.set_deck(deckString.toStdString());
PendingCommand *pend = parentGame->prepareGameCommand(cmd);
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
SLOT(deckSelectFinished(const Response &)));
parentGame->sendGameCommand(pend, playerId);
}
void DeckViewContainer::loadRemoteDeck()
{
DlgLoadRemoteDeck dlg(parentGame->getClientForPlayer(playerId), this);
if (dlg.exec()) {
Command_DeckSelect cmd;
cmd.set_deck_id(dlg.getDeckId());
PendingCommand *pend = parentGame->prepareGameCommand(cmd);
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
SLOT(deckSelectFinished(const Response &)));
parentGame->sendGameCommand(pend, playerId);
}
}
void DeckViewContainer::deckSelectFinished(const Response &r)
{
const Response_DeckDownload &resp = r.GetExtension(Response_DeckDownload::ext);
DeckLoader newDeck(QString::fromStdString(resp.deck()));
// TODO CHANGE THIS TO BE SELECTED BY UUID
PictureLoader::cacheCardPixmaps(CardDatabaseManager::getInstance()->getCards(newDeck.getCardList()));
setDeck(newDeck);
}
void DeckViewContainer::readyStart()
{
Command_ReadyStart cmd;
cmd.set_ready(!readyStartButton->getState());
parentGame->sendGameCommand(cmd, playerId);
}
void DeckViewContainer::sideboardLockButtonClicked()
{
Command_SetSideboardLock cmd;
cmd.set_locked(sideboardLockButton->getState());
parentGame->sendGameCommand(cmd, playerId);
}
void DeckViewContainer::sideboardPlanChanged()
{
Command_SetSideboardPlan cmd;
const QList<MoveCard_ToZone> &newPlan = deckView->getSideboardPlan();
for (const auto &i : newPlan)
cmd.add_move_list()->CopyFrom(i);
parentGame->sendGameCommand(cmd, playerId);
}
void DeckViewContainer::setReadyStart(bool ready)
{
readyStartButton->setState(ready);
deckView->setLocked(ready || !sideboardLockButton->getState());
sideboardLockButton->setEnabled(!readyStartButton->getState() && readyStartButton->isEnabled());
}
void DeckViewContainer::setSideboardLocked(bool locked)
{
sideboardLockButton->setState(!locked);
deckView->setLocked(readyStartButton->getState() || !sideboardLockButton->getState());
if (locked)
deckView->resetSideboardPlan();
}
void DeckViewContainer::setDeck(const DeckLoader &deck)
{
deckView->setDeck(deck);
readyStartButton->setEnabled(true);
sideboardLockButton->setState(false);
sideboardLockButton->setEnabled(true);
}
TabGame::TabGame(TabSupervisor *_tabSupervisor, GameReplay *_replay)
: Tab(_tabSupervisor), secondsElapsed(0), hostId(-1), localPlayerId(-1),
isLocalGame(_tabSupervisor->getIsLocalGame()), spectator(true), judge(false), gameStateKnown(false),
resuming(false), currentPhase(-1), activeCard(nullptr), gameClosed(false), replay(_replay), currentReplayStep(0),
sayLabel(nullptr), sayEdit(nullptr)
{
// THIS CTOR IS USED ON REPLAY
gameInfo.CopyFrom(replay->game_info());
gameInfo.set_spectators_omniscient(true);
// Create list: event number -> time [ms]
// Distribute simultaneous events evenly across 1 second.
unsigned int lastEventTimestamp = 0;
const int eventCount = replay->event_list_size();
for (int i = 0; i < eventCount; ++i) {
int j = i + 1;
while ((j < eventCount) && (replay->event_list(j).seconds_elapsed() == lastEventTimestamp))
++j;
const int numberEventsThisSecond = j - i;
for (int k = 0; k < numberEventsThisSecond; ++k)
replayTimeline.append(replay->event_list(i + k).seconds_elapsed() * 1000 +
(int)((qreal)k / (qreal)numberEventsThisSecond * 1000));
if (j < eventCount)
lastEventTimestamp = replay->event_list(j).seconds_elapsed();
i += numberEventsThisSecond - 1;
}
createCardInfoDock(true);
createPlayerListDock(true);
createMessageDock(true);
createPlayAreaWidget(true);
createDeckViewContainerWidget(true);
createReplayDock();
addDockWidget(Qt::RightDockWidgetArea, cardInfoDock);
addDockWidget(Qt::RightDockWidgetArea, playerListDock);
addDockWidget(Qt::RightDockWidgetArea, messageLayoutDock);
addDockWidget(Qt::BottomDockWidgetArea, replayDock);
mainWidget = new QStackedWidget(this);
mainWidget->addWidget(deckViewContainerWidget);
mainWidget->addWidget(gamePlayAreaWidget);
setCentralWidget(mainWidget);
createReplayMenuItems();
createViewMenuItems();
retranslateUi();
connect(&SettingsCache::instance().shortcuts(), SIGNAL(shortCutChanged()), this, SLOT(refreshShortcuts()));
refreshShortcuts();
messageLog->logReplayStarted(gameInfo.game_id());
this->installEventFilter(this);
QTimer::singleShot(0, this, SLOT(loadLayout()));
}
TabGame::TabGame(TabSupervisor *_tabSupervisor,
QList<AbstractClient *> &_clients,
const Event_GameJoined &event,
const QMap<int, QString> &_roomGameTypes)
: Tab(_tabSupervisor), clients(_clients), gameInfo(event.game_info()), roomGameTypes(_roomGameTypes),
hostId(event.host_id()), localPlayerId(event.player_id()), isLocalGame(_tabSupervisor->getIsLocalGame()),
spectator(event.spectator()), judge(event.judge()), gameStateKnown(false), resuming(event.resuming()),
currentPhase(-1), activeCard(nullptr), gameClosed(false), replay(nullptr), replayPlayButton(nullptr),
replayFastForwardButton(nullptr), aReplaySkipForward(nullptr), aReplaySkipBackward(nullptr),
aReplaySkipForwardBig(nullptr), aReplaySkipBackwardBig(nullptr), replayDock(nullptr)
{
// THIS CTOR IS USED ON GAMES
gameInfo.set_started(false);
createCardInfoDock();
createPlayerListDock();
createMessageDock();
createPlayAreaWidget();
createDeckViewContainerWidget();
addDockWidget(Qt::RightDockWidgetArea, cardInfoDock);
addDockWidget(Qt::RightDockWidgetArea, playerListDock);
addDockWidget(Qt::RightDockWidgetArea, messageLayoutDock);
mainWidget = new QStackedWidget(this);
mainWidget->addWidget(deckViewContainerWidget);
mainWidget->addWidget(gamePlayAreaWidget);
mainWidget->setContentsMargins(0, 0, 0, 0);
setCentralWidget(mainWidget);
createMenuItems();
createViewMenuItems();
retranslateUi();
connect(&SettingsCache::instance().shortcuts(), SIGNAL(shortCutChanged()), this, SLOT(refreshShortcuts()));
refreshShortcuts();
// append game to rooms game list for others to see
for (int i = gameInfo.game_types_size() - 1; i >= 0; i--)
gameTypes.append(roomGameTypes.find(gameInfo.game_types(i)).value());
this->installEventFilter(this);
QTimer::singleShot(0, this, SLOT(loadLayout()));
}
void TabGame::addMentionTag(const QString &value)
{
sayEdit->insert(value + " ");
sayEdit->setFocus();
}
void TabGame::linkCardToChat(const QString &cardName)
{
sayEdit->insert("[[" + cardName + "]] ");
sayEdit->setFocus();
}
void TabGame::emitUserEvent()
{
bool globalEvent = !spectator || SettingsCache::instance().getSpectatorNotificationsEnabled();
emit userEvent(globalEvent);
updatePlayerListDockTitle();
}
TabGame::~TabGame()
{
delete replay;
QMapIterator<int, Player *> i(players);
while (i.hasNext()) {
delete i.next().value();
}
players.clear();
emit gameClosing(this);
}
void TabGame::updatePlayerListDockTitle()
{
QString tabText = " | " + (replay ? tr("Replay") : tr("Game")) + " #" + QString::number(gameInfo.game_id());
QString userCountInfo = QString(" %1/%2").arg(players.size()).arg(gameInfo.max_players());
playerListDock->setWindowTitle(tr("Player List") + userCountInfo +
(playerListDock->isWindow() ? tabText : QString()));
}
void TabGame::retranslateUi()
{
QString tabText = " | " + (replay ? tr("Replay") : tr("Game")) + " #" + QString::number(gameInfo.game_id());
updatePlayerListDockTitle();
cardInfoDock->setWindowTitle(tr("Card Info") + (cardInfoDock->isWindow() ? tabText : QString()));
messageLayoutDock->setWindowTitle(tr("Messages") + (messageLayoutDock->isWindow() ? tabText : QString()));
if (replayDock)
replayDock->setWindowTitle(tr("Replay Timeline") + (replayDock->isWindow() ? tabText : QString()));
if (phasesMenu) {
for (int i = 0; i < phaseActions.size(); ++i)
phaseActions[i]->setText(phasesToolbar->getLongPhaseName(i));
phasesMenu->setTitle(tr("&Phases"));
}
gameMenu->setTitle(tr("&Game"));
if (aNextPhase) {
aNextPhase->setText(tr("Next &phase"));
}
if (aNextPhaseAction) {
aNextPhaseAction->setText(tr("Next phase with &action"));
}
if (aNextTurn) {
aNextTurn->setText(tr("Next &turn"));
}
if (aReverseTurn) {
aReverseTurn->setText(tr("Reverse turn order"));
}
if (aRemoveLocalArrows) {
aRemoveLocalArrows->setText(tr("&Remove all local arrows"));
}
if (aRotateViewCW) {
aRotateViewCW->setText(tr("Rotate View Cl&ockwise"));
}
if (aRotateViewCCW) {
aRotateViewCCW->setText(tr("Rotate View Co&unterclockwise"));
}
if (aGameInfo)
aGameInfo->setText(tr("Game &information"));
if (aConcede) {
aConcede->setText(tr("&Concede"));
}
if (aLeaveGame) {
aLeaveGame->setText(tr("&Leave game"));
}
if (aCloseReplay) {
aCloseReplay->setText(tr("C&lose replay"));
}
if (aFocusChat) {
aFocusChat->setText(tr("&Focus Chat"));
}
if (sayLabel) {
sayLabel->setText(tr("&Say:"));
}
viewMenu->setTitle(tr("&View"));
cardInfoDockMenu->setTitle(tr("Card Info"));
messageLayoutDockMenu->setTitle(tr("Messages"));
playerListDockMenu->setTitle(tr("Player List"));
aCardInfoDockVisible->setText(tr("Visible"));
aCardInfoDockFloating->setText(tr("Floating"));
aMessageLayoutDockVisible->setText(tr("Visible"));
aMessageLayoutDockFloating->setText(tr("Floating"));
aPlayerListDockVisible->setText(tr("Visible"));
aPlayerListDockFloating->setText(tr("Floating"));
if (replayDock) {
replayDockMenu->setTitle(tr("Replay Timeline"));
aReplayDockVisible->setText(tr("Visible"));
aReplayDockFloating->setText(tr("Floating"));
}
aResetLayout->setText(tr("Reset layout"));
cardInfoFrameWidget->retranslateUi();
QMapIterator<int, Player *> i(players);
while (i.hasNext())
i.next().value()->retranslateUi();
QMapIterator<int, DeckViewContainer *> j(deckViewContainers);
while (j.hasNext())
j.next().value()->retranslateUi();
scene->retranslateUi();
}
void TabGame::closeRequest()
{
actLeaveGame();
close();
}
void TabGame::replayNextEvent(Player::EventProcessingOptions options)
@@ -703,19 +498,23 @@ void TabGame::actConcede()
}
}
void TabGame::actLeaveGame()
/**
* Confirms the leave game and sends the leave game command, if applicable.
*
* @return True if the leave game is confirmed
*/
bool TabGame::leaveGame()
{
if (!gameClosed) {
if (!spectator)
if (QMessageBox::question(this, tr("Leave game"), tr("Are you sure you want to leave this game?"),
QMessageBox::Yes | QMessageBox::No, QMessageBox::No) != QMessageBox::Yes)
return;
return false;
if (!replay)
sendGameCommand(Command_LeaveGame());
}
scene->clearViews();
deleteLater();
return true;
}
void TabGame::actSay()
@@ -723,6 +522,12 @@ void TabGame::actSay()
if (completer->popup()->isVisible())
return;
if (sayEdit->text().startsWith("/card ")) {
cardInfoFrameWidget->setCard(sayEdit->text().mid(6));
sayEdit->clear();
return;
}
if (!sayEdit->text().isEmpty()) {
Command_GameSay cmd;
cmd.set_message(sayEdit->text().toStdString());
@@ -834,6 +639,15 @@ Player *TabGame::addPlayer(int playerId, const ServerInfo_User &info)
connect(deckView, SIGNAL(newCardAdded(AbstractCardItem *)), this, SLOT(newCardAdded(AbstractCardItem *)));
deckViewContainers.insert(playerId, deckView);
deckViewContainerLayout->addWidget(deckView);
// auto load deck for player if that debug setting is enabled
QString deckPath = SettingsCache::instance().debug().getDeckPathForPlayer(newPlayer->getName());
if (!deckPath.isEmpty()) {
QTimer::singleShot(0, this, [deckView, deckPath] {
deckView->loadDeckFromFile(deckPath);
deckView->readyAndUpdate();
});
}
}
gameMenu->insertMenu(playersSeparator, newPlayer->getPlayerMenu());
@@ -858,6 +672,9 @@ Player *TabGame::addPlayer(int playerId, const ServerInfo_User &info)
}
}
// update menu text when player concedes so that "concede" gets updated to "unconcede"
connect(newPlayer, &Player::playerCountChanged, this, &TabGame::retranslateUi);
emit playerAdded(newPlayer);
return newPlayer;
}
@@ -936,7 +753,7 @@ void TabGame::processGameEventContainer(const GameEventContainer &cont,
default: {
Player *player = players.value(playerId, 0);
if (!player) {
qDebug() << "unhandled game event: invalid player id";
qCDebug(TabGameLog) << "unhandled game event: invalid player id";
break;
}
player->processGameEvent(eventType, event, context, options);
@@ -1065,8 +882,7 @@ void TabGame::closeGame()
void TabGame::eventSpectatorSay(const Event_GameSay &event, int eventPlayerId, const GameEventContext & /*context*/)
{
const ServerInfo_User &userInfo = spectators.value(eventPlayerId);
messageLog->logSpectatorSay(QString::fromStdString(userInfo.name()), UserLevelFlags(userInfo.user_level()),
QString::fromStdString(userInfo.privlevel()), QString::fromStdString(event.message()));
messageLog->logSpectatorSay(userInfo, QString::fromStdString(event.message()));
}
void TabGame::eventSpectatorLeave(const Event_Leave &event, int eventPlayerId, const GameEventContext & /*context*/)
@@ -1273,6 +1089,7 @@ void TabGame::eventLeave(const Event_Leave &event, int eventPlayerId, const Game
players.remove(eventPlayerId);
emit playerRemoved(player);
player->clear();
scene->removePlayer(player);
player->deleteLater();
// Rearrange all remaining zones so that attachment relationship updates take place
@@ -1381,7 +1198,7 @@ void TabGame::eventSetActivePhase(const Event_SetActivePhase &event,
void TabGame::newCardAdded(AbstractCardItem *card)
{
connect(card, SIGNAL(hovered(AbstractCardItem *)), cardInfoFrameWidget, SLOT(setCard(AbstractCardItem *)));
connect(card, SIGNAL(showCardInfoPopup(QPoint, QString)), this, SLOT(showCardInfoPopup(QPoint, QString)));
connect(card, &AbstractCardItem::showCardInfoPopup, this, &TabGame::showCardInfoPopup);
connect(card, SIGNAL(deleteCardInfoPopup(QString)), this, SLOT(deleteCardInfoPopup(QString)));
connect(card, SIGNAL(cardShiftClicked(QString)), this, SLOT(linkCardToChat(QString)));
}
@@ -1489,7 +1306,7 @@ void TabGame::createMenuItems()
aConcede = new QAction(this);
connect(aConcede, SIGNAL(triggered()), this, SLOT(actConcede()));
aLeaveGame = new QAction(this);
connect(aLeaveGame, SIGNAL(triggered()), this, SLOT(actLeaveGame()));
connect(aLeaveGame, &QAction::triggered, this, [this] { closeRequest(); });
aFocusChat = new QAction(this);
connect(aFocusChat, SIGNAL(triggered()), sayEdit, SLOT(setFocus()));
aCloseReplay = nullptr;
@@ -1539,7 +1356,7 @@ void TabGame::createReplayMenuItems()
aFocusChat = nullptr;
aLeaveGame = nullptr;
aCloseReplay = new QAction(this);
connect(aCloseReplay, SIGNAL(triggered()), this, SLOT(actLeaveGame()));
connect(aCloseReplay, &QAction::triggered, this, [this] { closeRequest(); });
phasesMenu = nullptr;
gameMenu = new QMenu(this);
@@ -1742,22 +1559,22 @@ void TabGame::createReplayDock()
aReplaySkipForward = new QAction(timelineWidget);
timelineWidget->addAction(aReplaySkipForward);
connect(aReplaySkipForward, &QAction::triggered, this,
[=]() { timelineWidget->skipByAmount(ReplayTimelineWidget::SMALL_SKIP_MS); });
[this] { timelineWidget->skipByAmount(ReplayTimelineWidget::SMALL_SKIP_MS); });
aReplaySkipBackward = new QAction(timelineWidget);
timelineWidget->addAction(aReplaySkipBackward);
connect(aReplaySkipBackward, &QAction::triggered, this,
[=]() { timelineWidget->skipByAmount(-ReplayTimelineWidget::SMALL_SKIP_MS); });
[this] { timelineWidget->skipByAmount(-ReplayTimelineWidget::SMALL_SKIP_MS); });
aReplaySkipForwardBig = new QAction(timelineWidget);
timelineWidget->addAction(aReplaySkipForwardBig);
connect(aReplaySkipForwardBig, &QAction::triggered, this,
[=]() { timelineWidget->skipByAmount(ReplayTimelineWidget::BIG_SKIP_MS); });
[this] { timelineWidget->skipByAmount(ReplayTimelineWidget::BIG_SKIP_MS); });
aReplaySkipBackwardBig = new QAction(timelineWidget);
timelineWidget->addAction(aReplaySkipBackwardBig);
connect(aReplaySkipBackwardBig, &QAction::triggered, this,
[=]() { timelineWidget->skipByAmount(-ReplayTimelineWidget::BIG_SKIP_MS); });
[this] { timelineWidget->skipByAmount(-ReplayTimelineWidget::BIG_SKIP_MS); });
// buttons
replayPlayButton = new QToolButton;
@@ -1861,9 +1678,9 @@ void TabGame::createPlayerListDock(bool bReplay)
void TabGame::createMessageDock(bool bReplay)
{
messageLog = new MessageLogWidget(tabSupervisor, tabSupervisor, this);
messageLog = new MessageLogWidget(tabSupervisor, this);
connect(messageLog, SIGNAL(cardNameHovered(QString)), cardInfoFrameWidget, SLOT(setCard(QString)));
connect(messageLog, SIGNAL(showCardInfoPopup(QPoint, QString)), this, SLOT(showCardInfoPopup(QPoint, QString)));
connect(messageLog, &MessageLogWidget::showCardInfoPopup, this, &TabGame::showCardInfoPopup);
connect(messageLog, SIGNAL(deleteCardInfoPopup(QString)), this, SLOT(deleteCardInfoPopup(QString)));
if (!bReplay) {

View File

@@ -3,25 +3,28 @@
#include "../../client/tearoff_menu.h"
#include "../../game/player/player.h"
#include "../ui/widgets/visual_deck_storage/visual_deck_storage_widget.h"
#include "pb/event_leave.pb.h"
#include "pb/serverinfo_game.pb.h"
#include "tab.h"
#include <QCompleter>
#include <QLoggingCategory>
#include <QMap>
#include <QPushButton>
inline Q_LOGGING_CATEGORY(TabGameLog, "tab_game");
class UserListProxy;
class DeckViewContainer;
class AbstractClient;
class CardDatabase;
class GameView;
class DeckView;
class GameScene;
class CardInfoFrameWidget;
class MessageLogWidget;
class QTimer;
class QSplitter;
class QLabel;
class QPushButton;
class QToolButton;
class QMenu;
class ZoneViewLayout;
@@ -51,7 +54,6 @@ class Event_ReverseTurn;
class CardZone;
class AbstractCardItem;
class CardItem;
class TabGame;
class DeckLoader;
class QVBoxLayout;
class QHBoxLayout;
@@ -62,63 +64,13 @@ class LineEditCompleter;
class QDockWidget;
class QStackedWidget;
class ToggleButton : public QPushButton
{
Q_OBJECT
private:
bool state;
signals:
void stateChanged();
public:
ToggleButton(QWidget *parent = nullptr);
bool getState() const
{
return state;
}
void setState(bool _state);
protected:
void paintEvent(QPaintEvent *event);
};
class DeckViewContainer : public QWidget
{
Q_OBJECT
private:
QPushButton *loadLocalButton, *loadRemoteButton;
ToggleButton *readyStartButton, *sideboardLockButton;
DeckView *deckView;
TabGame *parentGame;
int playerId;
private slots:
void loadLocalDeck();
void loadRemoteDeck();
void readyStart();
void deckSelectFinished(const Response &r);
void sideboardPlanChanged();
void sideboardLockButtonClicked();
void updateSideboardLockButtonText();
void refreshShortcuts();
signals:
void newCardAdded(AbstractCardItem *card);
void notIdle();
public:
DeckViewContainer(int _playerId, TabGame *parent);
void retranslateUi();
void setButtonsVisible(bool _visible);
void setReadyStart(bool ready);
void setSideboardLocked(bool locked);
void setDeck(const DeckLoader &deck);
};
class TabGame : public Tab
{
Q_OBJECT
private:
QTimer *gameTimer;
int secondsElapsed;
const UserListProxy *userListProxy;
QList<AbstractClient *> clients;
ServerInfo_Game gameInfo;
QMap<int, QString> roomGameTypes;
@@ -176,9 +128,12 @@ private:
Player *addPlayer(int playerId, const ServerInfo_User &info);
bool isMainPlayerConceded() const;
void startGame(bool resuming);
void stopGame();
void closeGame();
bool leaveGame();
void eventSpectatorSay(const Event_GameSay &event, int eventPlayerId, const GameEventContext &context);
void eventSpectatorLeave(const Event_Leave &event, int eventPlayerId, const GameEventContext &context);
@@ -232,7 +187,6 @@ private slots:
void actGameInfo();
void actConcede();
void actLeaveGame();
void actRemoveLocalArrows();
void actRotateViewCW();
void actRotateViewCCW();
@@ -268,7 +222,7 @@ public:
~TabGame() override;
void retranslateUi() override;
void updatePlayerListDockTitle();
void closeRequest() override;
void closeRequest(bool forced = false) override;
const QMap<int, Player *> &getPlayers() const
{
return players;

View File

@@ -21,8 +21,7 @@
#include <QtGui>
#include <QtWidgets>
TabLog::TabLog(TabSupervisor *_tabSupervisor, AbstractClient *_client, QWidget *parent)
: Tab(_tabSupervisor, parent), client(_client)
TabLog::TabLog(TabSupervisor *_tabSupervisor, AbstractClient *_client) : Tab(_tabSupervisor), client(_client)
{
roomTable = new QTableWidget();
roomTable->setColumnCount(6);

View File

@@ -53,10 +53,10 @@ private slots:
void restartLayout();
public:
TabLog(TabSupervisor *_tabSupervisor, AbstractClient *_client, QWidget *parent = nullptr);
~TabLog();
void retranslateUi();
QString getTabText() const
TabLog(TabSupervisor *_tabSupervisor, AbstractClient *_client);
~TabLog() override;
void retranslateUi() override;
QString getTabText() const override
{
return tr("Logs");
}

View File

@@ -4,6 +4,7 @@
#include "../../main.h"
#include "../../server/chat_view/chat_view.h"
#include "../../server/pending_command.h"
#include "../../server/user/user_list_manager.h"
#include "../../settings/cache_settings.h"
#include "../game_logic/abstract_client.h"
#include "../sound_engine.h"
@@ -25,8 +26,8 @@ TabMessage::TabMessage(TabSupervisor *_tabSupervisor,
: Tab(_tabSupervisor), client(_client), ownUserInfo(new ServerInfo_User(_ownUserInfo)),
otherUserInfo(new ServerInfo_User(_otherUserInfo)), userOnline(true)
{
chatView = new ChatView(tabSupervisor, tabSupervisor, 0, true);
connect(chatView, SIGNAL(showCardInfoPopup(QPoint, QString)), this, SLOT(showCardInfoPopup(QPoint, QString)));
chatView = new ChatView(tabSupervisor, 0, true);
connect(chatView, &ChatView::showCardInfoPopup, this, &TabMessage::showCardInfoPopup);
connect(chatView, SIGNAL(deleteCardInfoPopup(QString)), this, SLOT(deleteCardInfoPopup(QString)));
connect(chatView, SIGNAL(addMentionTag(QString)), this, SLOT(addMentionTag(QString)));
sayEdit = new LineEditUnfocusable;
@@ -38,7 +39,7 @@ TabMessage::TabMessage(TabSupervisor *_tabSupervisor,
vbox->addWidget(sayEdit);
aLeave = new QAction(this);
connect(aLeave, SIGNAL(triggered()), this, SLOT(actLeave()));
connect(aLeave, &QAction::triggered, this, [this] { closeRequest(); });
messageMenu = new QMenu(this);
messageMenu->addAction(aLeave);
@@ -53,7 +54,6 @@ TabMessage::TabMessage(TabSupervisor *_tabSupervisor,
TabMessage::~TabMessage()
{
emit talkClosing(this);
delete ownUserInfo;
delete otherUserInfo;
}
@@ -86,9 +86,10 @@ QString TabMessage::getTabText() const
return tr("%1 - Private chat").arg(QString::fromStdString(otherUserInfo->name()));
}
void TabMessage::closeRequest()
void TabMessage::closeRequest(bool /*forced*/)
{
actLeave();
emit talkClosing(this);
close();
}
void TabMessage::sendMessage()
@@ -114,19 +115,11 @@ void TabMessage::messageSent(const Response &response)
"This user is ignoring you, they cannot see your messages in main chat and you cannot join their games."));
}
void TabMessage::actLeave()
{
deleteLater();
}
void TabMessage::processUserMessageEvent(const Event_UserMessage &event)
{
auto userInfo = event.sender_name() == otherUserInfo->name() ? otherUserInfo : ownUserInfo;
const UserLevelFlags userLevel(userInfo->user_level());
const QString userPriv = QString::fromStdString(userInfo->privlevel());
chatView->appendMessage(QString::fromStdString(event.message()), {}, QString::fromStdString(event.sender_name()),
userLevel, userPriv, true);
chatView->appendMessage(QString::fromStdString(event.message()), {}, *userInfo, true);
if (tabSupervisor->currentIndex() != tabSupervisor->indexOf(this))
soundEngine->playSound("private_message");
if (SettingsCache::instance().getShowMessagePopup() && shouldShowSystemPopup(event))
@@ -152,7 +145,7 @@ void TabMessage::showSystemPopup(const Event_UserMessage &event)
event.message().c_str());
connect(trayIcon, SIGNAL(messageClicked()), this, SLOT(messageClicked()));
} else {
qDebug() << "Error: trayIcon is NULL. TabMessage::showSystemPopup failed";
qCDebug(TabMessageLog) << "Error: trayIcon is NULL. TabMessage::showSystemPopup failed";
}
}

View File

@@ -3,6 +3,10 @@
#include "tab.h"
#include <QLoggingCategory>
inline Q_LOGGING_CATEGORY(TabMessageLog, "tab_message");
class AbstractClient;
class ChatView;
class LineEditUnfocusable;
@@ -29,7 +33,6 @@ signals:
void maximizeClient();
private slots:
void sendMessage();
void actLeave();
void messageSent(const Response &response);
void addMentionTag(QString mentionTag);
void messageClicked();
@@ -39,12 +42,12 @@ public:
AbstractClient *_client,
const ServerInfo_User &_ownUserInfo,
const ServerInfo_User &_otherUserInfo);
~TabMessage();
void retranslateUi();
void closeRequest();
void tabActivated();
~TabMessage() override;
void retranslateUi() override;
void closeRequest(bool forced = false) override;
void tabActivated() override;
QString getUserName() const;
QString getTabText() const;
QString getTabText() const override;
void processUserMessageEvent(const Event_UserMessage &event);

View File

@@ -15,6 +15,7 @@
#include <QAction>
#include <QApplication>
#include <QDesktopServices>
#include <QFileSystemModel>
#include <QGroupBox>
#include <QHBoxLayout>
@@ -23,9 +24,11 @@
#include <QMessageBox>
#include <QToolBar>
#include <QTreeView>
#include <QUrl>
#include <QVBoxLayout>
TabReplays::TabReplays(TabSupervisor *_tabSupervisor, AbstractClient *_client) : Tab(_tabSupervisor), client(_client)
TabReplays::TabReplays(TabSupervisor *_tabSupervisor, AbstractClient *_client, const ServerInfo_User *currentUserInfo)
: Tab(_tabSupervisor), client(_client)
{
localDirModel = new QFileSystemModel(this);
localDirModel->setRootPath(SettingsCache::instance().getReplaysPath());
@@ -36,16 +39,31 @@ TabReplays::TabReplays(TabSupervisor *_tabSupervisor, AbstractClient *_client) :
localDirView->setColumnHidden(1, true);
localDirView->setRootIndex(localDirModel->index(localDirModel->rootPath(), 0));
localDirView->setSortingEnabled(true);
localDirView->setSelectionMode(QAbstractItemView::ExtendedSelection);
localDirView->header()->setSectionResizeMode(QHeaderView::ResizeToContents);
localDirView->header()->setSortIndicator(0, Qt::AscendingOrder);
leftToolBar = new QToolBar;
// Left side layout
/* put an invisible dummy QToolBar in the leftmost column so that the main toolbar is centered.
* Really ugly workaround, but I couldn't figure out the proper way to make it centered */
QToolBar *dummyToolBar = new QToolBar(this);
QSizePolicy sizePolicy = dummyToolBar->sizePolicy();
sizePolicy.setRetainSizeWhenHidden(true);
dummyToolBar->setSizePolicy(sizePolicy);
dummyToolBar->setVisible(false);
leftToolBar = new QToolBar(this);
leftToolBar->setOrientation(Qt::Horizontal);
leftToolBar->setIconSize(QSize(32, 32));
QHBoxLayout *leftToolBarLayout = new QHBoxLayout;
leftToolBarLayout->addStretch();
leftToolBarLayout->addWidget(leftToolBar);
leftToolBarLayout->addStretch();
QToolBar *leftRightmostToolBar = new QToolBar(this);
leftRightmostToolBar->setOrientation(Qt::Horizontal);
leftRightmostToolBar->setIconSize(QSize(32, 32));
QGridLayout *leftToolBarLayout = new QGridLayout;
leftToolBarLayout->addWidget(dummyToolBar, 0, 0, Qt::AlignLeft);
leftToolBarLayout->addWidget(leftToolBar, 0, 1, Qt::AlignHCenter);
leftToolBarLayout->addWidget(leftRightmostToolBar, 0, 2, Qt::AlignRight);
QVBoxLayout *leftVbox = new QVBoxLayout;
leftVbox->addWidget(localDirView);
@@ -53,6 +71,7 @@ TabReplays::TabReplays(TabSupervisor *_tabSupervisor, AbstractClient *_client) :
leftGroupBox = new QGroupBox;
leftGroupBox->setLayout(leftVbox);
// Right side layout
rightToolBar = new QToolBar;
rightToolBar->setOrientation(Qt::Horizontal);
rightToolBar->setIconSize(QSize(32, 32));
@@ -69,17 +88,31 @@ TabReplays::TabReplays(TabSupervisor *_tabSupervisor, AbstractClient *_client) :
rightGroupBox = new QGroupBox;
rightGroupBox->setLayout(rightVbox);
// combine layouts
QHBoxLayout *hbox = new QHBoxLayout;
hbox->addWidget(leftGroupBox);
hbox->addWidget(rightGroupBox);
// Left side actions
aOpenLocalReplay = new QAction(this);
aOpenLocalReplay->setIcon(QPixmap("theme:icons/view"));
connect(aOpenLocalReplay, SIGNAL(triggered()), this, SLOT(actOpenLocalReplay()));
connect(localDirView, SIGNAL(doubleClicked(const QModelIndex &)), this, SLOT(actOpenLocalReplay()));
aRenameLocal = new QAction(this);
aRenameLocal->setIcon(QPixmap("theme:icons/pencil"));
connect(aRenameLocal, &QAction::triggered, this, &TabReplays::actRenameLocal);
aNewLocalFolder = new QAction(this);
aNewLocalFolder->setIcon(qApp->style()->standardIcon(QStyle::SP_FileDialogNewFolder));
connect(aNewLocalFolder, &QAction::triggered, this, &TabReplays::actNewLocalFolder);
aDeleteLocalReplay = new QAction(this);
aDeleteLocalReplay->setIcon(QPixmap("theme:icons/remove_row"));
connect(aDeleteLocalReplay, SIGNAL(triggered()), this, SLOT(actDeleteLocalReplay()));
aOpenReplaysFolder = new QAction(this);
aOpenReplaysFolder->setIcon(qApp->style()->standardIcon(QStyle::SP_DirOpenIcon));
connect(aOpenReplaysFolder, &QAction::triggered, this, &TabReplays::actOpenReplaysFolder);
// Right side actions
aOpenRemoteReplay = new QAction(this);
aOpenRemoteReplay->setIcon(QPixmap("theme:icons/view"));
connect(aOpenRemoteReplay, SIGNAL(triggered()), this, SLOT(actOpenRemoteReplay()));
@@ -94,8 +127,14 @@ TabReplays::TabReplays(TabSupervisor *_tabSupervisor, AbstractClient *_client) :
aDeleteRemoteReplay->setIcon(QPixmap("theme:icons/remove_row"));
connect(aDeleteRemoteReplay, SIGNAL(triggered()), this, SLOT(actDeleteRemoteReplay()));
// Add actions to toolbars
leftToolBar->addAction(aOpenLocalReplay);
leftToolBar->addAction(aRenameLocal);
leftToolBar->addAction(aNewLocalFolder);
leftToolBar->addAction(aDeleteLocalReplay);
leftRightmostToolBar->addAction(aOpenReplaysFolder);
rightToolBar->addAction(aOpenRemoteReplay);
rightToolBar->addAction(aDownload);
rightToolBar->addAction(aKeep);
@@ -109,6 +148,10 @@ TabReplays::TabReplays(TabSupervisor *_tabSupervisor, AbstractClient *_client) :
connect(client, SIGNAL(replayAddedEventReceived(const Event_ReplayAdded &)), this,
SLOT(replayAddedEventReceived(const Event_ReplayAdded &)));
connect(client, &AbstractClient::userInfoChanged, this, &TabReplays::handleConnected);
connect(client, &AbstractClient::statusChanged, this, &TabReplays::handleConnectionChanged);
setRemoteEnabled(currentUserInfo && currentUserInfo->user_level() & ServerInfo_User::IsRegistered);
}
void TabReplays::retranslateUi()
@@ -117,59 +160,173 @@ void TabReplays::retranslateUi()
rightGroupBox->setTitle(tr("Server replay storage"));
aOpenLocalReplay->setText(tr("Watch replay"));
aRenameLocal->setText(tr("Rename"));
aNewLocalFolder->setText(tr("New folder"));
aDeleteLocalReplay->setText(tr("Delete"));
aOpenReplaysFolder->setText(tr("Open replays folder"));
aOpenRemoteReplay->setText(tr("Watch replay"));
aDownload->setText(tr("Download replay"));
aKeep->setText(tr("Toggle expiration lock"));
aDeleteRemoteReplay->setText(tr("Delete"));
}
void TabReplays::handleConnected(const ServerInfo_User &userInfo)
{
setRemoteEnabled(userInfo.user_level() & ServerInfo_User::IsRegistered);
}
/**
* This is only responsible for handling the disconnect. The connect is already handled elsewhere
*/
void TabReplays::handleConnectionChanged(ClientStatus status)
{
if (status == StatusDisconnected) {
setRemoteEnabled(false);
}
}
void TabReplays::setRemoteEnabled(bool enabled)
{
aOpenRemoteReplay->setEnabled(enabled);
aDownload->setEnabled(enabled);
aKeep->setEnabled(enabled);
aDeleteRemoteReplay->setEnabled(enabled);
if (enabled) {
serverDirView->refreshTree();
} else {
serverDirView->clearTree();
}
}
void TabReplays::actLocalDoubleClick(const QModelIndex &curLeft)
{
if (!localDirModel->isDir(curLeft)) {
actOpenLocalReplay();
}
}
void TabReplays::actOpenLocalReplay()
{
QModelIndexList curLefts = localDirView->selectionModel()->selectedRows();
for (const auto &curLeft : curLefts) {
if (localDirModel->isDir(curLeft))
continue;
QString filePath = localDirModel->filePath(curLeft);
QFile f(filePath);
if (!f.open(QIODevice::ReadOnly))
continue;
QByteArray _data = f.readAll();
f.close();
GameReplay *replay = new GameReplay;
replay->ParseFromArray(_data.data(), _data.size());
emit openReplay(replay);
}
}
void TabReplays::actRenameLocal()
{
QModelIndexList curLefts = localDirView->selectionModel()->selectedRows();
for (const auto &curLeft : curLefts) {
const QFileInfo info = localDirModel->fileInfo(curLeft);
const QString oldName = info.baseName();
const QString title = info.isDir() ? tr("Rename local folder") : tr("Rename local file");
bool ok;
QString newName = QInputDialog::getText(this, title, tr("New name:"), QLineEdit::Normal, oldName, &ok);
if (!ok) { // terminate all remaining selections if user cancels
return;
}
if (newName.isEmpty() || oldName == newName) {
continue;
}
QString newFileName = newName;
if (!info.suffix().isEmpty()) {
newFileName += "." + info.suffix();
}
const QString newFilePath = QFileInfo(info.dir(), newFileName).filePath();
if (!QFile::rename(info.filePath(), newFilePath)) {
QMessageBox::critical(this, tr("Error"), tr("Rename failed"));
}
}
}
void TabReplays::actNewLocalFolder()
{
QModelIndex curLeft = localDirView->selectionModel()->currentIndex();
if (localDirModel->isDir(curLeft))
QModelIndex dirIndex;
if (curLeft.isValid() && !localDirModel->isDir(curLeft)) {
dirIndex = curLeft.parent();
} else {
dirIndex = curLeft;
}
bool ok;
QString folderName =
QInputDialog::getText(this, tr("New folder"), tr("Name of new folder:"), QLineEdit::Normal, "", &ok);
if (!ok || folderName.isEmpty())
return;
QString filePath = localDirModel->filePath(curLeft);
QFile f(filePath);
if (!f.open(QIODevice::ReadOnly))
return;
QByteArray _data = f.readAll();
f.close();
GameReplay *replay = new GameReplay;
replay->ParseFromArray(_data.data(), _data.size());
emit openReplay(replay);
localDirModel->mkdir(dirIndex, folderName);
}
void TabReplays::actDeleteLocalReplay()
{
QModelIndex curLeft = localDirView->selectionModel()->currentIndex();
if (!curLeft.isValid())
return;
QModelIndexList curLefts = localDirView->selectionModel()->selectedRows();
if (QMessageBox::warning(this, tr("Delete local file"),
tr("Are you sure you want to delete \"%1\"?").arg(localDirModel->fileName(curLeft)),
QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes)
if (curLefts.isEmpty()) {
return;
}
localDirModel->remove(curLeft);
if (QMessageBox::warning(this, tr("Delete local file"), tr("Are you sure you want to delete the selected files?"),
QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) {
return;
}
for (const auto &curLeft : curLefts) {
if (curLeft.isValid()) {
localDirModel->remove(curLeft);
}
}
}
void TabReplays::actOpenReplaysFolder()
{
QString dir = localDirModel->rootPath();
QDesktopServices::openUrl(QUrl::fromLocalFile(dir));
}
void TabReplays::actRemoteDoubleClick(const QModelIndex &curRight)
{
if (serverDirView->getReplay(curRight)) {
actOpenRemoteReplay();
}
}
void TabReplays::actOpenRemoteReplay()
{
ServerInfo_Replay const *curRight = serverDirView->getCurrentReplay();
if (!curRight)
return;
auto const curRights = serverDirView->getSelectedReplays();
Command_ReplayDownload cmd;
cmd.set_replay_id(curRight->replay_id());
for (const auto curRight : curRights) {
if (!curRight) {
continue;
}
PendingCommand *pend = client->prepareSessionCommand(cmd);
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
SLOT(openRemoteReplayFinished(const Response &)));
client->sendCommand(pend);
Command_ReplayDownload cmd;
cmd.set_replay_id(curRight->replay_id());
PendingCommand *pend = client->prepareSessionCommand(cmd);
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
SLOT(openRemoteReplayFinished(const Response &)));
client->sendCommand(pend);
}
}
void TabReplays::openRemoteReplayFinished(const Response &r)
@@ -186,34 +343,46 @@ void TabReplays::openRemoteReplayFinished(const Response &r)
void TabReplays::actDownload()
{
QString filePath;
QModelIndex curLeft = localDirView->selectionModel()->currentIndex();
if (!curLeft.isValid())
filePath = localDirModel->rootPath();
else {
while (!localDirModel->isDir(curLeft))
curLeft = curLeft.parent();
filePath = localDirModel->filePath(curLeft);
while (!localDirModel->isDir(curLeft)) {
curLeft = curLeft.parent();
}
ServerInfo_Replay const *curRight = serverDirView->getCurrentReplay();
if (!curRight) {
QMessageBox::information(this, tr("Downloading Replays"),
tr("Folder download is not yet supported. Please download replays individually."));
return;
for (const auto curRight : serverDirView->selectionModel()->selectedRows()) {
downloadNodeAtIndex(curLeft, curRight);
}
}
filePath += QString("/replay_%1.cor").arg(curRight->replay_id());
void TabReplays::downloadNodeAtIndex(const QModelIndex &curLeft, const QModelIndex &curRight)
{
if (const auto replayMatch = serverDirView->getReplayMatch(curRight)) {
// node at index is a folder
const QString name =
QString::number(replayMatch->game_id()) + "_" + QString::fromStdString(replayMatch->game_name());
Command_ReplayDownload cmd;
cmd.set_replay_id(curRight->replay_id());
const auto dirIndex = curLeft.isValid() ? curLeft : localDirModel->index(localDirModel->rootPath());
const auto newDirIndex = localDirModel->mkdir(dirIndex, name);
PendingCommand *pend = client->prepareSessionCommand(cmd);
pend->setExtraData(filePath);
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
SLOT(downloadFinished(Response, CommandContainer, QVariant)));
client->sendCommand(pend);
int rows = serverDirView->model()->rowCount(curRight);
for (int i = 0; i < rows; i++) {
const auto childIndex = serverDirView->model()->index(i, 0, curRight);
downloadNodeAtIndex(newDirIndex, childIndex);
}
} else if (const auto replay = serverDirView->getReplay(curRight)) {
// node at index is a replay
const QString dirPath = curLeft.isValid() ? localDirModel->filePath(curLeft) : localDirModel->rootPath();
const QString filePath = dirPath + QString("/replay_%1.cor").arg(replay->replay_id());
Command_ReplayDownload cmd;
cmd.set_replay_id(replay->replay_id());
PendingCommand *pend = client->prepareSessionCommand(cmd);
pend->setExtraData(filePath);
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
SLOT(downloadFinished(Response, CommandContainer, QVariant)));
client->sendCommand(pend);
}
// node at index was invalid
}
void TabReplays::downloadFinished(const Response &r,
@@ -235,18 +404,22 @@ void TabReplays::downloadFinished(const Response &r,
void TabReplays::actKeepRemoteReplay()
{
ServerInfo_ReplayMatch const *curRight = serverDirView->getCurrentReplayMatch();
if (!curRight)
const auto curRights = serverDirView->getSelectedReplayMatches();
if (curRights.isEmpty()) {
return;
}
Command_ReplayModifyMatch cmd;
cmd.set_game_id(curRight->game_id());
cmd.set_do_not_hide(!curRight->do_not_hide());
for (const auto curRight : curRights) {
Command_ReplayModifyMatch cmd;
cmd.set_game_id(curRight->game_id());
cmd.set_do_not_hide(!curRight->do_not_hide());
PendingCommand *pend = client->prepareSessionCommand(cmd);
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
SLOT(keepRemoteReplayFinished(Response, CommandContainer)));
client->sendCommand(pend);
PendingCommand *pend = client->prepareSessionCommand(cmd);
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
SLOT(keepRemoteReplayFinished(Response, CommandContainer)));
client->sendCommand(pend);
}
}
void TabReplays::keepRemoteReplayFinished(const Response &r, const CommandContainer &commandContainer)
@@ -265,21 +438,27 @@ void TabReplays::keepRemoteReplayFinished(const Response &r, const CommandContai
void TabReplays::actDeleteRemoteReplay()
{
ServerInfo_ReplayMatch const *curRight = serverDirView->getCurrentReplayMatch();
if (!curRight)
const auto curRights = serverDirView->getSelectedReplayMatches();
if (curRights.isEmpty()) {
return;
}
if (QMessageBox::warning(this, tr("Delete remote replay"),
tr("Are you sure you want to delete the replay of game %1?").arg(curRight->game_id()),
QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes)
tr("Are you sure you want to delete the selected replays?"),
QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) {
return;
}
Command_ReplayDeleteMatch cmd;
cmd.set_game_id(curRight->game_id());
for (const auto curRight : curRights) {
Command_ReplayDeleteMatch cmd;
cmd.set_game_id(curRight->game_id());
PendingCommand *pend = client->prepareSessionCommand(cmd);
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
SLOT(deleteRemoteReplayFinished(Response, CommandContainer)));
client->sendCommand(pend);
PendingCommand *pend = client->prepareSessionCommand(cmd);
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
SLOT(deleteRemoteReplayFinished(Response, CommandContainer)));
client->sendCommand(pend);
}
}
void TabReplays::deleteRemoteReplayFinished(const Response &r, const CommandContainer &commandContainer)
@@ -294,5 +473,11 @@ void TabReplays::deleteRemoteReplayFinished(const Response &r, const CommandCont
void TabReplays::replayAddedEventReceived(const Event_ReplayAdded &event)
{
serverDirView->addMatchInfo(event.match_info());
if (event.has_match_info()) {
// 99.9% of events will have match info (Normal Workflow)
serverDirView->addMatchInfo(event.match_info());
} else {
// When a Moderator force adds a replay, we need to refresh their view
serverDirView->refreshTree();
}
}

View File

@@ -1,8 +1,10 @@
#ifndef TAB_REPLAYS_H
#define TAB_REPLAYS_H
#include "../game_logic/abstract_client.h"
#include "tab.h"
class ServerInfo_User;
class Response;
class AbstractClient;
class QTreeView;
@@ -25,12 +27,27 @@ private:
RemoteReplayList_TreeWidget *serverDirView;
QGroupBox *leftGroupBox, *rightGroupBox;
QAction *aOpenLocalReplay, *aDeleteLocalReplay, *aOpenRemoteReplay, *aDownload, *aKeep, *aDeleteRemoteReplay;
private slots:
void actOpenLocalReplay();
QAction *aOpenLocalReplay, *aRenameLocal, *aNewLocalFolder, *aDeleteLocalReplay;
QAction *aOpenReplaysFolder;
QAction *aOpenRemoteReplay, *aDownload, *aKeep, *aDeleteRemoteReplay;
void setRemoteEnabled(bool enabled);
void downloadNodeAtIndex(const QModelIndex &curLeft, const QModelIndex &curRight);
private slots:
void handleConnected(const ServerInfo_User &userInfo);
void handleConnectionChanged(ClientStatus status);
void actLocalDoubleClick(const QModelIndex &curLeft);
void actRenameLocal();
void actOpenLocalReplay();
void actNewLocalFolder();
void actDeleteLocalReplay();
void actOpenReplaysFolder();
void actRemoteDoubleClick(const QModelIndex &curLeft);
void actOpenRemoteReplay();
void openRemoteReplayFinished(const Response &r);
@@ -48,11 +65,11 @@ signals:
void openReplay(GameReplay *replay);
public:
TabReplays(TabSupervisor *_tabSupervisor, AbstractClient *_client);
void retranslateUi();
QString getTabText() const
TabReplays(TabSupervisor *_tabSupervisor, AbstractClient *_client, const ServerInfo_User *currentUserInfo);
void retranslateUi() override;
QString getTabText() const override
{
return tr("Game replays");
return tr("Game Replays");
}
};

View File

@@ -6,7 +6,8 @@
#include "../../main.h"
#include "../../server/chat_view/chat_view.h"
#include "../../server/pending_command.h"
#include "../../server/user/user_list.h"
#include "../../server/user/user_list_manager.h"
#include "../../server/user/user_list_widget.h"
#include "../../settings/cache_settings.h"
#include "get_pb_extension.h"
#include "pb/event_join_room.pb.h"
@@ -37,7 +38,7 @@ TabRoom::TabRoom(TabSupervisor *_tabSupervisor,
ServerInfo_User *_ownUser,
const ServerInfo_Room &info)
: Tab(_tabSupervisor), client(_client), roomId(info.room_id()), roomName(QString::fromStdString(info.name())),
ownUser(_ownUser)
ownUser(_ownUser), userListProxy(_tabSupervisor->getUserListManager())
{
const int gameTypeListSize = info.gametype_list_size();
for (int i = 0; i < gameTypeListSize; ++i)
@@ -47,15 +48,15 @@ TabRoom::TabRoom(TabSupervisor *_tabSupervisor,
QMap<int, GameTypeMap> tempMap;
tempMap.insert(info.room_id(), gameTypes);
gameSelector = new GameSelector(client, tabSupervisor, this, QMap<int, QString>(), tempMap, true, true);
userList = new UserList(tabSupervisor, client, UserList::RoomList);
userList = new UserListWidget(tabSupervisor, client, UserListWidget::RoomList);
connect(userList, SIGNAL(openMessageDialog(const QString &, bool)), this,
SIGNAL(openMessageDialog(const QString &, bool)));
chatView = new ChatView(tabSupervisor, tabSupervisor, nullptr, true, this);
chatView = new ChatView(tabSupervisor, nullptr, true, this);
connect(chatView, SIGNAL(showMentionPopup(const QString &)), this, SLOT(actShowMentionPopup(const QString &)));
connect(chatView, SIGNAL(messageClickedSignal()), this, SLOT(focusTab()));
connect(chatView, SIGNAL(openMessageDialog(QString, bool)), this, SIGNAL(openMessageDialog(QString, bool)));
connect(chatView, SIGNAL(showCardInfoPopup(QPoint, QString)), this, SLOT(showCardInfoPopup(QPoint, QString)));
connect(chatView, &ChatView::showCardInfoPopup, this, &TabRoom::showCardInfoPopup);
connect(chatView, SIGNAL(deleteCardInfoPopup(QString)), this, SLOT(deleteCardInfoPopup(QString)));
connect(chatView, SIGNAL(addMentionTag(QString)), this, SLOT(addMentionTag(QString)));
connect(&SettingsCache::instance(), SIGNAL(chatMentionCompleterChanged()), this, SLOT(actCompleterChanged()));
@@ -101,7 +102,7 @@ TabRoom::TabRoom(TabSupervisor *_tabSupervisor,
hbox->addWidget(userList, 1);
aLeaveRoom = new QAction(this);
connect(aLeaveRoom, SIGNAL(triggered()), this, SLOT(actLeaveRoom()));
connect(aLeaveRoom, &QAction::triggered, this, [this] { closeRequest(); });
roomMenu = new QMenu(this);
roomMenu->addAction(aLeaveRoom);
@@ -135,11 +136,6 @@ TabRoom::TabRoom(TabSupervisor *_tabSupervisor,
setCentralWidget(mainWidget);
}
TabRoom::~TabRoom()
{
emit roomClosing(this);
}
void TabRoom::retranslateUi()
{
gameSelector->retranslateUi();
@@ -175,9 +171,11 @@ void TabRoom::actShowPopup(const QString &message)
}
}
void TabRoom::closeRequest()
void TabRoom::closeRequest(bool /*forced*/)
{
actLeaveRoom();
sendRoomCommand(prepareRoomCommand(Command_LeaveRoom()));
emit roomClosing(this);
close();
}
void TabRoom::tabActivated()
@@ -216,12 +214,6 @@ void TabRoom::sayFinished(const Response &response)
chatView->appendMessage(tr("You are flooding the chat. Please wait a couple of seconds."));
}
void TabRoom::actLeaveRoom()
{
sendRoomCommand(prepareRoomCommand(Command_LeaveRoom()));
deleteLater();
}
void TabRoom::actClearChat()
{
chatView->clearChat();
@@ -291,17 +283,15 @@ void TabRoom::processRoomSayEvent(const Event_RoomSay &event)
QString senderName = QString::fromStdString(event.name());
QString message = QString::fromStdString(event.message());
if (tabSupervisor->getUserListsTab()->getIgnoreList()->getUsers().contains(senderName))
if (userListProxy->isUserIgnored(senderName))
return;
UserListTWI *twi = userList->getUsers().value(senderName);
UserLevelFlags userLevel;
QString userPrivLevel;
ServerInfo_User userInfo = {};
if (twi) {
userLevel = UserLevelFlags(twi->getUserInfo().user_level());
userPrivLevel = QString::fromStdString(twi->getUserInfo().privlevel());
userInfo = twi->getUserInfo();
if (SettingsCache::instance().getIgnoreUnregisteredUsers() &&
!userLevel.testFlag(ServerInfo_User::IsRegistered))
!UserLevelFlags(userInfo.user_level()).testFlag(ServerInfo_User::IsRegistered))
return;
}
@@ -314,7 +304,7 @@ void TabRoom::processRoomSayEvent(const Event_RoomSay &event)
QString(QDateTime::fromMSecsSinceEpoch(event.time_of()).toLocalTime().toString("d MMM yyyy HH:mm:ss")) +
"] " + message;
chatView->appendMessage(message, event.message_type(), senderName, userLevel, userPrivLevel, true);
chatView->appendMessage(message, event.message_type(), userInfo, true);
emit userEvent(false);
}

View File

@@ -9,6 +9,8 @@
#include <QKeyEvent>
#include <QMap>
class UserListProxy;
class UserListManager;
namespace google
{
namespace protobuf
@@ -17,7 +19,7 @@ class Message;
}
} // namespace google
class AbstractClient;
class UserList;
class UserListWidget;
class QLabel;
class ChatView;
class QPushButton;
@@ -48,7 +50,8 @@ private:
QMap<int, QString> gameTypes;
GameSelector *gameSelector;
UserList *userList;
UserListWidget *userList;
const UserListProxy *userListProxy;
ChatView *chatView;
QLabel *sayLabel;
LineEditCompleter *sayEdit;
@@ -70,7 +73,6 @@ signals:
private slots:
void sendMessage();
void sayFinished(const Response &response);
void actLeaveRoom();
void actClearChat();
void actOpenChatSettings();
void addMentionTag(QString mentionTag);
@@ -91,10 +93,9 @@ public:
AbstractClient *_client,
ServerInfo_User *_ownUser,
const ServerInfo_Room &info);
~TabRoom();
void retranslateUi();
void closeRequest();
void tabActivated();
void retranslateUi() override;
void closeRequest(bool forced = false) override;
void tabActivated() override;
void processRoomEvent(const RoomEvent &event);
int getRoomId() const
{
@@ -108,7 +109,7 @@ public:
{
return roomName;
}
QString getTabText() const
QString getTabText() const override
{
return roomName;
}

View File

@@ -2,7 +2,7 @@
#include "../../client/game_logic/abstract_client.h"
#include "../../server/pending_command.h"
#include "../../server/user/user_list.h"
#include "../../server/user/user_list_widget.h"
#include "pb/event_list_rooms.pb.h"
#include "pb/event_server_message.pb.h"
#include "pb/response_join_room.pb.h"
@@ -138,8 +138,7 @@ void RoomSelector::joinClicked()
emit joinRoomRequest(id, true);
}
TabServer::TabServer(TabSupervisor *_tabSupervisor, AbstractClient *_client, QWidget *parent)
: Tab(_tabSupervisor, parent), client(_client)
TabServer::TabServer(TabSupervisor *_tabSupervisor, AbstractClient *_client) : Tab(_tabSupervisor), client(_client)
{
roomSelector = new RoomSelector(client);
serverInfoBox = new QTextBrowser;

View File

@@ -10,7 +10,7 @@
class AbstractClient;
class QTextEdit;
class QLabel;
class UserList;
class UserListWidget;
class QPushButton;
class Event_ListRooms;
@@ -34,7 +34,7 @@ signals:
void joinRoomRequest(int, bool setCurrent);
public:
RoomSelector(AbstractClient *_client, QWidget *parent = nullptr);
explicit RoomSelector(AbstractClient *_client, QWidget *parent = nullptr);
void retranslateUi();
};
@@ -55,9 +55,9 @@ private:
bool shouldEmitUpdate = false;
public:
TabServer(TabSupervisor *_tabSupervisor, AbstractClient *_client, QWidget *parent = nullptr);
void retranslateUi();
QString getTabText() const
TabServer(TabSupervisor *_tabSupervisor, AbstractClient *_client);
void retranslateUi() override;
QString getTabText() const override
{
return tr("Server");
}

View File

@@ -2,14 +2,15 @@
#include "../../client/game_logic/abstract_client.h"
#include "../../main.h"
#include "../../server/user/user_list.h"
#include "../../server/user/user_list_manager.h"
#include "../../server/user/user_list_widget.h"
#include "../../settings/cache_settings.h"
#include "../ui/pixel_map_generator.h"
#include "pb/event_game_joined.pb.h"
#include "pb/event_notify_user.pb.h"
#include "pb/event_user_message.pb.h"
#include "pb/game_event_container.pb.h"
#include "pb/moderator_commands.pb.h"
#include "pb/game_replay.pb.h"
#include "pb/room_commands.pb.h"
#include "pb/room_event.pb.h"
#include "pb/serverinfo_room.pb.h"
@@ -24,9 +25,9 @@
#include "tab_replays.h"
#include "tab_room.h"
#include "tab_server.h"
#include "visual_deck_storage/tab_deck_storage_visual.h"
#include <QApplication>
#include <QDebug>
#include <QMessageBox>
#include <QPainter>
#include <QSystemTrayIcon>
@@ -47,15 +48,15 @@ CloseButton::CloseButton(QWidget *parent) : QAbstractButton(parent)
{
setFocusPolicy(Qt::NoFocus);
setCursor(Qt::ArrowCursor);
resize(sizeHint());
resize(this->sizeHint());
}
QSize CloseButton::sizeHint() const
{
ensurePolished();
int width = style()->pixelMetric(QStyle::PM_TabCloseIndicatorWidth, 0, this);
int height = style()->pixelMetric(QStyle::PM_TabCloseIndicatorHeight, 0, this);
return QSize(width, height);
int width = style()->pixelMetric(QStyle::PM_TabCloseIndicatorWidth, nullptr, this);
int height = style()->pixelMetric(QStyle::PM_TabCloseIndicatorHeight, nullptr, this);
return {width, height};
}
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
@@ -87,10 +88,9 @@ void CloseButton::paintEvent(QPaintEvent * /*event*/)
if (isDown())
opt.state |= QStyle::State_Sunken;
if (const QTabBar *tb = qobject_cast<const QTabBar *>(parent())) {
if (const auto *tb = qobject_cast<const QTabBar *>(parent())) {
int index = tb->currentIndex();
QTabBar::ButtonPosition position =
(QTabBar::ButtonPosition)style()->styleHint(QStyle::SH_TabBar_CloseButtonPosition, 0, tb);
auto position = (QTabBar::ButtonPosition)style()->styleHint(QStyle::SH_TabBar_CloseButtonPosition, nullptr, tb);
if (tb->tabButton(index, position) == this)
opt.state |= QStyle::State_Selected;
}
@@ -98,9 +98,10 @@ void CloseButton::paintEvent(QPaintEvent * /*event*/)
style()->drawPrimitive(QStyle::PE_IndicatorTabClose, &opt, &p, this);
}
TabSupervisor::TabSupervisor(AbstractClient *_client, QWidget *parent)
: QTabWidget(parent), userInfo(0), client(_client), tabServer(0), tabUserLists(0), tabDeckStorage(0), tabReplays(0),
tabAdmin(0), tabLog(0)
TabSupervisor::TabSupervisor(AbstractClient *_client, QMenu *tabsMenu, QWidget *parent)
: QTabWidget(parent), userInfo(nullptr), client(_client), tabsMenu(tabsMenu), tabVisualDeckStorage(nullptr),
tabServer(nullptr), tabAccount(nullptr), tabDeckStorage(nullptr), tabReplays(nullptr), tabAdmin(nullptr),
tabLog(nullptr), isLocalGame(false)
{
setElideMode(Qt::ElideRight);
setMovable(true);
@@ -112,18 +113,56 @@ TabSupervisor::TabSupervisor(AbstractClient *_client, QWidget *parent)
tabBar()->setStyle(new MacOSTabFixStyle);
#endif
connect(this, SIGNAL(currentChanged(int)), this, SLOT(updateCurrent(int)));
userListManager = new UserListManager(client, this);
connect(client, SIGNAL(roomEventReceived(const RoomEvent &)), this, SLOT(processRoomEvent(const RoomEvent &)));
connect(client, SIGNAL(gameEventContainerReceived(const GameEventContainer &)), this,
SLOT(processGameEventContainer(const GameEventContainer &)));
connect(client, SIGNAL(gameJoinedEventReceived(const Event_GameJoined &)), this,
SLOT(gameJoined(const Event_GameJoined &)));
connect(client, SIGNAL(userMessageEventReceived(const Event_UserMessage &)), this,
SLOT(processUserMessageEvent(const Event_UserMessage &)));
connect(client, SIGNAL(maxPingTime(int, int)), this, SLOT(updatePingTime(int, int)));
connect(client, SIGNAL(notifyUserEventReceived(const Event_NotifyUser &)), this,
SLOT(processNotifyUserEvent(const Event_NotifyUser &)));
// connect tab changes
connect(this, &TabSupervisor::currentChanged, this, &TabSupervisor::updateCurrent);
// connect client
connect(client, &AbstractClient::roomEventReceived, this, &TabSupervisor::processRoomEvent);
connect(client, &AbstractClient::gameEventContainerReceived, this, &TabSupervisor::processGameEventContainer);
connect(client, &AbstractClient::gameJoinedEventReceived, this, &TabSupervisor::gameJoined);
connect(client, &AbstractClient::userMessageEventReceived, this, &TabSupervisor::processUserMessageEvent);
connect(client, &AbstractClient::maxPingTime, this, &TabSupervisor::updatePingTime);
connect(client, &AbstractClient::notifyUserEventReceived, this, &TabSupervisor::processNotifyUserEvent);
// create tabs menu actions
aTabDeckEditor = new QAction(this);
connect(aTabDeckEditor, &QAction::triggered, this, [this] { addDeckEditorTab(nullptr); });
aTabVisualDeckStorage = new QAction(this);
aTabVisualDeckStorage->setCheckable(true);
connect(aTabVisualDeckStorage, &QAction::triggered, this, &TabSupervisor::actTabVisualDeckStorage);
aTabServer = new QAction(this);
aTabServer->setCheckable(true);
connect(aTabServer, &QAction::triggered, this, &TabSupervisor::actTabServer);
aTabAccount = new QAction(this);
aTabAccount->setCheckable(true);
connect(aTabAccount, &QAction::triggered, this, &TabSupervisor::actTabAccount);
aTabDeckStorage = new QAction(this);
aTabDeckStorage->setCheckable(true);
connect(aTabDeckStorage, &QAction::triggered, this, &TabSupervisor::actTabDeckStorage);
aTabReplays = new QAction(this);
aTabReplays->setCheckable(true);
connect(aTabReplays, &QAction::triggered, this, &TabSupervisor::actTabReplays);
aTabAdmin = new QAction(this);
aTabAdmin->setCheckable(true);
connect(aTabAdmin, &QAction::triggered, this, &TabSupervisor::actTabAdmin);
aTabLog = new QAction(this);
aTabLog->setCheckable(true);
connect(aTabLog, &QAction::triggered, this, &TabSupervisor::actTabLog);
connect(&SettingsCache::instance().shortcuts(), &ShortcutsSettings::shortCutChanged, this,
&TabSupervisor::refreshShortcuts);
refreshShortcuts();
resetTabsMenu();
retranslateUi();
}
@@ -135,12 +174,23 @@ TabSupervisor::~TabSupervisor()
void TabSupervisor::retranslateUi()
{
// tab menu actions
aTabDeckEditor->setText(tr("Deck Editor"));
aTabVisualDeckStorage->setText(tr("&Visual Deck Storage"));
aTabServer->setText(tr("Server"));
aTabAccount->setText(tr("Account"));
aTabDeckStorage->setText(tr("Deck Storage"));
aTabReplays->setText(tr("Game Replays"));
aTabAdmin->setText(tr("Administration"));
aTabLog->setText(tr("Logs"));
// tabs
QList<Tab *> tabs;
tabs.append(tabServer);
tabs.append(tabReplays);
tabs.append(tabDeckStorage);
tabs.append(tabAdmin);
tabs.append(tabUserLists);
tabs.append(tabAccount);
tabs.append(tabLog);
QMapIterator<int, TabRoom *> roomIterator(roomTabs);
while (roomIterator.hasNext())
@@ -158,14 +208,21 @@ void TabSupervisor::retranslateUi()
while (messageIterator.hasNext())
tabs.append(messageIterator.next().value());
for (int i = 0; i < tabs.size(); ++i)
if (tabs[i]) {
int idx = indexOf(tabs[i]);
QString tabText = tabs[i]->getTabText();
for (auto &tab : tabs) {
if (tab) {
int idx = indexOf(tab);
QString tabText = tab->getTabText();
setTabText(idx, sanitizeTabName(tabText));
setTabToolTip(idx, sanitizeHtml(tabText));
tabs[i]->retranslateUi();
tab->retranslateUi();
}
}
}
void TabSupervisor::refreshShortcuts()
{
ShortcutsSettings &shortcuts = SettingsCache::instance().shortcuts();
aTabDeckEditor->setShortcuts(shortcuts.getShortcut("MainWindow/aDeckEditor"));
}
bool TabSupervisor::closeRequest()
@@ -178,7 +235,7 @@ bool TabSupervisor::closeRequest()
}
}
foreach (TabDeckEditor *tab, deckEditorTabs) {
for (TabDeckEditor *tab : deckEditorTabs) {
if (!tab->confirmClose())
return false;
}
@@ -191,71 +248,123 @@ AbstractClient *TabSupervisor::getClient() const
return localClients.isEmpty() ? client : localClients.first();
}
QString TabSupervisor::sanitizeTabName(QString dirty) const
QString TabSupervisor::sanitizeTabName(QString dirty)
{
return dirty.replace("&", "&&");
}
QString TabSupervisor::sanitizeHtml(QString dirty) const
QString TabSupervisor::sanitizeHtml(QString dirty)
{
return dirty.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;");
}
int TabSupervisor::myAddTab(Tab *tab)
/**
* If the action is not in the target checked state, then set it to that state by triggering the action.
* If the action is already in the target checked state, then do nothing.
*
* This allows us to programmatically trigger a QAction::triggered signal for a specific checked state.
*/
static void checkAndTrigger(QAction *checkableAction, bool checked)
{
connect(tab, SIGNAL(userEvent(bool)), this, SLOT(tabUserEvent(bool)));
connect(tab, SIGNAL(tabTextChanged(Tab *, QString)), this, SLOT(updateTabText(Tab *, QString)));
if (checkableAction->isChecked() != checked) {
checkableAction->trigger();
}
}
/**
* Opens the always-available tabs, depending on settings.
*/
void TabSupervisor::initStartupTabs()
{
addDeckEditorTab(nullptr);
checkAndTrigger(aTabVisualDeckStorage, SettingsCache::instance().getTabVisualDeckStorageOpen());
checkAndTrigger(aTabDeckStorage, SettingsCache::instance().getTabDeckStorageOpen());
checkAndTrigger(aTabReplays, SettingsCache::instance().getTabReplaysOpen());
}
/**
* Adds the tab to the TabSupervisor's tab bar.
*
* @param tab The Tab to add
* @param manager The menu action that corresponds to this tab, if this is a single-instance managed tab. Pass in
* nullptr if this is not a managed tab.
* @return The index of the added tab in the tab widget's tab menu
*/
int TabSupervisor::myAddTab(Tab *tab, QAction *manager)
{
connect(tab, &TabGame::userEvent, this, &TabSupervisor::tabUserEvent);
connect(tab, &TabGame::tabTextChanged, this, &TabSupervisor::updateTabText);
QString tabText = tab->getTabText();
int idx = addTab(tab, sanitizeTabName(tabText));
setTabToolTip(idx, sanitizeHtml(tabText));
addCloseButtonToTab(tab, idx, manager);
return idx;
}
/**
* Adds a usable close button to the tab.
*
* @param tab The Tab
* @param tabIndex The tab bar index of the tab
* @param manager The menu action that corresponds to this tab, if this is a single-instance managed tab. Pass in
* nullptr if this is not a managed tab.
*/
void TabSupervisor::addCloseButtonToTab(Tab *tab, int tabIndex, QAction *manager)
{
auto closeSide = static_cast<QTabBar::ButtonPosition>(
tabBar()->style()->styleHint(QStyle::SH_TabBar_CloseButtonPosition, nullptr, tabBar()));
auto *closeButton = new CloseButton(tab);
if (manager) {
// If managed, all close requests should go through the menu action
connect(closeButton, &CloseButton::clicked, this, [manager] { checkAndTrigger(manager, false); });
} else {
connect(closeButton, &CloseButton::clicked, tab, [tab] { tab->closeRequest(); });
}
tabBar()->setTabButton(tabIndex, closeSide, closeButton);
}
/**
* Resets the tabs menu to the tabs that are always available
*/
void TabSupervisor::resetTabsMenu()
{
tabsMenu->clear();
tabsMenu->addAction(aTabDeckEditor);
tabsMenu->addSeparator();
tabsMenu->addAction(aTabVisualDeckStorage);
tabsMenu->addAction(aTabDeckStorage);
tabsMenu->addAction(aTabReplays);
}
void TabSupervisor::start(const ServerInfo_User &_userInfo)
{
isLocalGame = false;
userInfo = new ServerInfo_User(_userInfo);
tabServer = new TabServer(this, client);
connect(tabServer, SIGNAL(roomJoined(const ServerInfo_Room &, bool)), this,
SLOT(addRoomTab(const ServerInfo_Room &, bool)));
myAddTab(tabServer);
userListManager->handleConnect();
tabUserLists = new TabUserLists(this, client, *userInfo);
connect(tabUserLists, SIGNAL(openMessageDialog(const QString &, bool)), this,
SLOT(addMessageTab(const QString &, bool)));
connect(tabUserLists, SIGNAL(userJoined(ServerInfo_User)), this, SLOT(processUserJoined(ServerInfo_User)));
connect(tabUserLists, SIGNAL(userLeft(const QString &)), this, SLOT(processUserLeft(const QString &)));
myAddTab(tabUserLists);
resetTabsMenu();
tabsMenu->addSeparator();
tabsMenu->addAction(aTabServer);
tabsMenu->addAction(aTabAccount);
checkAndTrigger(aTabServer, SettingsCache::instance().getTabServerOpen());
checkAndTrigger(aTabAccount, SettingsCache::instance().getTabAccountOpen());
updatePingTime(0, -1);
if (userInfo->user_level() & ServerInfo_User::IsRegistered) {
tabDeckStorage = new TabDeckStorage(this, client);
connect(tabDeckStorage, SIGNAL(openDeckEditor(const DeckLoader *)), this,
SLOT(addDeckEditorTab(const DeckLoader *)));
myAddTab(tabDeckStorage);
tabReplays = new TabReplays(this, client);
connect(tabReplays, SIGNAL(openReplay(GameReplay *)), this, SLOT(openReplay(GameReplay *)));
myAddTab(tabReplays);
} else {
tabDeckStorage = 0;
tabReplays = 0;
}
if (userInfo->user_level() & ServerInfo_User::IsModerator) {
tabAdmin = new TabAdmin(this, client, (userInfo->user_level() & ServerInfo_User::IsAdmin));
connect(tabAdmin, SIGNAL(adminLockChanged(bool)), this, SIGNAL(adminLockChanged(bool)));
myAddTab(tabAdmin);
tabsMenu->addSeparator();
tabsMenu->addAction(aTabAdmin);
tabsMenu->addAction(aTabLog);
tabLog = new TabLog(this, client);
myAddTab(tabLog);
} else {
tabAdmin = 0;
tabLog = 0;
checkAndTrigger(aTabAdmin, SettingsCache::instance().getTabAdminOpen());
checkAndTrigger(aTabLog, SettingsCache::instance().getTabLogOpen());
}
retranslateUi();
@@ -263,70 +372,186 @@ void TabSupervisor::start(const ServerInfo_User &_userInfo)
void TabSupervisor::startLocal(const QList<AbstractClient *> &_clients)
{
tabUserLists = 0;
tabDeckStorage = 0;
tabReplays = 0;
tabAdmin = 0;
tabLog = 0;
resetTabsMenu();
tabAccount = nullptr;
tabAdmin = nullptr;
tabLog = nullptr;
isLocalGame = true;
userInfo = new ServerInfo_User;
localClients = _clients;
for (int i = 0; i < localClients.size(); ++i)
connect(localClients[i], SIGNAL(gameEventContainerReceived(const GameEventContainer &)), this,
SLOT(processGameEventContainer(const GameEventContainer &)));
connect(localClients.first(), SIGNAL(gameJoinedEventReceived(const Event_GameJoined &)), this,
SLOT(localGameJoined(const Event_GameJoined &)));
connect(localClients[i], &AbstractClient::gameEventContainerReceived, this,
&TabSupervisor::processGameEventContainer);
connect(localClients.first(), &AbstractClient::gameJoinedEventReceived, this, &TabSupervisor::localGameJoined);
}
/**
* Call this when Cockatrice disconnects from the server in order to clean up.
*/
void TabSupervisor::stop()
{
if ((!client) && localClients.isEmpty())
return;
resetTabsMenu();
if (!localClients.isEmpty()) {
for (int i = 0; i < localClients.size(); ++i)
localClients[i]->deleteLater();
for (auto &localClient : localClients) {
localClient->deleteLater();
}
localClients.clear();
emit localGameEnded();
} else {
if (tabUserLists)
tabUserLists->deleteLater();
if (tabServer)
tabServer->deleteLater();
if (tabDeckStorage)
tabDeckStorage->deleteLater();
if (tabReplays)
tabReplays->deleteLater();
if (tabAdmin)
tabAdmin->deleteLater();
if (tabLog)
tabLog->deleteLater();
if (tabAccount) {
tabAccount->closeRequest(true);
}
if (tabServer) {
tabServer->closeRequest(true);
}
if (tabAdmin) {
tabAdmin->closeRequest(true);
}
if (tabLog) {
tabLog->closeRequest(true);
}
}
tabUserLists = 0;
tabServer = 0;
tabDeckStorage = 0;
tabReplays = 0;
tabAdmin = 0;
tabLog = 0;
QMapIterator<int, TabRoom *> roomIterator(roomTabs);
while (roomIterator.hasNext())
roomIterator.next().value()->deleteLater();
roomTabs.clear();
QList<Tab *> tabsToDelete;
QMapIterator<int, TabGame *> gameIterator(gameTabs);
while (gameIterator.hasNext())
gameIterator.next().value()->deleteLater();
gameTabs.clear();
for (auto i = roomTabs.cbegin(), end = roomTabs.cend(); i != end; ++i) {
tabsToDelete << i.value();
}
QListIterator<TabGame *> replayIterator(replayTabs);
while (replayIterator.hasNext())
replayIterator.next()->deleteLater();
replayTabs.clear();
for (auto i = gameTabs.cbegin(), end = gameTabs.cend(); i != end; ++i) {
tabsToDelete << i.value();
}
for (auto i = messageTabs.cbegin(), end = messageTabs.cend(); i != end; ++i) {
tabsToDelete << i.value();
}
for (const auto tab : tabsToDelete) {
tab->closeRequest(true);
}
userListManager->handleDisconnect();
delete userInfo;
userInfo = 0;
userInfo = nullptr;
}
void TabSupervisor::actTabVisualDeckStorage(bool checked)
{
SettingsCache::instance().setTabVisualDeckStorageOpen(checked);
if (checked && !tabVisualDeckStorage) {
tabVisualDeckStorage = new TabDeckStorageVisual(this);
myAddTab(tabVisualDeckStorage, aTabVisualDeckStorage);
connect(tabVisualDeckStorage, &Tab::closed, this, [this] {
tabVisualDeckStorage = nullptr;
aTabVisualDeckStorage->setChecked(false);
});
} else if (!checked && tabVisualDeckStorage) {
tabVisualDeckStorage->closeRequest();
}
}
void TabSupervisor::actTabServer(bool checked)
{
SettingsCache::instance().setTabServerOpen(checked);
if (checked && !tabServer) {
tabServer = new TabServer(this, client);
connect(tabServer, &TabServer::roomJoined, this, &TabSupervisor::addRoomTab);
myAddTab(tabServer, aTabServer);
connect(tabServer, &Tab::closed, this, [this] {
tabServer = nullptr;
aTabServer->setChecked(false);
});
} else if (!checked && tabServer) {
tabServer->closeRequest();
}
}
void TabSupervisor::actTabAccount(bool checked)
{
SettingsCache::instance().setTabAccountOpen(checked);
if (checked && !tabAccount) {
tabAccount = new TabAccount(this, client, *userInfo);
connect(tabAccount, &TabAccount::openMessageDialog, this, &TabSupervisor::addMessageTab);
connect(tabAccount, &TabAccount::userJoined, this, &TabSupervisor::processUserJoined);
connect(tabAccount, &TabAccount::userLeft, this, &TabSupervisor::processUserLeft);
myAddTab(tabAccount, aTabAccount);
connect(tabAccount, &Tab::closed, this, [this] {
tabAccount = nullptr;
aTabAccount->setChecked(false);
});
} else if (!checked && tabAccount) {
tabAccount->closeRequest();
}
}
void TabSupervisor::actTabDeckStorage(bool checked)
{
SettingsCache::instance().setTabDeckStorageOpen(checked);
if (checked && !tabDeckStorage) {
tabDeckStorage = new TabDeckStorage(this, client, userInfo);
connect(tabDeckStorage, &TabDeckStorage::openDeckEditor, this, &TabSupervisor::addDeckEditorTab);
myAddTab(tabDeckStorage, aTabDeckStorage);
connect(tabDeckStorage, &Tab::closed, this, [this] {
tabDeckStorage = nullptr;
aTabDeckStorage->setChecked(false);
});
} else if (!checked && tabDeckStorage) {
tabDeckStorage->closeRequest();
}
}
void TabSupervisor::actTabReplays(bool checked)
{
SettingsCache::instance().setTabReplaysOpen(checked);
if (checked && !tabReplays) {
tabReplays = new TabReplays(this, client, userInfo);
connect(tabReplays, &TabReplays::openReplay, this, &TabSupervisor::openReplay);
myAddTab(tabReplays, aTabReplays);
connect(tabReplays, &Tab::closed, this, [this] {
tabReplays = nullptr;
aTabReplays->setChecked(false);
});
} else if (!checked && tabReplays) {
tabReplays->closeRequest();
}
}
void TabSupervisor::actTabAdmin(bool checked)
{
SettingsCache::instance().setTabAdminOpen(checked);
if (checked && !tabAdmin) {
tabAdmin = new TabAdmin(this, client, (userInfo->user_level() & ServerInfo_User::IsAdmin));
connect(tabAdmin, &TabAdmin::adminLockChanged, this, &TabSupervisor::adminLockChanged);
myAddTab(tabAdmin, aTabAdmin);
connect(tabAdmin, &Tab::closed, this, [this] {
tabAdmin = nullptr;
aTabAdmin->setChecked(false);
});
} else if (!checked && tabAdmin) {
tabAdmin->closeRequest();
}
}
void TabSupervisor::actTabLog(bool checked)
{
SettingsCache::instance().setTabLogOpen(checked);
if (checked && !tabLog) {
tabLog = new TabLog(this, client);
myAddTab(tabLog, aTabLog);
connect(tabLog, &Tab::closed, this, [this] {
tabLog = nullptr;
aTabAdmin->setChecked(false);
});
} else if (!checked && tabLog) {
tabLog->closeRequest();
}
}
void TabSupervisor::updatePingTime(int value, int max)
@@ -339,22 +564,6 @@ void TabSupervisor::updatePingTime(int value, int max)
setTabIcon(indexOf(tabServer), QIcon(PingPixmapGenerator::generatePixmap(15, value, max)));
}
void TabSupervisor::closeButtonPressed()
{
Tab *tab = static_cast<Tab *>(static_cast<CloseButton *>(sender())->property("tab").value<QObject *>());
tab->closeRequest();
}
void TabSupervisor::addCloseButtonToTab(Tab *tab, int tabIndex)
{
QTabBar::ButtonPosition closeSide =
(QTabBar::ButtonPosition)tabBar()->style()->styleHint(QStyle::SH_TabBar_CloseButtonPosition, 0, tabBar());
CloseButton *closeButton = new CloseButton;
connect(closeButton, SIGNAL(clicked()), this, SLOT(closeButtonPressed()));
closeButton->setProperty("tab", QVariant::fromValue((QObject *)tab));
tabBar()->setTabButton(tabIndex, closeSide, closeButton);
}
void TabSupervisor::gameJoined(const Event_GameJoined &event)
{
QMap<int, QString> roomGameTypes;
@@ -366,23 +575,21 @@ void TabSupervisor::gameJoined(const Event_GameJoined &event)
roomGameTypes.insert(event.game_types(i).game_type_id(),
QString::fromStdString(event.game_types(i).description()));
TabGame *tab = new TabGame(this, QList<AbstractClient *>() << client, event, roomGameTypes);
connect(tab, SIGNAL(gameClosing(TabGame *)), this, SLOT(gameLeft(TabGame *)));
connect(tab, SIGNAL(openMessageDialog(const QString &, bool)), this, SLOT(addMessageTab(const QString &, bool)));
connect(tab, SIGNAL(openDeckEditor(const DeckLoader *)), this, SLOT(addDeckEditorTab(const DeckLoader *)));
int tabIndex = myAddTab(tab);
addCloseButtonToTab(tab, tabIndex);
auto *tab = new TabGame(this, QList<AbstractClient *>() << client, event, roomGameTypes);
connect(tab, &TabGame::gameClosing, this, &TabSupervisor::gameLeft);
connect(tab, &TabGame::openMessageDialog, this, &TabSupervisor::addMessageTab);
connect(tab, &TabGame::openDeckEditor, this, &TabSupervisor::addDeckEditorTab);
myAddTab(tab);
gameTabs.insert(event.game_info().game_id(), tab);
setCurrentWidget(tab);
}
void TabSupervisor::localGameJoined(const Event_GameJoined &event)
{
TabGame *tab = new TabGame(this, localClients, event, QMap<int, QString>());
connect(tab, SIGNAL(gameClosing(TabGame *)), this, SLOT(gameLeft(TabGame *)));
connect(tab, SIGNAL(openDeckEditor(const DeckLoader *)), this, SLOT(addDeckEditorTab(const DeckLoader *)));
int tabIndex = myAddTab(tab);
addCloseButtonToTab(tab, tabIndex);
auto *tab = new TabGame(this, localClients, event, QMap<int, QString>());
connect(tab, &TabGame::gameClosing, this, &TabSupervisor::gameLeft);
connect(tab, &TabGame::openDeckEditor, this, &TabSupervisor::addDeckEditorTab);
myAddTab(tab);
gameTabs.insert(event.game_info().game_id(), tab);
setCurrentWidget(tab);
@@ -407,12 +614,11 @@ void TabSupervisor::gameLeft(TabGame *tab)
void TabSupervisor::addRoomTab(const ServerInfo_Room &info, bool setCurrent)
{
TabRoom *tab = new TabRoom(this, client, userInfo, info);
connect(tab, SIGNAL(maximizeClient()), this, SLOT(maximizeMainWindow()));
connect(tab, SIGNAL(roomClosing(TabRoom *)), this, SLOT(roomLeft(TabRoom *)));
connect(tab, SIGNAL(openMessageDialog(const QString &, bool)), this, SLOT(addMessageTab(const QString &, bool)));
int tabIndex = myAddTab(tab);
addCloseButtonToTab(tab, tabIndex);
auto *tab = new TabRoom(this, client, userInfo, info);
connect(tab, &TabRoom::maximizeClient, this, &TabSupervisor::maximizeMainWindow);
connect(tab, &TabRoom::roomClosing, this, &TabSupervisor::roomLeft);
connect(tab, &TabRoom::openMessageDialog, this, &TabSupervisor::addMessageTab);
myAddTab(tab);
roomTabs.insert(info.room_id(), tab);
if (setCurrent)
setCurrentWidget(tab);
@@ -429,10 +635,9 @@ void TabSupervisor::roomLeft(TabRoom *tab)
void TabSupervisor::openReplay(GameReplay *replay)
{
TabGame *replayTab = new TabGame(this, replay);
connect(replayTab, SIGNAL(gameClosing(TabGame *)), this, SLOT(replayLeft(TabGame *)));
int tabIndex = myAddTab(replayTab);
addCloseButtonToTab(replayTab, tabIndex);
auto *replayTab = new TabGame(this, replay);
connect(replayTab, &TabGame::gameClosing, this, &TabSupervisor::replayLeft);
myAddTab(replayTab);
replayTabs.append(replayTab);
setCurrentWidget(replayTab);
}
@@ -451,11 +656,11 @@ TabMessage *TabSupervisor::addMessageTab(const QString &receiverName, bool focus
return nullptr;
ServerInfo_User otherUser;
UserListTWI *twi = tabUserLists->getAllUsersList()->getUsers().value(receiverName);
if (twi)
otherUser = twi->getUserInfo();
else
if (auto user = userListManager->getOnlineUser(receiverName)) {
otherUser = ServerInfo_User(*user);
} else {
otherUser.set_name(receiverName.toStdString());
}
TabMessage *tab;
tab = messageTabs.value(QString::fromStdString(otherUser.name()));
@@ -466,10 +671,9 @@ TabMessage *TabSupervisor::addMessageTab(const QString &receiverName, bool focus
}
tab = new TabMessage(this, client, *userInfo, otherUser);
connect(tab, SIGNAL(talkClosing(TabMessage *)), this, SLOT(talkLeft(TabMessage *)));
connect(tab, SIGNAL(maximizeClient()), this, SLOT(maximizeMainWindow()));
int tabIndex = myAddTab(tab);
addCloseButtonToTab(tab, tabIndex);
connect(tab, &TabMessage::talkClosing, this, &TabSupervisor::talkLeft);
connect(tab, &TabMessage::maximizeClient, this, &TabSupervisor::maximizeMainWindow);
myAddTab(tab);
messageTabs.insert(receiverName, tab);
if (focus)
setCurrentWidget(tab);
@@ -492,18 +696,29 @@ void TabSupervisor::talkLeft(TabMessage *tab)
TabDeckEditor *TabSupervisor::addDeckEditorTab(const DeckLoader *deckToOpen)
{
TabDeckEditor *tab = new TabDeckEditor(this);
auto *tab = new TabDeckEditor(this);
if (deckToOpen)
tab->setDeck(new DeckLoader(*deckToOpen));
connect(tab, SIGNAL(deckEditorClosing(TabDeckEditor *)), this, SLOT(deckEditorClosed(TabDeckEditor *)));
connect(tab, SIGNAL(openDeckEditor(const DeckLoader *)), this, SLOT(addDeckEditorTab(const DeckLoader *)));
int tabIndex = myAddTab(tab);
addCloseButtonToTab(tab, tabIndex);
connect(tab, &TabDeckEditor::deckEditorClosing, this, &TabSupervisor::deckEditorClosed);
connect(tab, &TabDeckEditor::openDeckEditor, this, &TabSupervisor::addDeckEditorTab);
myAddTab(tab);
deckEditorTabs.append(tab);
setCurrentWidget(tab);
return tab;
}
TabEdhRec *TabSupervisor::addEdhrecTab(const CardInfoPtr &cardToQuery, bool isCommander)
{
auto *tab = new TabEdhRec(this);
if (cardToQuery) {
tab->setCard(cardToQuery, isCommander);
}
myAddTab(tab);
setCurrentWidget(tab);
return tab;
}
void TabSupervisor::deckEditorClosed(TabDeckEditor *tab)
{
if (tab == currentWidget())
@@ -544,7 +759,7 @@ void TabSupervisor::processGameEventContainer(const GameEventContainer &cont)
if (tab)
tab->processGameEventContainer(cont, qobject_cast<AbstractClient *>(sender()), {});
else
qDebug() << "gameEvent: invalid gameId";
qCDebug(TabSupervisorLog) << "gameEvent: invalid gameId";
}
void TabSupervisor::processUserMessageEvent(const Event_UserMessage &event)
@@ -554,9 +769,9 @@ void TabSupervisor::processUserMessageEvent(const Event_UserMessage &event)
if (!tab)
tab = messageTabs.value(QString::fromStdString(event.receiver_name()));
if (!tab) {
UserListTWI *twi = tabUserLists->getAllUsersList()->getUsers().value(senderName);
if (twi) {
UserLevelFlags userLevel = UserLevelFlags(twi->getUserInfo().user_level());
const ServerInfo_User *onlineUserInfo = userListManager->getOnlineUser(senderName);
if (onlineUserInfo) {
auto userLevel = UserLevelFlags(onlineUserInfo->user_level());
if (SettingsCache::instance().getIgnoreUnregisteredUserMessages() &&
!userLevel.testFlag(ServerInfo_User::IsRegistered))
// Flags are additive, so reg/mod/admin are all IsRegistered
@@ -571,9 +786,9 @@ void TabSupervisor::processUserMessageEvent(const Event_UserMessage &event)
void TabSupervisor::actShowPopup(const QString &message)
{
qDebug() << "ACT SHOW POPUP";
qCDebug(TabSupervisorLog) << "ACT SHOW POPUP";
if (trayIcon && (QApplication::activeWindow() == nullptr || QApplication::focusWidget() == nullptr)) {
qDebug() << "LAUNCHING POPUP";
qCDebug(TabSupervisorLog) << "LAUNCHING POPUP";
// disconnect(trayIcon, SIGNAL(messageClicked()), nullptr, nullptr);
trayIcon->showMessage(message, tr("Click to view"));
// connect(trayIcon, SIGNAL(messageClicked()), chatView, SLOT(actMessageClicked()));
@@ -590,15 +805,15 @@ void TabSupervisor::processUserLeft(const QString &userName)
void TabSupervisor::processUserJoined(const ServerInfo_User &userInfoJoined)
{
QString userName = QString::fromStdString(userInfoJoined.name());
if (isUserBuddy(userName)) {
Tab *tab = static_cast<Tab *>(getUserListsTab());
if (tab != currentWidget()) {
tab->setContentsChanged(true);
QPixmap avatarPixmap =
UserLevelPixmapGenerator::generatePixmap(13, (UserLevelFlags)userInfoJoined.user_level(), true,
QString::fromStdString(userInfoJoined.privlevel()));
setTabIcon(indexOf(tab), QPixmap(avatarPixmap));
if (userListManager->isUserBuddy(userName)) {
if (auto *tab = getTabAccount()) {
if (tab != currentWidget()) {
tab->setContentsChanged(true);
QIcon avatarIcon = UserLevelPixmapGenerator::generateIcon(
13, (UserLevelFlags)userInfoJoined.user_level(), userInfoJoined.pawn_colors(), true,
QString::fromStdString(userInfoJoined.privlevel()));
setTabIcon(indexOf(tab), avatarIcon);
}
}
if (SettingsCache::instance().getBuddyConnectNotificationsEnabled()) {
@@ -686,57 +901,6 @@ void TabSupervisor::processNotifyUserEvent(const Event_NotifyUser &event)
}
}
bool TabSupervisor::isOwnUserRegistered() const
{
return userInfo != nullptr && (userInfo->user_level() & ServerInfo_User::IsRegistered) != 0;
}
QString TabSupervisor::getOwnUsername() const
{
return userInfo != nullptr ? QString::fromStdString(userInfo->name()) : QString();
}
bool TabSupervisor::isUserBuddy(const QString &userName) const
{
if (!getUserListsTab())
return false;
if (!getUserListsTab()->getBuddyList())
return false;
QMap<QString, UserListTWI *> buddyList = getUserListsTab()->getBuddyList()->getUsers();
bool senderIsBuddy = buddyList.contains(userName);
return senderIsBuddy;
}
bool TabSupervisor::isUserIgnored(const QString &userName) const
{
if (!getUserListsTab())
return false;
if (!getUserListsTab()->getIgnoreList())
return false;
QMap<QString, UserListTWI *> buddyList = getUserListsTab()->getIgnoreList()->getUsers();
bool senderIsBuddy = buddyList.contains(userName);
return senderIsBuddy;
}
const ServerInfo_User *TabSupervisor::getOnlineUser(const QString &userName) const
{
if (!getUserListsTab())
return nullptr;
if (!getUserListsTab()->getAllUsersList())
return nullptr;
QMap<QString, UserListTWI *> userList = getUserListsTab()->getAllUsersList()->getUsers();
const QString &userNameToMatchLower = userName.toLower();
QMap<QString, UserListTWI *>::iterator i;
for (i = userList.begin(); i != userList.end(); ++i)
if (i.key().toLower() == userNameToMatchLower) {
const ServerInfo_User &_userInfo = i.value()->getUserInfo();
return &_userInfo;
}
return nullptr;
};
bool TabSupervisor::switchToGameTabIfAlreadyExists(const int gameId)
{
bool isGameTabExists = false;

View File

@@ -2,14 +2,20 @@
#define TAB_SUPERVISOR_H
#include "../../deck/deck_loader.h"
#include "../../server/chat_view/user_list_proxy.h"
#include "../../server/user/user_list_proxy.h"
#include "api/edhrec/tab_edhrec.h"
#include "visual_deck_storage/tab_deck_storage_visual.h"
#include <QAbstractButton>
#include <QCommonStyle>
#include <QLoggingCategory>
#include <QMap>
#include <QProxyStyle>
#include <QTabWidget>
inline Q_LOGGING_CATEGORY(TabSupervisorLog, "tab_supervisor");
class UserListManager;
class QMenu;
class AbstractClient;
class Tab;
@@ -20,7 +26,7 @@ class TabDeckStorage;
class TabReplays;
class TabAdmin;
class TabMessage;
class TabUserLists;
class TabAccount;
class TabDeckEditor;
class TabLog;
class RoomEvent;
@@ -37,39 +43,42 @@ class MacOSTabFixStyle : public QProxyStyle
{
Q_OBJECT
public:
QRect subElementRect(SubElement, const QStyleOption *, const QWidget *) const;
QRect subElementRect(SubElement, const QStyleOption *, const QWidget *) const override;
};
class CloseButton : public QAbstractButton
{
Q_OBJECT
public:
CloseButton(QWidget *parent = nullptr);
QSize sizeHint() const;
inline QSize minimumSizeHint() const
explicit CloseButton(QWidget *parent = nullptr);
QSize sizeHint() const override;
inline QSize minimumSizeHint() const override
{
return sizeHint();
}
protected:
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
void enterEvent(QEnterEvent *event);
void enterEvent(QEnterEvent *event) override;
#else
void enterEvent(QEvent *event);
void enterEvent(QEvent *event) override;
#endif
void leaveEvent(QEvent *event);
void paintEvent(QPaintEvent *event);
void leaveEvent(QEvent *event) override;
void paintEvent(QPaintEvent *event) override;
};
class TabSupervisor : public QTabWidget, public UserlistProxy
class TabSupervisor : public QTabWidget
{
Q_OBJECT
private:
ServerInfo_User *userInfo;
AbstractClient *client;
UserListManager *userListManager;
QList<AbstractClient *> localClients;
QMenu *tabsMenu;
TabDeckStorageVisual *tabVisualDeckStorage;
TabServer *tabServer;
TabUserLists *tabUserLists;
TabAccount *tabAccount;
TabDeckStorage *tabDeckStorage;
TabReplays *tabReplays;
TabAdmin *tabAdmin;
@@ -79,16 +88,22 @@ private:
QList<TabGame *> replayTabs;
QMap<QString, TabMessage *> messageTabs;
QList<TabDeckEditor *> deckEditorTabs;
int myAddTab(Tab *tab);
void addCloseButtonToTab(Tab *tab, int tabIndex);
QString sanitizeTabName(QString dirty) const;
QString sanitizeHtml(QString dirty) const;
bool isLocalGame;
QAction *aTabDeckEditor, *aTabVisualDeckStorage, *aTabServer, *aTabAccount, *aTabDeckStorage, *aTabReplays,
*aTabAdmin, *aTabLog;
int myAddTab(Tab *tab, QAction *manager = nullptr);
void addCloseButtonToTab(Tab *tab, int tabIndex, QAction *manager);
static QString sanitizeTabName(QString dirty);
static QString sanitizeHtml(QString dirty);
void resetTabsMenu();
public:
TabSupervisor(AbstractClient *_client, QWidget *parent = nullptr);
~TabSupervisor();
explicit TabSupervisor(AbstractClient *_client, QMenu *tabsMenu, QWidget *parent = nullptr);
~TabSupervisor() override;
void retranslateUi();
void initStartupTabs();
void start(const ServerInfo_User &userInfo);
void startLocal(const QList<AbstractClient *> &_clients);
void stop();
@@ -100,28 +115,31 @@ public:
{
return gameTabs.size();
}
TabUserLists *getUserListsTab() const
TabAccount *getTabAccount() const
{
return tabUserLists;
return tabAccount;
}
ServerInfo_User *getUserInfo() const
{
return userInfo;
}
AbstractClient *getClient() const;
const UserListManager *getUserListManager() const
{
return userListManager;
}
const QMap<int, TabRoom *> &getRoomTabs() const
{
return roomTabs;
}
QList<TabDeckEditor *> getDeckEditorTabs() const
{
return deckEditorTabs;
}
bool getAdminLocked() const;
bool closeRequest();
bool isOwnUserRegistered() const;
QString getOwnUsername() const;
bool isUserBuddy(const QString &userName) const;
bool isUserIgnored(const QString &userName) const;
const ServerInfo_User *getOnlineUser(const QString &userName) const;
bool switchToGameTabIfAlreadyExists(const int gameId);
void actShowPopup(const QString &message);
static void actShowPopup(const QString &message);
signals:
void setMenu(const QList<QMenu *> &newMenuList = QList<QMenu *>());
void localGameEnded();
@@ -130,10 +148,20 @@ signals:
public slots:
TabDeckEditor *addDeckEditorTab(const DeckLoader *deckToOpen);
TabEdhRec *addEdhrecTab(const CardInfoPtr &cardToQuery, bool isCommander = false);
void openReplay(GameReplay *replay);
void maximizeMainWindow();
private slots:
void closeButtonPressed();
void refreshShortcuts();
void actTabVisualDeckStorage(bool checked);
void actTabServer(bool checked);
void actTabAccount(bool checked);
void actTabDeckStorage(bool checked);
void actTabReplays(bool checked);
void actTabAdmin(bool checked);
void actTabLog(bool checked);
void updateCurrent(int index);
void updatePingTime(int value, int max);
void gameJoined(const Event_GameJoined &event);

View File

@@ -0,0 +1,33 @@
#include "tab_deck_storage_visual.h"
#include "../../../game/cards/card_database_model.h"
#include "../../ui/widgets/cards/deck_preview_card_picture_widget.h"
#include "../../ui/widgets/visual_deck_storage/visual_deck_storage_widget.h"
#include "../tab_supervisor.h"
#include "pb/command_deck_del.pb.h"
#include <QMouseEvent>
TabDeckStorageVisual::TabDeckStorageVisual(TabSupervisor *_tabSupervisor)
: Tab(_tabSupervisor), visualDeckStorageWidget(new VisualDeckStorageWidget(this))
{
connect(this, &TabDeckStorageVisual::openDeckEditor, tabSupervisor, &TabSupervisor::addDeckEditorTab);
connect(visualDeckStorageWidget, &VisualDeckStorageWidget::deckPreviewDoubleClicked, this,
&TabDeckStorageVisual::actOpenLocalDeck);
auto *widget = new QWidget(this);
auto *layout = new QVBoxLayout(widget);
widget->setLayout(layout);
this->setCentralWidget(widget);
layout->addWidget(visualDeckStorageWidget);
}
void TabDeckStorageVisual::actOpenLocalDeck(QMouseEvent * /*event*/, DeckPreviewWidget *instance)
{
DeckLoader deckLoader;
if (!deckLoader.loadFromFile(instance->filePath, DeckLoader::getFormatFromName(instance->filePath), true)) {
return;
}
emit openDeckEditor(&deckLoader);
}

View File

@@ -0,0 +1,40 @@
#ifndef TAB_DECK_STORAGE_VISUAL_H
#define TAB_DECK_STORAGE_VISUAL_H
#include "../tab.h"
class AbstractClient;
class CommandContainer;
class DeckLoader;
class DeckPreviewWidget;
class QFileSystemModel;
class QGroupBox;
class QToolBar;
class QTreeView;
class QTreeWidget;
class QTreeWidgetItem;
class Response;
class VisualDeckStorageWidget;
class TabDeckStorageVisual final : public Tab
{
Q_OBJECT
public:
explicit TabDeckStorageVisual(TabSupervisor *_tabSupervisor);
void retranslateUi() override{};
[[nodiscard]] QString getTabText() const override
{
return tr("Visual Deck Storage");
}
public slots:
void actOpenLocalDeck(QMouseEvent * /*event*/, DeckPreviewWidget *instance);
signals:
void openDeckEditor(const DeckLoader *deckLoader);
private:
VisualDeckStorageWidget *visualDeckStorageWidget;
};
#endif

View File

@@ -14,7 +14,7 @@ TappedOutInterface::TappedOutInterface(CardDatabase &_cardDatabase, QObject *par
: QObject(parent), cardDatabase(_cardDatabase)
{
manager = new QNetworkAccessManager(this);
connect(manager, SIGNAL(finished(QNetworkReply *)), this, SLOT(queryFinished(QNetworkReply *)));
connect(manager, &QNetworkAccessManager::finished, this, &TappedOutInterface::queryFinished);
}
void TappedOutInterface::queryFinished(QNetworkReply *reply)
@@ -33,7 +33,7 @@ void TappedOutInterface::queryFinished(QNetworkReply *reply)
* can be extracted from the header. The http status is a 302 "redirect".
*/
QString deckUrl = reply->rawHeader("Location");
qDebug() << "Tappedout: good reply, http status" << httpStatus << "location" << deckUrl;
qCDebug(TappedOutInterfaceLog) << "Tappedout: good reply, http status" << httpStatus << "location" << deckUrl;
QDesktopServices::openUrl("https://tappedout.net" + deckUrl);
} else {
/*
@@ -57,8 +57,8 @@ void TappedOutInterface::queryFinished(QNetworkReply *reply)
}
QString errorMessage = errorMessageList.join("\n");
qDebug() << "Tappedout: bad reply, http status" << httpStatus << "size" << data.size() << "message"
<< errorMessage;
qCDebug(TappedOutInterfaceLog) << "Tappedout: bad reply, http status" << httpStatus << "size" << data.size()
<< "message" << errorMessage;
QMessageBox::critical(nullptr, tr("Error"), errorMessage);
}
@@ -115,7 +115,7 @@ struct CopyMainOrSide
}
};
void TappedOutInterface::copyDeckSplitMainAndSide(const DeckList &source, DeckList &mainboard, DeckList &sideboard)
void TappedOutInterface::copyDeckSplitMainAndSide(DeckList &source, DeckList &mainboard, DeckList &sideboard)
{
CopyMainOrSide copyMainOrSide(cardDatabase, mainboard, sideboard);
source.forEachCard(copyMainOrSide);

View File

@@ -4,8 +4,11 @@
#include "../game/cards/card_database.h"
#include "decklist.h"
#include <QLoggingCategory>
#include <QObject>
inline Q_LOGGING_CATEGORY(TappedOutInterfaceLog, "tapped_out_interface");
class QByteArray;
class QNetworkAccessManager;
class QNetworkReply;
@@ -24,13 +27,13 @@ private:
QNetworkAccessManager *manager;
CardDatabase &cardDatabase;
void copyDeckSplitMainAndSide(const DeckList &source, DeckList &mainboard, DeckList &sideboard);
void copyDeckSplitMainAndSide(DeckList &source, DeckList &mainboard, DeckList &sideboard);
private slots:
void queryFinished(QNetworkReply *reply);
void getAnalyzeRequestData(DeckList *deck, QByteArray *data);
public:
TappedOutInterface(CardDatabase &_cardDatabase, QObject *parent = nullptr);
explicit TappedOutInterface(CardDatabase &_cardDatabase, QObject *parent = nullptr);
void analyzeDeck(DeckList *deck);
};

View File

@@ -7,23 +7,23 @@
class TearOffMenu : public QMenu
{
public:
TearOffMenu(const QString &title, QWidget *parent = nullptr) : QMenu(title, parent)
explicit TearOffMenu(const QString &title, QWidget *parent = nullptr) : QMenu(title, parent)
{
connect(&SettingsCache::instance(), &SettingsCache::useTearOffMenusChanged, this,
[=](bool state) { setTearOffEnabled(state); });
[this](const bool state) { setTearOffEnabled(state); });
setTearOffEnabled(SettingsCache::instance().getUseTearOffMenus());
}
TearOffMenu(QWidget *parent = nullptr) : QMenu(parent)
explicit TearOffMenu(QWidget *parent = nullptr) : QMenu(parent)
{
connect(&SettingsCache::instance(), &SettingsCache::useTearOffMenusChanged, this,
[=](bool state) { setTearOffEnabled(state); });
[this](const bool state) { setTearOffEnabled(state); });
setTearOffEnabled(SettingsCache::instance().getUseTearOffMenus());
}
TearOffMenu *addTearOffMenu(const QString &title)
{
TearOffMenu *menu = new TearOffMenu(title, this);
auto *menu = new TearOffMenu(title, this);
addMenu(menu);
return menu;
}

View File

@@ -20,8 +20,12 @@
* @param hSpacing The horizontal spacing between items.
* @param vSpacing The vertical spacing between items.
*/
FlowLayout::FlowLayout(QWidget *parent, const int margin, const int hSpacing, const int vSpacing)
: QLayout(parent), horizontalMargin(hSpacing), verticalMargin(vSpacing)
FlowLayout::FlowLayout(QWidget *parent,
const Qt::Orientation _flowDirection,
const int margin,
const int hSpacing,
const int vSpacing)
: QLayout(parent), flowDirection(_flowDirection), horizontalMargin(hSpacing), verticalMargin(vSpacing)
{
setContentsMargins(margin, margin, margin, margin);
}
@@ -62,27 +66,51 @@ bool FlowLayout::hasHeightForWidth() const
*/
int FlowLayout::heightForWidth(const int width) const
{
int height = 0;
int rowWidth = 0;
int rowHeight = 0;
if (flowDirection == Qt::Vertical) {
int height = 0;
int rowWidth = 0;
int rowHeight = 0;
for (const QLayoutItem *item : items) {
if (item == nullptr || item->isEmpty()) {
continue;
}
for (const QLayoutItem *item : items) {
if (item == nullptr || item->isEmpty()) {
continue;
}
int itemWidth = item->sizeHint().width() + horizontalSpacing();
if (rowWidth + itemWidth > width) { // Start a new row if the row width exceeds available width
height += rowHeight + verticalSpacing();
rowWidth = itemWidth;
rowHeight = item->sizeHint().height() + verticalSpacing();
} else {
rowWidth += itemWidth;
rowHeight = qMax(rowHeight, item->sizeHint().height());
int itemWidth = item->sizeHint().width() + horizontalSpacing();
if (rowWidth + itemWidth > width) {
height += rowHeight + verticalSpacing();
rowWidth = itemWidth;
rowHeight = item->sizeHint().height();
} else {
rowWidth += itemWidth;
rowHeight = qMax(rowHeight, item->sizeHint().height());
}
}
height += rowHeight; // Add height of the last row
return height;
} else {
int width = 0;
int colWidth = 0;
int colHeight = 0;
for (const QLayoutItem *item : items) {
if (item == nullptr || item->isEmpty()) {
continue;
}
int itemHeight = item->sizeHint().height();
if (colHeight + itemHeight > width) {
width += colWidth;
colHeight = itemHeight;
colWidth = item->sizeHint().width();
} else {
colHeight += itemHeight;
colWidth = qMax(colWidth, item->sizeHint().width());
}
}
width += colWidth; // Add width of the last column
return width;
}
height += rowHeight; // Add the final row's height
return height;
}
/**
@@ -93,132 +121,420 @@ void FlowLayout::setGeometry(const QRect &rect)
{
QLayout::setGeometry(rect); // Sets the geometry of the layout based on the given rectangle.
int left, top, right, bottom;
getContentsMargins(&left, &top, &right, &bottom); // Retrieves the layout's content margins.
if (flowDirection == Qt::Horizontal) {
// If we have a parent scroll area, we're clamped to that, else we use our own rectangle.
const int availableWidth = getParentScrollAreaWidth() == 0 ? rect.width() : getParentScrollAreaWidth();
// Adjust the rectangle to exclude margins.
const QRect adjustedRect = rect.adjusted(+left, +top, -right, -bottom);
const int totalHeight = layoutAllRows(rect.x(), rect.y(), availableWidth);
// Calculate the available width for items, considering either the adjusted rectangle's width
// or the parent scroll area width, if applicable.
const int availableWidth = qMax(adjustedRect.width(), getParentScrollAreaWidth());
if (QWidget *parentWidgetPtr = parentWidget()) {
parentWidgetPtr->setFixedSize(availableWidth, totalHeight);
}
} else {
const int availableHeight = qMax(rect.height(), getParentScrollAreaHeight());
// Arrange all rows of items within the available width and get the total height used.
const int totalHeight = layoutAllRows(adjustedRect.x(), adjustedRect.y(), availableWidth);
const int totalWidth = layoutAllColumns(rect.x(), rect.y(), availableHeight);
// If the layout's parent is a QWidget, update its minimum size to ensure it can accommodate
// the arranged items' dimensions.
if (QWidget *parentWidgetPtr = parentWidget()) {
parentWidgetPtr->setMinimumSize(availableWidth, totalHeight);
if (QWidget *parentWidgetPtr = parentWidget()) {
parentWidgetPtr->setFixedSize(totalWidth, availableHeight);
}
}
}
/**
* @brief Arranges items in rows based on the available width.
* Items are added to a row until the row's width exceeds `availableWidth`.
* Then, a new row is started.
* @param originX The starting x-coordinate for the row layout.
* @param originY The starting y-coordinate for the row layout.
* @param availableWidth The available width to lay out items.
* @return The y-coordinate of the final row's end position.
* @brief Lays out items into rows according to the available width, starting from a given origin.
* Each row is arranged within `availableWidth`, wrapping to a new row as necessary.
* @param originX The x-coordinate for the layout start position.
* @param originY The y-coordinate for the layout start position.
* @param availableWidth The width within which each row is constrained.
* @return The total height after arranging all rows.
*/
int FlowLayout::layoutAllRows(const int originX, const int originY, const int availableWidth)
{
QVector<QLayoutItem *> rowItems; // Temporary storage for items in the current row.
int currentXPosition = originX; // Tracks the x-coordinate for placing items in the current row.
int currentYPosition = originY; // Tracks the y-coordinate, updated after each row.
QVector<QLayoutItem *> rowItems; // Holds items for the current row
int currentXPosition = originX; // Tracks the x-coordinate while placing items
int currentYPosition = originY; // Tracks the y-coordinate, moving down after each row
int rowHeight = 0; // Tracks the maximum height of items in the current row.
int rowHeight = 0; // Tracks the maximum height of items in the current row
// Iterate through all layout items to arrange them.
for (QLayoutItem *item : items) {
if (item == nullptr || item->isEmpty()) {
continue;
}
QSize itemSize = item->sizeHint(); // The suggested size for the item.
const int itemWidth = itemSize.width() + horizontalSpacing();
QSize itemSize = item->sizeHint(); // The suggested size for the current item
int itemWidth = itemSize.width() + horizontalSpacing(); // Item width plus spacing
// Check if the item fits in the current row's remaining width.
// Check if the current item fits in the remaining width of the current row
if (currentXPosition + itemWidth > availableWidth) {
// If not, layout the current row and start a new row.
// If not, layout the current row and start a new row
layoutSingleRow(rowItems, originX, currentYPosition);
rowItems.clear(); // Clear the temporary storage for the new row.
currentXPosition = originX; // Reset x-position to the start of the new row.
currentYPosition += rowHeight + verticalSpacing(); // Move y-position down for the new row.
rowHeight = 0; // Reset row height for the new row.
rowItems.clear(); // Reset the list for the new row
currentXPosition = originX; // Reset x-position to the row's start
currentYPosition += rowHeight + verticalSpacing(); // Move y-position down to the next row
rowHeight = 0; // Reset row height for the new row
}
// Add the item to the current row.
// Add the item to the current row
rowItems.append(item);
rowHeight = qMax(rowHeight, itemSize.height()); // Update the row height to the tallest item.
currentXPosition += itemSize.width() + horizontalSpacing(); // Move x-position for the next item.
rowHeight = qMax(rowHeight, itemSize.height()); // Update the row's height to the tallest item
currentXPosition += itemWidth + horizontalSpacing(); // Move x-position for the next item
}
// Layout the final row if there are remaining items.
// Layout the final row if there are any remaining items
layoutSingleRow(rowItems, originX, currentYPosition);
currentYPosition += rowHeight; // Add the final row's height
return currentYPosition;
// Return the total height used, including the last row's height
return currentYPosition + rowHeight;
}
/**
* @brief Helper function for arranging a single row of items within specified bounds.
* @param rowItems Items to be arranged in the row.
* @param x The x-coordinate for starting the row.
* @param y The y-coordinate for starting the row.
* @brief Arranges a single row of items within specified x and y starting positions.
* @param rowItems A list of items to be arranged in the row.
* @param x The starting x-coordinate for the row.
* @param y The starting y-coordinate for the row.
*/
void FlowLayout::layoutSingleRow(const QVector<QLayoutItem *> &rowItems, int x, const int y)
{
// Iterate through each item in the row and position it.
for (QLayoutItem *item : rowItems) {
if (item == nullptr || item->isEmpty()) {
continue;
}
QSize itemMaxSize = item->widget()->maximumSize(); // Get the item's maximum allowable size.
// Constrain the item's width and height to its size hint or maximum size.
int itemWidth = qMin(item->sizeHint().width(), itemMaxSize.width());
int itemHeight = qMin(item->sizeHint().height(), itemMaxSize.height());
// Set the item's geometry based on the calculated size and position.
// Get the maximum allowed size for the item
QSize itemMaxSize = item->widget()->maximumSize();
// Constrain the item's width and height to its size hint or maximum size
const int itemWidth = qMin(item->sizeHint().width(), itemMaxSize.width());
const int itemHeight = qMin(item->sizeHint().height(), itemMaxSize.height());
// Set the item's geometry based on the computed size and position
item->setGeometry(QRect(QPoint(x, y), QSize(itemWidth, itemHeight)));
// Move the x-position for the next item, including horizontal spacing.
// Move the x-position to the right, leaving space for horizontal spacing
x += itemWidth + horizontalSpacing();
}
}
/**
* @brief Returns the preferred size for this layout.
* @return The maximum of all item size hints as a QSize.
* @brief Lays out items into columns according to the available height, starting from a given origin.
* Each column is arranged within `availableHeight`, wrapping to a new column as necessary.
* @param originX The x-coordinate for the layout start position.
* @param originY The y-coordinate for the layout start position.
* @param availableHeight The height within which each column is constrained.
* @return The total width after arranging all columns.
*/
QSize FlowLayout::sizeHint() const
int FlowLayout::layoutAllColumns(const int originX, const int originY, const int availableHeight)
{
QSize size;
for (const QLayoutItem *item : items) {
if (item != nullptr && !item->isEmpty()) {
size = size.expandedTo(item->sizeHint());
QVector<QLayoutItem *> colItems; // Holds items for the current column
int currentXPosition = originX; // Tracks the x-coordinate while placing items
int currentYPosition = originY; // Tracks the y-coordinate, resetting for each new column
int colWidth = 0; // Tracks the maximum width of items in the current column
for (QLayoutItem *item : items) {
if (item == nullptr || item->isEmpty()) {
continue;
}
QSize itemSize = item->sizeHint(); // The suggested size for the current item
// Check if the current item fits in the remaining height of the current column
if (currentYPosition + itemSize.height() > availableHeight) {
// If not, layout the current column and start a new column
layoutSingleColumn(colItems, currentXPosition, originY);
colItems.clear(); // Reset the list for the new column
currentYPosition = originY; // Reset y-position to the column's start
currentXPosition += colWidth; // Move x-position to the next column
colWidth = 0; // Reset column width for the new column
}
// Add the item to the current column
colItems.append(item);
colWidth = qMax(colWidth, itemSize.width()); // Update the column's width to the widest item
currentYPosition += itemSize.height(); // Move y-position for the next item
}
return size.isValid() ? size : QSize(0, 0);
// Layout the final column if there are any remaining items
layoutSingleColumn(colItems, currentXPosition, originY);
// Return the total width used, including the last column's width
return currentXPosition + colWidth;
}
/**
* @brief Returns the minimum size required to display all layout items.
* @return The minimum QSize needed by the layout.
* @brief Arranges a single column of items within specified x and y starting positions.
* @param colItems A list of items to be arranged in the column.
* @param x The starting x-coordinate for the column.
* @param y The starting y-coordinate for the column.
*/
void FlowLayout::layoutSingleColumn(const QVector<QLayoutItem *> &colItems, const int x, int y)
{
for (QLayoutItem *item : colItems) {
if (item == nullptr) {
qCDebug(FlowLayoutLog) << "Item is null.";
continue;
}
if (item->isEmpty()) {
qCDebug(FlowLayoutLog) << "Skipping empty item.";
continue;
}
// Debugging: Print the item's widget class name and size hint
QWidget *widget = item->widget();
if (widget) {
qCDebug(FlowLayoutLog) << "Widget class:" << widget->metaObject()->className();
qCDebug(FlowLayoutLog) << "Widget size hint:" << widget->sizeHint();
qCDebug(FlowLayoutLog) << "Widget maximum size:" << widget->maximumSize();
qCDebug(FlowLayoutLog) << "Widget minimum size:" << widget->minimumSize();
// Debugging: Print child widgets
const QObjectList &children = widget->children();
qCDebug(FlowLayoutLog) << "Child widgets:";
for (QObject *child : children) {
if (QWidget *childWidget = qobject_cast<QWidget *>(child)) {
qCDebug(FlowLayoutLog) << " - Child widget class:" << childWidget->metaObject()->className();
qCDebug(FlowLayoutLog) << " Size hint:" << childWidget->sizeHint();
qCDebug(FlowLayoutLog) << " Maximum size:" << childWidget->maximumSize();
}
}
} else {
qCDebug(FlowLayoutLog) << "Item does not have a widget.";
}
// Get the maximum allowed size for the item
QSize itemMaxSize = widget->maximumSize();
// Constrain the item's width and height to its size hint or maximum size
const int itemWidth = qMin(item->sizeHint().width(), itemMaxSize.width());
const int itemHeight = qMin(item->sizeHint().height(), itemMaxSize.height());
// Debugging: Print the computed geometry
qCDebug(FlowLayoutLog) << "Computed geometry: x=" << x << ", y=" << y << ", width=" << itemWidth
<< ", height=" << itemHeight;
// Set the item's geometry based on the computed size and position
item->setGeometry(QRect(QPoint(x, y), QSize(itemWidth, itemHeight)));
// Move the y-position down by the item's height to place the next item below
y += itemHeight;
}
}
/**
* @brief Calculates the preferred size of the layout based on the flow direction.
* @return A QSize representing the ideal dimensions of the layout.
*/
QSize FlowLayout::sizeHint() const
{
if (flowDirection == Qt::Horizontal) {
return calculateSizeHintHorizontal();
} else {
return calculateSizeHintVertical();
}
}
/**
* @brief Calculates the minimum size required by the layout based on the flow direction.
* @return A QSize representing the minimum required dimensions.
*/
QSize FlowLayout::minimumSize() const
{
QSize size;
if (flowDirection == Qt::Horizontal) {
return calculateMinimumSizeHorizontal();
} else {
return calculateMinimumSizeVertical();
}
}
/**
* @brief Calculates the size hint for horizontal flow direction.
* @return A QSize representing the preferred dimensions.
*/
QSize FlowLayout::calculateSizeHintHorizontal() const
{
int maxWidth = 0; // Tracks the maximum width needed
int totalHeight = 0; // Tracks the total height across all rows
int rowHeight = 0; // Tracks the height of the current row
int currentWidth = 0; // Tracks the current row's width
const int availableWidth = getParentScrollAreaWidth() == 0 ? parentWidget()->width() : getParentScrollAreaWidth();
qCDebug(FlowLayoutLog) << "Calculating horizontal size hint. Available width:" << availableWidth;
for (const QLayoutItem *item : items) {
if (item != nullptr && !item->isEmpty()) {
size = size.expandedTo(item->minimumSize());
if (!item || item->isEmpty()) {
qCDebug(FlowLayoutLog) << "Skipping empty item.";
continue;
}
QSize itemSize = item->sizeHint();
int itemWidth = itemSize.width() + horizontalSpacing();
qCDebug(FlowLayoutLog) << "Processing item. Size:" << itemSize << "Width with spacing:" << itemWidth;
if (currentWidth + itemWidth > availableWidth) {
qCDebug(FlowLayoutLog) << "Row overflow. Current width:" << currentWidth << "Row height:" << rowHeight;
maxWidth = qMax(maxWidth, currentWidth);
totalHeight += rowHeight + verticalSpacing();
qCDebug(FlowLayoutLog) << "Updated total height:" << totalHeight << "Max width so far:" << maxWidth;
currentWidth = 0;
rowHeight = 0;
}
currentWidth += itemWidth;
rowHeight = qMax(rowHeight, itemSize.height());
qCDebug(FlowLayoutLog) << "Updated current width:" << currentWidth << "Updated row height:" << rowHeight;
}
size.setWidth(qMin(size.width(), getParentScrollAreaWidth()));
size.setHeight(qMin(size.height(), getParentScrollAreaHeight()));
// Account for the final row
maxWidth = qMax(maxWidth, currentWidth);
totalHeight += rowHeight;
qCDebug(FlowLayoutLog) << "Final total height:" << totalHeight << "Final max width:" << maxWidth;
return size.isValid() ? size : QSize(0, 0);
return QSize(maxWidth, totalHeight);
}
/**
* @brief Calculates the minimum size for horizontal flow direction.
* @return A QSize representing the minimum required dimensions.
*/
QSize FlowLayout::calculateMinimumSizeHorizontal() const
{
int maxWidth = 0; // Tracks the maximum width of a row
int totalHeight = 0; // Tracks the total height across all rows
int rowHeight = 0; // Tracks the height of the current row
int currentWidth = 0; // Tracks the current row's width
const int availableWidth = getParentScrollAreaWidth() == 0 ? parentWidget()->width() : getParentScrollAreaWidth();
qCDebug(FlowLayoutLog) << "Calculating horizontal minimum size. Available width:" << availableWidth;
for (const QLayoutItem *item : items) {
if (!item || item->isEmpty()) {
qCDebug(FlowLayoutLog) << "Skipping empty item.";
continue;
}
QSize itemMinSize = item->minimumSize();
int itemWidth = itemMinSize.width() + horizontalSpacing();
qCDebug(FlowLayoutLog) << "Processing item. Minimum size:" << itemMinSize << "Width with spacing:" << itemWidth;
if (currentWidth + itemWidth > availableWidth) {
qCDebug(FlowLayoutLog) << "Row overflow. Current width:" << currentWidth << "Row height:" << rowHeight;
maxWidth = qMax(maxWidth, currentWidth);
totalHeight += rowHeight + verticalSpacing();
qCDebug(FlowLayoutLog) << "Updated total height:" << totalHeight << "Max width so far:" << maxWidth;
currentWidth = 0;
rowHeight = 0;
}
currentWidth += itemWidth;
rowHeight = qMax(rowHeight, itemMinSize.height());
qCDebug(FlowLayoutLog) << "Updated current width:" << currentWidth << "Updated row height:" << rowHeight;
}
// Account for the final row
maxWidth = qMax(maxWidth, currentWidth);
totalHeight += rowHeight;
qCDebug(FlowLayoutLog) << "Final total height:" << totalHeight << "Final max width:" << maxWidth;
return QSize(maxWidth, totalHeight);
}
/**
* @brief Calculates the size hint for vertical flow direction.
* @return A QSize representing the preferred dimensions.
*/
QSize FlowLayout::calculateSizeHintVertical() const
{
int totalWidth = 0;
int maxHeight = 0;
int colWidth = 0;
int currentHeight = 0;
const int availableHeight = qMax(parentWidget()->height(), getParentScrollAreaHeight());
qCDebug(FlowLayoutLog) << "Calculating vertical size hint. Available height:" << availableHeight;
for (const QLayoutItem *item : items) {
if (!item || item->isEmpty()) {
qCDebug(FlowLayoutLog) << "Skipping empty item.";
continue;
}
QSize itemSize = item->sizeHint();
qCDebug(FlowLayoutLog) << "Processing item. Size:" << itemSize;
if (currentHeight + itemSize.height() > availableHeight) {
qCDebug(FlowLayoutLog) << "Column overflow. Current height:" << currentHeight
<< "Column width:" << colWidth;
totalWidth += colWidth + horizontalSpacing();
maxHeight = qMax(maxHeight, currentHeight);
qCDebug(FlowLayoutLog) << "Updated total width:" << totalWidth << "Max height so far:" << maxHeight;
currentHeight = 0;
colWidth = 0;
}
currentHeight += itemSize.height() + verticalSpacing();
colWidth = qMax(colWidth, itemSize.width());
qCDebug(FlowLayoutLog) << "Updated current height:" << currentHeight << "Updated column width:" << colWidth;
}
// Account for the final column
totalWidth += colWidth;
maxHeight = qMax(maxHeight, currentHeight);
qCDebug(FlowLayoutLog) << "Final total width:" << totalWidth << "Final max height:" << maxHeight;
return QSize(totalWidth, maxHeight);
}
/**
* @brief Calculates the minimum size for vertical flow direction.
* @return A QSize representing the minimum required dimensions.
*/
QSize FlowLayout::calculateMinimumSizeVertical() const
{
int totalWidth = 0; // Tracks the total width across all columns
int maxHeight = 0; // Tracks the maximum height of a column
int colWidth = 0; // Tracks the width of the current column
int currentHeight = 0; // Tracks the current column's height
const int availableHeight = qMax(parentWidget()->height(), getParentScrollAreaHeight());
qCDebug(FlowLayoutLog) << "Calculating vertical minimum size. Available height:" << availableHeight;
for (const QLayoutItem *item : items) {
if (!item || item->isEmpty()) {
qCDebug(FlowLayoutLog) << "Skipping empty item.";
continue;
}
QSize itemMinSize = item->minimumSize();
int itemHeight = itemMinSize.height() + verticalSpacing();
qCDebug(FlowLayoutLog) << "Processing item. Minimum size:" << itemMinSize
<< "Height with spacing:" << itemHeight;
if (currentHeight + itemHeight > availableHeight) {
qCDebug(FlowLayoutLog) << "Column overflow. Current height:" << currentHeight
<< "Column width:" << colWidth;
totalWidth += colWidth + horizontalSpacing();
maxHeight = qMax(maxHeight, currentHeight);
qCDebug(FlowLayoutLog) << "Updated total width:" << totalWidth << "Max height so far:" << maxHeight;
currentHeight = 0;
colWidth = 0;
}
currentHeight += itemHeight;
colWidth = qMax(colWidth, itemMinSize.width());
qCDebug(FlowLayoutLog) << "Updated current height:" << currentHeight << "Updated column width:" << colWidth;
}
// Account for the final column
totalWidth += colWidth;
maxHeight = qMax(maxHeight, currentHeight);
qCDebug(FlowLayoutLog) << "Final total width:" << totalWidth << "Final max height:" << maxHeight;
return QSize(totalWidth, maxHeight);
}
/**

View File

@@ -3,16 +3,22 @@
#include <QLayout>
#include <QList>
#include <QLoggingCategory>
#include <QWidget>
#include <qstyle.h>
inline Q_LOGGING_CATEGORY(FlowLayoutLog, "flow_layout");
class FlowLayout : public QLayout
{
public:
explicit FlowLayout(QWidget *parent = nullptr);
FlowLayout(QWidget *parent, int margin, int hSpacing, int vSpacing);
FlowLayout(QWidget *parent, Qt::Orientation _flowDirection, int margin = 0, int hSpacing = 0, int vSpacing = 0);
~FlowLayout() override;
QSize calculateMinimumSizeHorizontal() const;
QSize calculateSizeHintVertical() const;
QSize calculateMinimumSizeVertical() const;
void addItem(QLayoutItem *item) override;
[[nodiscard]] int count() const override;
[[nodiscard]] QLayoutItem *itemAt(int index) const override;
@@ -31,11 +37,15 @@ public:
void setGeometry(const QRect &rect) override;
virtual int layoutAllRows(int originX, int originY, int availableWidth);
virtual void layoutSingleRow(const QVector<QLayoutItem *> &rowItems, int x, int y);
int layoutAllColumns(int originX, int originY, int availableHeight);
void layoutSingleColumn(const QVector<QLayoutItem *> &colItems, int x, int y);
[[nodiscard]] QSize sizeHint() const override;
[[nodiscard]] QSize minimumSize() const override;
QSize calculateSizeHintHorizontal() const;
protected:
QList<QLayoutItem *> items; // List to store layout items
Qt::Orientation flowDirection;
int horizontalMargin;
int verticalMargin;
};

View File

@@ -1,142 +0,0 @@
#include "horizontal_flow_layout.h"
/**
* @brief Constructs a HorizontalFlowLayout instance with the specified parent widget.
* This layout arranges items in columns within the given height, automatically adjusting its width.
* @param parent The parent widget to which this layout belongs.
* @param margin The layout margin.
* @param hSpacing The horizontal spacing between items.
* @param vSpacing The vertical spacing between items.
*/
HorizontalFlowLayout::HorizontalFlowLayout(QWidget *parent, const int margin, const int hSpacing, const int vSpacing)
: FlowLayout(parent, margin, hSpacing, vSpacing)
{
}
/**
* @brief Destructor for HorizontalFlowLayout, responsible for cleaning up layout items.
*/
HorizontalFlowLayout::~HorizontalFlowLayout()
{
QLayoutItem *item;
while ((item = FlowLayout::takeAt(0))) {
delete item;
}
}
/**
* @brief Calculates the required width to display all items, given a specified height.
* This method arranges items into columns and determines the total width needed.
* @param height The available height for arranging layout items.
* @return The total width required to fit all items, organized in columns constrained by the given height.
*/
int HorizontalFlowLayout::heightForWidth(const int height) const
{
int width = 0;
int colWidth = 0;
int colHeight = 0;
for (const QLayoutItem *item : items) {
if (item == nullptr || item->isEmpty()) {
continue;
}
int itemHeight = item->sizeHint().height();
if (colHeight + itemHeight > height) {
width += colWidth;
colHeight = itemHeight;
colWidth = item->sizeHint().width();
} else {
colHeight += itemHeight;
colWidth = qMax(colWidth, item->sizeHint().width());
}
}
width += colWidth; // Add width of the last column
return width;
}
/**
* @brief Sets the geometry of the layout items, arranging them in columns within the given height.
* @param rect The rectangle area defining the layout space.
*/
void HorizontalFlowLayout::setGeometry(const QRect &rect)
{
const int availableHeight = qMax(rect.height(), getParentScrollAreaHeight());
const int totalWidth = layoutAllColumns(rect.x(), rect.y(), availableHeight);
if (QWidget *parentWidgetPtr = parentWidget()) {
parentWidgetPtr->setMinimumSize(totalWidth, availableHeight);
}
}
/**
* @brief Lays out items into columns according to the available height, starting from a given origin.
* Each column is arranged within `availableHeight`, wrapping to a new column as necessary.
* @param originX The x-coordinate for the layout start position.
* @param originY The y-coordinate for the layout start position.
* @param availableHeight The height within which each column is constrained.
* @return The total width after arranging all columns.
*/
int HorizontalFlowLayout::layoutAllColumns(const int originX, const int originY, const int availableHeight)
{
QVector<QLayoutItem *> colItems; // Holds items for the current column
int currentXPosition = originX; // Tracks the x-coordinate while placing items
int currentYPosition = originY; // Tracks the y-coordinate, resetting for each new column
int colWidth = 0; // Tracks the maximum width of items in the current column
for (QLayoutItem *item : items) {
if (item == nullptr || item->isEmpty()) {
continue;
}
QSize itemSize = item->sizeHint(); // The suggested size for the current item
// Check if the current item fits in the remaining height of the current column
if (currentYPosition + itemSize.height() > availableHeight) {
// If not, layout the current column and start a new column
layoutSingleColumn(colItems, currentXPosition, originY);
colItems.clear(); // Reset the list for the new column
currentYPosition = originY; // Reset y-position to the column's start
currentXPosition += colWidth; // Move x-position to the next column
colWidth = 0; // Reset column width for the new column
}
// Add the item to the current column
colItems.append(item);
colWidth = qMax(colWidth, itemSize.width()); // Update the column's width to the widest item
currentYPosition += itemSize.height(); // Move y-position for the next item
}
// Layout the final column if there are any remaining items
layoutSingleColumn(colItems, currentXPosition, originY);
// Return the total width used, including the last column's width
return currentXPosition + colWidth;
}
/**
* @brief Arranges a single column of items within specified x and y starting positions.
* @param colItems A list of items to be arranged in the column.
* @param x The starting x-coordinate for the column.
* @param y The starting y-coordinate for the column.
*/
void HorizontalFlowLayout::layoutSingleColumn(const QVector<QLayoutItem *> &colItems, const int x, int y)
{
for (QLayoutItem *item : colItems) {
if (item != nullptr && item->isEmpty()) {
continue;
}
// Get the maximum allowed size for the item
QSize itemMaxSize = item->widget()->maximumSize();
// Constrain the item's width and height to its size hint or maximum size
const int itemWidth = qMin(item->sizeHint().width(), itemMaxSize.width());
const int itemHeight = qMin(item->sizeHint().height(), itemMaxSize.height());
// Set the item's geometry based on the computed size and position
item->setGeometry(QRect(QPoint(x, y), QSize(itemWidth, itemHeight)));
// Move the y-position down by the item's height to place the next item below
y += itemHeight;
}
}

View File

@@ -1,19 +0,0 @@
#ifndef HORIZONTAL_FLOW_LAYOUT_H
#define HORIZONTAL_FLOW_LAYOUT_H
#include "flow_layout.h"
class HorizontalFlowLayout : public FlowLayout
{
public:
explicit HorizontalFlowLayout(QWidget *parent = nullptr, int margin = 0, int hSpacing = 0, int vSpacing = 0);
~HorizontalFlowLayout() override;
[[nodiscard]] int heightForWidth(int height) const override;
void setGeometry(const QRect &rect) override;
int layoutAllColumns(int originX, int originY, int availableHeight);
static void layoutSingleColumn(const QVector<QLayoutItem *> &colItems, int x, int y);
};
#endif // HORIZONTAL_FLOW_LAYOUT_H

View File

@@ -1,144 +0,0 @@
#include "vertical_flow_layout.h"
/**
* @brief Constructs a VerticalFlowLayout instance with the specified parent widget.
* This layout arranges items in rows within the given width, automatically adjusting its height.
* @param parent The parent widget to which this layout belongs.
* @param margin The layout margin.
* @param hSpacing The horizontal spacing between items.
* @param vSpacing The vertical spacing between items.
*/
VerticalFlowLayout::VerticalFlowLayout(QWidget *parent, const int margin, const int hSpacing, const int vSpacing)
: FlowLayout(parent, margin, hSpacing, vSpacing)
{
}
/**
* @brief Destructor for VerticalFlowLayout, responsible for cleaning up layout items.
*/
VerticalFlowLayout::~VerticalFlowLayout()
{
QLayoutItem *item;
while ((item = FlowLayout::takeAt(0))) {
delete item;
}
}
/**
* @brief Calculates the required height to display all items, given a specified width.
* This method arranges items into rows and determines the total height needed.
* @param width The available width for arranging layout items.
* @return The total height required to fit all items, organized in rows constrained by the given width.
*/
int VerticalFlowLayout::heightForWidth(const int width) const
{
int height = 0;
int rowWidth = 0;
int rowHeight = 0;
for (const QLayoutItem *item : items) {
if (item == nullptr || item->isEmpty()) {
continue;
}
int itemWidth = item->sizeHint().width() + horizontalSpacing();
if (rowWidth + itemWidth > width) {
height += rowHeight + verticalSpacing();
rowWidth = itemWidth;
rowHeight = item->sizeHint().height();
} else {
rowWidth += itemWidth;
rowHeight = qMax(rowHeight, item->sizeHint().height());
}
}
height += rowHeight; // Add height of the last row
return height;
}
/**
* @brief Sets the geometry of the layout items, arranging them in rows within the given width.
* @param rect The rectangle area defining the layout space.
*/
void VerticalFlowLayout::setGeometry(const QRect &rect)
{
// If we have a parent scroll area, we're clamped to that, else we use our own rectangle.
const int availableWidth = getParentScrollAreaWidth() == 0 ? rect.width() : getParentScrollAreaWidth();
const int totalHeight = layoutAllRows(rect.x(), rect.y(), availableWidth);
if (QWidget *parentWidgetPtr = parentWidget()) {
parentWidgetPtr->setMinimumSize(availableWidth, totalHeight);
}
}
/**
* @brief Lays out items into rows according to the available width, starting from a given origin.
* Each row is arranged within `availableWidth`, wrapping to a new row as necessary.
* @param originX The x-coordinate for the layout start position.
* @param originY The y-coordinate for the layout start position.
* @param availableWidth The width within which each row is constrained.
* @return The total height after arranging all rows.
*/
int VerticalFlowLayout::layoutAllRows(const int originX, const int originY, const int availableWidth)
{
QVector<QLayoutItem *> rowItems; // Holds items for the current row
int currentXPosition = originX; // Tracks the x-coordinate while placing items
int currentYPosition = originY; // Tracks the y-coordinate, moving down after each row
int rowHeight = 0; // Tracks the maximum height of items in the current row
for (QLayoutItem *item : items) {
if (item == nullptr || item->isEmpty()) {
continue;
}
QSize itemSize = item->sizeHint(); // The suggested size for the current item
int itemWidth = itemSize.width() + horizontalSpacing(); // Item width plus spacing
// Check if the current item fits in the remaining width of the current row
if (currentXPosition + itemWidth > availableWidth) {
// If not, layout the current row and start a new row
layoutSingleRow(rowItems, originX, currentYPosition);
rowItems.clear(); // Reset the list for the new row
currentXPosition = originX; // Reset x-position to the row's start
currentYPosition += rowHeight + verticalSpacing(); // Move y-position down to the next row
rowHeight = 0; // Reset row height for the new row
}
// Add the item to the current row
rowItems.append(item);
rowHeight = qMax(rowHeight, itemSize.height()); // Update the row's height to the tallest item
currentXPosition += itemWidth + horizontalSpacing(); // Move x-position for the next item
}
// Layout the final row if there are any remaining items
layoutSingleRow(rowItems, originX, currentYPosition);
// Return the total height used, including the last row's height
return currentYPosition + rowHeight;
}
/**
* @brief Arranges a single row of items within specified x and y starting positions.
* @param rowItems A list of items to be arranged in the row.
* @param x The starting x-coordinate for the row.
* @param y The starting y-coordinate for the row.
*/
void VerticalFlowLayout::layoutSingleRow(const QVector<QLayoutItem *> &rowItems, int x, const int y)
{
for (QLayoutItem *item : rowItems) {
if (item == nullptr || item->isEmpty()) {
continue;
}
// Get the maximum allowed size for the item
QSize itemMaxSize = item->widget()->maximumSize();
// Constrain the item's width and height to its size hint or maximum size
const int itemWidth = qMin(item->sizeHint().width(), itemMaxSize.width());
const int itemHeight = qMin(item->sizeHint().height(), itemMaxSize.height());
// Set the item's geometry based on the computed size and position
item->setGeometry(QRect(QPoint(x, y), QSize(itemWidth, itemHeight)));
// Move the x-position to the right, leaving space for horizontal spacing
x += itemWidth + horizontalSpacing();
}
}

View File

@@ -1,19 +0,0 @@
#ifndef VERTICAL_FLOW_LAYOUT_H
#define VERTICAL_FLOW_LAYOUT_H
#include "flow_layout.h"
class VerticalFlowLayout : public FlowLayout
{
public:
explicit VerticalFlowLayout(QWidget *parent = nullptr, int margin = 0, int hSpacing = 0, int vSpacing = 0);
~VerticalFlowLayout() override;
[[nodiscard]] int heightForWidth(int width) const override;
void setGeometry(const QRect &rect) override;
int layoutAllRows(int originX, int originY, int availableWidth) override;
void layoutSingleRow(const QVector<QLayoutItem *> &rowItems, int x, int y) override;
};
#endif // VERTICAL_FLOW_LAYOUT_H

View File

@@ -1,835 +0,0 @@
#include "picture_loader.h"
#include "../../game/cards/card_database_manager.h"
#include "../../settings/cache_settings.h"
#include <QApplication>
#include <QBuffer>
#include <QCryptographicHash>
#include <QDebug>
#include <QDirIterator>
#include <QFileInfo>
#include <QImageReader>
#include <QMovie>
#include <QNetworkAccessManager>
#include <QNetworkDiskCache>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QPainter>
#include <QPixmapCache>
#include <QRegularExpression>
#include <QScreen>
#include <QSet>
#include <QThread>
#include <QUrl>
#include <algorithm>
#include <utility>
// never cache more than 300 cards at once for a single deck
#define CACHED_CARD_PER_DECK_MAX 300
PictureToLoad::PictureToLoad(CardInfoPtr _card)
: card(std::move(_card)), urlTemplates(SettingsCache::instance().downloads().getAllURLs())
{
if (card) {
for (const auto &cardInfoPerSetList : card->getSets()) {
for (const auto &set : cardInfoPerSetList) {
sortedSets << set.getPtr();
}
}
if (sortedSets.empty()) {
sortedSets << CardSet::newInstance("", "", "", QDate());
}
std::sort(sortedSets.begin(), sortedSets.end(), SetDownloadPriorityComparator());
// If the user hasn't disabled arts other than their personal preference...
if (!SettingsCache::instance().getOverrideAllCardArtWithPersonalPreference()) {
// If the pixmapCacheKey corresponds to a specific set, we have to try to load it first.
for (const auto &cardInfoPerSetList : card->getSets()) {
for (const auto &set : cardInfoPerSetList) {
if (QLatin1String("card_") + card->getName() + QString("_") + QString(set.getProperty("uuid")) ==
card->getPixmapCacheKey()) {
long long setIndex = sortedSets.indexOf(set.getPtr());
CardSetPtr setForCardProviderID = sortedSets.takeAt(setIndex);
sortedSets.prepend(setForCardProviderID);
}
}
}
}
// The first time called, nextSet will also populate the Urls for the first set.
nextSet();
}
}
void PictureToLoad::populateSetUrls()
{
/* currentSetUrls is a list, populated each time a new set is requested for a particular card
and Urls are removed from it as a download is attempted from each one. Custom Urls for
a set are given higher priority, so should be placed first in the list. */
currentSetUrls.clear();
if (card && currentSet) {
QString setCustomURL = card->getCustomPicURL(currentSet->getShortName());
if (!setCustomURL.isEmpty()) {
currentSetUrls.append(setCustomURL);
}
}
for (const QString &urlTemplate : urlTemplates) {
QString transformedUrl = transformUrl(urlTemplate);
if (!transformedUrl.isEmpty()) {
currentSetUrls.append(transformedUrl);
}
}
/* Call nextUrl to make sure currentUrl is up-to-date
but we don't need the result here. */
(void)nextUrl();
}
bool PictureToLoad::nextSet()
{
if (!sortedSets.isEmpty()) {
currentSet = sortedSets.takeFirst();
populateSetUrls();
return true;
}
currentSet = {};
return false;
}
bool PictureToLoad::nextUrl()
{
if (!currentSetUrls.isEmpty()) {
currentUrl = currentSetUrls.takeFirst();
return true;
}
currentUrl = QString();
return false;
}
QString PictureToLoad::getSetName() const
{
if (currentSet) {
return currentSet->getCorrectedShortName();
} else {
return QString();
}
}
// Card back returned by gatherer when card is not found
QStringList PictureLoaderWorker::md5Blacklist = QStringList() << "db0c48db407a907c16ade38de048a441";
PictureLoaderWorker::PictureLoaderWorker()
: QObject(nullptr), picsPath(SettingsCache::instance().getPicsPath()),
customPicsPath(SettingsCache::instance().getCustomPicsPath()),
picDownload(SettingsCache::instance().getPicDownload()), downloadRunning(false), loadQueueRunning(false)
{
connect(this, SIGNAL(startLoadQueue()), this, SLOT(processLoadQueue()), Qt::QueuedConnection);
connect(&SettingsCache::instance(), SIGNAL(picsPathChanged()), this, SLOT(picsPathChanged()));
connect(&SettingsCache::instance(), SIGNAL(picDownloadChanged()), this, SLOT(picDownloadChanged()));
networkManager = new QNetworkAccessManager(this);
// We need a timeout to ensure requests don't hang indefinitely in case of
// cache corruption, see related Qt bug: https://bugreports.qt.io/browse/QTBUG-111397
// Use Qt's default timeout (30s, as of 2023-02-22)
#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0))
networkManager->setTransferTimeout();
#endif
auto cache = new QNetworkDiskCache(this);
cache->setCacheDirectory(SettingsCache::instance().getNetworkCachePath());
cache->setMaximumCacheSize(1024L * 1024L *
static_cast<qint64>(SettingsCache::instance().getNetworkCacheSizeInMB()));
// Note: the settings is in MB, but QNetworkDiskCache uses bytes
connect(&SettingsCache::instance(), &SettingsCache::networkCacheSizeChanged, cache,
[cache](int newSizeInMB) { cache->setMaximumCacheSize(1024L * 1024L * static_cast<qint64>(newSizeInMB)); });
networkManager->setCache(cache);
// Use a ManualRedirectPolicy since we keep track of redirects in picDownloadFinished
// We can't use NoLessSafeRedirectPolicy because it is not applied with AlwaysCache
networkManager->setRedirectPolicy(QNetworkRequest::ManualRedirectPolicy);
connect(networkManager, SIGNAL(finished(QNetworkReply *)), this, SLOT(picDownloadFinished(QNetworkReply *)));
cacheFilePath = SettingsCache::instance().getRedirectCachePath() + REDIRECT_CACHE_FILENAME;
loadRedirectCache();
cleanStaleEntries();
connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this,
&PictureLoaderWorker::saveRedirectCache);
pictureLoaderThread = new QThread;
pictureLoaderThread->start(QThread::LowPriority);
moveToThread(pictureLoaderThread);
}
PictureLoaderWorker::~PictureLoaderWorker()
{
pictureLoaderThread->deleteLater();
}
void PictureLoaderWorker::processLoadQueue()
{
if (loadQueueRunning) {
return;
}
loadQueueRunning = true;
while (true) {
mutex.lock();
if (loadQueue.isEmpty()) {
mutex.unlock();
loadQueueRunning = false;
return;
}
cardBeingLoaded = loadQueue.takeFirst();
mutex.unlock();
QString setName = cardBeingLoaded.getSetName();
QString cardName = cardBeingLoaded.getCard()->getName();
QString correctedCardName = cardBeingLoaded.getCard()->getCorrectedName();
qDebug().nospace() << "PictureLoader: [card: " << cardName << " set: " << setName
<< "]: Trying to load picture";
if (CardDatabaseManager::getInstance()->isProviderIdForPreferredPrinting(
cardName, cardBeingLoaded.getCard()->getPixmapCacheKey())) {
if (cardImageExistsOnDisk(setName, correctedCardName)) {
continue;
}
}
qDebug().nospace() << "PictureLoader: [card: " << cardName << " set: " << setName
<< "]: No custom picture, trying to download";
cardsToDownload.append(cardBeingLoaded);
cardBeingLoaded.clear();
if (!downloadRunning) {
startNextPicDownload();
}
}
}
bool PictureLoaderWorker::cardImageExistsOnDisk(QString &setName, QString &correctedCardname)
{
QImage image;
QImageReader imgReader;
imgReader.setDecideFormatFromContent(true);
QList<QString> picsPaths = QList<QString>();
QDirIterator it(customPicsPath, QDirIterator::Subdirectories | QDirIterator::FollowSymlinks);
// Recursively check all subdirectories of the CUSTOM folder
while (it.hasNext()) {
QString thisPath(it.next());
QFileInfo thisFileInfo(thisPath);
if (thisFileInfo.isFile() &&
(thisFileInfo.fileName() == correctedCardname || thisFileInfo.completeBaseName() == correctedCardname ||
thisFileInfo.baseName() == correctedCardname)) {
picsPaths << thisPath; // Card found in the CUSTOM directory, somewhere
}
}
if (!setName.isEmpty()) {
picsPaths << picsPath + "/" + setName + "/" + correctedCardname
// We no longer store downloaded images there, but don't just ignore
// stuff that old versions have put there.
<< picsPath + "/downloadedPics/" + setName + "/" + correctedCardname;
}
// Iterates through the list of paths, searching for images with the desired
// name with any QImageReader-supported
// extension
for (const auto &_picsPath : picsPaths) {
imgReader.setFileName(_picsPath);
if (imgReader.read(&image)) {
qDebug().nospace() << "PictureLoader: [card: " << correctedCardname << " set: " << setName
<< "]: Picture found on disk.";
imageLoaded(cardBeingLoaded.getCard(), image);
return true;
}
imgReader.setFileName(_picsPath + ".full");
if (imgReader.read(&image)) {
qDebug().nospace() << "PictureLoader: [card: " << correctedCardname << " set: " << setName
<< "]: Picture.full found on disk.";
imageLoaded(cardBeingLoaded.getCard(), image);
return true;
}
imgReader.setFileName(_picsPath + ".xlhq");
if (imgReader.read(&image)) {
qDebug().nospace() << "PictureLoader: [card: " << correctedCardname << " set: " << setName
<< "]: Picture.xlhq found on disk.";
imageLoaded(cardBeingLoaded.getCard(), image);
return true;
}
}
return false;
}
static int parse(const QString &urlTemplate,
const QString &propType,
const QString &cardName,
const QString &setName,
std::function<QString(const QString &)> getProperty,
QMap<QString, QString> &transformMap)
{
static const QRegularExpression rxFillWith("^(.+)_fill_with_(.+)$");
static const QRegularExpression rxSubStr("^(.+)_substr_(\\d+)_(\\d+)$");
const QRegularExpression rxCardProp("!" + propType + ":([^!]+)!");
auto matches = rxCardProp.globalMatch(urlTemplate);
while (matches.hasNext()) {
auto match = matches.next();
QString templatePropertyName = match.captured(1);
auto fillMatch = rxFillWith.match(templatePropertyName);
QString cardPropertyName;
QString fillWith;
int subStrPos = 0;
int subStrLen = -1;
if (fillMatch.hasMatch()) {
cardPropertyName = fillMatch.captured(1);
fillWith = fillMatch.captured(2);
} else {
fillWith = QString();
auto subStrMatch = rxSubStr.match(templatePropertyName);
if (subStrMatch.hasMatch()) {
cardPropertyName = subStrMatch.captured(1);
subStrPos = subStrMatch.captured(2).toInt();
subStrLen = subStrMatch.captured(3).toInt();
} else {
cardPropertyName = templatePropertyName;
}
}
QString propertyValue = getProperty(cardPropertyName);
if (propertyValue.isEmpty()) {
qDebug().nospace() << "PictureLoader: [card: " << cardName << " set: " << setName << "]: Requested "
<< propType << "property (" << cardPropertyName << ") for Url template (" << urlTemplate
<< ") is not available";
return 1;
} else {
int propLength = propertyValue.length();
if (subStrLen > 0) {
if (subStrPos + subStrLen > propLength) {
qDebug().nospace() << "PictureLoader: [card: " << cardName << " set: " << setName << "]: Requested "
<< propType << " property (" << cardPropertyName << ") for Url template ("
<< urlTemplate << ") is smaller than substr specification (" << subStrPos
<< " + " << subStrLen << " > " << propLength << ")";
return 1;
} else {
propertyValue = propertyValue.mid(subStrPos, subStrLen);
propLength = subStrLen;
}
}
if (!fillWith.isEmpty()) {
int fillLength = fillWith.length();
if (fillLength < propLength) {
qDebug().nospace() << "PictureLoader: [card: " << cardName << " set: " << setName << "]: Requested "
<< propType << " property (" << cardPropertyName << ") for Url template ("
<< urlTemplate << ") is longer than fill specification (" << fillWith << ")";
return 1;
} else {
propertyValue = fillWith.left(fillLength - propLength) + propertyValue;
}
}
transformMap["!" + propType + ":" + templatePropertyName + "!"] = propertyValue;
}
}
return 0;
}
QString PictureToLoad::transformUrl(const QString &urlTemplate) const
{
/* This function takes Url templates and substitutes actual card details
into the url. This is used for making Urls with follow a predictable format
for downloading images. If information is requested by the template that is
not populated for this specific card/set combination, an empty string is returned.*/
CardSetPtr set = getCurrentSet();
QMap<QString, QString> transformMap = QMap<QString, QString>();
QString setName = getSetName();
// name
QString cardName = card->getName();
transformMap["!name!"] = cardName;
transformMap["!name_lower!"] = card->getName().toLower();
transformMap["!corrected_name!"] = card->getCorrectedName();
transformMap["!corrected_name_lower!"] = card->getCorrectedName().toLower();
// card properties
if (parse(
urlTemplate, "prop", cardName, setName, [&](const QString &name) { return card->getProperty(name); },
transformMap)) {
return QString();
}
if (set) {
transformMap["!setcode!"] = set->getShortName();
transformMap["!setcode_lower!"] = set->getShortName().toLower();
transformMap["!setname!"] = set->getLongName();
transformMap["!setname_lower!"] = set->getLongName().toLower();
if (parse(
urlTemplate, "set", cardName, setName,
[&](const QString &name) { return card->getSetProperty(set->getShortName(), name); }, transformMap)) {
return QString();
}
}
// language setting
transformMap["!sflang!"] = QString(QCoreApplication::translate(
"PictureLoader", "en", "code for scryfall's language property, not available for all languages"));
QString transformedUrl = urlTemplate;
for (const QString &prop : transformMap.keys()) {
if (transformedUrl.contains(prop)) {
if (!transformMap[prop].isEmpty()) {
transformedUrl.replace(prop, QUrl::toPercentEncoding(transformMap[prop]));
} else {
/* This means the template is requesting information that is not
* populated in this card, so it should return an empty string,
* indicating an invalid Url.
*/
qDebug().nospace() << "PictureLoader: [card: " << cardName << " set: " << setName
<< "]: Requested information (" << prop << ") for Url template (" << urlTemplate
<< ") is not available";
return QString();
}
}
}
return transformedUrl;
}
void PictureLoaderWorker::startNextPicDownload()
{
if (cardsToDownload.isEmpty()) {
cardBeingDownloaded.clear();
downloadRunning = false;
return;
}
downloadRunning = true;
cardBeingDownloaded = cardsToDownload.takeFirst();
QString picUrl = cardBeingDownloaded.getCurrentUrl();
if (picUrl.isEmpty()) {
downloadRunning = false;
picDownloadFailed();
} else {
QUrl url(picUrl);
qDebug().nospace() << "PictureLoader: [card: " << cardBeingDownloaded.getCard()->getCorrectedName()
<< " set: " << cardBeingDownloaded.getSetName() << "]: Trying to fetch picture from url "
<< url.toDisplayString();
makeRequest(url);
}
}
void PictureLoaderWorker::picDownloadFailed()
{
/* Take advantage of short circuiting here to call the nextUrl until one
is not available. Only once nextUrl evaluates to false will this move
on to nextSet. If the Urls for a particular card are empty, this will
effectively go through the sets for that card. */
if (cardBeingDownloaded.nextUrl() || cardBeingDownloaded.nextSet()) {
mutex.lock();
loadQueue.prepend(cardBeingDownloaded);
mutex.unlock();
} else {
qDebug().nospace() << "PictureLoader: [card: " << cardBeingDownloaded.getCard()->getCorrectedName()
<< " set: " << cardBeingDownloaded.getSetName() << "]: Picture NOT found, "
<< (picDownload ? "download failed" : "downloads disabled")
<< ", no more url combinations to try: BAILING OUT";
imageLoaded(cardBeingDownloaded.getCard(), QImage());
cardBeingDownloaded.clear();
}
emit startLoadQueue();
}
bool PictureLoaderWorker::imageIsBlackListed(const QByteArray &picData)
{
QString md5sum = QCryptographicHash::hash(picData, QCryptographicHash::Md5).toHex();
return md5Blacklist.contains(md5sum);
}
QNetworkReply *PictureLoaderWorker::makeRequest(const QUrl &url)
{
// Check if the redirect is cached
QUrl cachedRedirect = getCachedRedirect(url);
if (!cachedRedirect.isEmpty()) {
qDebug().nospace() << "PictureLoader: [card: " << cardBeingDownloaded.getCard()->getCorrectedName()
<< " set: " << cardBeingDownloaded.getSetName() << "]: Using cached redirect for "
<< url.toDisplayString() << " to " << cachedRedirect.toDisplayString();
return makeRequest(cachedRedirect); // Use the cached redirect
}
QNetworkRequest req(url);
if (!picDownload) {
req.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysCache);
}
QNetworkReply *reply = networkManager->get(req);
connect(reply, &QNetworkReply::finished, this, [this, reply, url]() {
QVariant redirectTarget = reply->attribute(QNetworkRequest::RedirectionTargetAttribute);
if (redirectTarget.isValid()) {
QUrl redirectUrl = redirectTarget.toUrl();
if (redirectUrl.isRelative()) {
redirectUrl = url.resolved(redirectUrl);
}
cacheRedirect(url, redirectUrl);
qDebug().nospace() << "PictureLoader: [card: " << cardBeingDownloaded.getCard()->getCorrectedName()
<< " set: " << cardBeingDownloaded.getSetName() << "]: Caching redirect from "
<< url.toDisplayString() << " to " << redirectUrl.toDisplayString();
}
reply->deleteLater();
});
return reply;
}
void PictureLoaderWorker::cacheRedirect(const QUrl &originalUrl, const QUrl &redirectUrl)
{
redirectCache[originalUrl] = qMakePair(redirectUrl, QDateTime::currentDateTimeUtc());
saveRedirectCache();
}
QUrl PictureLoaderWorker::getCachedRedirect(const QUrl &originalUrl) const
{
if (redirectCache.contains(originalUrl)) {
return redirectCache[originalUrl].first;
}
return {};
}
void PictureLoaderWorker::loadRedirectCache()
{
QSettings settings(cacheFilePath, QSettings::IniFormat);
redirectCache.clear();
int size = settings.beginReadArray(REDIRECT_HEADER_NAME);
for (int i = 0; i < size; ++i) {
settings.setArrayIndex(i);
QUrl originalUrl = settings.value(REDIRECT_ORIGINAL_URL).toUrl();
QUrl redirectUrl = settings.value(REDIRECT_URL).toUrl();
QDateTime timestamp = settings.value(REDIRECT_TIMESTAMP).toDateTime();
if (originalUrl.isValid() && redirectUrl.isValid()) {
redirectCache[originalUrl] = qMakePair(redirectUrl, timestamp);
}
}
settings.endArray();
}
void PictureLoaderWorker::saveRedirectCache() const
{
QSettings settings(cacheFilePath, QSettings::IniFormat);
settings.beginWriteArray(REDIRECT_HEADER_NAME, static_cast<int>(redirectCache.size()));
int index = 0;
for (auto it = redirectCache.cbegin(); it != redirectCache.cend(); ++it) {
settings.setArrayIndex(index++);
settings.setValue(REDIRECT_ORIGINAL_URL, it.key());
settings.setValue(REDIRECT_URL, it.value().first);
settings.setValue(REDIRECT_TIMESTAMP, it.value().second);
}
settings.endArray();
}
void PictureLoaderWorker::cleanStaleEntries()
{
QDateTime now = QDateTime::currentDateTimeUtc();
auto it = redirectCache.begin();
while (it != redirectCache.end()) {
if (it.value().second.addDays(SettingsCache::instance().getRedirectCacheTtl()) < now) {
it = redirectCache.erase(it); // Remove stale entry
} else {
++it;
}
}
}
void PictureLoaderWorker::picDownloadFinished(QNetworkReply *reply)
{
bool isFromCache = reply->attribute(QNetworkRequest::SourceIsFromCacheAttribute).toBool();
if (reply->error()) {
if (isFromCache) {
qDebug().nospace() << "PictureLoader: [card: " << cardBeingDownloaded.getCard()->getName()
<< " set: " << cardBeingDownloaded.getSetName()
<< "]: Removing corrupted cache file for url " << reply->url().toDisplayString()
<< " and retrying (" << reply->errorString() << ")";
networkManager->cache()->remove(reply->url());
makeRequest(reply->url());
} else {
qDebug().nospace() << "PictureLoader: [card: " << cardBeingDownloaded.getCard()->getName()
<< " set: " << cardBeingDownloaded.getSetName()
<< "]: " << (picDownload ? "Download" : "Cache search") << " failed for url "
<< reply->url().toDisplayString() << " (" << reply->errorString() << ")";
picDownloadFailed();
startNextPicDownload();
}
reply->deleteLater();
return;
}
// List of status codes from https://doc.qt.io/qt-6/qnetworkreply.html#redirected
int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (statusCode == 301 || statusCode == 302 || statusCode == 303 || statusCode == 305 || statusCode == 307 ||
statusCode == 308) {
QUrl redirectUrl = reply->header(QNetworkRequest::LocationHeader).toUrl();
qDebug().nospace() << "PictureLoader: [card: " << cardBeingDownloaded.getCard()->getName()
<< " set: " << cardBeingDownloaded.getSetName() << "]: following "
<< (isFromCache ? "cached redirect" : "redirect") << " to " << redirectUrl.toDisplayString();
makeRequest(redirectUrl);
reply->deleteLater();
return;
}
// peek is used to keep the data in the buffer for use by QImageReader
const QByteArray &picData = reply->peek(reply->size());
if (imageIsBlackListed(picData)) {
qDebug().nospace() << "PictureLoader: [card: " << cardBeingDownloaded.getCard()->getName()
<< " set: " << cardBeingDownloaded.getSetName()
<< "]: Picture found, but blacklisted, will consider it as not found";
picDownloadFailed();
reply->deleteLater();
startNextPicDownload();
return;
}
QImage testImage;
QImageReader imgReader;
imgReader.setDecideFormatFromContent(true);
imgReader.setDevice(reply);
bool logSuccessMessage = false;
static const int riffHeaderSize = 12; // RIFF_HEADER_SIZE from webp/format_constants.h
auto replyHeader = reply->peek(riffHeaderSize);
if (replyHeader.startsWith("RIFF") && replyHeader.endsWith("WEBP")) {
auto imgBuf = QBuffer(this);
imgBuf.setData(reply->readAll());
auto movie = QMovie(&imgBuf);
movie.start();
movie.stop();
imageLoaded(cardBeingDownloaded.getCard(), movie.currentImage());
logSuccessMessage = true;
} else if (imgReader.read(&testImage)) {
imageLoaded(cardBeingDownloaded.getCard(), testImage);
logSuccessMessage = true;
} else {
qDebug().nospace() << "PictureLoader: [card: " << cardBeingDownloaded.getCard()->getName()
<< " set: " << cardBeingDownloaded.getSetName() << "]: Possible "
<< (isFromCache ? "cached" : "downloaded") << " picture at "
<< reply->url().toDisplayString() << " could not be loaded: " << reply->errorString();
picDownloadFailed();
}
if (logSuccessMessage) {
qDebug().nospace() << "PictureLoader: [card: " << cardBeingDownloaded.getCard()->getName()
<< " set: " << cardBeingDownloaded.getSetName() << "]: Image successfully "
<< (isFromCache ? "loaded from cached" : "downloaded from") << " url "
<< reply->url().toDisplayString();
}
reply->deleteLater();
startNextPicDownload();
}
void PictureLoaderWorker::enqueueImageLoad(CardInfoPtr card)
{
QMutexLocker locker(&mutex);
// avoid queueing the same card more than once
if (!card || card == cardBeingLoaded.getCard() || card == cardBeingDownloaded.getCard()) {
return;
}
for (const PictureToLoad &pic : loadQueue) {
if (pic.getCard() == card)
return;
}
for (const PictureToLoad &pic : cardsToDownload) {
if (pic.getCard() == card)
return;
}
loadQueue.append(PictureToLoad(card));
emit startLoadQueue();
}
void PictureLoaderWorker::picDownloadChanged()
{
QMutexLocker locker(&mutex);
picDownload = SettingsCache::instance().getPicDownload();
}
void PictureLoaderWorker::picsPathChanged()
{
QMutexLocker locker(&mutex);
picsPath = SettingsCache::instance().getPicsPath();
customPicsPath = SettingsCache::instance().getCustomPicsPath();
}
void PictureLoaderWorker::clearNetworkCache()
{
networkManager->cache()->clear();
}
PictureLoader::PictureLoader() : QObject(nullptr)
{
worker = new PictureLoaderWorker;
connect(&SettingsCache::instance(), SIGNAL(picsPathChanged()), this, SLOT(picsPathChanged()));
connect(&SettingsCache::instance(), SIGNAL(picDownloadChanged()), this, SLOT(picDownloadChanged()));
connect(worker, SIGNAL(imageLoaded(CardInfoPtr, const QImage &)), this,
SLOT(imageLoaded(CardInfoPtr, const QImage &)));
}
PictureLoader::~PictureLoader()
{
worker->deleteLater();
}
void PictureLoader::getCardBackPixmap(QPixmap &pixmap, QSize size)
{
QString backCacheKey = "_trice_card_back_" + QString::number(size.width()) + QString::number(size.height());
if (!QPixmapCache::find(backCacheKey, &pixmap)) {
qDebug() << "PictureLoader: cache fail for" << backCacheKey;
pixmap = QPixmap("theme:cardback").scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
QPixmapCache::insert(backCacheKey, pixmap);
}
}
void PictureLoader::getCardBackLoadingInProgressPixmap(QPixmap &pixmap, QSize size)
{
QString backCacheKey = "_trice_card_back_" + QString::number(size.width()) + QString::number(size.height());
if (!QPixmapCache::find(backCacheKey, &pixmap)) {
qDebug() << "PictureLoader: cache fail for" << backCacheKey;
pixmap = QPixmap("theme:cardback").scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
QPixmapCache::insert(backCacheKey, pixmap);
}
}
void PictureLoader::getCardBackLoadingFailedPixmap(QPixmap &pixmap, QSize size)
{
QString backCacheKey = "_trice_card_back_" + QString::number(size.width()) + QString::number(size.height());
if (!QPixmapCache::find(backCacheKey, &pixmap)) {
qDebug() << "PictureLoader: cache fail for" << backCacheKey;
pixmap = QPixmap("theme:cardback").scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
QPixmapCache::insert(backCacheKey, pixmap);
}
}
void PictureLoader::getPixmap(QPixmap &pixmap, CardInfoPtr card, QSize size)
{
if (card == nullptr) {
return;
}
// search for an exact size copy of the picture in cache
QString key = card->getPixmapCacheKey();
QString sizeKey = key + QLatin1Char('_') + QString::number(size.width()) + QString::number(size.height());
if (QPixmapCache::find(sizeKey, &pixmap))
return;
// load the image and create a copy of the correct size
QPixmap bigPixmap;
if (QPixmapCache::find(key, &bigPixmap)) {
QScreen *screen = qApp->primaryScreen();
qreal dpr = screen->devicePixelRatio();
pixmap = bigPixmap.scaled(size * dpr, Qt::KeepAspectRatio, Qt::SmoothTransformation);
pixmap.setDevicePixelRatio(dpr);
QPixmapCache::insert(sizeKey, pixmap);
return;
}
// add the card to the load queue
getInstance().worker->enqueueImageLoad(card);
}
void PictureLoader::imageLoaded(CardInfoPtr card, const QImage &image)
{
if (image.isNull()) {
QPixmapCache::insert(card->getPixmapCacheKey(), QPixmap());
} else {
if (card->getUpsideDownArt()) {
QImage mirrorImage = image.mirrored(true, true);
QPixmapCache::insert(card->getPixmapCacheKey(), QPixmap::fromImage(mirrorImage));
} else {
QPixmapCache::insert(card->getPixmapCacheKey(), QPixmap::fromImage(image));
}
}
card->emitPixmapUpdated();
}
void PictureLoader::clearPixmapCache(CardInfoPtr card)
{
if (card) {
QPixmapCache::remove(card->getPixmapCacheKey());
}
}
void PictureLoader::clearPixmapCache()
{
QPixmapCache::clear();
}
void PictureLoader::clearNetworkCache()
{
getInstance().worker->clearNetworkCache();
}
void PictureLoader::cacheCardPixmaps(QList<CardInfoPtr> cards)
{
QPixmap tmp;
int max = qMin(cards.size(), CACHED_CARD_PER_DECK_MAX);
for (int i = 0; i < max; ++i) {
const CardInfoPtr &card = cards.at(i);
if (!card) {
continue;
}
QString key = card->getPixmapCacheKey();
if (QPixmapCache::find(key, &tmp)) {
continue;
}
getInstance().worker->enqueueImageLoad(card);
}
}
void PictureLoader::picDownloadChanged()
{
QPixmapCache::clear();
}
void PictureLoader::picsPathChanged()
{
QPixmapCache::clear();
}

View File

@@ -1,161 +0,0 @@
#ifndef PICTURELOADER_H
#define PICTURELOADER_H
#include "../../game/cards/card_database.h"
#include <QList>
#include <QMap>
#include <QMutex>
#include <QNetworkRequest>
class QNetworkAccessManager;
class QNetworkReply;
class QThread;
#define REDIRECT_HEADER_NAME "redirects"
#define REDIRECT_ORIGINAL_URL "original"
#define REDIRECT_URL "redirect"
#define REDIRECT_TIMESTAMP "timestamp"
#define REDIRECT_CACHE_FILENAME "cache.ini"
class PictureToLoad
{
private:
class SetDownloadPriorityComparator
{
public:
/*
* Returns true if a has higher download priority than b
* Enabled sets have priority over disabled sets
* Both groups follows the user-defined order
*/
inline bool operator()(const CardSetPtr &a, const CardSetPtr &b) const
{
if (a->getEnabled()) {
return !b->getEnabled() || a->getSortKey() < b->getSortKey();
} else {
return !b->getEnabled() && a->getSortKey() < b->getSortKey();
}
}
};
CardInfoPtr card;
QList<CardSetPtr> sortedSets;
QList<QString> urlTemplates;
QList<QString> currentSetUrls;
QString currentUrl;
CardSetPtr currentSet;
public:
explicit PictureToLoad(CardInfoPtr _card = CardInfoPtr());
CardInfoPtr getCard() const
{
return card;
}
void clear()
{
card.clear();
}
QString getCurrentUrl() const
{
return currentUrl;
}
CardSetPtr getCurrentSet() const
{
return currentSet;
}
QString getSetName() const;
QString transformUrl(const QString &urlTemplate) const;
bool nextSet();
bool nextUrl();
void populateSetUrls();
};
class PictureLoaderWorker : public QObject
{
Q_OBJECT
public:
explicit PictureLoaderWorker();
~PictureLoaderWorker() override;
void enqueueImageLoad(CardInfoPtr card);
void clearNetworkCache();
private:
static QStringList md5Blacklist;
QThread *pictureLoaderThread;
QString picsPath, customPicsPath;
QList<PictureToLoad> loadQueue;
QMutex mutex;
QNetworkAccessManager *networkManager;
QHash<QUrl, QPair<QUrl, QDateTime>> redirectCache; // Stores redirect and timestamp
QString cacheFilePath; // Path to persistent storage
static constexpr int CacheTTLInDays = 30; // TODO: Make user configurable
QList<PictureToLoad> cardsToDownload;
PictureToLoad cardBeingLoaded;
PictureToLoad cardBeingDownloaded;
bool picDownload, downloadRunning, loadQueueRunning;
void startNextPicDownload();
bool cardImageExistsOnDisk(QString &setName, QString &correctedCardName);
bool imageIsBlackListed(const QByteArray &);
QNetworkReply *makeRequest(const QUrl &url);
void cacheRedirect(const QUrl &originalUrl, const QUrl &redirectUrl);
QUrl getCachedRedirect(const QUrl &originalUrl) const;
void loadRedirectCache();
void saveRedirectCache() const;
void cleanStaleEntries();
private slots:
void picDownloadFinished(QNetworkReply *reply);
void picDownloadFailed();
void picDownloadChanged();
void picsPathChanged();
public slots:
void processLoadQueue();
signals:
void startLoadQueue();
void imageLoaded(CardInfoPtr card, const QImage &image);
};
class PictureLoader : public QObject
{
Q_OBJECT
public:
static PictureLoader &getInstance()
{
static PictureLoader instance;
return instance;
}
private:
explicit PictureLoader();
~PictureLoader() override;
// Singleton - Don't implement copy constructor and assign operator
PictureLoader(PictureLoader const &);
void operator=(PictureLoader const &);
PictureLoaderWorker *worker;
public:
static void getPixmap(QPixmap &pixmap, CardInfoPtr card, QSize size);
static void getCardBackPixmap(QPixmap &pixmap, QSize size);
static void getCardBackLoadingInProgressPixmap(QPixmap &pixmap, QSize size);
static void getCardBackLoadingFailedPixmap(QPixmap &pixmap, QSize size);
static void clearPixmapCache(CardInfoPtr card);
static void clearPixmapCache();
static void cacheCardPixmaps(QList<CardInfoPtr> cards);
public slots:
static void clearNetworkCache();
private slots:
void picDownloadChanged();
void picsPathChanged();
public slots:
void imageLoaded(CardInfoPtr card, const QImage &image);
};
#endif

View File

@@ -0,0 +1,156 @@
#include "picture_loader.h"
#include "../../../settings/cache_settings.h"
#include <QApplication>
#include <QBuffer>
#include <QDebug>
#include <QDirIterator>
#include <QFileInfo>
#include <QMovie>
#include <QNetworkDiskCache>
#include <QNetworkRequest>
#include <QPainter>
#include <QPixmapCache>
#include <QScreen>
#include <QThread>
#include <algorithm>
#include <utility>
// never cache more than 300 cards at once for a single deck
#define CACHED_CARD_PER_DECK_MAX 300
PictureLoader::PictureLoader() : QObject(nullptr)
{
worker = new PictureLoaderWorker;
connect(&SettingsCache::instance(), SIGNAL(picsPathChanged()), this, SLOT(picsPathChanged()));
connect(&SettingsCache::instance(), SIGNAL(picDownloadChanged()), this, SLOT(picDownloadChanged()));
connect(worker, SIGNAL(imageLoaded(CardInfoPtr, const QImage &)), this,
SLOT(imageLoaded(CardInfoPtr, const QImage &)));
}
PictureLoader::~PictureLoader()
{
worker->deleteLater();
}
void PictureLoader::getCardBackPixmap(QPixmap &pixmap, QSize size)
{
QString backCacheKey = "_trice_card_back_" + QString::number(size.width()) + QString::number(size.height());
if (!QPixmapCache::find(backCacheKey, &pixmap)) {
qCDebug(PictureLoaderLog) << "PictureLoader: cache fail for" << backCacheKey;
pixmap = QPixmap("theme:cardback").scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
QPixmapCache::insert(backCacheKey, pixmap);
}
}
void PictureLoader::getCardBackLoadingInProgressPixmap(QPixmap &pixmap, QSize size)
{
QString backCacheKey = "_trice_card_back_" + QString::number(size.width()) + QString::number(size.height());
if (!QPixmapCache::find(backCacheKey, &pixmap)) {
qCDebug(PictureLoaderCardBackCacheFailLog) << "PictureLoader: cache fail for" << backCacheKey;
pixmap = QPixmap("theme:cardback").scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
QPixmapCache::insert(backCacheKey, pixmap);
}
}
void PictureLoader::getCardBackLoadingFailedPixmap(QPixmap &pixmap, QSize size)
{
QString backCacheKey = "_trice_card_back_" + QString::number(size.width()) + QString::number(size.height());
if (!QPixmapCache::find(backCacheKey, &pixmap)) {
qCDebug(PictureLoaderCardBackCacheFailLog) << "PictureLoader: cache fail for" << backCacheKey;
pixmap = QPixmap("theme:cardback").scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
QPixmapCache::insert(backCacheKey, pixmap);
}
}
void PictureLoader::getPixmap(QPixmap &pixmap, CardInfoPtr card, QSize size)
{
if (card == nullptr) {
return;
}
// search for an exact size copy of the picture in cache
QString key = card->getPixmapCacheKey();
QString sizeKey = key + QLatin1Char('_') + QString::number(size.width()) + QString::number(size.height());
if (QPixmapCache::find(sizeKey, &pixmap))
return;
// load the image and create a copy of the correct size
QPixmap bigPixmap;
if (QPixmapCache::find(key, &bigPixmap)) {
QScreen *screen = qApp->primaryScreen();
qreal dpr = screen->devicePixelRatio();
pixmap = bigPixmap.scaled(size * dpr, Qt::KeepAspectRatio, Qt::SmoothTransformation);
pixmap.setDevicePixelRatio(dpr);
QPixmapCache::insert(sizeKey, pixmap);
return;
}
// add the card to the load queue
qCDebug(PictureLoaderLog) << "Enqueuing " << card->getName() << " for " << card->getPixmapCacheKey();
getInstance().worker->enqueueImageLoad(card);
}
void PictureLoader::imageLoaded(CardInfoPtr card, const QImage &image)
{
if (image.isNull()) {
QPixmapCache::insert(card->getPixmapCacheKey(), QPixmap());
} else {
if (card->getUpsideDownArt()) {
QImage mirrorImage = image.mirrored(true, true);
QPixmapCache::insert(card->getPixmapCacheKey(), QPixmap::fromImage(mirrorImage));
} else {
QPixmapCache::insert(card->getPixmapCacheKey(), QPixmap::fromImage(image));
}
}
card->emitPixmapUpdated();
}
void PictureLoader::clearPixmapCache(CardInfoPtr card)
{
if (card) {
QPixmapCache::remove(card->getPixmapCacheKey());
}
}
void PictureLoader::clearPixmapCache()
{
QPixmapCache::clear();
}
void PictureLoader::clearNetworkCache()
{
getInstance().worker->clearNetworkCache();
}
void PictureLoader::cacheCardPixmaps(QList<CardInfoPtr> cards)
{
QPixmap tmp;
int max = qMin(cards.size(), CACHED_CARD_PER_DECK_MAX);
for (int i = 0; i < max; ++i) {
const CardInfoPtr &card = cards.at(i);
if (!card) {
continue;
}
QString key = card->getPixmapCacheKey();
if (QPixmapCache::find(key, &tmp)) {
continue;
}
getInstance().worker->enqueueImageLoad(card);
}
}
void PictureLoader::picDownloadChanged()
{
QPixmapCache::clear();
}
void PictureLoader::picsPathChanged()
{
QPixmapCache::clear();
}

View File

@@ -0,0 +1,50 @@
#ifndef PICTURELOADER_H
#define PICTURELOADER_H
#include "../../../game/cards/card_database.h"
#include "picture_loader_worker.h"
#include <QLoggingCategory>
inline Q_LOGGING_CATEGORY(PictureLoaderLog, "picture_loader");
inline Q_LOGGING_CATEGORY(PictureLoaderCardBackCacheFailLog, "picture_loader.card_back_cache_fail");
class PictureLoader : public QObject
{
Q_OBJECT
public:
static PictureLoader &getInstance()
{
static PictureLoader instance;
return instance;
}
private:
explicit PictureLoader();
~PictureLoader() override;
// Singleton - Don't implement copy constructor and assign operator
PictureLoader(PictureLoader const &);
void operator=(PictureLoader const &);
PictureLoaderWorker *worker;
public:
static void getPixmap(QPixmap &pixmap, CardInfoPtr card, QSize size);
static void getCardBackPixmap(QPixmap &pixmap, QSize size);
static void getCardBackLoadingInProgressPixmap(QPixmap &pixmap, QSize size);
static void getCardBackLoadingFailedPixmap(QPixmap &pixmap, QSize size);
static void clearPixmapCache(CardInfoPtr card);
static void clearPixmapCache();
static void cacheCardPixmaps(QList<CardInfoPtr> cards);
public slots:
static void clearNetworkCache();
private slots:
void picDownloadChanged();
void picsPathChanged();
public slots:
void imageLoaded(CardInfoPtr card, const QImage &image);
};
#endif

View File

@@ -0,0 +1,457 @@
#include "picture_loader_worker.h"
#include "../../../game/cards/card_database_manager.h"
#include "../../../settings/cache_settings.h"
#include <QBuffer>
#include <QDirIterator>
#include <QMovie>
#include <QNetworkDiskCache>
#include <QNetworkReply>
#include <QThread>
// Card back returned by gatherer when card is not found
QStringList PictureLoaderWorker::md5Blacklist = QStringList() << "db0c48db407a907c16ade38de048a441";
PictureLoaderWorker::PictureLoaderWorker()
: QObject(nullptr), picsPath(SettingsCache::instance().getPicsPath()),
customPicsPath(SettingsCache::instance().getCustomPicsPath()),
picDownload(SettingsCache::instance().getPicDownload()), downloadRunning(false), loadQueueRunning(false)
{
connect(this, SIGNAL(startLoadQueue()), this, SLOT(processLoadQueue()), Qt::QueuedConnection);
connect(&SettingsCache::instance(), SIGNAL(picsPathChanged()), this, SLOT(picsPathChanged()));
connect(&SettingsCache::instance(), SIGNAL(picDownloadChanged()), this, SLOT(picDownloadChanged()));
networkManager = new QNetworkAccessManager(this);
// We need a timeout to ensure requests don't hang indefinitely in case of
// cache corruption, see related Qt bug: https://bugreports.qt.io/browse/QTBUG-111397
// Use Qt's default timeout (30s, as of 2023-02-22)
#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0))
networkManager->setTransferTimeout();
#endif
auto cache = new QNetworkDiskCache(this);
cache->setCacheDirectory(SettingsCache::instance().getNetworkCachePath());
cache->setMaximumCacheSize(1024L * 1024L *
static_cast<qint64>(SettingsCache::instance().getNetworkCacheSizeInMB()));
// Note: the settings is in MB, but QNetworkDiskCache uses bytes
connect(&SettingsCache::instance(), &SettingsCache::networkCacheSizeChanged, cache,
[cache](int newSizeInMB) { cache->setMaximumCacheSize(1024L * 1024L * static_cast<qint64>(newSizeInMB)); });
networkManager->setCache(cache);
// Use a ManualRedirectPolicy since we keep track of redirects in picDownloadFinished
// We can't use NoLessSafeRedirectPolicy because it is not applied with AlwaysCache
networkManager->setRedirectPolicy(QNetworkRequest::ManualRedirectPolicy);
connect(networkManager, SIGNAL(finished(QNetworkReply *)), this, SLOT(picDownloadFinished(QNetworkReply *)));
cacheFilePath = SettingsCache::instance().getRedirectCachePath() + REDIRECT_CACHE_FILENAME;
loadRedirectCache();
cleanStaleEntries();
connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this,
&PictureLoaderWorker::saveRedirectCache);
pictureLoaderThread = new QThread;
pictureLoaderThread->start(QThread::LowPriority);
moveToThread(pictureLoaderThread);
}
PictureLoaderWorker::~PictureLoaderWorker()
{
pictureLoaderThread->deleteLater();
}
void PictureLoaderWorker::processLoadQueue()
{
if (loadQueueRunning) {
return;
}
loadQueueRunning = true;
while (true) {
mutex.lock();
if (loadQueue.isEmpty()) {
mutex.unlock();
loadQueueRunning = false;
return;
}
cardBeingLoaded = loadQueue.takeFirst();
mutex.unlock();
QString setName = cardBeingLoaded.getSetName();
QString cardName = cardBeingLoaded.getCard()->getName();
QString correctedCardName = cardBeingLoaded.getCard()->getCorrectedName();
qCDebug(PictureLoaderWorkerLog).nospace()
<< "[card: " << cardName << " set: " << setName << "]: Trying to load picture";
if (CardDatabaseManager::getInstance()->isProviderIdForPreferredPrinting(
cardName, cardBeingLoaded.getCard()->getPixmapCacheKey())) {
if (cardImageExistsOnDisk(setName, correctedCardName)) {
continue;
}
}
qCDebug(PictureLoaderWorkerLog).nospace()
<< "[card: " << cardName << " set: " << setName << "]: No custom picture, trying to download";
cardsToDownload.append(cardBeingLoaded);
cardBeingLoaded.clear();
if (!downloadRunning) {
startNextPicDownload();
}
}
}
bool PictureLoaderWorker::cardImageExistsOnDisk(QString &setName, QString &correctedCardname)
{
QImage image;
QImageReader imgReader;
imgReader.setDecideFormatFromContent(true);
QList<QString> picsPaths = QList<QString>();
QDirIterator it(customPicsPath, QDirIterator::Subdirectories | QDirIterator::FollowSymlinks);
// Recursively check all subdirectories of the CUSTOM folder
while (it.hasNext()) {
QString thisPath(it.next());
QFileInfo thisFileInfo(thisPath);
if (thisFileInfo.isFile() &&
(thisFileInfo.fileName() == correctedCardname || thisFileInfo.completeBaseName() == correctedCardname ||
thisFileInfo.baseName() == correctedCardname)) {
picsPaths << thisPath; // Card found in the CUSTOM directory, somewhere
}
}
if (!setName.isEmpty()) {
picsPaths << picsPath + "/" + setName + "/" + correctedCardname
// We no longer store downloaded images there, but don't just ignore
// stuff that old versions have put there.
<< picsPath + "/downloadedPics/" + setName + "/" + correctedCardname;
}
// Iterates through the list of paths, searching for images with the desired
// name with any QImageReader-supported
// extension
for (const auto &_picsPath : picsPaths) {
imgReader.setFileName(_picsPath);
if (imgReader.read(&image)) {
qCDebug(PictureLoaderWorkerLog).nospace()
<< "[card: " << correctedCardname << " set: " << setName << "]: Picture found on disk.";
imageLoaded(cardBeingLoaded.getCard(), image);
return true;
}
imgReader.setFileName(_picsPath + ".full");
if (imgReader.read(&image)) {
qCDebug(PictureLoaderWorkerLog).nospace()
<< "[card: " << correctedCardname << " set: " << setName << "]: Picture.full found on disk.";
imageLoaded(cardBeingLoaded.getCard(), image);
return true;
}
imgReader.setFileName(_picsPath + ".xlhq");
if (imgReader.read(&image)) {
qCDebug(PictureLoaderWorkerLog).nospace()
<< "[card: " << correctedCardname << " set: " << setName << "]: Picture.xlhq found on disk.";
imageLoaded(cardBeingLoaded.getCard(), image);
return true;
}
}
return false;
}
void PictureLoaderWorker::startNextPicDownload()
{
if (cardsToDownload.isEmpty()) {
cardBeingDownloaded.clear();
downloadRunning = false;
return;
}
downloadRunning = true;
cardBeingDownloaded = cardsToDownload.takeFirst();
QString picUrl = cardBeingDownloaded.getCurrentUrl();
if (picUrl.isEmpty()) {
downloadRunning = false;
picDownloadFailed();
} else {
QUrl url(picUrl);
qCDebug(PictureLoaderWorkerLog).nospace() << "[card: " << cardBeingDownloaded.getCard()->getCorrectedName()
<< " set: " << cardBeingDownloaded.getSetName()
<< "]: Trying to fetch picture from url " << url.toDisplayString();
makeRequest(url);
}
}
void PictureLoaderWorker::picDownloadFailed()
{
/* Take advantage of short circuiting here to call the nextUrl until one
is not available. Only once nextUrl evaluates to false will this move
on to nextSet. If the Urls for a particular card are empty, this will
effectively go through the sets for that card. */
if (cardBeingDownloaded.nextUrl() || cardBeingDownloaded.nextSet()) {
mutex.lock();
loadQueue.prepend(cardBeingDownloaded);
mutex.unlock();
} else {
qCDebug(PictureLoaderWorkerLog).nospace()
<< "[card: " << cardBeingDownloaded.getCard()->getCorrectedName()
<< " set: " << cardBeingDownloaded.getSetName() << "]: Picture NOT found, "
<< (picDownload ? "download failed" : "downloads disabled")
<< ", no more url combinations to try: BAILING OUT";
imageLoaded(cardBeingDownloaded.getCard(), QImage());
cardBeingDownloaded.clear();
}
emit startLoadQueue();
}
bool PictureLoaderWorker::imageIsBlackListed(const QByteArray &picData)
{
QString md5sum = QCryptographicHash::hash(picData, QCryptographicHash::Md5).toHex();
return md5Blacklist.contains(md5sum);
}
QNetworkReply *PictureLoaderWorker::makeRequest(const QUrl &url)
{
// Check if the redirect is cached
QUrl cachedRedirect = getCachedRedirect(url);
if (!cachedRedirect.isEmpty()) {
qCDebug(PictureLoaderWorkerLog).nospace()
<< "[card: " << cardBeingDownloaded.getCard()->getCorrectedName()
<< " set: " << cardBeingDownloaded.getSetName() << "]: Using cached redirect for " << url.toDisplayString()
<< " to " << cachedRedirect.toDisplayString();
return makeRequest(cachedRedirect); // Use the cached redirect
}
QNetworkRequest req(url);
if (!picDownload) {
req.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysCache);
}
QNetworkReply *reply = networkManager->get(req);
connect(reply, &QNetworkReply::finished, this, [this, reply, url]() {
QVariant redirectTarget = reply->attribute(QNetworkRequest::RedirectionTargetAttribute);
if (redirectTarget.isValid()) {
QUrl redirectUrl = redirectTarget.toUrl();
if (redirectUrl.isRelative()) {
redirectUrl = url.resolved(redirectUrl);
}
cacheRedirect(url, redirectUrl);
qCDebug(PictureLoaderWorkerLog).nospace()
<< "[card: " << cardBeingDownloaded.getCard()->getCorrectedName()
<< " set: " << cardBeingDownloaded.getSetName() << "]: Caching redirect from " << url.toDisplayString()
<< " to " << redirectUrl.toDisplayString();
}
reply->deleteLater();
});
return reply;
}
void PictureLoaderWorker::cacheRedirect(const QUrl &originalUrl, const QUrl &redirectUrl)
{
redirectCache[originalUrl] = qMakePair(redirectUrl, QDateTime::currentDateTimeUtc());
saveRedirectCache();
}
QUrl PictureLoaderWorker::getCachedRedirect(const QUrl &originalUrl) const
{
if (redirectCache.contains(originalUrl)) {
return redirectCache[originalUrl].first;
}
return {};
}
void PictureLoaderWorker::loadRedirectCache()
{
QSettings settings(cacheFilePath, QSettings::IniFormat);
redirectCache.clear();
int size = settings.beginReadArray(REDIRECT_HEADER_NAME);
for (int i = 0; i < size; ++i) {
settings.setArrayIndex(i);
QUrl originalUrl = settings.value(REDIRECT_ORIGINAL_URL).toUrl();
QUrl redirectUrl = settings.value(REDIRECT_URL).toUrl();
QDateTime timestamp = settings.value(REDIRECT_TIMESTAMP).toDateTime();
if (originalUrl.isValid() && redirectUrl.isValid()) {
redirectCache[originalUrl] = qMakePair(redirectUrl, timestamp);
}
}
settings.endArray();
}
void PictureLoaderWorker::saveRedirectCache() const
{
QSettings settings(cacheFilePath, QSettings::IniFormat);
settings.beginWriteArray(REDIRECT_HEADER_NAME, static_cast<int>(redirectCache.size()));
int index = 0;
for (auto it = redirectCache.cbegin(); it != redirectCache.cend(); ++it) {
settings.setArrayIndex(index++);
settings.setValue(REDIRECT_ORIGINAL_URL, it.key());
settings.setValue(REDIRECT_URL, it.value().first);
settings.setValue(REDIRECT_TIMESTAMP, it.value().second);
}
settings.endArray();
}
void PictureLoaderWorker::cleanStaleEntries()
{
QDateTime now = QDateTime::currentDateTimeUtc();
auto it = redirectCache.begin();
while (it != redirectCache.end()) {
if (it.value().second.addDays(SettingsCache::instance().getRedirectCacheTtl()) < now) {
it = redirectCache.erase(it); // Remove stale entry
} else {
++it;
}
}
}
void PictureLoaderWorker::picDownloadFinished(QNetworkReply *reply)
{
bool isFromCache = reply->attribute(QNetworkRequest::SourceIsFromCacheAttribute).toBool();
if (reply->error()) {
if (isFromCache) {
qCDebug(PictureLoaderWorkerLog).nospace()
<< "[card: " << cardBeingDownloaded.getCard()->getName() << " set: " << cardBeingDownloaded.getSetName()
<< "]: Removing corrupted cache file for url " << reply->url().toDisplayString() << " and retrying ("
<< reply->errorString() << ")";
networkManager->cache()->remove(reply->url());
makeRequest(reply->url());
} else {
qCDebug(PictureLoaderWorkerLog).nospace()
<< "[card: " << cardBeingDownloaded.getCard()->getName() << " set: " << cardBeingDownloaded.getSetName()
<< "]: " << (picDownload ? "Download" : "Cache search") << " failed for url "
<< reply->url().toDisplayString() << " (" << reply->errorString() << ")";
picDownloadFailed();
startNextPicDownload();
}
reply->deleteLater();
return;
}
// List of status codes from https://doc.qt.io/qt-6/qnetworkreply.html#redirected
int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (statusCode == 301 || statusCode == 302 || statusCode == 303 || statusCode == 305 || statusCode == 307 ||
statusCode == 308) {
QUrl redirectUrl = reply->header(QNetworkRequest::LocationHeader).toUrl();
qCDebug(PictureLoaderWorkerLog).nospace()
<< "[card: " << cardBeingDownloaded.getCard()->getName() << " set: " << cardBeingDownloaded.getSetName()
<< "]: following " << (isFromCache ? "cached redirect" : "redirect") << " to "
<< redirectUrl.toDisplayString();
makeRequest(redirectUrl);
reply->deleteLater();
return;
}
// peek is used to keep the data in the buffer for use by QImageReader
const QByteArray &picData = reply->peek(reply->size());
if (imageIsBlackListed(picData)) {
qCDebug(PictureLoaderWorkerLog).nospace()
<< "[card: " << cardBeingDownloaded.getCard()->getName() << " set: " << cardBeingDownloaded.getSetName()
<< "]: Picture found, but blacklisted, will consider it as not found";
picDownloadFailed();
reply->deleteLater();
startNextPicDownload();
return;
}
QImage testImage;
QImageReader imgReader;
imgReader.setDecideFormatFromContent(true);
imgReader.setDevice(reply);
bool logSuccessMessage = false;
static const int riffHeaderSize = 12; // RIFF_HEADER_SIZE from webp/format_constants.h
auto replyHeader = reply->peek(riffHeaderSize);
if (replyHeader.startsWith("RIFF") && replyHeader.endsWith("WEBP")) {
auto imgBuf = QBuffer(this);
imgBuf.setData(reply->readAll());
auto movie = QMovie(&imgBuf);
movie.start();
movie.stop();
imageLoaded(cardBeingDownloaded.getCard(), movie.currentImage());
logSuccessMessage = true;
} else if (imgReader.read(&testImage)) {
imageLoaded(cardBeingDownloaded.getCard(), testImage);
logSuccessMessage = true;
} else {
qCDebug(PictureLoaderWorkerLog).nospace()
<< "[card: " << cardBeingDownloaded.getCard()->getName() << " set: " << cardBeingDownloaded.getSetName()
<< "]: Possible " << (isFromCache ? "cached" : "downloaded") << " picture at "
<< reply->url().toDisplayString() << " could not be loaded: " << reply->errorString();
picDownloadFailed();
}
if (logSuccessMessage) {
qCDebug(PictureLoaderWorkerLog).nospace()
<< "[card: " << cardBeingDownloaded.getCard()->getName() << " set: " << cardBeingDownloaded.getSetName()
<< "]: Image successfully " << (isFromCache ? "loaded from cached" : "downloaded from") << " url "
<< reply->url().toDisplayString();
}
reply->deleteLater();
startNextPicDownload();
}
void PictureLoaderWorker::enqueueImageLoad(CardInfoPtr card)
{
QMutexLocker locker(&mutex);
// avoid queueing the same card more than once
if (!card || card == cardBeingLoaded.getCard() || card == cardBeingDownloaded.getCard()) {
return;
}
for (const PictureToLoad &pic : loadQueue) {
if (pic.getCard() == card)
return;
}
for (const PictureToLoad &pic : cardsToDownload) {
if (pic.getCard() == card)
return;
}
loadQueue.append(PictureToLoad(card));
emit startLoadQueue();
}
void PictureLoaderWorker::picDownloadChanged()
{
QMutexLocker locker(&mutex);
picDownload = SettingsCache::instance().getPicDownload();
}
void PictureLoaderWorker::picsPathChanged()
{
QMutexLocker locker(&mutex);
picsPath = SettingsCache::instance().getPicsPath();
customPicsPath = SettingsCache::instance().getCustomPicsPath();
}
void PictureLoaderWorker::clearNetworkCache()
{
networkManager->cache()->clear();
}

View File

@@ -0,0 +1,69 @@
#ifndef PICTURE_LOADER_WORKER_H
#define PICTURE_LOADER_WORKER_H
#include "../../../game/cards/card_database.h"
#include "picture_to_load.h"
#include <QLoggingCategory>
#include <QMutex>
#include <QNetworkAccessManager>
#include <QObject>
#define REDIRECT_HEADER_NAME "redirects"
#define REDIRECT_ORIGINAL_URL "original"
#define REDIRECT_URL "redirect"
#define REDIRECT_TIMESTAMP "timestamp"
#define REDIRECT_CACHE_FILENAME "cache.ini"
inline Q_LOGGING_CATEGORY(PictureLoaderWorkerLog, "picture_loader.worker");
class PictureLoaderWorker : public QObject
{
Q_OBJECT
public:
explicit PictureLoaderWorker();
~PictureLoaderWorker() override;
void enqueueImageLoad(CardInfoPtr card);
void clearNetworkCache();
private:
static QStringList md5Blacklist;
QThread *pictureLoaderThread;
QString picsPath, customPicsPath;
QList<PictureToLoad> loadQueue;
QMutex mutex;
QNetworkAccessManager *networkManager;
QHash<QUrl, QPair<QUrl, QDateTime>> redirectCache; // Stores redirect and timestamp
QString cacheFilePath; // Path to persistent storage
static constexpr int CacheTTLInDays = 30; // TODO: Make user configurable
QList<PictureToLoad> cardsToDownload;
PictureToLoad cardBeingLoaded;
PictureToLoad cardBeingDownloaded;
bool picDownload, downloadRunning, loadQueueRunning;
void startNextPicDownload();
bool cardImageExistsOnDisk(QString &setName, QString &correctedCardName);
bool imageIsBlackListed(const QByteArray &);
QNetworkReply *makeRequest(const QUrl &url);
void cacheRedirect(const QUrl &originalUrl, const QUrl &redirectUrl);
QUrl getCachedRedirect(const QUrl &originalUrl) const;
void loadRedirectCache();
void saveRedirectCache() const;
void cleanStaleEntries();
private slots:
void picDownloadFinished(QNetworkReply *reply);
void picDownloadFailed();
void picDownloadChanged();
void picsPathChanged();
public slots:
void processLoadQueue();
signals:
void startLoadQueue();
void imageLoaded(CardInfoPtr card, const QImage &image);
};
#endif // PICTURE_LOADER_WORKER_H

Some files were not shown because too many files have changed in this diff Show More