Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
C
Clubphp
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Administrator
Clubphp
Commits
6e57c40a
Commit
6e57c40a
authored
May 24, 2026
by
Mahmoud Aglan
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
cool
parent
1bf257c9
Changes
5
Show whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
22 additions
and
177 deletions
+22
-177
Dockerfile
Dockerfile
+0
-21
TutorialController.php
app/Modules/Tutorials/Controllers/TutorialController.php
+10
-87
book.php
app/Modules/Tutorials/Views/book.php
+9
-67
export_pdf.php
app/Modules/Tutorials/Views/export_pdf.php
+2
-1
php.ini
docker/php.ini
+1
-1
No files found.
Dockerfile
View file @
6e57c40a
...
@@ -62,27 +62,6 @@ COPY . /var/www/html/
...
@@ -62,27 +62,6 @@ COPY . /var/www/html/
WORKDIR
/var/www/html
WORKDIR
/var/www/html
RUN
composer
install
--no-dev
--optimize-autoloader
--no-interaction
--no-scripts
RUN
composer
install
--no-dev
--optimize-autoloader
--no-interaction
--no-scripts
# ── Create optimized JPG copies of screenshots for PDF export ──
RUN if
[
-d
/var/www/html/public/assets/tutorials/screenshots
]
;
then
\
php
-r
'
\
$dir = "/var/www/html/public/assets/tutorials/screenshots";
\
foreach (glob("$dir/*.png") as $f) {
\
$img = @imagecreatefrompng($f);
\
if (!$img) continue;
\
$w = imagesx($img); $h = imagesy($img);
\
if ($w > 900) {
\
$nw = 900; $nh = (int)($h * 900 / $w);
\
$dst = imagecreatetruecolor($nw, $nh);
\
imagecopyresampled($dst, $img, 0, 0, 0, 0, $nw, $nh, $w, $h);
\
imagedestroy($img); $img = $dst;
\
}
\
$jpg = substr($f, 0, -4) . ".jpg";
\
imagejpeg($img, $jpg, 55);
\
imagedestroy($img);
\
}
\
'
;
\
fi
# ── Create storage directories ──
# ── Create storage directories ──
RUN
mkdir
-p
\
RUN
mkdir
-p
\
/var/www/html/storage/logs
\
/var/www/html/storage/logs
\
...
...
app/Modules/Tutorials/Controllers/TutorialController.php
View file @
6e57c40a
...
@@ -558,10 +558,6 @@ class TutorialController extends Controller
...
@@ -558,10 +558,6 @@ class TutorialController extends Controller
public
function
exportPdf
(
Request
$request
)
:
Response
public
function
exportPdf
(
Request
$request
)
:
Response
{
{
set_time_limit
(
600
);
$dlToken
=
$_GET
[
'dl'
]
??
''
;
$screenshotsPath
=
realpath
(
__DIR__
.
'/../../../../public/assets/tutorials/screenshots'
);
$screenshotsPath
=
realpath
(
__DIR__
.
'/../../../../public/assets/tutorials/screenshots'
);
$viewsPath
=
realpath
(
__DIR__
.
'/../Views'
);
$viewsPath
=
realpath
(
__DIR__
.
'/../Views'
);
...
@@ -572,70 +568,17 @@ class TutorialController extends Controller
...
@@ -572,70 +568,17 @@ class TutorialController extends Controller
include
__DIR__
.
'/../Views/export_pdf.php'
;
include
__DIR__
.
'/../Views/export_pdf.php'
;
$html
=
ob_get_clean
();
$html
=
ob_get_clean
();
$wkhtmltopdf
=
null
;
$printBar
=
'<div id="printBar" style="position:fixed;top:0;left:0;right:0;z-index:99999;background:#1E40AF;color:#fff;padding:12px 24px;display:flex;align-items:center;justify-content:space-between;font-family:sans-serif;box-shadow:0 2px 8px rgba(0,0,0,.2);">'
foreach
([
'/usr/local/bin/wkhtmltopdf'
,
'/usr/bin/wkhtmltopdf'
]
as
$path
)
{
.
'<span style="font-size:14px;">لحفظ الكتاب كـ PDF: اضغط <b>Ctrl+P</b> (أو <b>⌘+P</b>) ثم اختر "حفظ كـ PDF"</span>'
if
(
file_exists
(
$path
)
&&
is_executable
(
$path
))
{
.
'<button onclick="document.getElementById(\'printBar\').remove();window.print();" style="background:#fff;color:#1E40AF;border:none;padding:8px 20px;border-radius:6px;font-size:14px;font-weight:700;cursor:pointer;">طباعة / حفظ PDF</button>'
$wkhtmltopdf
=
$path
;
.
'</div><div style="height:52px;"></div>'
;
break
;
$html
=
str_replace
(
'<body>'
,
'<body>'
.
$printBar
,
$html
);
}
}
if
(
!
$wkhtmltopdf
)
{
$wkhtmltopdf
=
trim
((
string
)
shell_exec
(
'which wkhtmltopdf 2>/dev/null'
));
}
if
(
$wkhtmltopdf
)
{
$tmpInput
=
tempnam
(
sys_get_temp_dir
(),
'pdf_in_'
)
.
'.html'
;
$tmpOutput
=
tempnam
(
sys_get_temp_dir
(),
'pdf_out_'
)
.
'.pdf'
;
file_put_contents
(
$tmpInput
,
$html
);
$cmd
=
escapeshellarg
(
$wkhtmltopdf
)
.
' --encoding utf-8 --page-size A4'
.
' --margin-top 15 --margin-bottom 15 --margin-left 12 --margin-right 12'
.
' --enable-local-file-access'
.
' --no-stop-slow-scripts'
.
' --disable-javascript'
.
' --image-quality 60'
.
' --image-dpi 100'
.
' --lowquality'
.
' '
.
escapeshellarg
(
$tmpInput
)
.
' '
.
escapeshellarg
(
$tmpOutput
)
.
' 2>&1'
;
exec
(
$cmd
,
$output
,
$returnCode
);
if
(
$returnCode
>
1
)
{
error_log
(
'wkhtmltopdf failed (code '
.
$returnCode
.
'): '
.
implode
(
"
\n
"
,
$output
));
}
@
unlink
(
$tmpInput
);
if
((
$returnCode
===
0
||
$returnCode
===
1
)
&&
file_exists
(
$tmpOutput
)
&&
filesize
(
$tmpOutput
)
>
0
)
{
$printCss
=
'<style>@media print { #printBar, #printBar + div { display:none !important; } }</style>'
;
$pdfContent
=
file_get_contents
(
$tmpOutput
);
$html
=
str_replace
(
'</head>'
,
$printCss
.
'</head>'
,
$html
);
@
unlink
(
$tmpOutput
);
if
(
$dlToken
)
{
setcookie
(
'book_download'
,
$dlToken
,
time
()
+
120
,
'/'
);
}
$response
=
new
Response
();
$response
=
new
Response
();
return
$response
->
html
(
$pdfContent
,
200
)
->
withHeaders
([
return
$response
->
html
(
$html
);
'Content-Type'
=>
'application/pdf'
,
'Content-Disposition'
=>
'attachment; filename="Book-of-the-ERP.pdf"'
,
'Content-Length'
=>
(
string
)
strlen
(
$pdfContent
),
]);
}
@
unlink
(
$tmpOutput
);
}
if
(
$dlToken
)
{
setcookie
(
'book_download'
,
$dlToken
,
time
()
+
120
,
'/'
);
}
$response
=
new
Response
();
return
$response
->
html
(
$html
,
200
)
->
withHeaders
([
'Content-Type'
=>
'text/html; charset=utf-8'
,
'Content-Disposition'
=>
'attachment; filename="Book-of-the-ERP.html"'
,
]);
}
}
public
function
collectBookData
(
string
$screenshotsPath
,
string
$viewsPath
)
:
array
public
function
collectBookData
(
string
$screenshotsPath
,
string
$viewsPath
)
:
array
...
@@ -693,13 +636,9 @@ class TutorialController extends Controller
...
@@ -693,13 +636,9 @@ class TutorialController extends Controller
$categories
=
TutorialRegistry
::
getCategories
(
$sectionKey
);
$categories
=
TutorialRegistry
::
getCategories
(
$sectionKey
);
$screenshot
=
null
;
$screenshot
=
null
;
if
(
$screenshotsPath
)
{
if
(
$screenshotsPath
)
{
$base
=
$sectionKey
;
$baseAlt
=
str_replace
(
'-'
,
'_'
,
$sectionKey
);
$possibleFiles
=
[
$possibleFiles
=
[
$screenshotsPath
.
'/'
.
$base
.
'.jpg'
,
$screenshotsPath
.
'/'
.
$sectionKey
.
'.png'
,
$screenshotsPath
.
'/'
.
$base
.
'.png'
,
$screenshotsPath
.
'/'
.
str_replace
(
'-'
,
'_'
,
$sectionKey
)
.
'.png'
,
$screenshotsPath
.
'/'
.
$baseAlt
.
'.jpg'
,
$screenshotsPath
.
'/'
.
$baseAlt
.
'.png'
,
];
];
foreach
(
$possibleFiles
as
$f
)
{
foreach
(
$possibleFiles
as
$f
)
{
if
(
file_exists
(
$f
))
{
if
(
file_exists
(
$f
))
{
...
@@ -740,22 +679,6 @@ class TutorialController extends Controller
...
@@ -740,22 +679,6 @@ class TutorialController extends Controller
$content
=
preg_replace
(
'/<div class="tut-breadcrumb">.*?<\/div>/s'
,
''
,
$content
);
$content
=
preg_replace
(
'/<div class="tut-breadcrumb">.*?<\/div>/s'
,
''
,
$content
);
$content
=
str_replace
(
'loading="lazy"'
,
''
,
$content
);
$content
=
str_replace
(
'loading="lazy"'
,
''
,
$content
);
$publicPath
=
realpath
(
__DIR__
.
'/../../../../public'
);
$content
=
(
string
)
preg_replace_callback
(
'#src="(/assets/[^"]+)"#'
,
function
(
array
$m
)
use
(
$publicPath
)
:
string
{
$rel
=
$m
[
1
];
if
(
str_ends_with
(
$rel
,
'.png'
))
{
$jpgPath
=
$publicPath
.
substr
(
$rel
,
0
,
-
4
)
.
'.jpg'
;
if
(
file_exists
(
$jpgPath
))
{
return
'src="file://'
.
$jpgPath
.
'"'
;
}
}
return
'src="file://'
.
$publicPath
.
$rel
.
'"'
;
},
$content
);
$content
=
preg_replace
(
'/<i\s+data-lucide="[^"]*"[^>]*><\/i>/'
,
''
,
$content
);
$content
=
preg_replace
(
'/<i\s+data-lucide="[^"]*"[^>]*><\/i>/'
,
''
,
$content
);
return
$content
;
return
$content
;
...
...
app/Modules/Tutorials/Views/book.php
View file @
6e57c40a
...
@@ -125,83 +125,27 @@
...
@@ -125,83 +125,27 @@
</style>
</style>
<script>
<script>
var
exportSteps
=
[
{
pct
:
5
,
label
:
'تحضير البيانات...'
},
{
pct
:
12
,
label
:
'تجميع شروحات العضوية...'
},
{
pct
:
20
,
label
:
'تجميع شروحات الأنشطة الرياضية...'
},
{
pct
:
28
,
label
:
'تجميع شروحات الخزنة...'
},
{
pct
:
35
,
label
:
'تجميع باقي الأقسام...'
},
{
pct
:
45
,
label
:
'تضمين لقطات الشاشة...'
},
{
pct
:
55
,
label
:
'توليد ملف PDF...'
},
{
pct
:
65
,
label
:
'معالجة الصفحات...'
},
{
pct
:
72
,
label
:
'تنسيق المحتوى...'
},
{
pct
:
80
,
label
:
'ضغط الملف...'
},
{
pct
:
88
,
label
:
'المراجعة النهائية...'
},
{
pct
:
94
,
label
:
'جاري الانتهاء...'
},
];
var
stepTimer
=
null
;
var
cookieTimer
=
null
;
function
startExport
()
{
function
startExport
()
{
document
.
getElementById
(
'beforeExport'
).
style
.
display
=
'none'
;
document
.
getElementById
(
'beforeExport'
).
style
.
display
=
'none'
;
document
.
getElementById
(
'duringExport'
).
style
.
display
=
'block'
;
document
.
getElementById
(
'duringExport'
).
style
.
display
=
'block'
;
document
.
getElementById
(
'afterExport'
).
style
.
display
=
'none'
;
document
.
getElementById
(
'afterExport'
).
style
.
display
=
'none'
;
document
.
getElementById
(
'errorExport'
).
style
.
display
=
'none'
;
document
.
getElementById
(
'errorExport'
).
style
.
display
=
'none'
;
var
stepIndex
=
0
;
var
progressBar
=
document
.
getElementById
(
'progressBar'
);
var
progressBar
=
document
.
getElementById
(
'progressBar'
);
var
progressLabel
=
document
.
getElementById
(
'progressLabel'
);
var
progressLabel
=
document
.
getElementById
(
'progressLabel'
);
var
progressStep
=
document
.
getElementById
(
'progressStep'
);
var
progressStep
=
document
.
getElementById
(
'progressStep'
);
progressBar
.
style
.
width
=
'30%'
;
progressLabel
.
textContent
=
'30%'
;
progressStep
.
textContent
=
'جاري فتح الكتاب...'
;
function
animateStep
()
{
var
w
=
window
.
open
(
'/tutorials/export-pdf'
,
'_blank'
);
if
(
stepIndex
>=
exportSteps
.
length
)
return
;
var
step
=
exportSteps
[
stepIndex
];
setTimeout
(
function
()
{
progressBar
.
style
.
width
=
step
.
pct
+
'%'
;
progressLabel
.
textContent
=
step
.
pct
+
'%'
;
progressStep
.
textContent
=
step
.
label
;
stepIndex
++
;
if
(
stepIndex
<
exportSteps
.
length
)
{
stepTimer
=
setTimeout
(
animateStep
,
8000
);
}
}
animateStep
();
// Use hidden iframe — browsers don't timeout iframe downloads
var
token
=
Date
.
now
().
toString
();
var
iframe
=
document
.
createElement
(
'iframe'
);
iframe
.
style
.
display
=
'none'
;
iframe
.
setAttribute
(
'id'
,
'exportFrame'
);
iframe
.
src
=
'/tutorials/export-pdf?dl='
+
token
;
document
.
body
.
appendChild
(
iframe
);
// Poll for download cookie (set by server when response is sent)
cookieTimer
=
setInterval
(
function
()
{
if
(
document
.
cookie
.
indexOf
(
'book_download='
+
token
)
!==
-
1
)
{
clearInterval
(
cookieTimer
);
clearTimeout
(
stepTimer
);
// Clear the cookie
document
.
cookie
=
'book_download=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT'
;
progressBar
.
style
.
width
=
'100%'
;
progressBar
.
style
.
width
=
'100%'
;
progressLabel
.
textContent
=
'100%'
;
progressLabel
.
textContent
=
'100%'
;
progressStep
.
textContent
=
'اكتمل! تم التحميل ✓'
;
progressStep
.
textContent
=
'تم فتح الكتاب ✓'
;
setTimeout
(
function
()
{
finishExport
(
true
);
},
1500
);
setTimeout
(
function
()
{
finishExport
(
true
);
},
1000
);
// Clean up iframe
var
f
=
document
.
getElementById
(
'exportFrame'
);
if
(
f
)
setTimeout
(
function
()
{
f
.
remove
();
},
5000
);
}
},
2000
);
},
2000
);
// Safety timeout: 10 minutes max
setTimeout
(
function
()
{
if
(
document
.
getElementById
(
'duringExport'
).
style
.
display
!==
'none'
)
{
clearInterval
(
cookieTimer
);
clearTimeout
(
stepTimer
);
document
.
getElementById
(
'errorMessage'
).
textContent
=
'انتهت المهلة. قد يكون الملف قد تم تحميله — تحقق من مجلد التنزيلات.'
;
finishExport
(
false
);
var
f
=
document
.
getElementById
(
'exportFrame'
);
if
(
f
)
f
.
remove
();
}
},
600000
);
}
}
function
finishExport
(
success
)
{
function
finishExport
(
success
)
{
...
@@ -219,8 +163,6 @@ function resetExport() {
...
@@ -219,8 +163,6 @@ function resetExport() {
document
.
getElementById
(
'afterExport'
).
style
.
display
=
'none'
;
document
.
getElementById
(
'afterExport'
).
style
.
display
=
'none'
;
document
.
getElementById
(
'errorExport'
).
style
.
display
=
'none'
;
document
.
getElementById
(
'errorExport'
).
style
.
display
=
'none'
;
document
.
getElementById
(
'progressBar'
).
style
.
width
=
'0%'
;
document
.
getElementById
(
'progressBar'
).
style
.
width
=
'0%'
;
clearInterval
(
cookieTimer
);
clearTimeout
(
stepTimer
);
}
}
document
.
addEventListener
(
'DOMContentLoaded'
,
function
()
{
document
.
addEventListener
(
'DOMContentLoaded'
,
function
()
{
...
...
app/Modules/Tutorials/Views/export_pdf.php
View file @
6e57c40a
...
@@ -889,8 +889,9 @@ $sectionIcons = [
...
@@ -889,8 +889,9 @@ $sectionIcons = [
</div>
</div>
<?php
if
(
!
empty
(
$section
[
'screenshot'
])
&&
file_exists
(
$section
[
'screenshot'
]))
:
?>
<?php
if
(
!
empty
(
$section
[
'screenshot'
])
&&
file_exists
(
$section
[
'screenshot'
]))
:
?>
<?php
$webPath
=
str_replace
(
realpath
(
__DIR__
.
'/../../../../public'
),
''
,
$section
[
'screenshot'
]);
?>
<div
style=
"text-align:center;margin: 24px 0;page-break-inside:avoid;"
>
<div
style=
"text-align:center;margin: 24px 0;page-break-inside:avoid;"
>
<img
src=
"
file://
<?=
$section
[
'screenshot'
]
?>
"
class=
"tutorial-screenshot"
alt=
"
<?=
htmlspecialchars
(
$section
[
'title'
],
ENT_QUOTES
,
'UTF-8'
)
?>
"
>
<img
src=
"
<?=
$webPath
?>
"
class=
"tutorial-screenshot"
alt=
"
<?=
htmlspecialchars
(
$section
[
'title'
],
ENT_QUOTES
,
'UTF-8'
)
?>
"
>
</div>
</div>
<?php
endif
;
?>
<?php
endif
;
?>
...
...
docker/php.ini
View file @
6e57c40a
...
@@ -2,7 +2,7 @@
...
@@ -2,7 +2,7 @@
upload_max_filesize
=
20M
upload_max_filesize
=
20M
post_max_size
=
25M
post_max_size
=
25M
memory_limit
=
2048
M
memory_limit
=
1024
M
max_execution_time
=
1800
max_execution_time
=
1800
max_input_time
=
240
max_input_time
=
240
max_input_vars
=
5000
max_input_vars
=
5000
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment