@@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
22import 'package:file_picker/file_picker.dart' ;
33import 'dart:io' ;
44import 'package:path/path.dart' as p;
5+ import 'dart:math' ;
6+ import 'dart:convert' ;
57
68void main () {
79 runApp (const GamedeckApp ());
@@ -29,8 +31,8 @@ class GamedeckApp extends StatelessWidget {
2931 style: ElevatedButton .styleFrom (
3032 backgroundColor: const Color (0xFF007BFF ),
3133 foregroundColor: Colors .white,
32- shape: RoundedRectangleBorder (borderRadius: BorderRadius .circular (8 )),
33- padding: const EdgeInsets .symmetric (horizontal: 20 , vertical: 12 ),
34+ shape: RoundedRectangleBorder (borderRadius: BorderRadius .circular (8 )),
35+ padding: const EdgeInsets .symmetric (horizontal: 20 , vertical: 12 ),
3436 ),
3537 ),
3638 ),
@@ -42,8 +44,33 @@ class GamedeckApp extends StatelessWidget {
4244class Game {
4345 final String name;
4446 final String path;
47+ String ? coverPath;
48+ bool isFavorite;
4549
46- Game ({required this .name, required this .path});
50+ Game ({
51+ required this .name,
52+ required this .path,
53+ this .coverPath,
54+ this .isFavorite = false ,
55+ });
56+
57+ factory Game .fromJson (Map <String , dynamic > json) {
58+ return Game (
59+ name: json['name' ],
60+ path: json['path' ],
61+ coverPath: json['coverPath' ],
62+ isFavorite: json['isFavorite' ] ?? false ,
63+ );
64+ }
65+
66+ Map <String , dynamic > toJson () {
67+ return {
68+ 'name' : name,
69+ 'path' : path,
70+ 'coverPath' : coverPath,
71+ 'isFavorite' : isFavorite,
72+ };
73+ }
4774}
4875
4976class GamedeckHomePage extends StatefulWidget {
@@ -55,6 +82,49 @@ class GamedeckHomePage extends StatefulWidget {
5582
5683class _GamedeckHomePageState extends State <GamedeckHomePage > {
5784 List <Game > games = [];
85+ String ? backgroundPath;
86+
87+ @override
88+ void initState () {
89+ super .initState ();
90+ _loadData ();
91+ }
92+
93+ Future <void > _loadData () async {
94+ try {
95+ final gamesFile = File ('games.json' );
96+ if (await gamesFile.exists ()) {
97+ final jsonStr = await gamesFile.readAsString ();
98+ final List <dynamic > jsonList = json.decode (jsonStr)['games' ];
99+ setState (() {
100+ games = jsonList.map ((json) => Game .fromJson (json)).toList ();
101+ });
102+ }
103+
104+ final bgFile = File ('background.txt' );
105+ if (await bgFile.exists ()) {
106+ backgroundPath = await bgFile.readAsString ();
107+ setState (() {});
108+ }
109+ } catch (e) {
110+ print ('Error loading data: $e ' );
111+ }
112+ }
113+
114+ Future <void > _saveData () async {
115+ try {
116+ final gamesFile = File ('games.json' );
117+ final map = {'games' : games.map ((g) => g.toJson ()).toList ()};
118+ await gamesFile.writeAsString (json.encode (map));
119+
120+ if (backgroundPath != null ) {
121+ final bgFile = File ('background.txt' );
122+ await bgFile.writeAsString (backgroundPath! );
123+ }
124+ } catch (e) {
125+ print ('Error saving data: $e ' );
126+ }
127+ }
58128
59129 Future <void > _addGame () async {
60130 FilePickerResult ? result = await FilePicker .platform.pickFiles (
@@ -65,9 +135,17 @@ class _GamedeckHomePageState extends State<GamedeckHomePage> {
65135 String ? filePath = file.path;
66136 if (filePath != null ) {
67137 String gameName = p.basenameWithoutExtension (file.name).replaceAll ('_' , ' ' ).trim ();
138+
139+ // Pick cover image (optional)
140+ FilePickerResult ? coverResult = await FilePicker .platform.pickFiles (
141+ type: FileType .image,
142+ );
143+ String ? coverPath = coverResult? .files.first.path;
144+
68145 setState (() {
69- games.add (Game (name: gameName, path: filePath));
146+ games.add (Game (name: gameName, path: filePath, coverPath : coverPath ));
70147 });
148+ await _saveData ();
71149 ScaffoldMessenger .of (context).showSnackBar (
72150 SnackBar (content: Text ('Dodano grę: $gameName ' )),
73151 );
@@ -89,47 +167,165 @@ class _GamedeckHomePageState extends State<GamedeckHomePage> {
89167 }
90168 }
91169
92- @override
93- Widget build (BuildContext context) {
94- return Scaffold (
95- appBar: AppBar (
96- title: const Text ('GAMEDECK PC' , style: TextStyle (fontSize: 24 )),
97- centerTitle: false ,
98- actions: [
99- Padding (
100- padding: const EdgeInsets .only (right: 16.0 ),
101- child: ElevatedButton .icon (
102- onPressed: _addGame,
103- icon: const Icon (Icons .add),
104- label: const Text ('Dodaj Grę' ),
170+ Future <void > _launchRandom () async {
171+ if (games.isEmpty) {
172+ ScaffoldMessenger .of (context).showSnackBar (
173+ const SnackBar (content: Text ('Brak gier do losowego uruchomienia' )),
174+ );
175+ return ;
176+ }
177+ final randomIndex = Random ().nextInt (games.length);
178+ _launchGame (games[randomIndex].path);
179+ }
180+
181+ void _showOptions () {
182+ showDialog (
183+ context: context,
184+ builder: (context) => AlertDialog (
185+ title: const Text ('Opcje' ),
186+ content: Column (
187+ mainAxisSize: MainAxisSize .min,
188+ children: [
189+ ElevatedButton (
190+ onPressed: () async {
191+ Navigator .pop (context);
192+ FilePickerResult ? res = await FilePicker .platform.pickFiles (
193+ type: FileType .image,
194+ );
195+ if (res != null ) {
196+ setState (() {
197+ backgroundPath = res.files.first.path;
198+ });
199+ await _saveData ();
200+ }
201+ },
202+ child: const Text ('Zmień tapetę' ),
105203 ),
204+ ],
205+ ),
206+ actions: [
207+ TextButton (
208+ onPressed: () => Navigator .pop (context),
209+ child: const Text ('Zamknij' ),
106210 ),
107211 ],
108212 ),
109- body: games.isEmpty
110- ? const Center (
213+ );
214+ }
215+
216+ Widget _buildGameList (List <Game > gameList) {
217+ if (gameList.isEmpty) {
218+ return const Center (
111219 child: Text (
112- 'Brak gier w bibliotece.\n Dodaj swoją pierwszą grę!' ,
113- textAlign: TextAlign .center,
220+ 'Brak gier w tej sekcji.' ,
114221 style: TextStyle (fontSize: 18 , color: Colors .white70),
115222 ),
116- )
117- : GridView .builder (
118- padding: const EdgeInsets .all (20 ),
119- gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent (
120- maxCrossAxisExtent: 300 ,
121- childAspectRatio: 0.75 ,
122- crossAxisSpacing: 20 ,
123- mainAxisSpacing: 20 ,
223+ );
224+ }
225+ return GridView .builder (
226+ padding: const EdgeInsets .all (20 ),
227+ gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent (
228+ maxCrossAxisExtent: 300 ,
229+ childAspectRatio: 0.75 ,
230+ crossAxisSpacing: 20 ,
231+ mainAxisSpacing: 20 ,
232+ ),
233+ itemCount: gameList.length,
234+ itemBuilder: (context, index) {
235+ final game = gameList[index];
236+ return GameCard (
237+ game: game,
238+ onPlay: () => _launchGame (game.path),
239+ onToggleFavorite: () {
240+ setState (() {
241+ game.isFavorite = ! game.isFavorite;
242+ });
243+ _saveData ();
244+ },
245+ );
246+ },
247+ );
248+ }
249+
250+ @override
251+ Widget build (BuildContext context) {
252+ return DefaultTabController (
253+ length: 2 ,
254+ child: Scaffold (
255+ appBar: AppBar (
256+ title: const Text ('GAMEDECK PC' , style: TextStyle (fontSize: 24 )),
257+ centerTitle: false ,
258+ actions: [
259+ Padding (
260+ padding: const EdgeInsets .only (right: 16.0 ),
261+ child: ElevatedButton .icon (
262+ onPressed: _addGame,
263+ icon: const Icon (Icons .add),
264+ label: const Text ('Dodaj Grę' ),
265+ ),
266+ ),
267+ ],
268+ bottom: const TabBar (
269+ tabs: [
270+ Tab (text: 'Wszystkie' ),
271+ Tab (text: 'Ulubione' ),
272+ ],
273+ ),
274+ ),
275+ body: Container (
276+ decoration: BoxDecoration (
277+ gradient: backgroundPath == null
278+ ? const LinearGradient (
279+ begin: Alignment .topCenter,
280+ end: Alignment .bottomCenter,
281+ colors: [Color (0xFF1A1A1A ), Color (0xFF001A4D )],
282+ )
283+ : null ,
284+ image: backgroundPath != null
285+ ? DecorationImage (
286+ image: FileImage (File (backgroundPath! )),
287+ fit: BoxFit .cover,
288+ )
289+ : null ,
290+ ),
291+ child: games.isEmpty
292+ ? const Center (
293+ child: Text (
294+ 'Brak gier w bibliotece.\n Dodaj swoją pierwszą grę!' ,
295+ textAlign: TextAlign .center,
296+ style: TextStyle (fontSize: 18 , color: Colors .white70),
297+ ),
298+ )
299+ : TabBarView (
300+ children: [
301+ _buildGameList (games),
302+ _buildGameList (games.where ((g) => g.isFavorite).toList ()),
303+ ],
304+ ),
305+ ),
306+ bottomNavigationBar: BottomAppBar (
307+ color: const Color (0xFF121212 ),
308+ child: Row (
309+ mainAxisAlignment: MainAxisAlignment .spaceEvenly,
310+ children: [
311+ ElevatedButton .icon (
312+ onPressed: () {},
313+ icon: const Icon (Icons .arrow_forward),
314+ label: const Text ('Nawiguj' ),
315+ ),
316+ ElevatedButton .icon (
317+ onPressed: _showOptions,
318+ icon: const Icon (Icons .settings),
319+ label: const Text ('Opcje' ),
320+ ),
321+ ElevatedButton .icon (
322+ onPressed: _launchRandom,
323+ icon: const Icon (Icons .shuffle),
324+ label: const Text ('Losowa Gra' ),
325+ ),
326+ ],
327+ ),
124328 ),
125- itemCount: games.length,
126- itemBuilder: (context, index) {
127- final game = games[index];
128- return GameCard (
129- game: game,
130- onPlay: () => _launchGame (game.path),
131- );
132- },
133329 ),
134330 );
135331 }
@@ -138,11 +334,20 @@ class _GamedeckHomePageState extends State<GamedeckHomePage> {
138334class GameCard extends StatelessWidget {
139335 final Game game;
140336 final VoidCallback onPlay;
337+ final VoidCallback onToggleFavorite;
141338
142- const GameCard ({super .key, required this .game, required this .onPlay});
339+ const GameCard ({
340+ super .key,
341+ required this .game,
342+ required this .onPlay,
343+ required this .onToggleFavorite,
344+ });
143345
144346 @override
145347 Widget build (BuildContext context) {
348+ final placeholderUrl =
349+ 'https://via.placeholder.com/200x260/2A2A2A/FFFFFF?text=${game .name .substring (0 , 1 )}' ;
350+
146351 return Card (
147352 elevation: 8 ,
148353 shape: RoundedRectangleBorder (borderRadius: BorderRadius .circular (16 )),
@@ -153,23 +358,44 @@ class GameCard extends StatelessWidget {
153358 crossAxisAlignment: CrossAxisAlignment .stretch,
154359 children: [
155360 Expanded (
156- child: Image .network (
157- 'https://via.placeholder.com/200x260/2A2A2A/FFFFFF?text=${game .name .substring (0 , 1 )}' ,
158- fit: BoxFit .cover,
159- ),
361+ child: game.coverPath != null
362+ ? Image .file (
363+ File (game.coverPath! ),
364+ fit: BoxFit .cover,
365+ errorBuilder: (context, error, stackTrace) => Image .network (
366+ placeholderUrl,
367+ fit: BoxFit .cover,
368+ ),
369+ )
370+ : Image .network (
371+ placeholderUrl,
372+ fit: BoxFit .cover,
373+ ),
160374 ),
161375 Padding (
162376 padding: const EdgeInsets .all (12.0 ),
163377 child: Column (
164378 crossAxisAlignment: CrossAxisAlignment .start,
165379 children: [
166- Text (
167- game.name,
168- style: Theme .of (context).textTheme.titleLarge? .copyWith (fontSize: 18 ),
169- maxLines: 1 ,
170- overflow: TextOverflow .ellipsis,
380+ Row (
381+ children: [
382+ Expanded (
383+ child: Text (
384+ game.name,
385+ style: Theme .of (context).textTheme.titleLarge? .copyWith (fontSize: 18 ),
386+ maxLines: 1 ,
387+ overflow: TextOverflow .ellipsis,
388+ ),
389+ ),
390+ IconButton (
391+ icon: Icon (
392+ game.isFavorite ? Icons .favorite : Icons .favorite_border,
393+ color: game.isFavorite ? Colors .red : null ,
394+ ),
395+ onPressed: onToggleFavorite,
396+ ),
397+ ],
171398 ),
172- const SizedBox (height: 8 ),
173399 ElevatedButton .icon (
174400 onPressed: onPlay,
175401 icon: const Icon (Icons .play_arrow),
0 commit comments