Reorder PublicFileViewer header: download button left, close button right, add 4px top padding
This commit is contained in:
@@ -59,15 +59,22 @@ class _PublicFileViewerState extends State<PublicFileViewer> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _initializeVideoPlayer() async {
|
Future<void> _initializeVideoPlayer() async {
|
||||||
if (_fileData?['downloadUrl'] != null) {
|
final url = _fileData?['viewUrl'] ?? _fileData?['downloadUrl'];
|
||||||
_videoController = VideoPlayerController.networkUrl(
|
if (url != null) {
|
||||||
Uri.parse(_fileData!['downloadUrl']),
|
_videoController = VideoPlayerController.networkUrl(Uri.parse(url));
|
||||||
);
|
|
||||||
await _videoController!.initialize();
|
await _videoController!.initialize();
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String? _getViewUrl() {
|
||||||
|
return _fileData?['viewUrl'] ?? _fileData?['downloadUrl'];
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _getDownloadUrl() {
|
||||||
|
return _fileData?['downloadUrl'];
|
||||||
|
}
|
||||||
|
|
||||||
bool _isVideoFile() {
|
bool _isVideoFile() {
|
||||||
final mimeType = _fileData?['capabilities']?['mimeType'] ?? '';
|
final mimeType = _fileData?['capabilities']?['mimeType'] ?? '';
|
||||||
return mimeType.toString().startsWith('video/');
|
return mimeType.toString().startsWith('video/');
|
||||||
@@ -93,20 +100,24 @@ class _PublicFileViewerState extends State<PublicFileViewer> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _downloadFile() {
|
void _downloadFile() {
|
||||||
if (_fileData != null && _fileData!['downloadUrl'] != null) {
|
final downloadUrl = _getDownloadUrl();
|
||||||
|
if (downloadUrl != null) {
|
||||||
// Trigger download directly in browser
|
// Trigger download directly in browser
|
||||||
final anchor = web.HTMLAnchorElement()
|
final anchor = web.HTMLAnchorElement()
|
||||||
..href = _fileData!['downloadUrl']
|
..href = downloadUrl
|
||||||
..download = _fileData!['fileName'] ?? 'download';
|
..download = _fileData!['fileName'] ?? 'download';
|
||||||
anchor.click();
|
anchor.click();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildFilePreview() {
|
Widget _buildFilePreview() {
|
||||||
|
final viewUrl = _getViewUrl();
|
||||||
|
if (viewUrl == null) return const SizedBox();
|
||||||
|
|
||||||
if (_isPdfFile()) {
|
if (_isPdfFile()) {
|
||||||
return Expanded(
|
return Expanded(
|
||||||
child: SfPdfViewer.network(
|
child: SfPdfViewer.network(
|
||||||
_fileData!['downloadUrl'],
|
viewUrl,
|
||||||
canShowScrollHead: false,
|
canShowScrollHead: false,
|
||||||
canShowScrollStatus: false,
|
canShowScrollStatus: false,
|
||||||
enableDoubleTapZooming: true,
|
enableDoubleTapZooming: true,
|
||||||
@@ -123,7 +134,7 @@ class _PublicFileViewerState extends State<PublicFileViewer> {
|
|||||||
} else if (_isAudioFile()) {
|
} else if (_isAudioFile()) {
|
||||||
return AudioPlayerBar(
|
return AudioPlayerBar(
|
||||||
fileName: _fileData!['fileName'] ?? 'Audio',
|
fileName: _fileData!['fileName'] ?? 'Audio',
|
||||||
fileUrl: _fileData!['downloadUrl'],
|
fileUrl: viewUrl,
|
||||||
);
|
);
|
||||||
} else if (_isDocumentFile()) {
|
} else if (_isDocumentFile()) {
|
||||||
return Expanded(
|
return Expanded(
|
||||||
@@ -195,30 +206,31 @@ class _PublicFileViewerState extends State<PublicFileViewer> {
|
|||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
backgroundColor: AppTheme.primaryBackground,
|
backgroundColor: AppTheme.primaryBackground,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
leading: IconButton(
|
leading: _fileData != null
|
||||||
icon: const Icon(Icons.close, color: AppTheme.primaryText),
|
? Padding(
|
||||||
onPressed: () => context.go('/'),
|
padding: const EdgeInsets.only(left: 16, top: 4),
|
||||||
),
|
child: ModernGlassButton(
|
||||||
|
onPressed: _downloadFile,
|
||||||
|
child: const Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.download, size: 18),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Text('Download'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
title: Text(
|
title: Text(
|
||||||
_fileData?['fileName'] ?? 'Shared File',
|
_fileData?['fileName'] ?? 'Shared File',
|
||||||
style: TextStyle(color: AppTheme.primaryText),
|
style: TextStyle(color: AppTheme.primaryText),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
if (_fileData != null)
|
IconButton(
|
||||||
Padding(
|
icon: const Icon(Icons.close, color: AppTheme.primaryText),
|
||||||
padding: const EdgeInsets.only(right: 8),
|
onPressed: () => context.go('/'),
|
||||||
child: ModernGlassButton(
|
),
|
||||||
onPressed: _downloadFile,
|
|
||||||
child: const Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Icon(Icons.download, size: 18),
|
|
||||||
SizedBox(width: 8),
|
|
||||||
Text('Download'),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: _isLoading
|
body: _isLoading
|
||||||
|
|||||||
@@ -359,6 +359,9 @@ func NewRouter(cfg *config.Config, db *database.DB, jwtManager *jwt.Manager, aut
|
|||||||
r.Get("/share/{token}/download", func(w http.ResponseWriter, req *http.Request) {
|
r.Get("/share/{token}/download", func(w http.ResponseWriter, req *http.Request) {
|
||||||
publicFileDownloadHandler(w, req, db, cfg, jwtManager)
|
publicFileDownloadHandler(w, req, db, cfg, jwtManager)
|
||||||
})
|
})
|
||||||
|
r.Get("/share/{token}/view", func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
publicFileViewHandler(w, req, db, cfg, jwtManager)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
return r
|
return r
|
||||||
@@ -2915,7 +2918,7 @@ func publicFileShareHandler(w http.ResponseWriter, r *http.Request, db *database
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build download URL
|
// Build URLs
|
||||||
scheme := "https"
|
scheme := "https"
|
||||||
if proto := r.Header.Get("X-Forwarded-Proto"); proto != "" {
|
if proto := r.Header.Get("X-Forwarded-Proto"); proto != "" {
|
||||||
scheme = proto
|
scheme = proto
|
||||||
@@ -2924,6 +2927,7 @@ func publicFileShareHandler(w http.ResponseWriter, r *http.Request, db *database
|
|||||||
}
|
}
|
||||||
host := "www.b0esche.cloud"
|
host := "www.b0esche.cloud"
|
||||||
downloadPath := fmt.Sprintf("%s://%s/public/share/%s/download?token=%s", scheme, host, token, url.QueryEscape(viewerToken))
|
downloadPath := fmt.Sprintf("%s://%s/public/share/%s/download?token=%s", scheme, host, token, url.QueryEscape(viewerToken))
|
||||||
|
viewPath := fmt.Sprintf("%s://%s/public/share/%s/view?token=%s", scheme, host, token, url.QueryEscape(viewerToken))
|
||||||
|
|
||||||
// Determine file type
|
// Determine file type
|
||||||
isPdf := strings.HasSuffix(strings.ToLower(file.Name), ".pdf")
|
isPdf := strings.HasSuffix(strings.ToLower(file.Name), ".pdf")
|
||||||
@@ -2933,6 +2937,7 @@ func publicFileShareHandler(w http.ResponseWriter, r *http.Request, db *database
|
|||||||
FileName string `json:"fileName"`
|
FileName string `json:"fileName"`
|
||||||
FileSize int64 `json:"fileSize"`
|
FileSize int64 `json:"fileSize"`
|
||||||
DownloadUrl string `json:"downloadUrl"`
|
DownloadUrl string `json:"downloadUrl"`
|
||||||
|
ViewUrl string `json:"viewUrl,omitempty"`
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
Capabilities struct {
|
Capabilities struct {
|
||||||
CanEdit bool `json:"canEdit"`
|
CanEdit bool `json:"canEdit"`
|
||||||
@@ -2946,6 +2951,12 @@ func publicFileShareHandler(w http.ResponseWriter, r *http.Request, db *database
|
|||||||
DownloadUrl: downloadPath,
|
DownloadUrl: downloadPath,
|
||||||
Token: viewerToken,
|
Token: viewerToken,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set view URL for PDFs and videos (for inline viewing)
|
||||||
|
if isPdf || strings.HasPrefix(mimeType, "video/") {
|
||||||
|
viewerSession.ViewUrl = viewPath
|
||||||
|
}
|
||||||
|
|
||||||
viewerSession.Capabilities.CanEdit = false
|
viewerSession.Capabilities.CanEdit = false
|
||||||
viewerSession.Capabilities.CanAnnotate = false
|
viewerSession.Capabilities.CanAnnotate = false
|
||||||
viewerSession.Capabilities.IsPdf = isPdf
|
viewerSession.Capabilities.IsPdf = isPdf
|
||||||
@@ -3050,6 +3061,102 @@ func publicFileDownloadHandler(w http.ResponseWriter, r *http.Request, db *datab
|
|||||||
io.Copy(w, resp.Body)
|
io.Copy(w, resp.Body)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func publicFileViewHandler(w http.ResponseWriter, r *http.Request, db *database.DB, cfg *config.Config, jwtManager *jwt.Manager) {
|
||||||
|
token := chi.URLParam(r, "token")
|
||||||
|
if token == "" {
|
||||||
|
errors.WriteError(w, errors.CodeInvalidArgument, "Token required", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
viewerToken := r.URL.Query().Get("token")
|
||||||
|
if viewerToken == "" {
|
||||||
|
errors.WriteError(w, errors.CodeInvalidArgument, "Viewer token required", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
link, err := db.GetFileShareLinkByToken(r.Context(), token)
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
errors.WriteError(w, errors.CodeNotFound, "Link not found or expired", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
errors.LogError(r, err, "Failed to get share link")
|
||||||
|
errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify viewer token (contains org ID for org files, empty for personal)
|
||||||
|
claims, err := jwtManager.Validate(viewerToken)
|
||||||
|
if err != nil {
|
||||||
|
errors.LogError(r, err, "Invalid viewer token")
|
||||||
|
errors.WriteError(w, errors.CodeUnauthenticated, "Invalid token", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if link.OrgID == nil {
|
||||||
|
if len(claims.OrgIDs) != 0 {
|
||||||
|
errors.WriteError(w, errors.CodeUnauthenticated, "Invalid token", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if len(claims.OrgIDs) == 0 {
|
||||||
|
errors.WriteError(w, errors.CodeUnauthenticated, "Invalid token", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
orgID, err := uuid.Parse(claims.OrgIDs[0])
|
||||||
|
if err != nil {
|
||||||
|
errors.WriteError(w, errors.CodeUnauthenticated, "Invalid token", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if *link.OrgID != orgID {
|
||||||
|
errors.WriteError(w, errors.CodeUnauthenticated, "Invalid token", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get file metadata
|
||||||
|
file, err := db.GetFileByID(r.Context(), link.FileID)
|
||||||
|
if err != nil {
|
||||||
|
errors.LogError(r, err, "Failed to get file")
|
||||||
|
errors.WriteError(w, errors.CodeNotFound, "File not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if file.UserID == nil {
|
||||||
|
errors.WriteError(w, errors.CodeNotFound, "File not accessible", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get WebDAV client for the file's owner
|
||||||
|
client, err := getUserWebDAVClient(r.Context(), db, *file.UserID, cfg.NextcloudURL, cfg.NextcloudUser, cfg.NextcloudPass)
|
||||||
|
if err != nil {
|
||||||
|
errors.LogError(r, err, "Failed to get WebDAV client")
|
||||||
|
errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stream file
|
||||||
|
resp, err := client.Download(r.Context(), file.Path, "")
|
||||||
|
if err != nil {
|
||||||
|
errors.LogError(r, err, "Failed to download file")
|
||||||
|
errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// Copy headers
|
||||||
|
for k, v := range resp.Header {
|
||||||
|
w.Header()[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure inline viewing behavior (no Content-Disposition attachment)
|
||||||
|
w.Header().Del("Content-Disposition")
|
||||||
|
w.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=\"%s\"", file.Name))
|
||||||
|
|
||||||
|
// Copy body
|
||||||
|
io.Copy(w, resp.Body)
|
||||||
|
}
|
||||||
|
|
||||||
func getUserFileShareLinkHandler(w http.ResponseWriter, r *http.Request, db *database.DB) {
|
func getUserFileShareLinkHandler(w http.ResponseWriter, r *http.Request, db *database.DB) {
|
||||||
userIDStr, _ := middleware.GetUserID(r.Context())
|
userIDStr, _ := middleware.GetUserID(r.Context())
|
||||||
userID, _ := uuid.Parse(userIDStr)
|
userID, _ := uuid.Parse(userIDStr)
|
||||||
|
|||||||
Reference in New Issue
Block a user