Datenbank bereinigt / Gitea-Integration gefixt
Dieser Commit ist enthalten in:
committet von
Server Deploy
Ursprung
395598c2b0
Commit
c21be47428
@ -431,81 +431,163 @@
|
||||
font-weight: var(--font-medium);
|
||||
}
|
||||
|
||||
/* Upload Types */
|
||||
.admin-upload-types {
|
||||
/* =========================
|
||||
Extension Settings (New)
|
||||
========================= */
|
||||
|
||||
.admin-upload-extensions {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.admin-upload-types h3 {
|
||||
.admin-upload-extensions h3 {
|
||||
font-size: var(--text-sm);
|
||||
font-weight: var(--font-medium);
|
||||
color: var(--text-primary);
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
|
||||
/* Upload Category */
|
||||
.upload-category {
|
||||
/* Extension Tags Container */
|
||||
.extension-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
padding: 1rem;
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border-light);
|
||||
border-radius: var(--radius-lg);
|
||||
margin-bottom: 0.75rem;
|
||||
overflow: hidden;
|
||||
transition: all var(--transition-fast);
|
||||
min-height: 50px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.upload-category:hover {
|
||||
border-color: var(--border-default);
|
||||
.extension-empty {
|
||||
color: var(--text-muted);
|
||||
font-size: var(--text-sm);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.upload-category.disabled {
|
||||
opacity: 0.5;
|
||||
/* Single Extension Tag */
|
||||
.extension-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 6px 10px;
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
border-radius: var(--radius-full);
|
||||
font-size: var(--text-sm);
|
||||
font-weight: var(--font-medium);
|
||||
font-family: var(--font-mono, monospace);
|
||||
}
|
||||
|
||||
.upload-category.disabled .upload-category-types {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.upload-category-header {
|
||||
padding: 0.75rem 1rem;
|
||||
background: var(--bg-card);
|
||||
}
|
||||
|
||||
.upload-category-toggle {
|
||||
.extension-tag-remove {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.upload-category-toggle input[type="checkbox"] {
|
||||
justify-content: center;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
accent-color: var(--primary);
|
||||
padding: 0;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
transition: background var(--transition-fast);
|
||||
}
|
||||
|
||||
.upload-category-title {
|
||||
.extension-tag-remove:hover {
|
||||
background: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.extension-tag-remove svg {
|
||||
stroke: white;
|
||||
}
|
||||
|
||||
/* Add Extension Group */
|
||||
.extension-add-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.extension-add-group label {
|
||||
display: block;
|
||||
font-size: var(--text-sm);
|
||||
font-weight: var(--font-medium);
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.upload-category-types {
|
||||
padding: 0.75rem 1rem;
|
||||
.extension-input-row {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.extension-input {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
background: var(--bg-input);
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: var(--radius-lg);
|
||||
color: var(--text-primary);
|
||||
font-size: var(--text-sm);
|
||||
font-family: var(--font-mono, monospace);
|
||||
}
|
||||
|
||||
.extension-input:focus {
|
||||
border-color: var(--primary);
|
||||
outline: none;
|
||||
box-shadow: var(--shadow-focus);
|
||||
}
|
||||
|
||||
.extension-input::placeholder {
|
||||
color: var(--text-muted);
|
||||
font-family: var(--font-primary);
|
||||
}
|
||||
|
||||
/* Suggestions */
|
||||
.extension-suggestions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 1rem;
|
||||
background: var(--bg-secondary);
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
.extension-suggestions-label {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-secondary);
|
||||
font-weight: var(--font-medium);
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.extension-suggestions-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.upload-type-tag {
|
||||
display: inline-block;
|
||||
.extension-suggestion {
|
||||
padding: 4px 10px;
|
||||
background: var(--primary-light);
|
||||
color: var(--primary);
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px dashed var(--border-default);
|
||||
border-radius: var(--radius-full);
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--text-xs);
|
||||
font-weight: var(--font-medium);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.extension-suggestion:hover {
|
||||
background: var(--primary-light);
|
||||
border-color: var(--primary);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.extension-no-suggestions {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Upload Actions */
|
||||
@ -515,6 +597,30 @@
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
/* Passwort-Input Gruppe */
|
||||
.password-input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.password-input-group input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.password-input-group .btn {
|
||||
padding: 0.5rem;
|
||||
min-width: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.password-input-group .btn svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.admin-header {
|
||||
padding: 1rem;
|
||||
|
||||
555
frontend/css/coding.css
Normale Datei
555
frontend/css/coding.css
Normale Datei
@ -0,0 +1,555 @@
|
||||
/**
|
||||
* TASKMATE - Coding Tab Styles
|
||||
* ============================
|
||||
* Styling für die Coding-Verzeichnis-Verwaltung
|
||||
*/
|
||||
|
||||
/* View Container */
|
||||
.view-coding {
|
||||
padding: 1.5rem;
|
||||
max-width: 1600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.coding-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.coding-header-centered {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.coding-header h2 {
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* Grid Layout */
|
||||
.coding-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
/* Kachel */
|
||||
.coding-tile {
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow-sm);
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.coding-tile:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
/* Farbbalken oben */
|
||||
.coding-tile-color {
|
||||
height: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Tile Header */
|
||||
.coding-tile-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
padding: 1rem 1rem 0.5rem;
|
||||
}
|
||||
|
||||
.coding-tile-icon {
|
||||
font-size: 2rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.coding-tile-menu {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0.25rem;
|
||||
cursor: pointer;
|
||||
color: var(--text-muted);
|
||||
border-radius: 4px;
|
||||
transition: background 0.2s, color 0.2s;
|
||||
}
|
||||
|
||||
.coding-tile-menu:hover {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* Tile Content */
|
||||
.coding-tile-content {
|
||||
padding: 0 1rem;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.coding-tile-name {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.25rem;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.coding-tile-path {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
font-family: var(--font-mono, monospace);
|
||||
word-break: break-all;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.coding-tile-description {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary);
|
||||
margin-top: 0.5rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* CLAUDE.md Badge */
|
||||
.coding-tile-badge {
|
||||
display: inline-block;
|
||||
background: linear-gradient(135deg, #F59E0B, #D97706);
|
||||
color: white;
|
||||
font-size: 0.65rem;
|
||||
padding: 0.2rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
margin-top: 0.5rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
/* Git Status */
|
||||
.coding-tile-status {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem 1rem;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.git-branch-badge {
|
||||
background: var(--bg-tertiary);
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
font-family: var(--font-mono, monospace);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.git-status-badge {
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.git-status-badge.loading {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.git-status-badge.clean {
|
||||
background: rgba(16, 185, 129, 0.15);
|
||||
color: #10B981;
|
||||
}
|
||||
|
||||
.git-status-badge.dirty {
|
||||
background: rgba(245, 158, 11, 0.15);
|
||||
color: #F59E0B;
|
||||
}
|
||||
|
||||
.git-status-badge.error {
|
||||
background: rgba(239, 68, 68, 0.15);
|
||||
color: #EF4444;
|
||||
}
|
||||
|
||||
.git-status-badge.ahead {
|
||||
background: rgba(59, 130, 246, 0.15);
|
||||
color: #3B82F6;
|
||||
}
|
||||
|
||||
.git-status-badge.behind {
|
||||
background: rgba(139, 92, 246, 0.15);
|
||||
color: #8B5CF6;
|
||||
}
|
||||
|
||||
/* Action Buttons */
|
||||
.coding-tile-actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.btn-claude {
|
||||
flex: 1;
|
||||
background: linear-gradient(135deg, #F59E0B, #D97706);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.75rem;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s, transform 0.2s;
|
||||
}
|
||||
|
||||
.btn-claude:hover {
|
||||
opacity: 0.9;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-claude:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.btn-codex {
|
||||
flex: 1;
|
||||
background: linear-gradient(135deg, #10B981, #059669);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.75rem;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s, transform 0.2s;
|
||||
}
|
||||
|
||||
.btn-codex:hover {
|
||||
opacity: 0.9;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-codex:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* Git Actions */
|
||||
.coding-tile-git {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
padding: 0 1rem 1rem;
|
||||
border-top: 1px solid var(--border-color);
|
||||
padding-top: 0.75rem;
|
||||
}
|
||||
|
||||
.coding-tile-git .btn {
|
||||
flex: 1;
|
||||
font-size: 0.75rem;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
/* Empty State */
|
||||
.coding-empty {
|
||||
text-align: center;
|
||||
padding: 4rem 2rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.coding-empty .empty-icon {
|
||||
margin-bottom: 1rem;
|
||||
color: var(--text-muted);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.coding-empty .empty-icon svg {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
.coding-empty h3 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.coding-empty p {
|
||||
font-size: 0.875rem;
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Command Modal */
|
||||
.command-box {
|
||||
background: var(--bg-tertiary);
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.command-box code {
|
||||
font-family: var(--font-mono, monospace);
|
||||
font-size: 0.875rem;
|
||||
word-break: break-all;
|
||||
flex: 1;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* Color Presets */
|
||||
.color-presets {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.color-preset {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid transparent;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s, border-color 0.2s;
|
||||
}
|
||||
|
||||
.color-preset:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.color-preset.selected {
|
||||
border-color: var(--text-primary);
|
||||
box-shadow: 0 0 0 2px var(--bg-secondary);
|
||||
}
|
||||
|
||||
.color-picker-custom {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.color-picker-custom::-webkit-color-swatch-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.color-picker-custom::-webkit-color-swatch {
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
/* Gitea Section in Modal */
|
||||
.coding-gitea-section {
|
||||
margin-top: 1rem;
|
||||
padding: 1rem;
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.coding-gitea-section summary {
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.coding-gitea-section summary:hover {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.view-coding {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.coding-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.coding-header {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.coding-header h2 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.coding-header .btn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.coding-tile-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.coding-tile-git {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.coding-tile-git .btn {
|
||||
flex: 1 1 calc(50% - 0.25rem);
|
||||
}
|
||||
|
||||
.command-box {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.command-box code {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.coding-empty {
|
||||
padding: 2rem 1rem;
|
||||
}
|
||||
|
||||
.coding-tile-git .btn {
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* CLAUDE.md Textarea im Modal */
|
||||
#coding-claude-instructions {
|
||||
font-family: var(--font-mono, 'Consolas', 'Monaco', monospace);
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.5;
|
||||
resize: vertical;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
/* Hint unter Labels */
|
||||
.label-hint {
|
||||
font-weight: normal;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.form-hint {
|
||||
display: block;
|
||||
margin-top: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* CLAUDE.md Tabs */
|
||||
.claude-tabs {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.claude-tab {
|
||||
padding: 0.5rem 1rem;
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-secondary);
|
||||
border-radius: 6px 6px 0 0;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
transition: background 0.2s, color 0.2s, border-color 0.2s;
|
||||
}
|
||||
|
||||
.claude-tab:hover {
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.claude-tab.active {
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
border-bottom-color: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.claude-content {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.claude-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* CLAUDE.md Link */
|
||||
.claude-link-container {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.claude-link {
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem 1rem;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.claude-link:hover:not(:disabled) {
|
||||
background: var(--bg-secondary);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.claude-link:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.claude-icon {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.claude-text {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.claude-link:disabled .claude-text {
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* CLAUDE.md Modal */
|
||||
.claude-md-viewer {
|
||||
width: 100%;
|
||||
height: 70vh;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.claude-md-display {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: var(--bg-tertiary);
|
||||
padding: 1.5rem;
|
||||
font-family: var(--font-mono, 'Consolas', 'Monaco', monospace);
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
overflow-y: auto;
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
border: none;
|
||||
outline: none;
|
||||
resize: none;
|
||||
}
|
||||
@ -413,6 +413,14 @@
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
|
||||
/* Avatar Container für mehrere Avatare */
|
||||
.list-cell-assignee .avatar-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.list-cell-assignee .avatar {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
@ -424,6 +432,12 @@
|
||||
font-weight: var(--font-semibold);
|
||||
color: white;
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.list-cell-assignee .avatar:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.list-cell-assignee select {
|
||||
@ -440,6 +454,28 @@
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Hide assignee dropdown - show only avatars */
|
||||
.list-cell-assignee .assignee-select {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Empty avatar placeholder */
|
||||
.list-cell-assignee .avatar-empty {
|
||||
background: var(--border-color) !important;
|
||||
color: var(--text-muted);
|
||||
border: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
/* Show dropdown when editing */
|
||||
.list-cell-assignee.editing .assignee-select {
|
||||
display: block;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.list-cell-assignee.editing .avatar-container {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Empty State */
|
||||
.list-empty {
|
||||
display: flex;
|
||||
|
||||
472
frontend/css/mobile.css
Normale Datei
472
frontend/css/mobile.css
Normale Datei
@ -0,0 +1,472 @@
|
||||
/**
|
||||
* TASKMATE - Mobile Styles
|
||||
* ========================
|
||||
* Touch-optimierte Mobile-Erfahrung
|
||||
* Nur auf mobilen Breakpoints angewendet
|
||||
*/
|
||||
|
||||
/* ========================================
|
||||
DESKTOP: Mobile-Elemente verstecken
|
||||
======================================== */
|
||||
|
||||
@media (min-width: 769px) {
|
||||
.hamburger-btn,
|
||||
.mobile-menu,
|
||||
.mobile-menu-overlay,
|
||||
.swipe-indicator {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
MOBILE STYLES (max-width: 768px)
|
||||
======================================== */
|
||||
|
||||
@media (max-width: 768px) {
|
||||
|
||||
/* ========================================
|
||||
HAMBURGER BUTTON
|
||||
======================================== */
|
||||
|
||||
.hamburger-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
padding: 10px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
z-index: calc(var(--z-modal) + 10);
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.hamburger-line {
|
||||
display: block;
|
||||
width: 24px;
|
||||
height: 2px;
|
||||
background: var(--text-primary);
|
||||
border-radius: 2px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.hamburger-line + .hamburger-line {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
/* Hamburger zu X Animation */
|
||||
.hamburger-btn.active .hamburger-line:nth-child(1) {
|
||||
transform: translateY(8px) rotate(45deg);
|
||||
}
|
||||
|
||||
.hamburger-btn.active .hamburger-line:nth-child(2) {
|
||||
opacity: 0;
|
||||
transform: scaleX(0);
|
||||
}
|
||||
|
||||
.hamburger-btn.active .hamburger-line:nth-child(3) {
|
||||
transform: translateY(-8px) rotate(-45deg);
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
MOBILE SLIDE-IN MENU
|
||||
======================================== */
|
||||
|
||||
.mobile-menu {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 280px;
|
||||
max-width: 85vw;
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
background: var(--bg-card);
|
||||
box-shadow: var(--shadow-xl);
|
||||
z-index: var(--z-modal);
|
||||
transform: translateX(-100%);
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.mobile-menu.open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
/* Overlay */
|
||||
.mobile-menu-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: var(--overlay-bg);
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
z-index: calc(var(--z-modal) - 1);
|
||||
transition: opacity 0.3s ease, visibility 0.3s ease;
|
||||
}
|
||||
|
||||
.mobile-menu-overlay.visible {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
/* Menu Header */
|
||||
.mobile-menu-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: var(--spacing-4);
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mobile-menu-title {
|
||||
font-size: var(--text-lg);
|
||||
font-weight: var(--font-bold);
|
||||
color: var(--primary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.mobile-menu-close {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
border-radius: var(--radius-md);
|
||||
transition: background 0.2s;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.mobile-menu-close:hover,
|
||||
.mobile-menu-close:active {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
/* Menu Sections */
|
||||
.mobile-menu-section {
|
||||
padding: var(--spacing-4);
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
.mobile-menu-label {
|
||||
display: block;
|
||||
font-size: var(--text-xs);
|
||||
font-weight: var(--font-semibold);
|
||||
color: var(--text-tertiary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
margin-bottom: var(--spacing-2);
|
||||
}
|
||||
|
||||
/* Project Select */
|
||||
.mobile-project-select {
|
||||
width: 100%;
|
||||
padding: var(--spacing-3);
|
||||
font-size: var(--text-base);
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--bg-input);
|
||||
color: var(--text-primary);
|
||||
font-family: var(--font-primary);
|
||||
}
|
||||
|
||||
/* Navigation */
|
||||
.mobile-menu-nav {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-1);
|
||||
}
|
||||
|
||||
.mobile-nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-3);
|
||||
padding: var(--spacing-3) var(--spacing-4);
|
||||
font-size: var(--text-base);
|
||||
font-weight: var(--font-medium);
|
||||
color: var(--text-secondary);
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mobile-nav-item:hover,
|
||||
.mobile-nav-item:active {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.mobile-nav-item.active {
|
||||
background: var(--primary-light);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.mobile-nav-item svg {
|
||||
flex-shrink: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
/* User Section */
|
||||
.mobile-menu-user {
|
||||
margin-top: auto;
|
||||
padding: var(--spacing-4);
|
||||
border-top: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
.mobile-user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-3);
|
||||
margin-bottom: var(--spacing-4);
|
||||
}
|
||||
|
||||
.mobile-user-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: var(--radius-full);
|
||||
background: var(--primary);
|
||||
color: var(--text-inverse);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: var(--font-semibold);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mobile-user-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.mobile-user-name {
|
||||
font-weight: var(--font-semibold);
|
||||
color: var(--text-primary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.mobile-user-role {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.mobile-menu-btn {
|
||||
width: 100%;
|
||||
padding: var(--spacing-3);
|
||||
font-size: var(--text-sm);
|
||||
font-weight: var(--font-medium);
|
||||
background: var(--bg-tertiary);
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
margin-bottom: var(--spacing-2);
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.mobile-menu-btn:hover,
|
||||
.mobile-menu-btn:active {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.mobile-menu-btn-danger {
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
.mobile-menu-btn-danger:hover,
|
||||
.mobile-menu-btn-danger:active {
|
||||
background: var(--error-bg);
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
HEADER ANPASSUNGEN
|
||||
======================================== */
|
||||
|
||||
/* Desktop-Navigation verstecken */
|
||||
.header-center .view-tabs {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.header-left .project-selector {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Header Layout anpassen */
|
||||
.header-left {
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
TOUCH DRAG & DROP FEEDBACK
|
||||
======================================== */
|
||||
|
||||
.task-card.touch-dragging {
|
||||
transform: scale(1.03);
|
||||
box-shadow: var(--shadow-xl);
|
||||
opacity: 0.95;
|
||||
z-index: 1000;
|
||||
transition: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.task-card.touch-drag-placeholder {
|
||||
opacity: 0.3;
|
||||
border: 2px dashed var(--border-default);
|
||||
}
|
||||
|
||||
.column-body.touch-drag-over {
|
||||
background: var(--primary-light);
|
||||
border: 2px dashed var(--primary);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
SWIPE INDIKATOREN
|
||||
======================================== */
|
||||
|
||||
.swipe-indicator {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 40px;
|
||||
height: 80px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--overlay-bg);
|
||||
color: var(--text-inverse);
|
||||
z-index: var(--z-tooltip);
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.swipe-indicator.left {
|
||||
left: 0;
|
||||
border-radius: 0 var(--radius-lg) var(--radius-lg) 0;
|
||||
}
|
||||
|
||||
.swipe-indicator.right {
|
||||
right: 0;
|
||||
border-radius: var(--radius-lg) 0 0 var(--radius-lg);
|
||||
}
|
||||
|
||||
.swipe-indicator.visible {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.swipe-indicator svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
BOARD VIEW - HORIZONTAL SCROLLING
|
||||
======================================== */
|
||||
|
||||
.view-board .board {
|
||||
scroll-snap-type: x mandatory;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.view-board .column {
|
||||
scroll-snap-align: start;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
PREVENT TEXT SELECTION DURING GESTURES
|
||||
======================================== */
|
||||
|
||||
.is-swiping,
|
||||
.is-swiping *,
|
||||
.is-touch-dragging,
|
||||
.is-touch-dragging * {
|
||||
user-select: none !important;
|
||||
-webkit-user-select: none !important;
|
||||
-webkit-touch-callout: none !important;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
BODY SCROLL LOCK
|
||||
======================================== */
|
||||
|
||||
body.mobile-menu-open {
|
||||
overflow: hidden;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
TOUCH-FREUNDLICHE ELEMENTE
|
||||
======================================== */
|
||||
|
||||
/* Groessere Touch-Targets */
|
||||
.calendar-day {
|
||||
min-height: 70px;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
/* Task-Karten */
|
||||
.task-card {
|
||||
touch-action: pan-y;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
min-height: 44px;
|
||||
min-width: 44px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
EXTRA SMALL MOBILE (max 480px)
|
||||
======================================== */
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.mobile-menu {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.hamburger-btn {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.hamburger-line {
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.hamburger-line + .hamburger-line {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.hamburger-btn.active .hamburger-line:nth-child(1) {
|
||||
transform: translateY(7px) rotate(45deg);
|
||||
}
|
||||
|
||||
.hamburger-btn.active .hamburger-line:nth-child(3) {
|
||||
transform: translateY(-7px) rotate(-45deg);
|
||||
}
|
||||
}
|
||||
@ -25,8 +25,10 @@
|
||||
<link rel="stylesheet" href="css/proposals.css">
|
||||
<link rel="stylesheet" href="css/notifications.css">
|
||||
<link rel="stylesheet" href="css/gitea.css">
|
||||
<link rel="stylesheet" href="css/coding.css">
|
||||
<link rel="stylesheet" href="css/knowledge.css">
|
||||
<link rel="stylesheet" href="css/responsive.css">
|
||||
<link rel="stylesheet" href="css/mobile.css">
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" type="image/svg+xml" href="assets/icons/task.svg">
|
||||
@ -102,98 +104,29 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dateitypen nach Kategorien -->
|
||||
<div class="admin-upload-types">
|
||||
<h3>Erlaubte Dateiformate</h3>
|
||||
<!-- Erlaubte Dateiendungen -->
|
||||
<div class="admin-upload-extensions">
|
||||
<h3>Erlaubte Dateiendungen</h3>
|
||||
|
||||
<!-- Bildformate -->
|
||||
<div class="upload-category" data-category="images">
|
||||
<div class="upload-category-header">
|
||||
<label class="upload-category-toggle">
|
||||
<input type="checkbox" id="upload-cat-images" checked>
|
||||
<span class="upload-category-title">Bildformate</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="upload-category-types">
|
||||
<span class="upload-type-tag" data-type="image/jpeg">JPEG</span>
|
||||
<span class="upload-type-tag" data-type="image/png">PNG</span>
|
||||
<span class="upload-type-tag" data-type="image/gif">GIF</span>
|
||||
<span class="upload-type-tag" data-type="image/webp">WebP</span>
|
||||
<span class="upload-type-tag" data-type="image/svg+xml">SVG</span>
|
||||
<!-- Aktive Endungen als Tags -->
|
||||
<div id="extension-tags" class="extension-tags">
|
||||
<!-- Tags werden dynamisch gerendert -->
|
||||
</div>
|
||||
|
||||
<!-- Neue Endung hinzufügen -->
|
||||
<div class="extension-add-group">
|
||||
<label for="extension-input">Neue Endung hinzufügen</label>
|
||||
<div class="extension-input-row">
|
||||
<input type="text" id="extension-input" class="extension-input" placeholder="z.B. xlsx" maxlength="10">
|
||||
<button type="button" id="btn-add-extension" class="btn btn-secondary">+ Hinzufügen</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dokumentformate -->
|
||||
<div class="upload-category" data-category="documents">
|
||||
<div class="upload-category-header">
|
||||
<label class="upload-category-toggle">
|
||||
<input type="checkbox" id="upload-cat-documents" checked>
|
||||
<span class="upload-category-title">Dokumentformate</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="upload-category-types">
|
||||
<span class="upload-type-tag" data-type="application/pdf">PDF</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Office-Formate -->
|
||||
<div class="upload-category" data-category="office">
|
||||
<div class="upload-category-header">
|
||||
<label class="upload-category-toggle">
|
||||
<input type="checkbox" id="upload-cat-office" checked>
|
||||
<span class="upload-category-title">Office-Formate</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="upload-category-types">
|
||||
<span class="upload-type-tag" data-type="application/msword">DOC</span>
|
||||
<span class="upload-type-tag" data-type="application/vnd.openxmlformats-officedocument.wordprocessingml.document">DOCX</span>
|
||||
<span class="upload-type-tag" data-type="application/vnd.ms-excel">XLS</span>
|
||||
<span class="upload-type-tag" data-type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet">XLSX</span>
|
||||
<span class="upload-type-tag" data-type="application/vnd.ms-powerpoint">PPT</span>
|
||||
<span class="upload-type-tag" data-type="application/vnd.openxmlformats-officedocument.presentationml.presentation">PPTX</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Textformate -->
|
||||
<div class="upload-category" data-category="text">
|
||||
<div class="upload-category-header">
|
||||
<label class="upload-category-toggle">
|
||||
<input type="checkbox" id="upload-cat-text" checked>
|
||||
<span class="upload-category-title">Textformate</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="upload-category-types">
|
||||
<span class="upload-type-tag" data-type="text/plain">TXT</span>
|
||||
<span class="upload-type-tag" data-type="text/csv">CSV</span>
|
||||
<span class="upload-type-tag" data-type="text/markdown">Markdown</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Archivformate -->
|
||||
<div class="upload-category" data-category="archives">
|
||||
<div class="upload-category-header">
|
||||
<label class="upload-category-toggle">
|
||||
<input type="checkbox" id="upload-cat-archives" checked>
|
||||
<span class="upload-category-title">Archivformate</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="upload-category-types">
|
||||
<span class="upload-type-tag" data-type="application/zip">ZIP</span>
|
||||
<span class="upload-type-tag" data-type="application/x-rar-compressed">RAR</span>
|
||||
<span class="upload-type-tag" data-type="application/x-7z-compressed">7Z</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Datenformate -->
|
||||
<div class="upload-category" data-category="data">
|
||||
<div class="upload-category-header">
|
||||
<label class="upload-category-toggle">
|
||||
<input type="checkbox" id="upload-cat-data" checked>
|
||||
<span class="upload-category-title">Datenformate</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="upload-category-types">
|
||||
<span class="upload-type-tag" data-type="application/json">JSON</span>
|
||||
<!-- Vorschläge -->
|
||||
<div class="extension-suggestions">
|
||||
<span class="extension-suggestions-label">Vorschläge:</span>
|
||||
<div id="extension-suggestions-list" class="extension-suggestions-list">
|
||||
<!-- Vorschläge werden dynamisch gerendert -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -212,6 +145,13 @@
|
||||
<!-- Header -->
|
||||
<header class="header">
|
||||
<div class="header-left">
|
||||
<!-- Hamburger Menu Button (Mobile) -->
|
||||
<button id="hamburger-btn" class="hamburger-btn" aria-label="Menu" aria-expanded="false">
|
||||
<span class="hamburger-line"></span>
|
||||
<span class="hamburger-line"></span>
|
||||
<span class="hamburger-line"></span>
|
||||
</button>
|
||||
|
||||
<h1 class="logo">TaskMate</h1>
|
||||
|
||||
<!-- Project Selector -->
|
||||
@ -235,7 +175,7 @@
|
||||
<button class="view-tab" data-view="list">Liste</button>
|
||||
<button class="view-tab" data-view="calendar">Kalender</button>
|
||||
<button class="view-tab" data-view="proposals">Genehmigung</button>
|
||||
<button class="view-tab" data-view="gitea">Gitea</button>
|
||||
<button class="view-tab" data-view="coding">Coding</button>
|
||||
<button class="view-tab" data-view="knowledge">Wissen</button>
|
||||
</nav>
|
||||
</div>
|
||||
@ -509,383 +449,29 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Gitea View -->
|
||||
<div id="view-gitea" class="view view-gitea hidden">
|
||||
|
||||
<!-- Modus-Schalter -->
|
||||
<div id="gitea-mode-switch" class="gitea-mode-switch">
|
||||
<button class="gitea-mode-btn active" data-mode="server">
|
||||
<svg viewBox="0 0 24 24" width="18" height="18"><rect x="2" y="2" width="20" height="8" rx="2" stroke="currentColor" stroke-width="2" fill="none"/><rect x="2" y="14" width="20" height="8" rx="2" stroke="currentColor" stroke-width="2" fill="none"/><circle cx="6" cy="6" r="1" fill="currentColor"/><circle cx="6" cy="18" r="1" fill="currentColor"/></svg>
|
||||
Server-Anwendung
|
||||
</button>
|
||||
<button class="gitea-mode-btn" data-mode="project">
|
||||
<svg viewBox="0 0 24 24" width="18" height="18"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" stroke="currentColor" stroke-width="2" fill="none"/></svg>
|
||||
Projekt-Repository
|
||||
<!-- Coding View -->
|
||||
<div id="view-coding" class="view view-coding hidden">
|
||||
<!-- Header -->
|
||||
<div class="coding-header coding-header-centered">
|
||||
<button id="add-coding-directory-btn" class="btn btn-primary">
|
||||
<svg viewBox="0 0 24 24" width="18" height="18"><path d="M12 5v14M5 12h14" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/></svg>
|
||||
Anwendung hinzufügen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Server-Modus Ansicht -->
|
||||
<div id="gitea-server-mode" class="gitea-section">
|
||||
|
||||
<!-- Server Repository Info Header -->
|
||||
<div class="gitea-repo-header">
|
||||
<div class="repo-info">
|
||||
<h2 id="server-repo-name">
|
||||
<svg viewBox="0 0 24 24" width="24" height="24"><rect x="2" y="2" width="20" height="8" rx="2" stroke="currentColor" stroke-width="2" fill="none"/><rect x="2" y="14" width="20" height="8" rx="2" stroke="currentColor" stroke-width="2" fill="none"/><circle cx="6" cy="6" r="1" fill="currentColor"/><circle cx="6" cy="18" r="1" fill="currentColor"/></svg>
|
||||
<span>TaskMate Server</span>
|
||||
</h2>
|
||||
<a id="server-repo-url" class="repo-url" href="#" target="_blank"></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Server-Pfad Anzeige -->
|
||||
<div class="gitea-local-path">
|
||||
<span class="path-label">Server-Pfad:</span>
|
||||
<code id="server-local-path-display">/home/claude-dev/TaskMate</code>
|
||||
</div>
|
||||
|
||||
<!-- Server Git-Status Panel -->
|
||||
<div id="server-status-section" class="gitea-status-panel">
|
||||
<div class="status-grid">
|
||||
<div class="status-item">
|
||||
<span class="status-label">Branch</span>
|
||||
<div class="branch-select-group">
|
||||
<select id="server-branch-select" class="branch-select">
|
||||
<!-- Branches dynamisch -->
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span class="status-label">Status</span>
|
||||
<span id="server-status-indicator" class="status-badge">Prüfe...</span>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span class="status-label">Änderungen</span>
|
||||
<span id="server-changes-count" class="changes-count">0</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Server Git-Operationen -->
|
||||
<div id="server-operations-section" class="gitea-operations-panel">
|
||||
<h3>Git-Operationen</h3>
|
||||
<div class="operations-grid">
|
||||
<button id="btn-server-fetch" class="btn btn-secondary operation-btn">
|
||||
<svg viewBox="0 0 24 24" width="18" height="18"><path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" stroke="currentColor" stroke-width="2" fill="none"/><path d="M3 3v5h5" stroke="currentColor" stroke-width="2" fill="none"/><path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16" stroke="currentColor" stroke-width="2" fill="none"/><path d="M21 21v-5h-5" stroke="currentColor" stroke-width="2" fill="none"/></svg>
|
||||
Fetch
|
||||
</button>
|
||||
<button id="btn-server-pull" class="btn btn-secondary operation-btn">
|
||||
<svg viewBox="0 0 24 24" width="18" height="18"><path d="M12 5v14M19 12l-7 7-7-7" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
Pull
|
||||
</button>
|
||||
<button id="btn-server-push" class="btn btn-primary operation-btn">
|
||||
<svg viewBox="0 0 24 24" width="18" height="18"><path d="M12 19V5M5 12l7-7 7 7" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
Push
|
||||
</button>
|
||||
<button id="btn-server-commit" class="btn btn-secondary operation-btn">
|
||||
<svg viewBox="0 0 24 24" width="18" height="18"><circle cx="12" cy="12" r="4" stroke="currentColor" stroke-width="2" fill="none"/><path d="M12 2v6M12 16v6M2 12h6M16 12h6" stroke="currentColor" stroke-width="2" fill="none"/></svg>
|
||||
Commit
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Server Änderungen-Liste -->
|
||||
<div id="server-changes-section" class="gitea-changes-panel hidden">
|
||||
<h3>Geänderte Dateien</h3>
|
||||
<div id="server-changes-list" class="changes-list">
|
||||
<!-- Dynamisch gefüllt -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Server Commit-Historie -->
|
||||
<div id="server-commits-section" class="gitea-commits-panel">
|
||||
<div class="commits-header">
|
||||
<h3>Letzte Commits</h3>
|
||||
<button id="btn-server-clear-commits" class="btn btn-small btn-secondary" title="Alle aus Anzeige entfernen">
|
||||
Alle ausblenden
|
||||
</button>
|
||||
</div>
|
||||
<div id="server-commits-list" class="commits-list">
|
||||
<!-- Dynamisch gefüllt -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Grid für Kacheln -->
|
||||
<div id="coding-grid" class="coding-grid">
|
||||
<!-- Kacheln werden per JS gerendert -->
|
||||
</div>
|
||||
|
||||
<!-- Projekt-Modus: Browser-Upload Ansicht -->
|
||||
<div id="gitea-browser-upload" class="gitea-section hidden">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="gitea-config-header">
|
||||
<h2>
|
||||
<svg viewBox="0 0 24 24" width="24" height="24"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M17 8l-5-5-5 5M12 3v12" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
Lokales Verzeichnis hochladen
|
||||
</h2>
|
||||
<p>Wählen Sie ein Verzeichnis von Ihrem Computer und pushen Sie es direkt ins Gitea.</p>
|
||||
<!-- Empty State -->
|
||||
<div id="coding-empty" class="coding-empty hidden">
|
||||
<div class="empty-icon">
|
||||
<svg viewBox="0 0 24 24" width="64" height="64"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" stroke="currentColor" stroke-width="2" fill="none"/></svg>
|
||||
</div>
|
||||
|
||||
<!-- Browser-Kompatibilität Hinweis -->
|
||||
<div id="browser-upload-compat" class="browser-compat-notice hidden">
|
||||
<svg viewBox="0 0 24 24" width="20" height="20"><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" fill="none"/><path d="M12 8v4M12 16h.01" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/></svg>
|
||||
<span>Die Verzeichnis-Auswahl funktioniert nur in Chrome, Edge oder Opera. In anderen Browsern können Sie Dateien per Drag & Drop hochladen.</span>
|
||||
</div>
|
||||
|
||||
<!-- Schritt 1: Repository auswählen -->
|
||||
<div class="upload-step">
|
||||
<div class="step-header">
|
||||
<span class="step-number">1</span>
|
||||
<span class="step-title">Ziel-Repository auswählen</span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="gitea-repo-select-group">
|
||||
<select id="browser-upload-repo-select" class="form-control">
|
||||
<option value="">-- Repository wählen --</option>
|
||||
</select>
|
||||
<button type="button" id="btn-refresh-upload-repos" class="btn btn-icon" title="Repositories aktualisieren">
|
||||
<svg viewBox="0 0 24 24" width="18" height="18"><path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" stroke="currentColor" stroke-width="2" fill="none"/><path d="M3 3v5h5" stroke="currentColor" stroke-width="2" fill="none"/><path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16" stroke="currentColor" stroke-width="2" fill="none"/><path d="M21 21v-5h-5" stroke="currentColor" stroke-width="2" fill="none"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="browser-upload-branch">Ziel-Branch</label>
|
||||
<input type="text" id="browser-upload-branch" class="form-control" value="main" placeholder="main">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Schritt 2: Verzeichnis auswählen -->
|
||||
<div class="upload-step">
|
||||
<div class="step-header">
|
||||
<span class="step-number">2</span>
|
||||
<span class="step-title">Verzeichnis auswählen</span>
|
||||
</div>
|
||||
<div class="directory-picker">
|
||||
<button type="button" id="btn-select-directory" class="btn btn-secondary btn-lg">
|
||||
<svg viewBox="0 0 24 24" width="20" height="20"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" stroke="currentColor" stroke-width="2" fill="none"/></svg>
|
||||
Verzeichnis auswählen
|
||||
</button>
|
||||
<div id="drop-zone" class="drop-zone hidden">
|
||||
<svg viewBox="0 0 24 24" width="48" height="48"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M17 8l-5-5-5 5M12 3v12" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
<p>Oder Verzeichnis hierher ziehen</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Schritt 3: Datei-Vorschau (erscheint nach Auswahl) -->
|
||||
<div id="upload-preview-section" class="upload-step hidden">
|
||||
<div class="step-header">
|
||||
<span class="step-number">3</span>
|
||||
<span class="step-title">Ausgewählte Dateien</span>
|
||||
<span id="upload-file-count" class="file-count">0 Dateien</span>
|
||||
</div>
|
||||
<div id="upload-files-list" class="upload-files-list">
|
||||
<!-- Dynamisch gefüllt -->
|
||||
</div>
|
||||
<div class="excluded-info">
|
||||
<small>Automatisch ausgeschlossen: .git, node_modules, __pycache__, .env, *.log</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Schritt 4: Commit und Push -->
|
||||
<div id="upload-commit-section" class="upload-step hidden">
|
||||
<div class="step-header">
|
||||
<span class="step-number">4</span>
|
||||
<span class="step-title">Commit erstellen und pushen</span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="browser-upload-commit-message">Commit-Nachricht</label>
|
||||
<textarea id="browser-upload-commit-message" class="form-control" rows="2" placeholder="Beschreiben Sie Ihre Änderungen..."></textarea>
|
||||
</div>
|
||||
<div class="upload-actions">
|
||||
<button type="button" id="btn-cancel-upload" class="btn btn-secondary">
|
||||
Abbrechen
|
||||
</button>
|
||||
<button type="button" id="btn-execute-upload" class="btn btn-primary">
|
||||
<svg viewBox="0 0 24 24" width="18" height="18"><path d="M12 19V5M5 12l7-7 7 7" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
Commit & Push
|
||||
</button>
|
||||
</div>
|
||||
<!-- Progress Bar -->
|
||||
<div id="upload-progress-container" class="upload-progress hidden">
|
||||
<div class="progress-bar">
|
||||
<div id="upload-progress-bar" class="progress-fill"></div>
|
||||
</div>
|
||||
<span id="upload-progress-text">0%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>Keine Anwendungen</h3>
|
||||
<p>Füge deine erste Server-Anwendung hinzu, um mit Claude oder Codex zu arbeiten.</p>
|
||||
</div>
|
||||
|
||||
<!-- Projekt-Modus: Kein Projekt (altes Element, jetzt versteckt) -->
|
||||
<div id="gitea-no-project" class="gitea-empty-state hidden">
|
||||
<svg viewBox="0 0 24 24" width="64" height="64"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
<h3>Kein Projekt ausgewählt</h3>
|
||||
<p>Wählen Sie ein Projekt aus, um die Git-Integration zu konfigurieren.</p>
|
||||
</div>
|
||||
|
||||
<!-- Konfiguration (wenn nicht verknüpft) -->
|
||||
<div id="gitea-config-section" class="gitea-section hidden">
|
||||
<div class="gitea-config-header">
|
||||
<h2>Repository-Konfiguration</h2>
|
||||
<p>Verknüpfen Sie dieses Projekt mit einem Git-Repository</p>
|
||||
</div>
|
||||
|
||||
<!-- Gitea-Verbindungsstatus -->
|
||||
<div id="gitea-connection-status" class="gitea-connection-status">
|
||||
<span class="status-indicator"></span>
|
||||
<span class="status-text">Prüfe Verbindung...</span>
|
||||
</div>
|
||||
|
||||
<form id="gitea-config-form" class="gitea-config-form">
|
||||
<!-- Repository-Auswahl -->
|
||||
<div class="form-group">
|
||||
<label for="gitea-repo-select">Bestehendes Repository auswählen</label>
|
||||
<div class="gitea-repo-select-group">
|
||||
<select id="gitea-repo-select" class="form-control">
|
||||
<option value="">-- Repository wählen --</option>
|
||||
</select>
|
||||
<button type="button" id="btn-refresh-repos" class="btn btn-icon" title="Repositories aktualisieren">
|
||||
<svg viewBox="0 0 24 24" width="18" height="18"><path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" stroke="currentColor" stroke-width="2" fill="none"/><path d="M3 3v5h5" stroke="currentColor" stroke-width="2" fill="none"/><path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16" stroke="currentColor" stroke-width="2" fill="none"/><path d="M21 21v-5h-5" stroke="currentColor" stroke-width="2" fill="none"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gitea-divider">
|
||||
<span>oder</span>
|
||||
</div>
|
||||
|
||||
<!-- Neues Repository erstellen -->
|
||||
<div class="form-group">
|
||||
<button type="button" id="btn-create-repo" class="btn btn-secondary btn-block">
|
||||
<svg viewBox="0 0 24 24" width="18" height="18"><path d="M12 5v14M5 12h14" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/></svg>
|
||||
Neues Repository erstellen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="gitea-divider"></div>
|
||||
|
||||
<!-- Lokaler Pfad -->
|
||||
<div class="form-group">
|
||||
<label for="local-path-input">Lokaler Pfad (wo Claude Code arbeitet)</label>
|
||||
<input type="text" id="local-path-input" class="form-control"
|
||||
placeholder="z.B. D:\Projekte\MeinProjekt">
|
||||
<span id="path-validation-result" class="form-hint"></span>
|
||||
</div>
|
||||
|
||||
<!-- Default Branch -->
|
||||
<div class="form-group">
|
||||
<label for="default-branch-input">Standard-Branch</label>
|
||||
<input type="text" id="default-branch-input" class="form-control"
|
||||
value="main" placeholder="main">
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary" id="btn-save-config">
|
||||
<svg viewBox="0 0 24 24" width="18" height="18"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z" stroke="currentColor" stroke-width="2" fill="none"/><path d="M17 21v-8H7v8M7 3v5h8" stroke="currentColor" stroke-width="2" fill="none"/></svg>
|
||||
Konfiguration speichern
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Hauptansicht (wenn verknüpft) -->
|
||||
<div id="gitea-main-section" class="gitea-section hidden">
|
||||
|
||||
<!-- Repository-Info-Header -->
|
||||
<div class="gitea-repo-header">
|
||||
<div class="repo-info">
|
||||
<h2 id="gitea-repo-name">
|
||||
<svg viewBox="0 0 24 24" width="24" height="24"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22" stroke="currentColor" stroke-width="2" fill="none"/></svg>
|
||||
<span>Repository</span>
|
||||
</h2>
|
||||
<a id="gitea-repo-url" class="repo-url" href="#" target="_blank"></a>
|
||||
</div>
|
||||
<div class="repo-actions">
|
||||
<button id="btn-edit-config" class="btn btn-icon" title="Konfiguration bearbeiten">
|
||||
<svg viewBox="0 0 24 24" width="18" height="18"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" stroke="currentColor" stroke-width="2" fill="none"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" stroke="currentColor" stroke-width="2" fill="none"/></svg>
|
||||
</button>
|
||||
<button id="btn-remove-config" class="btn btn-icon btn-danger-hover" title="Konfiguration entfernen">
|
||||
<svg viewBox="0 0 24 24" width="18" height="18"><path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" stroke="currentColor" stroke-width="2" fill="none"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Lokaler Pfad Anzeige -->
|
||||
<div class="gitea-local-path">
|
||||
<span class="path-label">Lokaler Pfad:</span>
|
||||
<code id="gitea-local-path-display"></code>
|
||||
</div>
|
||||
|
||||
<!-- Git-Status Panel -->
|
||||
<div id="gitea-status-section" class="gitea-status-panel">
|
||||
<div class="status-grid">
|
||||
<div class="status-item">
|
||||
<span class="status-label">Branch</span>
|
||||
<div class="branch-select-group">
|
||||
<select id="branch-select" class="branch-select">
|
||||
<!-- Branches dynamisch -->
|
||||
</select>
|
||||
<button id="btn-rename-branch" class="btn btn-small btn-icon" title="Branch umbenennen">
|
||||
<svg viewBox="0 0 24 24" width="16" height="16"><path d="M17 3a2.85 2.85 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z" stroke="currentColor" stroke-width="2" fill="none"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span class="status-label">Status</span>
|
||||
<span id="git-status-indicator" class="status-badge">Prüfe...</span>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span class="status-label">Änderungen</span>
|
||||
<span id="git-changes-count" class="changes-count">0</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Git-Operationen -->
|
||||
<div id="gitea-operations-section" class="gitea-operations-panel">
|
||||
<h3>Git-Operationen</h3>
|
||||
<div class="operations-grid">
|
||||
<button id="btn-git-fetch" class="btn btn-secondary operation-btn">
|
||||
<svg viewBox="0 0 24 24" width="18" height="18"><path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" stroke="currentColor" stroke-width="2" fill="none"/><path d="M3 3v5h5" stroke="currentColor" stroke-width="2" fill="none"/><path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16" stroke="currentColor" stroke-width="2" fill="none"/><path d="M21 21v-5h-5" stroke="currentColor" stroke-width="2" fill="none"/></svg>
|
||||
Fetch
|
||||
</button>
|
||||
<button id="btn-git-pull" class="btn btn-secondary operation-btn">
|
||||
<svg viewBox="0 0 24 24" width="18" height="18"><path d="M12 5v14M19 12l-7 7-7-7" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
Pull
|
||||
</button>
|
||||
<button id="btn-git-push" class="btn btn-primary operation-btn">
|
||||
<svg viewBox="0 0 24 24" width="18" height="18"><path d="M12 19V5M5 12l7-7 7 7" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
Push
|
||||
</button>
|
||||
<button id="btn-git-commit" class="btn btn-secondary operation-btn">
|
||||
<svg viewBox="0 0 24 24" width="18" height="18"><circle cx="12" cy="12" r="4" stroke="currentColor" stroke-width="2" fill="none"/><path d="M12 2v6M12 16v6M2 12h6M16 12h6" stroke="currentColor" stroke-width="2" fill="none"/></svg>
|
||||
Commit
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Änderungen-Liste -->
|
||||
<div id="gitea-changes-section" class="gitea-changes-panel hidden">
|
||||
<h3>Geänderte Dateien</h3>
|
||||
<div id="git-changes-list" class="changes-list">
|
||||
<!-- Dynamisch gefüllt -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Commit-Historie -->
|
||||
<div id="gitea-commits-section" class="gitea-commits-panel">
|
||||
<div class="commits-header">
|
||||
<h3>Letzte Commits</h3>
|
||||
<button id="btn-clear-commits" class="btn btn-small btn-secondary" title="Alle aus Anzeige entfernen">
|
||||
Alle ausblenden
|
||||
</button>
|
||||
</div>
|
||||
<div id="git-commits-list" class="commits-list">
|
||||
<!-- Dynamisch gefüllt -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Knowledge View (Wissensmanagement) -->
|
||||
@ -1389,7 +975,21 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="user-password">Passwort <span id="password-hint" class="form-hint">(automatisch generiert)</span></label>
|
||||
<input type="text" id="user-password" minlength="8" readonly>
|
||||
<div class="password-input-group">
|
||||
<input type="text" id="user-password" minlength="8" readonly>
|
||||
<button type="button" id="edit-password-btn" class="btn btn-secondary btn-sm" title="Passwort bearbeiten">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" stroke="currentColor" stroke-width="2" fill="none"/>
|
||||
<path d="m18.5 2.5 3 3L12 15l-4 1 1-4 9.5-9.5z" stroke="currentColor" stroke-width="2" fill="none"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button" id="generate-password-btn" class="btn btn-secondary btn-sm" title="Neues Passwort generieren">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M1 4v6h6" stroke="currentColor" stroke-width="2" fill="none"/>
|
||||
<path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10" stroke="currentColor" stroke-width="2" fill="none"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="user-role">Rolle</label>
|
||||
@ -1908,9 +1508,152 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Coding Directory Modal -->
|
||||
<div id="coding-modal" class="modal hidden">
|
||||
<div class="modal-content modal-medium">
|
||||
<div class="modal-header">
|
||||
<h3 id="coding-modal-title">Anwendung hinzufügen</h3>
|
||||
<button class="modal-close" aria-label="Schliessen">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label for="coding-name">Anwendungsname *</label>
|
||||
<input type="text" id="coding-name" class="form-control" placeholder="z.B. TaskMate" required>
|
||||
<small class="form-hint" id="coding-path-hint">Ordner: /home/claude-dev/<span id="coding-path-preview">...</span></small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="coding-description">Beschreibung</label>
|
||||
<textarea id="coding-description" class="form-control" rows="2" placeholder="Optionale Beschreibung..."></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Farbe</label>
|
||||
<div class="color-presets" id="coding-color-presets">
|
||||
<!-- Farb-Buttons werden per JS gerendert -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>CLAUDE.md</label>
|
||||
<div class="claude-link-container">
|
||||
<button type="button" id="coding-claude-link" class="claude-link" disabled>
|
||||
<span class="claude-icon">📄</span>
|
||||
<span class="claude-text">Keine CLAUDE.md vorhanden</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<details class="coding-gitea-section">
|
||||
<summary>Gitea-Repository verknüpfen (optional)</summary>
|
||||
<div class="form-group" style="margin-top: 1rem;">
|
||||
<label for="coding-gitea-repo">Repository</label>
|
||||
<select id="coding-gitea-repo" class="form-control">
|
||||
<option value="">-- Kein Repository --</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="coding-branch">Standard-Branch</label>
|
||||
<input type="text" id="coding-branch" class="form-control" value="main" placeholder="main">
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button id="coding-delete-btn" class="btn btn-danger hidden">Löschen</button>
|
||||
<button class="btn btn-secondary modal-cancel">Abbrechen</button>
|
||||
<button id="coding-save-btn" class="btn btn-primary">Speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Coding Command Modal -->
|
||||
<div id="coding-command-modal" class="modal hidden">
|
||||
<div class="modal-content modal-small">
|
||||
<div class="modal-header">
|
||||
<h3>Befehl ausführen</h3>
|
||||
<button class="modal-close" aria-label="Schliessen">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p id="coding-command-hint">Führe diesen Befehl in WSL aus:</p>
|
||||
<div class="command-box">
|
||||
<code id="coding-command-text"></code>
|
||||
<button id="coding-copy-command" class="btn btn-sm btn-secondary">Kopieren</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast Container -->
|
||||
<div id="toast-container" class="toast-container"></div>
|
||||
|
||||
<!-- Mobile Navigation Menu -->
|
||||
<nav id="mobile-menu" class="mobile-menu" aria-hidden="true">
|
||||
<div class="mobile-menu-header">
|
||||
<h2 class="mobile-menu-title">TaskMate</h2>
|
||||
<button id="mobile-menu-close" class="mobile-menu-close" aria-label="Menu schliessen">
|
||||
<svg viewBox="0 0 24 24" width="24" height="24">
|
||||
<path d="M18 6L6 18M6 6l12 12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mobile-menu-section">
|
||||
<label class="mobile-menu-label">Projekt</label>
|
||||
<select id="mobile-project-select" class="mobile-project-select">
|
||||
<!-- Populated by JS -->
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mobile-menu-section">
|
||||
<label class="mobile-menu-label">Ansicht</label>
|
||||
<div class="mobile-menu-nav">
|
||||
<button class="mobile-nav-item active" data-view="board">
|
||||
<svg viewBox="0 0 24 24" width="20" height="20"><rect x="3" y="3" width="7" height="9" rx="1" stroke="currentColor" stroke-width="2" fill="none"/><rect x="14" y="3" width="7" height="5" rx="1" stroke="currentColor" stroke-width="2" fill="none"/><rect x="14" y="12" width="7" height="9" rx="1" stroke="currentColor" stroke-width="2" fill="none"/><rect x="3" y="16" width="7" height="5" rx="1" stroke="currentColor" stroke-width="2" fill="none"/></svg>
|
||||
<span>Board</span>
|
||||
</button>
|
||||
<button class="mobile-nav-item" data-view="list">
|
||||
<svg viewBox="0 0 24 24" width="20" height="20"><path d="M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
|
||||
<span>Liste</span>
|
||||
</button>
|
||||
<button class="mobile-nav-item" data-view="calendar">
|
||||
<svg viewBox="0 0 24 24" width="20" height="20"><rect x="3" y="4" width="18" height="18" rx="2" stroke="currentColor" stroke-width="2" fill="none"/><line x1="16" y1="2" x2="16" y2="6" stroke="currentColor" stroke-width="2"/><line x1="8" y1="2" x2="8" y2="6" stroke="currentColor" stroke-width="2"/><line x1="3" y1="10" x2="21" y2="10" stroke="currentColor" stroke-width="2"/></svg>
|
||||
<span>Kalender</span>
|
||||
</button>
|
||||
<button class="mobile-nav-item" data-view="proposals">
|
||||
<svg viewBox="0 0 24 24" width="20" height="20"><path d="M9 11l3 3L22 4" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11" stroke="currentColor" stroke-width="2" fill="none"/></svg>
|
||||
<span>Genehmigung</span>
|
||||
</button>
|
||||
<button class="mobile-nav-item" data-view="coding">
|
||||
<svg viewBox="0 0 24 24" width="20" height="20"><polyline points="16 18 22 12 16 6" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/><polyline points="8 6 2 12 8 18" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
<span>Coding</span>
|
||||
</button>
|
||||
<button class="mobile-nav-item" data-view="knowledge">
|
||||
<svg viewBox="0 0 24 24" width="20" height="20"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20" stroke="currentColor" stroke-width="2" fill="none"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z" stroke="currentColor" stroke-width="2" fill="none"/></svg>
|
||||
<span>Wissen</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mobile-menu-section mobile-menu-user">
|
||||
<div class="mobile-user-info">
|
||||
<span id="mobile-user-avatar" class="mobile-user-avatar">U</span>
|
||||
<div class="mobile-user-details">
|
||||
<span id="mobile-user-name" class="mobile-user-name">Benutzer</span>
|
||||
<span id="mobile-user-role" class="mobile-user-role">Angemeldet</span>
|
||||
</div>
|
||||
</div>
|
||||
<button id="mobile-admin-btn" class="mobile-menu-btn hidden">Admin-Bereich</button>
|
||||
<button id="mobile-logout-btn" class="mobile-menu-btn mobile-menu-btn-danger">Abmelden</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Mobile Menu Overlay -->
|
||||
<div id="mobile-menu-overlay" class="mobile-menu-overlay"></div>
|
||||
|
||||
<!-- Swipe Indicators -->
|
||||
<div id="swipe-indicator-left" class="swipe-indicator left">
|
||||
<svg viewBox="0 0 24 24" width="24" height="24"><path d="M15 18l-6-6 6-6" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
</div>
|
||||
<div id="swipe-indicator-right" class="swipe-indicator right">
|
||||
<svg viewBox="0 0 24 24" width="24" height="24"><path d="M9 18l6-6-6-6" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
</div>
|
||||
|
||||
<!-- Onboarding Tour -->
|
||||
<div id="onboarding-overlay" class="onboarding-overlay hidden">
|
||||
<div id="onboarding-tooltip" class="onboarding-tooltip">
|
||||
@ -1934,6 +1677,21 @@
|
||||
<img id="lightbox-image" src="" alt="">
|
||||
</div>
|
||||
|
||||
<!-- CLAUDE.md Modal -->
|
||||
<div id="claude-md-modal" class="modal hidden">
|
||||
<div class="modal-content modal-large">
|
||||
<div class="modal-header">
|
||||
<h3>CLAUDE.md (Nur-Lesen)</h3>
|
||||
<button class="modal-close" aria-label="Schließen">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="claude-md-viewer">
|
||||
<pre id="claude-md-content" class="claude-md-display"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Socket.io Client -->
|
||||
<script src="/socket.io/socket.io.js"></script>
|
||||
|
||||
|
||||
@ -14,7 +14,17 @@ class AdminManager {
|
||||
this.users = [];
|
||||
this.currentEditUser = null;
|
||||
this.uploadSettings = null;
|
||||
this.allowedExtensions = ['pdf', 'docx', 'txt'];
|
||||
this.initialized = false;
|
||||
|
||||
// Vorschläge für häufige Dateiendungen
|
||||
this.extensionSuggestions = [
|
||||
'xlsx', 'pptx', 'doc', 'xls', 'ppt', // Office
|
||||
'png', 'jpg', 'gif', 'svg', 'webp', // Bilder
|
||||
'csv', 'json', 'xml', 'md', // Daten
|
||||
'zip', 'rar', '7z', // Archive
|
||||
'odt', 'ods', 'rtf' // OpenDocument
|
||||
];
|
||||
}
|
||||
|
||||
async init() {
|
||||
@ -52,7 +62,10 @@ class AdminManager {
|
||||
// Upload Settings Elements
|
||||
this.uploadMaxSizeInput = $('#upload-max-size');
|
||||
this.saveUploadSettingsBtn = $('#btn-save-upload-settings');
|
||||
this.uploadCategories = $$('.upload-category');
|
||||
this.extensionTagsContainer = $('#extension-tags');
|
||||
this.extensionInput = $('#extension-input');
|
||||
this.addExtensionBtn = $('#btn-add-extension');
|
||||
this.extensionSuggestionsList = $('#extension-suggestions-list');
|
||||
|
||||
this.bindEvents();
|
||||
this.initialized = true;
|
||||
@ -88,13 +101,20 @@ class AdminManager {
|
||||
// Upload Settings - Save Button
|
||||
this.saveUploadSettingsBtn?.addEventListener('click', () => this.saveUploadSettings());
|
||||
|
||||
// Upload Settings - Category Toggles
|
||||
this.uploadCategories?.forEach(category => {
|
||||
const checkbox = category.querySelector('input[type="checkbox"]');
|
||||
checkbox?.addEventListener('change', () => {
|
||||
this.toggleUploadCategory(category, checkbox.checked);
|
||||
});
|
||||
// Upload Settings - Add Extension
|
||||
this.addExtensionBtn?.addEventListener('click', () => this.addExtensionFromInput());
|
||||
|
||||
// Enter-Taste im Input-Feld
|
||||
this.extensionInput?.addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
this.addExtensionFromInput();
|
||||
}
|
||||
});
|
||||
|
||||
// Password-Buttons
|
||||
$('#edit-password-btn')?.addEventListener('click', () => this.togglePasswordEdit());
|
||||
$('#generate-password-btn')?.addEventListener('click', () => this.generatePassword());
|
||||
}
|
||||
|
||||
async loadUsers() {
|
||||
@ -222,8 +242,12 @@ class AdminManager {
|
||||
this.emailInput.value = user.email || '';
|
||||
this.emailInput.disabled = false;
|
||||
|
||||
// Passwort-Feld bei Bearbeitung ausblenden
|
||||
this.passwordInput.closest('.form-group').style.display = 'none';
|
||||
// Passwort-Feld für Bearbeitung vorbereiten
|
||||
this.passwordInput.closest('.form-group').style.display = 'block';
|
||||
this.passwordInput.value = '';
|
||||
this.passwordInput.placeholder = 'Neues Passwort (leer lassen = unverändert)';
|
||||
this.passwordInput.readOnly = true;
|
||||
this.passwordHint.textContent = '(optional - leer lassen für unverändert)';
|
||||
|
||||
this.roleSelect.value = user.role || 'user';
|
||||
|
||||
@ -381,6 +405,7 @@ class AdminManager {
|
||||
async loadUploadSettings() {
|
||||
try {
|
||||
this.uploadSettings = await api.getUploadSettings();
|
||||
this.allowedExtensions = this.uploadSettings.allowedExtensions || ['pdf', 'docx', 'txt'];
|
||||
this.renderUploadSettings();
|
||||
} catch (error) {
|
||||
console.error('Error loading upload settings:', error);
|
||||
@ -395,36 +420,108 @@ class AdminManager {
|
||||
this.uploadMaxSizeInput.value = this.uploadSettings.maxFileSizeMB || 15;
|
||||
}
|
||||
|
||||
// Kategorien setzen
|
||||
const categoryMap = {
|
||||
'images': 'upload-cat-images',
|
||||
'documents': 'upload-cat-documents',
|
||||
'office': 'upload-cat-office',
|
||||
'text': 'upload-cat-text',
|
||||
'archives': 'upload-cat-archives',
|
||||
'data': 'upload-cat-data'
|
||||
};
|
||||
// Extension-Tags rendern
|
||||
this.renderExtensionTags();
|
||||
|
||||
Object.entries(categoryMap).forEach(([category, checkboxId]) => {
|
||||
const checkbox = $(`#${checkboxId}`);
|
||||
const categoryEl = $(`.upload-category[data-category="${category}"]`);
|
||||
// Vorschläge rendern
|
||||
this.renderExtensionSuggestions();
|
||||
}
|
||||
|
||||
if (checkbox && this.uploadSettings.allowedTypes?.[category]) {
|
||||
const isEnabled = this.uploadSettings.allowedTypes[category].enabled;
|
||||
checkbox.checked = isEnabled;
|
||||
this.toggleUploadCategory(categoryEl, isEnabled);
|
||||
}
|
||||
renderExtensionTags() {
|
||||
if (!this.extensionTagsContainer) return;
|
||||
|
||||
if (this.allowedExtensions.length === 0) {
|
||||
this.extensionTagsContainer.innerHTML = '<span class="extension-empty">Keine Endungen definiert</span>';
|
||||
return;
|
||||
}
|
||||
|
||||
this.extensionTagsContainer.innerHTML = this.allowedExtensions.map(ext => `
|
||||
<span class="extension-tag" data-extension="${ext}">
|
||||
.${ext}
|
||||
<button type="button" class="extension-tag-remove" data-remove="${ext}" title="Entfernen">
|
||||
<svg viewBox="0 0 24 24" width="12" height="12"><path d="M18 6L6 18M6 6l12 12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
|
||||
</button>
|
||||
</span>
|
||||
`).join('');
|
||||
|
||||
// Remove-Buttons Event Listener
|
||||
this.extensionTagsContainer.querySelectorAll('.extension-tag-remove').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const ext = btn.dataset.remove;
|
||||
this.removeExtension(ext);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
toggleUploadCategory(categoryEl, enabled) {
|
||||
if (!categoryEl) return;
|
||||
renderExtensionSuggestions() {
|
||||
if (!this.extensionSuggestionsList) return;
|
||||
|
||||
if (enabled) {
|
||||
categoryEl.classList.remove('disabled');
|
||||
} else {
|
||||
categoryEl.classList.add('disabled');
|
||||
// Nur Vorschläge anzeigen, die noch nicht aktiv sind
|
||||
const availableSuggestions = this.extensionSuggestions.filter(
|
||||
ext => !this.allowedExtensions.includes(ext)
|
||||
);
|
||||
|
||||
if (availableSuggestions.length === 0) {
|
||||
this.extensionSuggestionsList.innerHTML = '<span class="extension-no-suggestions">Alle Vorschläge bereits hinzugefügt</span>';
|
||||
return;
|
||||
}
|
||||
|
||||
this.extensionSuggestionsList.innerHTML = availableSuggestions.map(ext => `
|
||||
<button type="button" class="extension-suggestion" data-suggestion="${ext}">+ ${ext}</button>
|
||||
`).join('');
|
||||
|
||||
// Suggestion-Buttons Event Listener
|
||||
this.extensionSuggestionsList.querySelectorAll('.extension-suggestion').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const ext = btn.dataset.suggestion;
|
||||
this.addExtension(ext);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
addExtensionFromInput() {
|
||||
const input = this.extensionInput?.value?.trim().toLowerCase();
|
||||
if (!input) return;
|
||||
|
||||
// Punkt am Anfang entfernen falls vorhanden
|
||||
const ext = input.replace(/^\./, '');
|
||||
|
||||
if (this.addExtension(ext)) {
|
||||
this.extensionInput.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
addExtension(ext) {
|
||||
// Validierung: nur alphanumerisch, 1-10 Zeichen
|
||||
if (!/^[a-z0-9]{1,10}$/.test(ext)) {
|
||||
this.showToast('Ungültige Dateiendung (nur Buchstaben/Zahlen, max. 10 Zeichen)', 'error');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Prüfen ob bereits vorhanden
|
||||
if (this.allowedExtensions.includes(ext)) {
|
||||
this.showToast(`Endung .${ext} bereits vorhanden`, 'error');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Hinzufügen
|
||||
this.allowedExtensions.push(ext);
|
||||
this.renderExtensionTags();
|
||||
this.renderExtensionSuggestions();
|
||||
return true;
|
||||
}
|
||||
|
||||
removeExtension(ext) {
|
||||
// Prüfen ob mindestens eine Endung übrig bleibt
|
||||
if (this.allowedExtensions.length <= 1) {
|
||||
this.showToast('Mindestens eine Dateiendung muss erlaubt sein', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
this.allowedExtensions = this.allowedExtensions.filter(e => e !== ext);
|
||||
this.renderExtensionTags();
|
||||
this.renderExtensionSuggestions();
|
||||
}
|
||||
|
||||
async saveUploadSettings() {
|
||||
@ -437,51 +534,17 @@ class AdminManager {
|
||||
return;
|
||||
}
|
||||
|
||||
// Kategorien sammeln
|
||||
const allowedTypes = {
|
||||
images: {
|
||||
enabled: $('#upload-cat-images')?.checked ?? true,
|
||||
types: ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml']
|
||||
},
|
||||
documents: {
|
||||
enabled: $('#upload-cat-documents')?.checked ?? true,
|
||||
types: ['application/pdf']
|
||||
},
|
||||
office: {
|
||||
enabled: $('#upload-cat-office')?.checked ?? true,
|
||||
types: [
|
||||
'application/msword',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'application/vnd.ms-excel',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'application/vnd.ms-powerpoint',
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.presentation'
|
||||
]
|
||||
},
|
||||
text: {
|
||||
enabled: $('#upload-cat-text')?.checked ?? true,
|
||||
types: ['text/plain', 'text/csv', 'text/markdown']
|
||||
},
|
||||
archives: {
|
||||
enabled: $('#upload-cat-archives')?.checked ?? true,
|
||||
types: ['application/zip', 'application/x-rar-compressed', 'application/x-7z-compressed']
|
||||
},
|
||||
data: {
|
||||
enabled: $('#upload-cat-data')?.checked ?? true,
|
||||
types: ['application/json']
|
||||
}
|
||||
};
|
||||
|
||||
// Prüfen ob mindestens eine Kategorie aktiviert ist
|
||||
const hasEnabledCategory = Object.values(allowedTypes).some(cat => cat.enabled);
|
||||
if (!hasEnabledCategory) {
|
||||
this.showToast('Mindestens eine Dateikategorie muss aktiviert sein', 'error');
|
||||
if (this.allowedExtensions.length === 0) {
|
||||
this.showToast('Mindestens eine Dateiendung muss erlaubt sein', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
await api.updateUploadSettings({ maxFileSizeMB, allowedTypes });
|
||||
await api.updateUploadSettings({
|
||||
maxFileSizeMB,
|
||||
allowedExtensions: this.allowedExtensions
|
||||
});
|
||||
|
||||
this.uploadSettings = { maxFileSizeMB, allowedTypes };
|
||||
this.uploadSettings = { maxFileSizeMB, allowedExtensions: this.allowedExtensions };
|
||||
this.showToast('Upload-Einstellungen gespeichert', 'success');
|
||||
} catch (error) {
|
||||
console.error('Error saving upload settings:', error);
|
||||
@ -489,6 +552,72 @@ class AdminManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Passwort-Bearbeitung umschalten
|
||||
*/
|
||||
togglePasswordEdit() {
|
||||
const passwordInput = $('#user-password');
|
||||
const editBtn = $('#edit-password-btn');
|
||||
const hint = $('#password-hint');
|
||||
|
||||
if (!passwordInput || !editBtn) return;
|
||||
|
||||
if (passwordInput.readOnly) {
|
||||
// Bearbeitung aktivieren
|
||||
passwordInput.readOnly = false;
|
||||
passwordInput.focus();
|
||||
passwordInput.select();
|
||||
editBtn.innerHTML = `
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M20 6L9 17l-5-5" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
`;
|
||||
editBtn.title = "Bearbeitung bestätigen";
|
||||
hint.textContent = "(bearbeiten)";
|
||||
} else {
|
||||
// Bearbeitung beenden
|
||||
passwordInput.readOnly = true;
|
||||
editBtn.innerHTML = `
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" stroke="currentColor" stroke-width="2" fill="none"/>
|
||||
<path d="m18.5 2.5 3 3L12 15l-4 1 1-4 9.5-9.5z" stroke="currentColor" stroke-width="2" fill="none"/>
|
||||
</svg>
|
||||
`;
|
||||
editBtn.title = "Passwort bearbeiten";
|
||||
hint.textContent = this.currentEditUser ? "(geändert)" : "(automatisch generiert)";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Neues Passwort generieren
|
||||
*/
|
||||
generatePassword() {
|
||||
const passwordInput = $('#user-password');
|
||||
const hint = $('#password-hint');
|
||||
|
||||
if (!passwordInput) return;
|
||||
|
||||
// Starkes Passwort generieren (12 Zeichen)
|
||||
const charset = 'ABCDEFGHJKMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789!@#$%&*';
|
||||
let password = '';
|
||||
for (let i = 0; i < 12; i++) {
|
||||
password += charset.charAt(Math.floor(Math.random() * charset.length));
|
||||
}
|
||||
|
||||
passwordInput.value = password;
|
||||
passwordInput.readOnly = false;
|
||||
|
||||
if (hint) {
|
||||
hint.textContent = "(neu generiert)";
|
||||
}
|
||||
|
||||
// Passwort kurz markieren
|
||||
passwordInput.focus();
|
||||
passwordInput.select();
|
||||
|
||||
this.showToast('Neues Passwort generiert', 'success');
|
||||
}
|
||||
|
||||
show() {
|
||||
this.adminScreen?.classList.add('active');
|
||||
}
|
||||
|
||||
@ -7,9 +7,25 @@ class ApiClient {
|
||||
constructor() {
|
||||
this.baseUrl = '/api';
|
||||
this.token = null;
|
||||
this.refreshToken = null;
|
||||
this.csrfToken = null;
|
||||
this.refreshingToken = false;
|
||||
this.requestQueue = [];
|
||||
this.refreshTimer = null;
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// Token aus Storage laden
|
||||
this.token = localStorage.getItem('auth_token');
|
||||
this.refreshToken = localStorage.getItem('refresh_token');
|
||||
this.csrfToken = sessionStorage.getItem('csrf_token');
|
||||
console.log('[API] init() - Token loaded:', this.token ? this.token.substring(0, 20) + '...' : 'NULL');
|
||||
|
||||
// Starte Timer wenn Token und Refresh-Token vorhanden sind
|
||||
if (this.token && this.refreshToken) {
|
||||
this.startTokenRefreshTimer();
|
||||
}
|
||||
}
|
||||
|
||||
// Token Management
|
||||
@ -18,10 +34,22 @@ class ApiClient {
|
||||
this.token = token;
|
||||
if (token) {
|
||||
localStorage.setItem('auth_token', token);
|
||||
// Starte proaktiven Token-Refresh Timer (nach 10 Minuten)
|
||||
this.startTokenRefreshTimer();
|
||||
} else {
|
||||
this.token = null;
|
||||
localStorage.removeItem('auth_token');
|
||||
localStorage.removeItem('current_user');
|
||||
this.clearTokenRefreshTimer();
|
||||
}
|
||||
}
|
||||
|
||||
setRefreshToken(token) {
|
||||
this.refreshToken = token;
|
||||
if (token) {
|
||||
localStorage.setItem('refresh_token', token);
|
||||
} else {
|
||||
localStorage.removeItem('refresh_token');
|
||||
}
|
||||
}
|
||||
|
||||
@ -49,6 +77,94 @@ class ApiClient {
|
||||
return token;
|
||||
}
|
||||
|
||||
// Refresh Access Token using Refresh Token
|
||||
async refreshAccessToken() {
|
||||
if (this.refreshingToken) {
|
||||
// Warte auf laufenden Refresh
|
||||
return new Promise((resolve) => {
|
||||
const checkRefresh = () => {
|
||||
if (!this.refreshingToken) {
|
||||
resolve();
|
||||
} else {
|
||||
setTimeout(checkRefresh, 100);
|
||||
}
|
||||
};
|
||||
checkRefresh();
|
||||
});
|
||||
}
|
||||
|
||||
this.refreshingToken = true;
|
||||
try {
|
||||
const refreshToken = localStorage.getItem('refresh_token');
|
||||
if (!refreshToken) {
|
||||
throw new Error('Kein Refresh-Token vorhanden');
|
||||
}
|
||||
|
||||
console.log('[API] Refreshing access token...');
|
||||
const response = await fetch('/api/auth/refresh', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ refreshToken })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Refresh failed: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
this.setToken(data.token);
|
||||
if (data.csrfToken) {
|
||||
this.setCsrfToken(data.csrfToken);
|
||||
}
|
||||
|
||||
console.log('[API] Token refresh successful');
|
||||
window.dispatchEvent(new CustomEvent('auth:token-refreshed', {
|
||||
detail: { token: data.token }
|
||||
}));
|
||||
|
||||
} catch (error) {
|
||||
console.log('[API] Token refresh error:', error.message);
|
||||
throw error;
|
||||
} finally {
|
||||
this.refreshingToken = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle authentication failure
|
||||
handleAuthFailure() {
|
||||
console.log('[API] Authentication failed - clearing tokens');
|
||||
this.setToken(null);
|
||||
this.setRefreshToken(null);
|
||||
this.setCsrfToken(null);
|
||||
window.dispatchEvent(new CustomEvent('auth:logout'));
|
||||
}
|
||||
|
||||
// Proaktiver Token-Refresh Timer
|
||||
startTokenRefreshTimer() {
|
||||
this.clearTokenRefreshTimer();
|
||||
// Refresh nach 10 Minuten (Token läuft nach 15 Minuten ab)
|
||||
this.refreshTimer = setTimeout(async () => {
|
||||
if (this.refreshToken && !this.refreshingToken) {
|
||||
try {
|
||||
console.log('[API] Proactive token refresh...');
|
||||
await this.refreshAccessToken();
|
||||
} catch (error) {
|
||||
console.log('[API] Proactive refresh failed:', error.message);
|
||||
// Bei Fehler nicht automatisch ausloggen, warten bis Token wirklich abläuft
|
||||
}
|
||||
}
|
||||
}, 10 * 60 * 1000); // 10 Minuten
|
||||
}
|
||||
|
||||
clearTokenRefreshTimer() {
|
||||
if (this.refreshTimer) {
|
||||
clearTimeout(this.refreshTimer);
|
||||
this.refreshTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Base Request Method
|
||||
async request(endpoint, options = {}) {
|
||||
const url = `${this.baseUrl}${endpoint}`;
|
||||
@ -58,6 +174,11 @@ class ApiClient {
|
||||
...options.headers
|
||||
};
|
||||
|
||||
// Sicherstellen, dass Token aktuell ist
|
||||
if (!this.token && localStorage.getItem('auth_token')) {
|
||||
this.init();
|
||||
}
|
||||
|
||||
// Add auth token
|
||||
const token = this.getToken();
|
||||
console.log('[API] Request:', endpoint, 'Token:', token ? token.substring(0, 20) + '...' : 'NULL');
|
||||
@ -103,22 +224,25 @@ class ApiClient {
|
||||
|
||||
// Handle 401 Unauthorized
|
||||
if (response.status === 401) {
|
||||
// Token der für diesen Request verwendet wurde
|
||||
const requestToken = token;
|
||||
const currentToken = localStorage.getItem('auth_token');
|
||||
|
||||
console.log('[API] 401 received for:', endpoint);
|
||||
console.log('[API] Request token:', requestToken ? requestToken.substring(0, 20) + '...' : 'NULL');
|
||||
console.log('[API] Current token:', currentToken ? currentToken.substring(0, 20) + '...' : 'NULL');
|
||||
|
||||
// Nur ausloggen wenn der Token der gleiche ist (kein neuer Login in der Zwischenzeit)
|
||||
if (!currentToken || currentToken === requestToken) {
|
||||
console.log('[API] Token invalid, triggering logout');
|
||||
this.setToken(null);
|
||||
window.dispatchEvent(new CustomEvent('auth:logout'));
|
||||
} else {
|
||||
console.log('[API] 401 ignored - new login occurred while request was in flight');
|
||||
|
||||
// Versuche Token mit Refresh-Token zu erneuern
|
||||
if (this.refreshToken && !this.refreshingToken && !options._tokenRefreshAttempted) {
|
||||
console.log('[API] Attempting token refresh...');
|
||||
try {
|
||||
await this.refreshAccessToken();
|
||||
// Wiederhole original Request mit neuem Token
|
||||
return this.request(endpoint, { ...options, _tokenRefreshAttempted: true });
|
||||
} catch (refreshError) {
|
||||
console.log('[API] Token refresh failed:', refreshError.message);
|
||||
// Fallback zum Logout
|
||||
this.handleAuthFailure();
|
||||
throw new ApiError('Sitzung abgelaufen', 401);
|
||||
}
|
||||
}
|
||||
|
||||
// Kein Refresh-Token oder Refresh bereits versucht
|
||||
this.handleAuthFailure();
|
||||
throw new ApiError('Sitzung abgelaufen', 401);
|
||||
}
|
||||
|
||||
@ -297,6 +421,12 @@ class ApiClient {
|
||||
const response = await this.post('/auth/login', { username, password });
|
||||
console.log('[API] login() response:', response ? 'OK' : 'NULL', 'token:', response?.token ? 'EXISTS' : 'MISSING');
|
||||
this.setToken(response.token);
|
||||
|
||||
// Store refresh token if provided (new auth system)
|
||||
if (response.refreshToken) {
|
||||
this.setRefreshToken(response.refreshToken);
|
||||
}
|
||||
|
||||
// Store CSRF token from login response
|
||||
if (response.csrfToken) {
|
||||
this.setCsrfToken(response.csrfToken);
|
||||
@ -309,6 +439,7 @@ class ApiClient {
|
||||
await this.post('/auth/logout', {});
|
||||
} finally {
|
||||
this.setToken(null);
|
||||
this.setRefreshToken(null);
|
||||
this.setCsrfToken(null);
|
||||
}
|
||||
}
|
||||
@ -1071,6 +1202,62 @@ class ApiClient {
|
||||
async searchKnowledge(query) {
|
||||
return this.get(`/knowledge/search?q=${encodeURIComponent(query)}`);
|
||||
}
|
||||
|
||||
// =====================
|
||||
// CODING
|
||||
// =====================
|
||||
|
||||
async getCodingDirectories() {
|
||||
return this.get('/coding/directories');
|
||||
}
|
||||
|
||||
async createCodingDirectory(data) {
|
||||
return this.post('/coding/directories', data);
|
||||
}
|
||||
|
||||
async updateCodingDirectory(id, data) {
|
||||
return this.put(`/coding/directories/${id}`, data);
|
||||
}
|
||||
|
||||
async deleteCodingDirectory(id) {
|
||||
return this.delete(`/coding/directories/${id}`);
|
||||
}
|
||||
|
||||
async getCodingDirectoryStatus(id) {
|
||||
return this.get(`/coding/directories/${id}/status`);
|
||||
}
|
||||
|
||||
async codingGitFetch(id) {
|
||||
return this.post(`/coding/directories/${id}/fetch`);
|
||||
}
|
||||
|
||||
async codingGitPull(id) {
|
||||
return this.post(`/coding/directories/${id}/pull`);
|
||||
}
|
||||
|
||||
async codingGitPush(id, force = false) {
|
||||
return this.post(`/coding/directories/${id}/push`, { force });
|
||||
}
|
||||
|
||||
async codingGitCommit(id, message) {
|
||||
return this.post(`/coding/directories/${id}/commit`, { message });
|
||||
}
|
||||
|
||||
async getCodingDirectoryBranches(id) {
|
||||
return this.get(`/coding/directories/${id}/branches`);
|
||||
}
|
||||
|
||||
async codingGitCheckout(id, branch) {
|
||||
return this.post(`/coding/directories/${id}/checkout`, { branch });
|
||||
}
|
||||
|
||||
async getCodingDirectoryCommits(id, limit = 20) {
|
||||
return this.get(`/coding/directories/${id}/commits?limit=${limit}`);
|
||||
}
|
||||
|
||||
async validateCodingPath(path) {
|
||||
return this.post('/coding/validate-path', { path });
|
||||
}
|
||||
}
|
||||
|
||||
// Custom API Error Class
|
||||
|
||||
@ -21,6 +21,8 @@ import proposalsManager from './proposals.js';
|
||||
import notificationManager from './notifications.js';
|
||||
import giteaManager from './gitea.js';
|
||||
import knowledgeManager from './knowledge.js';
|
||||
import codingManager from './coding.js';
|
||||
import mobileManager from './mobile.js';
|
||||
import { $, $$, debounce, getFromStorage, setToStorage } from './utils.js';
|
||||
|
||||
class App {
|
||||
@ -80,11 +82,20 @@ class App {
|
||||
// Initialize gitea manager
|
||||
await giteaManager.init();
|
||||
|
||||
// Initialize coding manager
|
||||
await codingManager.init();
|
||||
|
||||
// Initialize knowledge manager
|
||||
await knowledgeManager.init();
|
||||
|
||||
// Initialize mobile features
|
||||
mobileManager.init();
|
||||
|
||||
// Update UI
|
||||
this.updateUserMenu();
|
||||
|
||||
// Dispatch event for mobile menu
|
||||
document.dispatchEvent(new CustomEvent('projects:loaded'));
|
||||
}
|
||||
|
||||
async initializeAdminApp() {
|
||||
@ -321,6 +332,32 @@ class App {
|
||||
window.addEventListener('online', () => this.handleOnline());
|
||||
window.addEventListener('offline', () => this.handleOffline());
|
||||
|
||||
// Mobile events
|
||||
document.addEventListener('project:selected', (e) => {
|
||||
const projectId = e.detail?.projectId;
|
||||
if (projectId) {
|
||||
this.loadProject(projectId);
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('auth:logout', () => {
|
||||
authManager.logout();
|
||||
});
|
||||
|
||||
document.addEventListener('admin:open', () => {
|
||||
// Redirect to admin screen for admins
|
||||
if (authManager.isAdmin()) {
|
||||
this.showAdminScreen();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('task:move', async (e) => {
|
||||
const { taskId, columnId, position } = e.detail;
|
||||
if (taskId && columnId !== undefined) {
|
||||
await boardManager.moveTask(taskId, columnId, position);
|
||||
}
|
||||
});
|
||||
|
||||
// Close modal on overlay click
|
||||
$('.modal-overlay')?.addEventListener('click', () => {
|
||||
// Check if task-modal is open - let it handle its own close (with auto-save)
|
||||
@ -617,11 +654,11 @@ class App {
|
||||
proposalsManager.resetToActiveView();
|
||||
}
|
||||
|
||||
// Show/hide gitea manager
|
||||
if (view === 'gitea') {
|
||||
giteaManager.show();
|
||||
// Show/hide coding manager
|
||||
if (view === 'coding') {
|
||||
codingManager.show();
|
||||
} else {
|
||||
giteaManager.hide();
|
||||
codingManager.hide();
|
||||
}
|
||||
|
||||
// Show/hide knowledge manager
|
||||
@ -978,6 +1015,9 @@ class App {
|
||||
|
||||
const userRole = $('#user-role');
|
||||
if (userRole) userRole.textContent = user.role === 'admin' ? 'Administrator' : 'Benutzer';
|
||||
|
||||
// Notify mobile menu
|
||||
document.dispatchEvent(new CustomEvent('user:updated'));
|
||||
}
|
||||
|
||||
toggleUserMenu() {
|
||||
|
||||
777
frontend/js/coding.js
Normale Datei
777
frontend/js/coding.js
Normale Datei
@ -0,0 +1,777 @@
|
||||
/**
|
||||
* TASKMATE - Coding Manager
|
||||
* =========================
|
||||
* Verwaltung von Server-Anwendungen mit Claude/Codex Integration
|
||||
*/
|
||||
|
||||
import api from './api.js';
|
||||
import { escapeHtml } from './utils.js';
|
||||
|
||||
// Toast-Funktion (verwendet das globale Toast-Event)
|
||||
function showToast(message, type = 'info') {
|
||||
window.dispatchEvent(new CustomEvent('toast:show', {
|
||||
detail: { message, type }
|
||||
}));
|
||||
}
|
||||
|
||||
// Basis-Pfad für alle Anwendungen auf dem Server
|
||||
const BASE_PATH = '/home/claude-dev';
|
||||
|
||||
// Farb-Presets für Anwendungen
|
||||
const COLOR_PRESETS = [
|
||||
'#4F46E5', // Indigo
|
||||
'#7C3AED', // Violet
|
||||
'#EC4899', // Pink
|
||||
'#EF4444', // Red
|
||||
'#F59E0B', // Amber
|
||||
'#10B981', // Emerald
|
||||
'#06B6D4', // Cyan
|
||||
'#3B82F6', // Blue
|
||||
'#8B5CF6', // Purple
|
||||
'#6366F1' // Indigo Light
|
||||
];
|
||||
|
||||
class CodingManager {
|
||||
constructor() {
|
||||
this.initialized = false;
|
||||
this.directories = [];
|
||||
this.refreshInterval = null;
|
||||
this.editingDirectory = null;
|
||||
this.giteaRepos = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Manager initialisieren
|
||||
*/
|
||||
async init() {
|
||||
if (this.initialized) return;
|
||||
|
||||
this.bindEvents();
|
||||
this.initialized = true;
|
||||
|
||||
console.log('[CodingManager] Initialisiert');
|
||||
}
|
||||
|
||||
/**
|
||||
* Event-Listener binden
|
||||
*/
|
||||
bindEvents() {
|
||||
// Add-Button
|
||||
const addBtn = document.getElementById('add-coding-directory-btn');
|
||||
if (addBtn) {
|
||||
addBtn.addEventListener('click', () => this.openModal());
|
||||
}
|
||||
|
||||
// Modal Events
|
||||
const modal = document.getElementById('coding-modal');
|
||||
if (modal) {
|
||||
// Close-Button
|
||||
modal.querySelector('.modal-close')?.addEventListener('click', () => this.closeModal());
|
||||
modal.querySelector('.modal-cancel')?.addEventListener('click', () => this.closeModal());
|
||||
|
||||
// Save-Button
|
||||
document.getElementById('coding-save-btn')?.addEventListener('click', () => this.handleSave());
|
||||
|
||||
// Delete-Button
|
||||
document.getElementById('coding-delete-btn')?.addEventListener('click', () => this.handleDelete());
|
||||
|
||||
// Backdrop-Click
|
||||
modal.addEventListener('click', (e) => {
|
||||
if (e.target === modal) this.closeModal();
|
||||
});
|
||||
|
||||
// Farb-Presets
|
||||
this.renderColorPresets();
|
||||
|
||||
// Name-Eingabe für Pfad-Preview
|
||||
const nameInput = document.getElementById('coding-name');
|
||||
if (nameInput) {
|
||||
nameInput.addEventListener('input', () => this.updatePathPreview());
|
||||
}
|
||||
|
||||
// CLAUDE.md Link Event
|
||||
const claudeLink = document.getElementById('coding-claude-link');
|
||||
if (claudeLink) {
|
||||
claudeLink.addEventListener('click', () => this.openClaudeModal());
|
||||
}
|
||||
}
|
||||
|
||||
// Command-Modal Events
|
||||
const cmdModal = document.getElementById('coding-command-modal');
|
||||
if (cmdModal) {
|
||||
cmdModal.querySelector('.modal-close')?.addEventListener('click', () => this.closeCommandModal());
|
||||
cmdModal.addEventListener('click', (e) => {
|
||||
if (e.target === cmdModal) this.closeCommandModal();
|
||||
});
|
||||
|
||||
document.getElementById('coding-copy-command')?.addEventListener('click', () => this.copyCommand());
|
||||
}
|
||||
|
||||
// Gitea-Repo Dropdown laden bei Details-Toggle
|
||||
const giteaSection = document.querySelector('.coding-gitea-section');
|
||||
if (giteaSection) {
|
||||
giteaSection.addEventListener('toggle', (e) => {
|
||||
if (e.target.open) {
|
||||
this.loadGiteaRepos();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// CLAUDE.md Modal Events
|
||||
const claudeModal = document.getElementById('claude-md-modal');
|
||||
if (claudeModal) {
|
||||
// Close-Button
|
||||
claudeModal.querySelector('.modal-close')?.addEventListener('click', () => this.closeClaudeModal());
|
||||
|
||||
// Backdrop-Click
|
||||
claudeModal.addEventListener('click', (e) => {
|
||||
if (e.target === claudeModal) this.closeClaudeModal();
|
||||
});
|
||||
|
||||
// ESC-Taste
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && !claudeModal.classList.contains('hidden')) {
|
||||
this.closeClaudeModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Farb-Presets rendern
|
||||
*/
|
||||
renderColorPresets() {
|
||||
const container = document.getElementById('coding-color-presets');
|
||||
if (!container) return;
|
||||
|
||||
container.innerHTML = COLOR_PRESETS.map(color => `
|
||||
<button type="button" class="color-preset" data-color="${color}" style="background-color: ${color};" title="${color}"></button>
|
||||
`).join('') + `
|
||||
<input type="color" id="coding-color-custom" class="color-picker-custom" value="#4F46E5" title="Eigene Farbe">
|
||||
`;
|
||||
|
||||
// Event-Listener für Presets
|
||||
container.querySelectorAll('.color-preset').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
container.querySelectorAll('.color-preset').forEach(b => b.classList.remove('selected'));
|
||||
btn.classList.add('selected');
|
||||
document.getElementById('coding-color-custom').value = btn.dataset.color;
|
||||
});
|
||||
});
|
||||
|
||||
// Custom Color Input
|
||||
document.getElementById('coding-color-custom')?.addEventListener('input', (e) => {
|
||||
container.querySelectorAll('.color-preset').forEach(b => b.classList.remove('selected'));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Pfad-Preview aktualisieren
|
||||
*/
|
||||
updatePathPreview() {
|
||||
const nameInput = document.getElementById('coding-name');
|
||||
const preview = document.getElementById('coding-path-preview');
|
||||
if (!nameInput || !preview) return;
|
||||
|
||||
const name = nameInput.value.trim();
|
||||
preview.textContent = name || '...';
|
||||
}
|
||||
|
||||
// switchClaudeTab entfernt - CLAUDE.md ist jetzt nur readonly
|
||||
|
||||
/**
|
||||
* CLAUDE.md Link aktualisieren
|
||||
*/
|
||||
updateClaudeLink(content, projectName) {
|
||||
const link = document.getElementById('coding-claude-link');
|
||||
const textSpan = link?.querySelector('.claude-text');
|
||||
|
||||
// Debug entfernt
|
||||
|
||||
if (!link || !textSpan) {
|
||||
console.error('CLAUDE.md link elements not found!');
|
||||
return;
|
||||
}
|
||||
|
||||
// Content für Modal speichern
|
||||
this.currentClaudeContent = content;
|
||||
this.currentProjectName = projectName;
|
||||
|
||||
if (content) {
|
||||
link.disabled = false;
|
||||
textSpan.textContent = `CLAUDE.md anzeigen (${Math.round(content.length / 1024)}KB)`;
|
||||
} else {
|
||||
link.disabled = true;
|
||||
textSpan.textContent = 'Keine CLAUDE.md vorhanden';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* CLAUDE.md Modal öffnen
|
||||
*/
|
||||
openClaudeModal() {
|
||||
if (!this.currentClaudeContent) {
|
||||
console.warn('No CLAUDE.md content to display');
|
||||
return;
|
||||
}
|
||||
|
||||
const modal = document.getElementById('claude-md-modal');
|
||||
const overlay = document.querySelector('.modal-overlay');
|
||||
const content = document.getElementById('claude-md-content');
|
||||
const title = modal?.querySelector('.modal-header h3');
|
||||
|
||||
if (!modal || !content) {
|
||||
console.error('CLAUDE.md modal elements not found!');
|
||||
return;
|
||||
}
|
||||
|
||||
// Titel setzen
|
||||
if (title && this.currentProjectName) {
|
||||
title.textContent = `CLAUDE.md - ${this.currentProjectName}`;
|
||||
}
|
||||
|
||||
// Content setzen
|
||||
content.textContent = this.currentClaudeContent;
|
||||
|
||||
// Modal anzeigen
|
||||
modal.classList.remove('hidden');
|
||||
modal.classList.add('visible');
|
||||
if (overlay) {
|
||||
overlay.classList.remove('hidden');
|
||||
overlay.classList.add('visible');
|
||||
}
|
||||
|
||||
// Modal opened
|
||||
}
|
||||
|
||||
/**
|
||||
* CLAUDE.md Modal schließen
|
||||
*/
|
||||
closeClaudeModal() {
|
||||
const modal = document.getElementById('claude-md-modal');
|
||||
const overlay = document.querySelector('.modal-overlay');
|
||||
|
||||
if (modal) {
|
||||
modal.classList.remove('visible');
|
||||
setTimeout(() => modal.classList.add('hidden'), 200);
|
||||
}
|
||||
|
||||
if (overlay) {
|
||||
overlay.classList.remove('visible');
|
||||
setTimeout(() => overlay.classList.add('hidden'), 200);
|
||||
}
|
||||
|
||||
// Modal closed
|
||||
}
|
||||
|
||||
/**
|
||||
* Gitea-Repositories laden
|
||||
*/
|
||||
async loadGiteaRepos() {
|
||||
try {
|
||||
const select = document.getElementById('coding-gitea-repo');
|
||||
if (!select) return;
|
||||
|
||||
// Lade-Indikator
|
||||
select.innerHTML = '<option value="">Laden...</option>';
|
||||
|
||||
const result = await api.getGiteaRepositories();
|
||||
console.log('Gitea API Response:', result);
|
||||
this.giteaRepos = result?.repositories || [];
|
||||
|
||||
select.innerHTML = '<option value="">-- Kein Repository --</option>' +
|
||||
this.giteaRepos.map(repo => `
|
||||
<option value="${repo.cloneUrl}" data-owner="${repo.owner || ''}" data-name="${repo.name}">
|
||||
${escapeHtml(repo.fullName)}
|
||||
</option>
|
||||
`).join('');
|
||||
|
||||
// Wenn Editing, vorhandenen Wert setzen
|
||||
if (this.editingDirectory?.giteaRepoUrl) {
|
||||
select.value = this.editingDirectory.giteaRepoUrl;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Gitea-Repos:', error);
|
||||
const select = document.getElementById('coding-gitea-repo');
|
||||
if (select) {
|
||||
select.innerHTML = '<option value="">Fehler beim Laden</option>';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Anwendungen laden
|
||||
*/
|
||||
async loadDirectories() {
|
||||
try {
|
||||
this.directories = await api.getCodingDirectories();
|
||||
this.render();
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Anwendungen:', error);
|
||||
showToast('Fehler beim Laden der Anwendungen', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* View rendern
|
||||
*/
|
||||
render() {
|
||||
const grid = document.getElementById('coding-grid');
|
||||
const empty = document.getElementById('coding-empty');
|
||||
|
||||
if (!grid) return;
|
||||
|
||||
if (this.directories.length === 0) {
|
||||
grid.innerHTML = '';
|
||||
grid.classList.add('hidden');
|
||||
empty?.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
empty?.classList.add('hidden');
|
||||
grid.classList.remove('hidden');
|
||||
|
||||
grid.innerHTML = this.directories.map(dir => this.renderTile(dir)).join('');
|
||||
|
||||
// Event-Listener für Tiles
|
||||
this.bindTileEvents();
|
||||
|
||||
// Git-Status für jede Anwendung laden
|
||||
this.directories.forEach(dir => this.updateTileStatus(dir.id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Einzelne Kachel rendern
|
||||
*/
|
||||
renderTile(directory) {
|
||||
const hasGitea = !!directory.giteaRepoUrl;
|
||||
|
||||
return `
|
||||
<div class="coding-tile" data-id="${directory.id}">
|
||||
<div class="coding-tile-color" style="background-color: ${directory.color || '#4F46E5'}"></div>
|
||||
|
||||
<div class="coding-tile-header">
|
||||
<span class="coding-tile-icon">📁</span>
|
||||
</div>
|
||||
|
||||
<div class="coding-tile-content">
|
||||
<div class="coding-tile-name">${escapeHtml(directory.name)}</div>
|
||||
<div class="coding-tile-path">${escapeHtml(directory.localPath)}</div>
|
||||
${directory.description ? `<div class="coding-tile-description">${escapeHtml(directory.description)}</div>` : ''}
|
||||
${directory.hasCLAUDEmd ? '<div class="coding-tile-badge">CLAUDE.md</div>' : ''}
|
||||
</div>
|
||||
|
||||
<div class="coding-tile-status" id="coding-status-${directory.id}">
|
||||
<span class="git-status-badge loading">Lade...</span>
|
||||
</div>
|
||||
|
||||
<div class="coding-tile-actions">
|
||||
<button class="btn-claude" data-id="${directory.id}" data-path="${escapeHtml(directory.localPath)}" title="SSH-Befehl für Claude kopieren">
|
||||
Claude starten
|
||||
</button>
|
||||
</div>
|
||||
|
||||
${hasGitea ? `
|
||||
<div class="coding-tile-git">
|
||||
<button class="btn btn-sm btn-secondary coding-git-fetch" data-id="${directory.id}">Fetch</button>
|
||||
<button class="btn btn-sm btn-secondary coding-git-pull" data-id="${directory.id}">Pull</button>
|
||||
<button class="btn btn-sm btn-secondary coding-git-push" data-id="${directory.id}">Push</button>
|
||||
<button class="btn btn-sm btn-secondary coding-git-commit" data-id="${directory.id}">Commit</button>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event-Listener für Tiles binden
|
||||
*/
|
||||
bindTileEvents() {
|
||||
// Kachel-Klick für Modal
|
||||
document.querySelectorAll('.coding-tile').forEach(tile => {
|
||||
tile.addEventListener('click', (e) => {
|
||||
// Nicht triggern wenn Button-Kind geklickt wird
|
||||
if (e.target.closest('button')) return;
|
||||
|
||||
const id = parseInt(tile.dataset.id);
|
||||
const dir = this.directories.find(d => d.id === id);
|
||||
if (dir) this.openModal(dir);
|
||||
});
|
||||
});
|
||||
|
||||
// Claude-Buttons
|
||||
document.querySelectorAll('.btn-claude').forEach(btn => {
|
||||
btn.addEventListener('click', () => this.launchClaude(btn.dataset.path));
|
||||
});
|
||||
|
||||
// Git-Buttons
|
||||
document.querySelectorAll('.coding-git-fetch').forEach(btn => {
|
||||
btn.addEventListener('click', () => this.gitFetch(parseInt(btn.dataset.id)));
|
||||
});
|
||||
|
||||
document.querySelectorAll('.coding-git-pull').forEach(btn => {
|
||||
btn.addEventListener('click', () => this.gitPull(parseInt(btn.dataset.id)));
|
||||
});
|
||||
|
||||
document.querySelectorAll('.coding-git-push').forEach(btn => {
|
||||
btn.addEventListener('click', () => this.gitPush(parseInt(btn.dataset.id)));
|
||||
});
|
||||
|
||||
document.querySelectorAll('.coding-git-commit').forEach(btn => {
|
||||
btn.addEventListener('click', () => this.promptCommit(parseInt(btn.dataset.id)));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Git-Status für eine Kachel aktualisieren
|
||||
*/
|
||||
async updateTileStatus(id) {
|
||||
const statusEl = document.getElementById(`coding-status-${id}`);
|
||||
if (!statusEl) return;
|
||||
|
||||
try {
|
||||
const status = await api.getCodingDirectoryStatus(id);
|
||||
|
||||
if (!status.isGitRepo) {
|
||||
statusEl.innerHTML = '<span class="git-status-badge">Kein Git-Repo</span>';
|
||||
return;
|
||||
}
|
||||
|
||||
const statusClass = status.isClean ? 'clean' : 'dirty';
|
||||
const statusText = status.isClean ? 'Clean' : `${status.changes?.length || 0} Änderungen`;
|
||||
|
||||
statusEl.innerHTML = `
|
||||
<span class="git-branch-badge">${escapeHtml(status.branch)}</span>
|
||||
<span class="git-status-badge ${statusClass}">${statusText}</span>
|
||||
${status.ahead > 0 ? `<span class="git-status-badge ahead">↑${status.ahead}</span>` : ''}
|
||||
${status.behind > 0 ? `<span class="git-status-badge behind">↓${status.behind}</span>` : ''}
|
||||
`;
|
||||
} catch (error) {
|
||||
statusEl.innerHTML = '<span class="git-status-badge error">Fehler</span>';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Claude Code starten - SSH-Befehl kopieren
|
||||
*/
|
||||
async launchClaude(path) {
|
||||
const command = `ssh claude-dev@91.99.192.14 -t "cd ${path} && claude"`;
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(command);
|
||||
this.showCommandModal(
|
||||
command,
|
||||
'Befehl kopiert! Öffne Terminal/CMD und füge ein (Strg+V). Passwort: z0E1Al}q2H?Yqd!O'
|
||||
);
|
||||
showToast('SSH-Befehl kopiert!', 'success');
|
||||
} catch (error) {
|
||||
// Fallback wenn Clipboard nicht verfügbar
|
||||
this.showCommandModal(
|
||||
command,
|
||||
'Kopiere diesen Befehl und füge ihn im Terminal ein. Passwort: z0E1Al}q2H?Yqd!O'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Command-Modal anzeigen
|
||||
*/
|
||||
showCommandModal(command, hint) {
|
||||
const modal = document.getElementById('coding-command-modal');
|
||||
const hintEl = document.getElementById('coding-command-hint');
|
||||
const textEl = document.getElementById('coding-command-text');
|
||||
|
||||
if (!modal || !textEl) return;
|
||||
|
||||
hintEl.textContent = hint || 'Führe diesen Befehl aus:';
|
||||
textEl.textContent = command;
|
||||
this.currentCommand = command;
|
||||
|
||||
modal.classList.remove('hidden');
|
||||
}
|
||||
|
||||
/**
|
||||
* Command-Modal schließen
|
||||
*/
|
||||
closeCommandModal() {
|
||||
const modal = document.getElementById('coding-command-modal');
|
||||
if (modal) modal.classList.add('hidden');
|
||||
}
|
||||
|
||||
/**
|
||||
* Befehl in Zwischenablage kopieren
|
||||
*/
|
||||
async copyCommand() {
|
||||
if (!this.currentCommand) return;
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(this.currentCommand);
|
||||
showToast('Befehl kopiert!', 'success');
|
||||
} catch (error) {
|
||||
console.error('Kopieren fehlgeschlagen:', error);
|
||||
showToast('Kopieren fehlgeschlagen', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Git Fetch
|
||||
*/
|
||||
async gitFetch(id) {
|
||||
try {
|
||||
showToast('Fetch läuft...', 'info');
|
||||
await api.codingGitFetch(id);
|
||||
showToast('Fetch erfolgreich', 'success');
|
||||
this.updateTileStatus(id);
|
||||
} catch (error) {
|
||||
showToast('Fetch fehlgeschlagen', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Git Pull
|
||||
*/
|
||||
async gitPull(id) {
|
||||
try {
|
||||
showToast('Pull läuft...', 'info');
|
||||
await api.codingGitPull(id);
|
||||
showToast('Pull erfolgreich', 'success');
|
||||
this.updateTileStatus(id);
|
||||
} catch (error) {
|
||||
showToast('Pull fehlgeschlagen: ' + (error.message || 'Unbekannter Fehler'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Git Push
|
||||
*/
|
||||
async gitPush(id) {
|
||||
try {
|
||||
showToast('Push läuft...', 'info');
|
||||
await api.codingGitPush(id);
|
||||
showToast('Push erfolgreich', 'success');
|
||||
this.updateTileStatus(id);
|
||||
} catch (error) {
|
||||
showToast('Push fehlgeschlagen: ' + (error.message || 'Unbekannter Fehler'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Commit-Dialog anzeigen
|
||||
*/
|
||||
promptCommit(id) {
|
||||
const message = prompt('Commit-Nachricht eingeben:');
|
||||
if (message && message.trim()) {
|
||||
this.gitCommit(id, message.trim());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Git Commit
|
||||
*/
|
||||
async gitCommit(id, message) {
|
||||
try {
|
||||
showToast('Commit läuft...', 'info');
|
||||
await api.codingGitCommit(id, message);
|
||||
showToast('Commit erfolgreich', 'success');
|
||||
this.updateTileStatus(id);
|
||||
} catch (error) {
|
||||
showToast('Commit fehlgeschlagen: ' + (error.message || 'Unbekannter Fehler'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Modal öffnen
|
||||
*/
|
||||
openModal(directory = null) {
|
||||
this.editingDirectory = directory;
|
||||
|
||||
const modal = document.getElementById('coding-modal');
|
||||
const overlay = document.querySelector('.modal-overlay');
|
||||
const title = document.getElementById('coding-modal-title');
|
||||
const deleteBtn = document.getElementById('coding-delete-btn');
|
||||
|
||||
if (!modal) return;
|
||||
|
||||
// Titel setzen
|
||||
title.textContent = directory ? 'Anwendung bearbeiten' : 'Anwendung hinzufügen';
|
||||
|
||||
// Delete-Button anzeigen/verstecken
|
||||
if (deleteBtn) {
|
||||
deleteBtn.classList.toggle('hidden', !directory);
|
||||
}
|
||||
|
||||
// Felder füllen
|
||||
document.getElementById('coding-name').value = directory?.name || '';
|
||||
document.getElementById('coding-description').value = directory?.description || '';
|
||||
document.getElementById('coding-branch').value = directory?.defaultBranch || 'main';
|
||||
|
||||
// CLAUDE.md: Nur aus Dateisystem anzeigen
|
||||
const claudeContent = directory?.claudeMdFromDisk || '';
|
||||
this.updateClaudeLink(claudeContent, directory?.name);
|
||||
|
||||
// Pfad-Preview aktualisieren
|
||||
this.updatePathPreview();
|
||||
|
||||
// Farbe setzen
|
||||
const color = directory?.color || '#4F46E5';
|
||||
document.getElementById('coding-color-custom').value = color;
|
||||
document.querySelectorAll('.color-preset').forEach(btn => {
|
||||
btn.classList.toggle('selected', btn.dataset.color === color);
|
||||
});
|
||||
|
||||
// Gitea-Sektion zurücksetzen
|
||||
const giteaSection = document.querySelector('.coding-gitea-section');
|
||||
if (giteaSection) {
|
||||
giteaSection.open = !!directory?.giteaRepoUrl;
|
||||
}
|
||||
|
||||
// Repos laden wenn nötig
|
||||
if (directory?.giteaRepoUrl) {
|
||||
this.loadGiteaRepos();
|
||||
}
|
||||
|
||||
// Modal und Overlay anzeigen
|
||||
modal.classList.remove('hidden');
|
||||
modal.classList.add('visible');
|
||||
if (overlay) {
|
||||
overlay.classList.remove('hidden');
|
||||
overlay.classList.add('visible');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Modal schließen
|
||||
*/
|
||||
closeModal() {
|
||||
const modal = document.getElementById('coding-modal');
|
||||
const overlay = document.querySelector('.modal-overlay');
|
||||
|
||||
if (modal) {
|
||||
modal.classList.remove('visible');
|
||||
setTimeout(() => modal.classList.add('hidden'), 200);
|
||||
this.editingDirectory = null;
|
||||
}
|
||||
|
||||
if (overlay) {
|
||||
overlay.classList.remove('visible');
|
||||
setTimeout(() => overlay.classList.add('hidden'), 200);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Speichern-Handler
|
||||
*/
|
||||
async handleSave() {
|
||||
const name = document.getElementById('coding-name').value.trim();
|
||||
const description = document.getElementById('coding-description').value.trim();
|
||||
// CLAUDE.md wird nicht mehr gespeichert - nur readonly
|
||||
const defaultBranch = document.getElementById('coding-branch').value.trim() || 'main';
|
||||
|
||||
// Pfad automatisch aus Name generieren
|
||||
const localPath = `${BASE_PATH}/${name}`;
|
||||
|
||||
// Farbe ermitteln
|
||||
const selectedPreset = document.querySelector('.color-preset.selected');
|
||||
const color = selectedPreset?.dataset.color || document.getElementById('coding-color-custom').value;
|
||||
|
||||
// Gitea-Daten
|
||||
const giteaSelect = document.getElementById('coding-gitea-repo');
|
||||
const giteaRepoUrl = giteaSelect?.value || null;
|
||||
const giteaRepoOwner = giteaSelect?.selectedOptions[0]?.dataset.owner || null;
|
||||
const giteaRepoName = giteaSelect?.selectedOptions[0]?.dataset.name || null;
|
||||
|
||||
if (!name) {
|
||||
showToast('Anwendungsname ist erforderlich', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const data = {
|
||||
name,
|
||||
localPath,
|
||||
description,
|
||||
color,
|
||||
// claudeInstructions entfernt - CLAUDE.md ist readonly
|
||||
giteaRepoUrl,
|
||||
giteaRepoOwner,
|
||||
giteaRepoName,
|
||||
defaultBranch
|
||||
};
|
||||
|
||||
try {
|
||||
if (this.editingDirectory) {
|
||||
await api.updateCodingDirectory(this.editingDirectory.id, data);
|
||||
showToast('Anwendung aktualisiert', 'success');
|
||||
} else {
|
||||
await api.createCodingDirectory(data);
|
||||
showToast('Anwendung hinzugefügt', 'success');
|
||||
}
|
||||
|
||||
this.closeModal();
|
||||
await this.loadDirectories();
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Speichern:', error);
|
||||
showToast(error.message || 'Fehler beim Speichern', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Löschen-Handler
|
||||
*/
|
||||
async handleDelete() {
|
||||
if (!this.editingDirectory) return;
|
||||
|
||||
if (!confirm(`Anwendung "${this.editingDirectory.name}" wirklich löschen?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await api.deleteCodingDirectory(this.editingDirectory.id);
|
||||
showToast('Anwendung gelöscht', 'success');
|
||||
this.closeModal();
|
||||
await this.loadDirectories();
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Löschen:', error);
|
||||
showToast('Fehler beim Löschen', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-Refresh starten
|
||||
*/
|
||||
startAutoRefresh() {
|
||||
this.stopAutoRefresh();
|
||||
// Alle 30 Sekunden aktualisieren
|
||||
this.refreshInterval = setInterval(() => {
|
||||
this.directories.forEach(dir => this.updateTileStatus(dir.id));
|
||||
}, 30000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-Refresh stoppen
|
||||
*/
|
||||
stopAutoRefresh() {
|
||||
if (this.refreshInterval) {
|
||||
clearInterval(this.refreshInterval);
|
||||
this.refreshInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* View anzeigen
|
||||
*/
|
||||
async show() {
|
||||
await this.loadDirectories();
|
||||
this.startAutoRefresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* View verstecken
|
||||
*/
|
||||
hide() {
|
||||
this.stopAutoRefresh();
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton-Instanz erstellen und exportieren
|
||||
const codingManager = new CodingManager();
|
||||
export default codingManager;
|
||||
@ -86,6 +86,13 @@ class ListViewManager {
|
||||
this.contentElement.addEventListener('click', (e) => this.handleContentClick(e));
|
||||
this.contentElement.addEventListener('change', (e) => this.handleContentChange(e));
|
||||
this.contentElement.addEventListener('dblclick', (e) => this.handleDoubleClick(e));
|
||||
|
||||
// Stop editing when clicking outside
|
||||
document.addEventListener('click', (e) => {
|
||||
if (this.editingCell && !this.editingCell.contains(e.target)) {
|
||||
this.stopEditing();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -407,19 +414,53 @@ class ListViewManager {
|
||||
const users = store.get('users');
|
||||
const cell = createElement('div', { className: 'list-cell list-cell-assignee list-cell-editable' });
|
||||
|
||||
const assignedUser = users.find(u => u.id === task.assignedTo);
|
||||
|
||||
// Avatar
|
||||
if (assignedUser) {
|
||||
const avatar = createElement('div', {
|
||||
className: 'avatar',
|
||||
style: { backgroundColor: assignedUser.color || '#6366F1' }
|
||||
}, [getInitials(assignedUser.displayName)]);
|
||||
cell.appendChild(avatar);
|
||||
// Sammle alle zugewiesenen Benutzer aus assignees Array
|
||||
const assignedUserIds = new Set();
|
||||
|
||||
// Verwende das assignees Array vom Backend
|
||||
if (task.assignees && Array.isArray(task.assignees)) {
|
||||
task.assignees.forEach(assignee => {
|
||||
if (assignee && assignee.id) {
|
||||
assignedUserIds.add(assignee.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Fallback: Füge assigned_to hinzu falls assignees leer ist
|
||||
if (assignedUserIds.size === 0 && task.assignedTo) {
|
||||
assignedUserIds.add(task.assignedTo);
|
||||
}
|
||||
|
||||
// User dropdown
|
||||
// Container für mehrere Avatare
|
||||
const avatarContainer = createElement('div', { className: 'avatar-container' });
|
||||
|
||||
if (assignedUserIds.size > 0) {
|
||||
// Erstelle Avatar für jeden zugewiesenen Benutzer
|
||||
Array.from(assignedUserIds).forEach(userId => {
|
||||
const user = users.find(u => u.id === userId);
|
||||
if (user) {
|
||||
const avatar = createElement('div', {
|
||||
className: 'avatar',
|
||||
style: { backgroundColor: user.color || '#6366F1' },
|
||||
title: user.displayName // Tooltip zeigt Name beim Hover
|
||||
}, [getInitials(user.displayName)]);
|
||||
avatarContainer.appendChild(avatar);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Placeholder für "nicht zugewiesen"
|
||||
const placeholder = createElement('div', {
|
||||
className: 'avatar avatar-empty',
|
||||
title: 'Nicht zugewiesen'
|
||||
}, ['?']);
|
||||
avatarContainer.appendChild(placeholder);
|
||||
}
|
||||
|
||||
cell.appendChild(avatarContainer);
|
||||
|
||||
// User dropdown (versteckt, nur für Bearbeitung)
|
||||
const select = createElement('select', {
|
||||
className: 'assignee-select hidden',
|
||||
dataset: { field: 'assignedTo', taskId: task.id }
|
||||
});
|
||||
|
||||
@ -445,6 +486,24 @@ class ListViewManager {
|
||||
// =====================
|
||||
|
||||
handleContentClick(e) {
|
||||
// Handle avatar click for assignee editing
|
||||
if (e.target.classList.contains('avatar') || e.target.classList.contains('avatar-empty')) {
|
||||
const cell = e.target.closest('.list-cell-assignee');
|
||||
if (cell) {
|
||||
this.startEditingAssignee(cell);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle click on avatar container (wenn man neben Avatar klickt)
|
||||
if (e.target.classList.contains('avatar-container')) {
|
||||
const cell = e.target.closest('.list-cell-assignee');
|
||||
if (cell) {
|
||||
this.startEditingAssignee(cell);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const target = e.target.closest('[data-action]');
|
||||
if (!target) return;
|
||||
|
||||
@ -456,6 +515,35 @@ class ListViewManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start editing assignee
|
||||
*/
|
||||
startEditingAssignee(cell) {
|
||||
// Stop any current editing
|
||||
this.stopEditing();
|
||||
|
||||
// Add editing class to show dropdown and hide avatar
|
||||
cell.classList.add('editing');
|
||||
|
||||
// Focus the select element
|
||||
const select = cell.querySelector('.assignee-select');
|
||||
if (select) {
|
||||
select.focus();
|
||||
}
|
||||
|
||||
this.editingCell = cell;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop editing
|
||||
*/
|
||||
stopEditing() {
|
||||
if (this.editingCell) {
|
||||
this.editingCell.classList.remove('editing');
|
||||
this.editingCell = null;
|
||||
}
|
||||
}
|
||||
|
||||
handleContentChange(e) {
|
||||
const target = e.target;
|
||||
const field = target.dataset.field;
|
||||
@ -463,6 +551,11 @@ class ListViewManager {
|
||||
|
||||
if (field && taskId) {
|
||||
this.updateTaskField(parseInt(taskId), field, target.value);
|
||||
|
||||
// Stop editing after change for assignee field
|
||||
if (field === 'assignedTo') {
|
||||
this.stopEditing();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
696
frontend/js/mobile.js
Normale Datei
696
frontend/js/mobile.js
Normale Datei
@ -0,0 +1,696 @@
|
||||
/**
|
||||
* TASKMATE - Mobile Module
|
||||
* ========================
|
||||
* Touch-Gesten, Hamburger-Menu, Swipe-Navigation
|
||||
*/
|
||||
|
||||
import { $, $$ } from './utils.js';
|
||||
|
||||
class MobileManager {
|
||||
constructor() {
|
||||
// State
|
||||
this.isMenuOpen = false;
|
||||
this.isMobile = false;
|
||||
this.currentView = 'board';
|
||||
|
||||
// Swipe state
|
||||
this.touchStartX = 0;
|
||||
this.touchStartY = 0;
|
||||
this.touchCurrentX = 0;
|
||||
this.touchCurrentY = 0;
|
||||
this.touchStartTime = 0;
|
||||
this.isSwiping = false;
|
||||
this.swipeDirection = null;
|
||||
|
||||
// Touch drag & drop state
|
||||
this.touchDraggedElement = null;
|
||||
this.touchDragPlaceholder = null;
|
||||
this.touchDragStartX = 0;
|
||||
this.touchDragStartY = 0;
|
||||
this.touchDragOffsetX = 0;
|
||||
this.touchDragOffsetY = 0;
|
||||
this.touchDragScrollInterval = null;
|
||||
this.longPressTimer = null;
|
||||
|
||||
// Constants
|
||||
this.SWIPE_THRESHOLD = 50;
|
||||
this.SWIPE_VELOCITY_THRESHOLD = 0.3;
|
||||
this.MOBILE_BREAKPOINT = 768;
|
||||
this.LONG_PRESS_DURATION = 300;
|
||||
|
||||
// View order for swipe navigation
|
||||
this.viewOrder = ['board', 'list', 'calendar', 'proposals', 'gitea', 'knowledge'];
|
||||
|
||||
// DOM elements
|
||||
this.hamburgerBtn = null;
|
||||
this.mobileMenu = null;
|
||||
this.mobileOverlay = null;
|
||||
this.mainContent = null;
|
||||
this.swipeIndicatorLeft = null;
|
||||
this.swipeIndicatorRight = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize mobile features
|
||||
*/
|
||||
init() {
|
||||
// Check if mobile
|
||||
this.checkMobile();
|
||||
window.addEventListener('resize', () => this.checkMobile());
|
||||
|
||||
// Cache DOM elements
|
||||
this.hamburgerBtn = $('#hamburger-btn');
|
||||
this.mobileMenu = $('#mobile-menu');
|
||||
this.mobileOverlay = $('#mobile-menu-overlay');
|
||||
this.mainContent = $('.main-content');
|
||||
this.swipeIndicatorLeft = $('#swipe-indicator-left');
|
||||
this.swipeIndicatorRight = $('#swipe-indicator-right');
|
||||
|
||||
// Bind events
|
||||
this.bindMenuEvents();
|
||||
this.bindSwipeEvents();
|
||||
this.bindTouchDragEvents();
|
||||
|
||||
// Listen for view changes
|
||||
document.addEventListener('view:changed', (e) => {
|
||||
this.currentView = e.detail?.view || 'board';
|
||||
this.updateActiveNavItem(this.currentView);
|
||||
});
|
||||
|
||||
// Listen for project changes
|
||||
document.addEventListener('projects:loaded', () => {
|
||||
this.populateMobileProjectSelect();
|
||||
});
|
||||
|
||||
// Listen for user updates
|
||||
document.addEventListener('user:updated', () => {
|
||||
this.updateUserInfo();
|
||||
});
|
||||
|
||||
console.log('[Mobile] Initialized');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if current viewport is mobile
|
||||
*/
|
||||
checkMobile() {
|
||||
this.isMobile = window.innerWidth <= this.MOBILE_BREAKPOINT;
|
||||
}
|
||||
|
||||
// =====================
|
||||
// HAMBURGER MENU
|
||||
// =====================
|
||||
|
||||
/**
|
||||
* Bind menu events
|
||||
*/
|
||||
bindMenuEvents() {
|
||||
// Hamburger button
|
||||
this.hamburgerBtn?.addEventListener('click', () => this.toggleMenu());
|
||||
|
||||
// Close button
|
||||
$('#mobile-menu-close')?.addEventListener('click', () => this.closeMenu());
|
||||
|
||||
// Overlay click
|
||||
this.mobileOverlay?.addEventListener('click', () => this.closeMenu());
|
||||
|
||||
// Navigation items
|
||||
$$('.mobile-nav-item').forEach(item => {
|
||||
item.addEventListener('click', () => {
|
||||
const view = item.dataset.view;
|
||||
this.switchView(view);
|
||||
this.closeMenu();
|
||||
});
|
||||
});
|
||||
|
||||
// Project selector
|
||||
$('#mobile-project-select')?.addEventListener('change', (e) => {
|
||||
const projectId = parseInt(e.target.value);
|
||||
if (projectId) {
|
||||
document.dispatchEvent(new CustomEvent('project:selected', {
|
||||
detail: { projectId }
|
||||
}));
|
||||
this.closeMenu();
|
||||
}
|
||||
});
|
||||
|
||||
// Admin button
|
||||
$('#mobile-admin-btn')?.addEventListener('click', () => {
|
||||
this.closeMenu();
|
||||
document.dispatchEvent(new CustomEvent('admin:open'));
|
||||
});
|
||||
|
||||
// Logout button
|
||||
$('#mobile-logout-btn')?.addEventListener('click', () => {
|
||||
this.closeMenu();
|
||||
document.dispatchEvent(new CustomEvent('auth:logout'));
|
||||
});
|
||||
|
||||
// Escape key to close
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && this.isMenuOpen) {
|
||||
this.closeMenu();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle menu open/close
|
||||
*/
|
||||
toggleMenu() {
|
||||
if (this.isMenuOpen) {
|
||||
this.closeMenu();
|
||||
} else {
|
||||
this.openMenu();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open mobile menu
|
||||
*/
|
||||
openMenu() {
|
||||
this.isMenuOpen = true;
|
||||
this.hamburgerBtn?.classList.add('active');
|
||||
this.hamburgerBtn?.setAttribute('aria-expanded', 'true');
|
||||
this.mobileMenu?.classList.add('open');
|
||||
this.mobileMenu?.setAttribute('aria-hidden', 'false');
|
||||
this.mobileOverlay?.classList.add('visible');
|
||||
document.body.classList.add('mobile-menu-open');
|
||||
|
||||
// Update user info when menu opens
|
||||
this.updateUserInfo();
|
||||
this.populateMobileProjectSelect();
|
||||
|
||||
// Focus close button
|
||||
setTimeout(() => {
|
||||
$('#mobile-menu-close')?.focus();
|
||||
}, 300);
|
||||
}
|
||||
|
||||
/**
|
||||
* Close mobile menu
|
||||
*/
|
||||
closeMenu() {
|
||||
this.isMenuOpen = false;
|
||||
this.hamburgerBtn?.classList.remove('active');
|
||||
this.hamburgerBtn?.setAttribute('aria-expanded', 'false');
|
||||
this.mobileMenu?.classList.remove('open');
|
||||
this.mobileMenu?.setAttribute('aria-hidden', 'true');
|
||||
this.mobileOverlay?.classList.remove('visible');
|
||||
document.body.classList.remove('mobile-menu-open');
|
||||
|
||||
// Return focus
|
||||
this.hamburgerBtn?.focus();
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch to a different view
|
||||
*/
|
||||
switchView(view) {
|
||||
if (!this.viewOrder.includes(view)) return;
|
||||
|
||||
this.currentView = view;
|
||||
|
||||
// Update desktop tabs
|
||||
$$('.view-tab').forEach(tab => {
|
||||
tab.classList.toggle('active', tab.dataset.view === view);
|
||||
});
|
||||
|
||||
// Show/hide views
|
||||
$$('.view').forEach(v => {
|
||||
const viewName = v.id.replace('view-', '');
|
||||
const isActive = viewName === view;
|
||||
v.classList.toggle('active', isActive);
|
||||
v.classList.toggle('hidden', !isActive);
|
||||
});
|
||||
|
||||
// Update mobile nav
|
||||
this.updateActiveNavItem(view);
|
||||
|
||||
// Dispatch event for other modules
|
||||
document.dispatchEvent(new CustomEvent('view:changed', {
|
||||
detail: { view }
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update active nav item in mobile menu
|
||||
*/
|
||||
updateActiveNavItem(view) {
|
||||
$$('.mobile-nav-item').forEach(item => {
|
||||
item.classList.toggle('active', item.dataset.view === view);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate project select dropdown
|
||||
*/
|
||||
populateMobileProjectSelect() {
|
||||
const select = $('#mobile-project-select');
|
||||
const desktopSelect = $('#project-select');
|
||||
if (!select || !desktopSelect) return;
|
||||
|
||||
// Copy options from desktop select
|
||||
select.innerHTML = desktopSelect.innerHTML;
|
||||
select.value = desktopSelect.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update user info in mobile menu
|
||||
*/
|
||||
updateUserInfo() {
|
||||
const avatar = $('#mobile-user-avatar');
|
||||
const name = $('#mobile-user-name');
|
||||
const role = $('#mobile-user-role');
|
||||
const adminBtn = $('#mobile-admin-btn');
|
||||
|
||||
// Get user info from desktop user dropdown
|
||||
const desktopAvatar = $('#user-avatar');
|
||||
const desktopDropdown = $('.user-dropdown');
|
||||
|
||||
if (avatar && desktopAvatar) {
|
||||
avatar.textContent = desktopAvatar.textContent;
|
||||
avatar.style.backgroundColor = desktopAvatar.style.backgroundColor || 'var(--primary)';
|
||||
}
|
||||
|
||||
if (name) {
|
||||
const usernameEl = desktopDropdown?.querySelector('.user-info strong');
|
||||
name.textContent = usernameEl?.textContent || 'Benutzer';
|
||||
}
|
||||
|
||||
if (role) {
|
||||
const roleEl = desktopDropdown?.querySelector('.user-info span:not(strong)');
|
||||
role.textContent = roleEl?.textContent || 'Angemeldet';
|
||||
}
|
||||
|
||||
// Show admin button for admins
|
||||
if (adminBtn) {
|
||||
const isAdmin = role?.textContent?.toLowerCase().includes('admin');
|
||||
adminBtn.classList.toggle('hidden', !isAdmin);
|
||||
}
|
||||
}
|
||||
|
||||
// =====================
|
||||
// SWIPE NAVIGATION
|
||||
// =====================
|
||||
|
||||
/**
|
||||
* Bind swipe events
|
||||
*/
|
||||
bindSwipeEvents() {
|
||||
if (!this.mainContent) return;
|
||||
|
||||
this.mainContent.addEventListener('touchstart', (e) => this.handleSwipeStart(e), { passive: true });
|
||||
this.mainContent.addEventListener('touchmove', (e) => this.handleSwipeMove(e), { passive: false });
|
||||
this.mainContent.addEventListener('touchend', (e) => this.handleSwipeEnd(e), { passive: true });
|
||||
this.mainContent.addEventListener('touchcancel', () => this.resetSwipe(), { passive: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle swipe start
|
||||
*/
|
||||
handleSwipeStart(e) {
|
||||
if (!this.isMobile) return;
|
||||
|
||||
// Don't swipe if menu is open
|
||||
if (this.isMenuOpen) return;
|
||||
|
||||
// Don't swipe if modal is open
|
||||
if ($('.modal-overlay:not(.hidden)')) return;
|
||||
|
||||
// Don't swipe on scrollable elements
|
||||
const target = e.target;
|
||||
if (target.closest('.column-body') ||
|
||||
target.closest('.modal') ||
|
||||
target.closest('.calendar-grid') ||
|
||||
target.closest('.knowledge-entry-list') ||
|
||||
target.closest('.list-table') ||
|
||||
target.closest('input') ||
|
||||
target.closest('textarea') ||
|
||||
target.closest('select')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only single touch
|
||||
if (e.touches.length !== 1) return;
|
||||
|
||||
this.touchStartX = e.touches[0].clientX;
|
||||
this.touchStartY = e.touches[0].clientY;
|
||||
this.touchStartTime = Date.now();
|
||||
this.isSwiping = false;
|
||||
this.swipeDirection = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle swipe move
|
||||
*/
|
||||
handleSwipeMove(e) {
|
||||
if (!this.isMobile || this.touchStartX === 0) return;
|
||||
|
||||
const touch = e.touches[0];
|
||||
this.touchCurrentX = touch.clientX;
|
||||
this.touchCurrentY = touch.clientY;
|
||||
|
||||
const deltaX = this.touchCurrentX - this.touchStartX;
|
||||
const deltaY = this.touchCurrentY - this.touchStartY;
|
||||
|
||||
// Determine direction on first significant movement
|
||||
if (!this.swipeDirection && (Math.abs(deltaX) > 10 || Math.abs(deltaY) > 10)) {
|
||||
if (Math.abs(deltaX) > Math.abs(deltaY) * 1.5) {
|
||||
this.swipeDirection = 'horizontal';
|
||||
this.isSwiping = true;
|
||||
document.body.classList.add('is-swiping');
|
||||
} else {
|
||||
this.swipeDirection = 'vertical';
|
||||
this.resetSwipe();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.swipeDirection !== 'horizontal') return;
|
||||
|
||||
// Prevent scroll
|
||||
e.preventDefault();
|
||||
|
||||
// Show indicators
|
||||
const currentIndex = this.viewOrder.indexOf(this.currentView);
|
||||
|
||||
if (deltaX > this.SWIPE_THRESHOLD && currentIndex > 0) {
|
||||
this.swipeIndicatorLeft?.classList.add('visible');
|
||||
this.swipeIndicatorRight?.classList.remove('visible');
|
||||
} else if (deltaX < -this.SWIPE_THRESHOLD && currentIndex < this.viewOrder.length - 1) {
|
||||
this.swipeIndicatorRight?.classList.add('visible');
|
||||
this.swipeIndicatorLeft?.classList.remove('visible');
|
||||
} else {
|
||||
this.swipeIndicatorLeft?.classList.remove('visible');
|
||||
this.swipeIndicatorRight?.classList.remove('visible');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle swipe end
|
||||
*/
|
||||
handleSwipeEnd() {
|
||||
if (!this.isSwiping || this.swipeDirection !== 'horizontal') {
|
||||
this.resetSwipe();
|
||||
return;
|
||||
}
|
||||
|
||||
const deltaX = this.touchCurrentX - this.touchStartX;
|
||||
const deltaTime = Date.now() - this.touchStartTime;
|
||||
const velocity = Math.abs(deltaX) / deltaTime;
|
||||
|
||||
// Valid swipe?
|
||||
const isValidSwipe = Math.abs(deltaX) > this.SWIPE_THRESHOLD || velocity > this.SWIPE_VELOCITY_THRESHOLD;
|
||||
|
||||
if (isValidSwipe) {
|
||||
const currentIndex = this.viewOrder.indexOf(this.currentView);
|
||||
|
||||
if (deltaX > 0 && currentIndex > 0) {
|
||||
// Swipe right - previous view
|
||||
this.switchView(this.viewOrder[currentIndex - 1]);
|
||||
} else if (deltaX < 0 && currentIndex < this.viewOrder.length - 1) {
|
||||
// Swipe left - next view
|
||||
this.switchView(this.viewOrder[currentIndex + 1]);
|
||||
}
|
||||
}
|
||||
|
||||
this.resetSwipe();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset swipe state
|
||||
*/
|
||||
resetSwipe() {
|
||||
this.touchStartX = 0;
|
||||
this.touchStartY = 0;
|
||||
this.touchCurrentX = 0;
|
||||
this.touchCurrentY = 0;
|
||||
this.touchStartTime = 0;
|
||||
this.isSwiping = false;
|
||||
this.swipeDirection = null;
|
||||
document.body.classList.remove('is-swiping');
|
||||
this.swipeIndicatorLeft?.classList.remove('visible');
|
||||
this.swipeIndicatorRight?.classList.remove('visible');
|
||||
}
|
||||
|
||||
// =====================
|
||||
// TOUCH DRAG & DROP
|
||||
// =====================
|
||||
|
||||
/**
|
||||
* Bind touch drag events
|
||||
*/
|
||||
bindTouchDragEvents() {
|
||||
const board = $('#board');
|
||||
if (!board) return;
|
||||
|
||||
board.addEventListener('touchstart', (e) => this.handleTouchDragStart(e), { passive: false });
|
||||
board.addEventListener('touchmove', (e) => this.handleTouchDragMove(e), { passive: false });
|
||||
board.addEventListener('touchend', (e) => this.handleTouchDragEnd(e), { passive: true });
|
||||
board.addEventListener('touchcancel', () => this.cancelTouchDrag(), { passive: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle touch drag start
|
||||
*/
|
||||
handleTouchDragStart(e) {
|
||||
if (!this.isMobile) return;
|
||||
|
||||
const taskCard = e.target.closest('.task-card');
|
||||
if (!taskCard) return;
|
||||
|
||||
// Cancel if multi-touch
|
||||
if (e.touches.length > 1) {
|
||||
this.cancelTouchDrag();
|
||||
return;
|
||||
}
|
||||
|
||||
const touch = e.touches[0];
|
||||
this.touchDragStartX = touch.clientX;
|
||||
this.touchDragStartY = touch.clientY;
|
||||
|
||||
// Long press to start drag
|
||||
this.longPressTimer = setTimeout(() => {
|
||||
this.startTouchDrag(taskCard, touch);
|
||||
}, this.LONG_PRESS_DURATION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start touch drag
|
||||
*/
|
||||
startTouchDrag(taskCard, touch) {
|
||||
this.touchDraggedElement = taskCard;
|
||||
const rect = taskCard.getBoundingClientRect();
|
||||
|
||||
// Calculate offset
|
||||
this.touchDragOffsetX = touch.clientX - rect.left;
|
||||
this.touchDragOffsetY = touch.clientY - rect.top;
|
||||
|
||||
// Create placeholder
|
||||
this.touchDragPlaceholder = document.createElement('div');
|
||||
this.touchDragPlaceholder.className = 'task-card touch-drag-placeholder';
|
||||
this.touchDragPlaceholder.style.height = rect.height + 'px';
|
||||
taskCard.parentNode.insertBefore(this.touchDragPlaceholder, taskCard);
|
||||
|
||||
// Style dragged element
|
||||
taskCard.classList.add('touch-dragging');
|
||||
taskCard.style.position = 'fixed';
|
||||
taskCard.style.left = rect.left + 'px';
|
||||
taskCard.style.top = rect.top + 'px';
|
||||
taskCard.style.width = rect.width + 'px';
|
||||
taskCard.style.zIndex = '1000';
|
||||
|
||||
document.body.classList.add('is-touch-dragging');
|
||||
|
||||
// Haptic feedback
|
||||
if (navigator.vibrate) {
|
||||
navigator.vibrate(50);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle touch drag move
|
||||
*/
|
||||
handleTouchDragMove(e) {
|
||||
// Cancel long press if finger moved
|
||||
if (this.longPressTimer && !this.touchDraggedElement) {
|
||||
const touch = e.touches[0];
|
||||
const deltaX = Math.abs(touch.clientX - this.touchDragStartX);
|
||||
const deltaY = Math.abs(touch.clientY - this.touchDragStartY);
|
||||
if (deltaX > 10 || deltaY > 10) {
|
||||
clearTimeout(this.longPressTimer);
|
||||
this.longPressTimer = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.touchDraggedElement) return;
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
const touch = e.touches[0];
|
||||
const taskCard = this.touchDraggedElement;
|
||||
|
||||
// Move element
|
||||
taskCard.style.left = (touch.clientX - this.touchDragOffsetX) + 'px';
|
||||
taskCard.style.top = (touch.clientY - this.touchDragOffsetY) + 'px';
|
||||
|
||||
// Find drop target
|
||||
taskCard.style.pointerEvents = 'none';
|
||||
const elemBelow = document.elementFromPoint(touch.clientX, touch.clientY);
|
||||
taskCard.style.pointerEvents = '';
|
||||
|
||||
const columnBody = elemBelow?.closest('.column-body');
|
||||
|
||||
// Remove previous indicators
|
||||
$$('.column-body.touch-drag-over').forEach(el => el.classList.remove('touch-drag-over'));
|
||||
|
||||
if (columnBody) {
|
||||
columnBody.classList.add('touch-drag-over');
|
||||
}
|
||||
|
||||
// Auto-scroll
|
||||
this.autoScrollWhileDragging(touch);
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-scroll while dragging near edges
|
||||
*/
|
||||
autoScrollWhileDragging(touch) {
|
||||
const board = $('#board');
|
||||
if (!board) return;
|
||||
|
||||
const boardRect = board.getBoundingClientRect();
|
||||
const scrollThreshold = 50;
|
||||
const scrollSpeed = 8;
|
||||
|
||||
// Clear existing interval
|
||||
if (this.touchDragScrollInterval) {
|
||||
clearInterval(this.touchDragScrollInterval);
|
||||
this.touchDragScrollInterval = null;
|
||||
}
|
||||
|
||||
// Scroll left
|
||||
if (touch.clientX < boardRect.left + scrollThreshold) {
|
||||
this.touchDragScrollInterval = setInterval(() => {
|
||||
board.scrollLeft -= scrollSpeed;
|
||||
}, 16);
|
||||
}
|
||||
// Scroll right
|
||||
else if (touch.clientX > boardRect.right - scrollThreshold) {
|
||||
this.touchDragScrollInterval = setInterval(() => {
|
||||
board.scrollLeft += scrollSpeed;
|
||||
}, 16);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle touch drag end
|
||||
*/
|
||||
handleTouchDragEnd(e) {
|
||||
// Clear long press timer
|
||||
if (this.longPressTimer) {
|
||||
clearTimeout(this.longPressTimer);
|
||||
this.longPressTimer = null;
|
||||
}
|
||||
|
||||
if (!this.touchDraggedElement) return;
|
||||
|
||||
const touch = e.changedTouches[0];
|
||||
|
||||
// Find drop target
|
||||
this.touchDraggedElement.style.pointerEvents = 'none';
|
||||
const elemBelow = document.elementFromPoint(touch.clientX, touch.clientY);
|
||||
this.touchDraggedElement.style.pointerEvents = '';
|
||||
|
||||
const columnBody = elemBelow?.closest('.column-body');
|
||||
|
||||
if (columnBody) {
|
||||
const columnId = parseInt(columnBody.closest('.column').dataset.columnId);
|
||||
const taskId = parseInt(this.touchDraggedElement.dataset.taskId);
|
||||
const position = this.calculateDropPosition(columnBody, touch.clientY);
|
||||
|
||||
// Dispatch move event
|
||||
document.dispatchEvent(new CustomEvent('task:move', {
|
||||
detail: { taskId, columnId, position }
|
||||
}));
|
||||
}
|
||||
|
||||
this.cleanupTouchDrag();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate drop position in column
|
||||
*/
|
||||
calculateDropPosition(columnBody, mouseY) {
|
||||
const taskCards = Array.from(columnBody.querySelectorAll('.task-card:not(.touch-dragging):not(.touch-drag-placeholder)'));
|
||||
let position = taskCards.length;
|
||||
|
||||
for (let i = 0; i < taskCards.length; i++) {
|
||||
const rect = taskCards[i].getBoundingClientRect();
|
||||
if (mouseY < rect.top + rect.height / 2) {
|
||||
position = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return position;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel touch drag
|
||||
*/
|
||||
cancelTouchDrag() {
|
||||
if (this.longPressTimer) {
|
||||
clearTimeout(this.longPressTimer);
|
||||
this.longPressTimer = null;
|
||||
}
|
||||
this.cleanupTouchDrag();
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup after touch drag
|
||||
*/
|
||||
cleanupTouchDrag() {
|
||||
// Clear scroll interval
|
||||
if (this.touchDragScrollInterval) {
|
||||
clearInterval(this.touchDragScrollInterval);
|
||||
this.touchDragScrollInterval = null;
|
||||
}
|
||||
|
||||
// Reset dragged element
|
||||
if (this.touchDraggedElement) {
|
||||
this.touchDraggedElement.classList.remove('touch-dragging');
|
||||
this.touchDraggedElement.style.position = '';
|
||||
this.touchDraggedElement.style.left = '';
|
||||
this.touchDraggedElement.style.top = '';
|
||||
this.touchDraggedElement.style.width = '';
|
||||
this.touchDraggedElement.style.zIndex = '';
|
||||
this.touchDraggedElement.style.transform = '';
|
||||
}
|
||||
|
||||
// Remove placeholder
|
||||
if (this.touchDragPlaceholder) {
|
||||
this.touchDragPlaceholder.remove();
|
||||
this.touchDragPlaceholder = null;
|
||||
}
|
||||
|
||||
// Remove indicators
|
||||
$$('.column-body.touch-drag-over').forEach(el => el.classList.remove('touch-drag-over'));
|
||||
|
||||
document.body.classList.remove('is-touch-dragging');
|
||||
|
||||
// Reset state
|
||||
this.touchDraggedElement = null;
|
||||
this.touchDragStartX = 0;
|
||||
this.touchDragStartY = 0;
|
||||
this.touchDragOffsetX = 0;
|
||||
this.touchDragOffsetY = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Create and export singleton
|
||||
const mobileManager = new MobileManager();
|
||||
|
||||
export default mobileManager;
|
||||
@ -122,6 +122,13 @@ class NotificationManager {
|
||||
updateBadge(count) {
|
||||
this.unreadCount = count;
|
||||
|
||||
// Sicherstellen, dass badge-Element existiert
|
||||
if (!this.badge) {
|
||||
this.badge = document.getElementById('notification-badge');
|
||||
}
|
||||
|
||||
if (!this.badge) return; // Wenn immer noch nicht gefunden, abbrechen
|
||||
|
||||
if (count > 0) {
|
||||
this.badge.textContent = count > 99 ? '99+' : count;
|
||||
this.badge.classList.remove('hidden');
|
||||
@ -441,6 +448,12 @@ class NotificationManager {
|
||||
this.unreadCount = 0;
|
||||
this.isDropdownOpen = false;
|
||||
this.closeDropdown();
|
||||
|
||||
// Elements neu binden falls nötig
|
||||
if (!this.badge || !this.bellContainer) {
|
||||
this.bindElements();
|
||||
}
|
||||
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
* Offline support and caching
|
||||
*/
|
||||
|
||||
const CACHE_VERSION = '152';
|
||||
const CACHE_VERSION = '189';
|
||||
const CACHE_NAME = 'taskmate-v' + CACHE_VERSION;
|
||||
const STATIC_CACHE_NAME = 'taskmate-static-v' + CACHE_VERSION;
|
||||
const DYNAMIC_CACHE_NAME = 'taskmate-dynamic-v' + CACHE_VERSION;
|
||||
@ -39,12 +39,16 @@ const STATIC_ASSETS = [
|
||||
'/js/notifications.js',
|
||||
'/js/gitea.js',
|
||||
'/js/knowledge.js',
|
||||
'/js/coding.js',
|
||||
'/js/mobile.js',
|
||||
'/css/list.css',
|
||||
'/css/mobile.css',
|
||||
'/css/admin.css',
|
||||
'/css/proposals.css',
|
||||
'/css/notifications.css',
|
||||
'/css/gitea.css',
|
||||
'/css/knowledge.css'
|
||||
'/css/knowledge.css',
|
||||
'/css/coding.css'
|
||||
];
|
||||
|
||||
// API routes to cache
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren